无法弄清楚如何突出显示或选择 InstancedMesh 中的单个对象并使用 React 三纤维显示自定义 HTML 工具提示

jar*_*jar 5 javascript three.js reactjs react-three-fiber

我一直在尝试通过 React 应用程序使用 Three.js 的InstancedMesh来显示 3D 散点图(因此使用 React Three Fiber)。

几天后,我获得了球体的 3D 散点图,并为它们着色。不过,我希望能够通过鼠标单击/悬停来选择/突出显示各个球体。我遵循了一个简单的教程,使用了onPointerOverandonPointerOut但似乎不起作用,也许是因为它不适用于 InstancedMesh 对象。

看起来我需要使用 raycaster,但我不清楚该怎么做。任何建议都会非常有帮助。

设置步骤 -

npx create-react-app demo
cd demo
npm install three
npm i @react-three/fiber
npm i @react-three/drei
Run Code Online (Sandbox Code Playgroud)

显示不同颜色球体的当前代码 -

应用程序.jsx

npx create-react-app demo
cd demo
npm install three
npm i @react-three/fiber
npm i @react-three/drei
Run Code Online (Sandbox Code Playgroud)

IScatter.jsx

import React from 'react'
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import Spheres from "./IScatter";
import * as THREE from "three";

function App() {
  return (
   <div>
      <Canvas style={{width:"100%",height:"100vh"}}>
        <OrbitControls enableZoom={true} />
        <ambientLight intensity={0.5} />
        <pointLight position={[10, 10, 10]}/>
        <Suspense fallback={null}>
        <primitive object={new THREE.AxesHelper(1.5)} />
        <Spheres />
        </Suspense>
      </Canvas>
      </div>
  );
}

export default App;
Run Code Online (Sandbox Code Playgroud)

我想更改所选球体的不透明度,也许可以将它们放大一点,以便我们知道它选择了什么。如果可能的话,每当选择球体时,我还想显示与球体关联的 HTML 内容,该内容与画布相邻,远离球体散点图。

编辑: 我本质上想实现这个 - https://thirdjs.org/examples/webgl_instancing_raycast但使用反应三纤维并显示球体的相关内容。

Piw*_*oli 2

我尽了最大努力来解决这个问题,结果如下。

这是我的沙箱:https://codesandbox.io/s/relaxed-rgb-yb1v1v ?file=/src/IScatter.jsx

首先,我创建了一个新的Scene.jsx,因为三纤维显然不喜欢 Canvas 之外的某些(不记得具体是什么)引用或钩子用法。

我的想法还应该Scene负责处理任何鼠标或相机事件,并且仅以一种或另一种方式将更改传递给其他组件。

基本上App变成了这样:

function App() {
  return (
    <div>
      <Canvas style={{ width: "100%", height: "100vh" }}>
        <Scene />
      </Canvas>
    </div>
  );
}
Run Code Online (Sandbox Code Playgroud)

现在所有旧App代码都位于Scene.

查看raycast 示例代码,我们可以看到,为了使用raycaster,我们需要...嗯... raycaster instancefor one,而且还需要camera instance和 a mouse position

为了使用camera instance三光纤,我找到了这个问题和答案,它指导我们使用useThree钩子:

const {
    camera,
    gl: { domElement }
  } = useThree();
Run Code Online (Sandbox Code Playgroud)

现在,这本身并没有多大帮助,因为它useThree().camera并不完全“绑定”到OrbitControls您正在使用的组件。为了解决这个问题,我们使用组件args的 propOrbitControls来将我们的相机绑定/传递给它:

<OrbitControls 
    enableZoom={true} 
    args={[camera, domElement]} 
    onChange={handleControls}
/>
Run Code Online (Sandbox Code Playgroud)

您可能会注意到那里还有一个onChange-prop 。每当相机旋转等发生变化时,该事件就会触发,但由于某种未知的原因,除了“某些内容发生了变化”之外,事件对象基本上不包含任何其他信息。

我希望事件对象包含相机信息或其他内容。不管怎样,因为它似乎是这样工作的,而且既然我们已经将 传递camera给了OrbitControls组件,每当这个更改事件触发时,我们就知道 也camera已经更新了。我的第一直觉当然是拥有这样的东西:

const [myCamera, setMyCamera] = useState(camera);

const handleControls = (e) => {
    setMyCamera(camera);
};
Run Code Online (Sandbox Code Playgroud)

所以我自己的相机状态,只要OrbitControls. 这里的问题是,虽然这种方法有效,但类似的事情useEffect(() => { }, [myCamera]);永远不会因为另一个未知的原因而触发。

那么,当遇到 useState + useEffect 触发问题时,沮丧的 React 开发人员会做什么呢?进行“强制状态更改”实现,handleControls当前的样子如下:

const [cameraSignal, setCameraSignal] = useState(0);

const handleControls = (e) => {
    setCameraSignal((s) => s + 1);
};
Run Code Online (Sandbox Code Playgroud)

然后我们将其传递cameraSignal给您的Spheres组件:

再说一遍,useThree().camera更新确实很漂亮,我们只是没有办法对此做出反应,因为直接更改它不会触发 useEffect,但是通过这个 hackycameraSignal实现,我们现在有一种方法可以在相机更改时触发 useEffects 。

所以现在我们已经camera解决了。接下来是mouse. 幸运的是,这是一个简单得多的问题:

