jar*_*jar 5 javascript three.js reactjs react-three-fiber
我一直在尝试通过 React 应用程序使用 Three.js 的InstancedMesh来显示 3D 散点图(因此使用 React Three Fiber)。
几天后,我获得了球体的 3D 散点图,并为它们着色。不过,我希望能够通过鼠标单击/悬停来选择/突出显示各个球体。我遵循了一个简单的教程,使用了onPointerOver
andonPointerOut
但似乎不起作用,也许是因为它不适用于 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但使用反应三纤维并显示球体的相关内容。
我尽了最大努力来解决这个问题,结果如下。
这是我的沙箱: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 instance
for 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)
这就是camera
并mouse
解决了。我们可以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,而不会在每个渲染循环中被覆盖。
此外,我会看看这里,看看是否可以更好地处理鼠标和相机事件。
归档时间: |
|
查看次数: |
1305 次 |
最近记录: |