我一直在尝试通过React应用程序使用three.js的InstancedMesh来显示3D散点图(因此使用React three-fiber)。
几天后,我能够获得球体的3D散点图,并对其进行着色但是,我希望能够在鼠标单击/悬停时选择/高亮显示各个球体我遵循了一个使用onPointerOver
和onPointerOut
的简单教程,但这似乎不起作用,可能是因为它不适用于InstancedMesh对象。
看起来我需要使用raycaster,但我不清楚该怎么做。任何建议都会很有帮助。
设置步骤-
npx create-react-app demo
cd demo
npm install three
npm i @react-three/fiber
npm i @react-three/drei
显示不同颜色球体的当前代码-
应用程序.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;
IScatter.jsx
import * as THREE from "three";
import React, { useRef, useState } from "react";
import { useEffect } from "react";
import { DoubleSide } from "three";
const points = [ [1, 0, -1], [0, 1, -0.5], [0.5, 0.5, 0.5], [1,0.25,-1], [1,0,1], [0,1,0.5] ];
const content = ["Hello", "World", "Hello World", "Shoes", "Drone", "Foo Bar"];
const colors = [0,0,0,5,5,5];
const tempSphere = new THREE.Object3D();
const Spheres = () => {
const material = new THREE.MeshLambertMaterial({ opacity: 0.5, side: THREE.DoubleSide, transparent: true,});
const spheresGeometry = new THREE.SphereBufferGeometry(0.25, 15, 15);
const ref = useRef();
const [active, setActive] = useState(false);
const [hover, setHover] = useState(false);
useEffect(() => {
points.map(function (val, row) {
console.log(val, row);
tempSphere.position.set(val[0], val[1], val[2]);
tempSphere.updateMatrix();
ref.current.setMatrixAt(row, tempSphere.matrix);
ref.current.setColorAt(row, new THREE.Color(`hsl(${colors[row]*100}, 100%, 50%)`));
});
ref.current.instanceMatrix.needsUpdate = true;
// ref.current.instanceColor.needsUpdate = true;
});
return (
<instancedMesh onClick={() => {
setActive(!active);
}}
onPointerOver={() => {
setHover(true);
}}
onPointerOut={() => {
setHover(false);
}} ref={ref} rotation={[0,0,0]} args={[spheresGeometry, material, 15]} opacity={active?0.9:0.5} />
);
};
export default Spheres;
我想改变选定球体的不透明度,也许可以让它们变大一点,这样我们就知道它选择了什么。如果可能的话,我还想在选择球体时,显示与球体相关联的HTML内容,该内容与画布相邻,远离球体的散点图。
编辑:我本质上想实现这个-https://threejs.org/examples/webgl_instancing_raycast但是使用反应三纤维并且还显示了球体的相关含量。
我尽了最大努力,结果如下。
这是我的沙盒:https://codesandbox.io/s/relaxed-rgb-yb1v1v?file=/src/IScatter.jsx
首先,我制作了一个新的Scene.jsx
,因为Three Fiber显然不喜欢Canvas之外的某些引用或挂钩用法(记不清具体是什么)。
我的想法也是Scene
应该负责处理任何鼠标或相机事件,并且只将更改以某种方式传递给其他组件。
基本上App
变成了这样:
function App() {
return (
<div>
<Canvas style={{ width: "100%", height: "100vh" }}>
<Scene />
</Canvas>
</div>
);
}
所有旧的App
代码现在都位于Scene
中。
查看raycast示例代码,我们可以看到,为了使用raycaster
,我们需要。。。好raycaster instance
用于一个,而且还包括camera instance
和mouse position
。
为了用三根光纤获得camera instance
,我发现了这个SO问答,它指导我们使用useThree
钩子:
const {
camera,
gl: { domElement }
} = useThree();
现在这本身对我们没有太大帮助,因为useThree().camera
并不完全是";绑定";到您正在使用的OrbitControls
组件。为了解决这个问题,我们使用OrbitControls
组件的args
道具,afaik,将我们的相机绑定/传递给它:
<OrbitControls
enableZoom={true}
args={[camera, domElement]}
onChange={handleControls}
/>
您可能会注意到那里还有一个onChange
道具。每当摄像机旋转等改变时,事件都会触发,但由于某种未知原因,事件对象基本上不包含除";有些事情已经改变了";。
我希望事件对象包括相机信息或其他信息。不管怎样,因为它似乎是这样工作的,而且由于我们已经将camera
传递给了OrbitControls
组件,所以无论何时触发此更改事件,我们都知道camera
也已更新。我的第一本能当然是有这样的东西:
const [myCamera, setMyCamera] = useState(camera);
const handleControls = (e) => {
setMyCamera(camera);
};
所以我自己的相机状态,只要OrbitControls
发生变化,就可以设置任何相机的状态。这里的问题是,虽然这种方法有效,但像useEffect(() => { }, [myCamera]);
这样的东西永远不会因为另一个未知的原因而触发。
那么,当遇到useState+useEffect触发问题时,沮丧的React开发人员会怎么做呢?使";力状态改变";实现,以下是handleControls
当前的样子:
const [cameraSignal, setCameraSignal] = useState(0);
const handleControls = (e) => {
setCameraSignal((s) => s + 1);
};
然后我们将cameraSignal
传递给您的Spheres
组件:
同样,useThree().camera
确实更新得很好,我们只是没有办法对此做出反应,因为useEffect不会被直接更改触发,但有了这个破解的cameraSignal
实现,我们现在有了在相机更改时触发useEffect的方法。
现在我们解决了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);
};
}, []);
这就是求解的camera
和mouse
。我们可以使用相同的useThree
钩子获得raycaster
实例,所以让我们来了解实际的光线投射实现:
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]);
在这里,我们基本上只是使用全新的逻辑来遵循raycast示例代码,以获得mouse
和camera
数据。
在示例使用mesh
的地方,如:
const intersection = raycaster.intersectObject(mesh);
我们使用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;
});
这很奇怪,因为React Fiber组件是如何工作的。我认为您在这里的意图可能是初始化网格,但这基本上起到了渲染更新循环的作用。
我不得不做出另一个这样的裁判来保持改变后的颜色,或者说;渲染更新循环";会一直用初始颜色覆盖颜色:
const instanceColors=useRef(新数组(points.length).fill(红色));
然后使用光线投射,我不能直接使用setColorAt(因为上面的useEffect只会覆盖颜色),但我更新了颜色ref:
instanceColors.current[instanceId] = new THREE.Color().setHex(Math.random() * 0xffffff);
这一切都会导致InstancedMesh的光线投射和颜色变化,但如果你想从中获得更多性能等,我会考虑一些重构。
*额外注释
我遵循了一个使用onPointerOver和onPointerOut的简单教程但这似乎不起作用,也许是因为它不是为InstancedMesh对象。
我相信你是对的,InstancedMesh是一个不同的野兽,从某种意义上说,它并不是一个可以点击或悬停的明确的单个3D对象,实际上恰恰相反。为了使用InstancedMesh,可能需要手动完成所有初始化、更新以及相机和指针事件,但我不能确定,因为这是我第一次使用Three Fiber。😅它看起来有点乱,但我以前见过这样的东西,所以这样做不一定是最错误的。
编辑
我想我已经解决了前面提到的问题,请参阅这个沙箱的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++;
}
}
}
}, []);
通过查看光线投射函数可以明显看出这一点:
meshRef.current.setColorAt(
instanceId,
new THREE.Color().setHex(Math.random() * 0xffffff)
);
我们现在可以直接使用setColorAt,而不会在每个渲染循环中覆盖它。
此外,我会看看这里,看看鼠标和相机事件是否可以处理得更好。