const [mousePos, setMousePos] = useState({ x: 0, y: 0 });

const handleMouse = (event) => {
    const mouse = {};
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    setMousePos(mouse);
};

useEffect(() => {
    document.addEventListener("mousemove", handleMouse);
    return () => {
        document.removeEventListener("mousemove", handleMouse);
    };
}, []);
Run Code Online (Sandbox Code Playgroud)

这就是cameramouse解决了。我们可以raycaster使用相同的useThree钩子获取实例,所以让我们开始实际的光线投射实现:

const ts = useThree();
const [camera, setCamera] = useState(ts.camera);
const instanceColors = useRef(new Array(points.length).fill(red));

useEffect(() => {
    let mouse = new THREE.Vector2(mousePos.x, mousePos.y);
    ts.raycaster.setFromCamera(mouse, camera);

    const intersection = ts.raycaster.intersectObject(ref.current);

    if (intersection.length > 0) {
        const instanceId = intersection[0].instanceId;
        ref.current.getColorAt(instanceId, color);
        if (color.equals(red)) {
        instanceColors.current[instanceId] = new THREE.Color().setHex(Math.random() * 0xffffff);
        ref.current.instanceColor.needsUpdate = true;
        }
    }
}, [mousePos]);

useEffect(() => {
    setCamera(ts.camera);
}, [cameraSignal]);
Run Code Online (Sandbox Code Playgroud)

在这里,我们基本上只是遵循光线投射示例代码与我们全新的逻辑来获取mouse数据camera

在示例使用的地方,mesh例如:

const intersection = raycaster.intersectObject(mesh);
Run Code Online (Sandbox Code Playgroud)

我们使用ref.current它,因为它似乎与 React Fiber 一样工作,并且ref.current确实是InstancedMesh您创建的实际内容。

但也存在一些问题

我认为总体上如何使用 React Fiber 存在一些问题。查看(此处的文档)[https://openbase.com/js/@andrewray/react- Three-Fiber/documentation] 和(此处)[https://docs.pmnd.rs/react- Three-Fiber/ api/objects],有很多可以改进的地方,包括我的代码,这是一种使当前解决方案起作用的黑客方法。

我认为最大的问题之一是:

useEffect(() => {
    points.map(function (val, row) {
      tempSphere.position.set(val[0], val[1], val[2]);
      tempSphere.updateMatrix();
      ref.current.setMatrixAt(row, tempSphere.matrix);
      ref.current.setColorAt(row, instanceColors.current[row]);
    });

    ref.current.instanceMatrix.needsUpdate = true;
    //ref.current.instanceColor.needsUpdate = true;
  });
Run Code Online (Sandbox Code Playgroud)

由于 React Fiber 组件的工作原理,这很奇怪。我认为您的意图可能是初始化网格,但这基本上充当渲染更新循环。

我必须制作另一个像这样的引用来保存更改后的颜色,否则这个“渲染更新循环”将始终用初始颜色覆盖颜色:

const instanceColors = useRef(new Array(points.length).fill(red));

然后使用光线投射,我无法直接使用setColorAt(因为上面的 useEffect 只会覆盖颜色),而是更新了颜色引用:

instanceColors.current[instanceId] = new THREE.Color().setHex(Math.random() * 0xffffff);
Run Code Online (Sandbox Code Playgroud)

这一切都会导致 InstancedMesh 的工作光线投射和颜色变化,但如果您想从中获得更多性能等,我会考虑一些重构。

*额外注释

我遵循了一个使用 onPointerOver 和 onPointerOut 的简单教程,但这似乎不起作用,可能是因为它不适用于 InstancedMesh 对象。

我相信你是对的,从某种意义上说,InstancedMesh 是一种不同的野兽,它并不完全是一个可以单击或悬停在其上的明确的单个 3D 对象,实际上恰恰相反。可能为了使用 InstancedMesh,需要手动完成所有初始化、更新以及相机和指针事件的工作,但我不能肯定地说,因为这是第一次我使用三光纤。它看起来有点乱,但我以前见过类似的东西,所以这样做不一定是最错误的方法。

编辑

我想我解决了上述问题,请参阅此沙箱的 IScatter.jsx:https://codesandbox.io/s/stoic-hertz-qbbkpu? file=/src/IScatter.jsx (它运行得更好)

我将一些内容从 useEffect 甚至组件中移出。现在它实际上应该只初始化 InstancedMesh 一次。我们仍然有一个 useEffect 进行初始化,但它只在组件挂载时运行一次:

useEffect(() => {
    let i = 0;
    const offset = (amount - 1) / 2;

    for (let x = 0; x < amount; x++) {
      for (let y = 0; y < amount; y++) {
        for (let z = 0; z < amount; z++) {
          matrix.setPosition(offset - x, offset - y, offset - z);
          meshRef.current.setMatrixAt(i, matrix);
          meshRef.current.setColorAt(i, white);
          i++;
        }
      }
    }
  }, []);
Run Code Online (Sandbox Code Playgroud)

通过查看光线投射函数可以明显看出这一点:

meshRef.current.setColorAt(
    instanceId,
    new THREE.Color().setHex(Math.random() * 0xffffff)
);
Run Code Online (Sandbox Code Playgroud)

我们现在可以直接使用 setColorAt,而不会在每个渲染循环中被覆盖。

此外,我会看看这里,看看是否可以更好地处理鼠标和相机事件。