three.js射线拾取点击位置与屏幕坐标映射
three.js小白的学习之路。
在使用three.js搭建可视化场景的时候,不可避免的要添加点击事件,需要用到three.js提供的Raycaster类。
在使用过程中,一般是给WebGLRender的DOM监听click点击事件:
const renderer = new Three.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.domElement.addEventListener("click", (evt) => {// ………………
});
然后在里面声明一个Raycaster,然后经过一个坐标转换,就可以监听到鼠标点击的是哪一个模型了:
renderer.domElement.addEventListener("click", (evt) => {const x = (evt.offsetX / window.innerWidth) * 2 - 1;const y = -(evt.offsetY / window.innerHeight) * 2 + 1;const raycaster = new Three.Raycaster();raycaster.setFromCamera(new Three.Vector2(x, y), camera);const intersects = raycaster.intersectObjects(scene.children);console.log(intersects);intersects.forEach((item) => {item.object.material.color.set(0xffff00);});
});
核心就在下面两行代码上:
const x = (evt.offsetX / window.innerWidth) * 2 - 1;const y = -(evt.offsetY / window.innerHeight) * 2 + 1;
两行代码其实就是屏幕坐标到二维坐标的一个转换。先看一下setFromCamera函数针对这个变量的解释:
意思是:鼠标的二维坐标,以归一化设备坐标(NDC)表示——X和Y分量应在-1和1之间。
归一化之后的坐标大概就是下面的示意图,X轴正方向向右,左侧是-1,右侧是1;Y轴正方向是向上,上方是1,下方是-1:
而原本的屏幕坐标是下面这样的,X轴正方向向右,Y轴正方向向下:
当我们点击屏幕上的一个点时,可以获取到这个点的坐标,也就是offsetX和offsetY这两个分量:
将上述两个分量从鼠标的二维坐标转换到NDC,我们只需要将其归一化,值变为[0, 1],在乘以2,值变为[0, 2],在减去1,值变为[-1, 1],就能达到我们的目的。由于Y轴两个坐标系的正方向是相反的,所以最后的结果需要取反:
const x = (evt.offsetX / window.innerWidth) * 2 - 1;
const y = -((evt.offsetX / window.innerWidth) * 2 - 1);
也就是:
const x = (evt.offsetX / window.innerWidth) * 2 - 1;const y = -(evt.offsetY / window.innerHeight) * 2 + 1;
这样就可以正常获取到鼠标点击了。
下面写个小案例,将场景里的方块进行点击变色:
// 生成一些立方体
const generateBox = (color: string, position: Three.Vector3) => {const geo = new Three.BoxGeometry(50, 50, 50);const material = new Three.MeshLambertMaterial({color: new Three.Color(color),});const box = new Three.Mesh(geo, material);box.position.copy(position.clone());return box;
};
const box1 = generateBox("blue", new Three.Vector3(0, 0, 0));
const box2 = generateBox("green", new Three.Vector3(0, 0, 100));
const box3 = generateBox("red", new Three.Vector3(100, 0, 0));
const box4 = generateBox("orange", new Three.Vector3(100, 0, 100));
scene.add(box1, box2, box3, box4);const renderer = new Three.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight); // 占满全屏renderer.domElement.addEventListener("click", (evt) => {const x = (evt.offsetX / window.innerWidth) * 2 - 1;const y = -(evt.offsetY / window.innerHeight) * 2 + 1;const raycaster = new Three.Raycaster();raycaster.setFromCamera(new Three.Vector2(x, y), camera);const intersects = raycaster.intersectObjects(scene.children);console.log(intersects);if (intersects.length > 0) {intersects[0].object.material.color.set(0xffffff);}
});
使用intersectObjects方法计算射线是否与模型相交,该方法返回相交的模型数组。如果数组长度大于0,只取第一个相交的模型让其材质变为白色(第一个相交的是距离屏幕最近的)。
效果如下:
可以看到即使点击过程中有两个模型,我们取得是第一个,所以最先点到的模型材质会发生变化。完美实现。