在 Three.js,一种常用的交互方式是允许用户通过点击来选中场景中的三维物体。选中(或拾取)物体通常是通过使用射线投射(Raycasting)来实现的。射线投射是一种计算从一个点(如摄像机)发出,沿着特定方向(如鼠标点击的方向)的射线与场景中物体相交的过程。
1.什么是射线投射?
- 射线投射是3D图形中一种常见的技术,用于确定虚拟3D空间中的一条直线(射线)与物体的交点。在 Three.js 中,这可以用来检测用户的鼠标点击与场景中哪些物体相交。
2.射线投射的原理
1.射线的定义
- 射线在数学上通常定义为一个起点(原点)和一个方向。射线是单向的,从原点沿着特定方向无限延伸。
2.发射射线
- 在计算机图形学中,射线通常从观察者的视点(例如,一个虚拟摄像机)发射出来。在用户交互场景(例如使用鼠标点击屏幕)中,射线会从摄像机位置,沿着经过鼠标点击位置的方向发射。
3.检测交点
- 射线投射的核心是计算射线与场景中对象的交点。这包括确定射线是否与任何对象相交,以及在哪里相交。这通常通过数学公式和算法来完成,比如解算射线方程和物体(如三角形、球体等)的方程的交点。
4.处理交点信息
- 一旦计算出交点,就可以使用这些信息来进行各种操作,比如确定哪个物体被选中、计算光线在场景中的路径(如在光线追踪中)、处理物理碰撞等。
3.使用 Three.js 中的 Raycaster 实现选中物体的功能
1.创建场景
- 首先我们创建一个线框立方体。
js
let scene, camera, renderer, cube, cubeEdges, raycaster, mouse;
// 创建场景、相机和渲染器
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 添加立方体(用于射线投射检测)和立方体的线框(用于视觉展示)
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0, transparent: true });
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 线框立方体
const edges = new THREE.EdgesGeometry(geometry);
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00 });
cubeEdges = new THREE.LineSegments(edges, lineMaterial);
scene.add(cubeEdges);
// 添加光源
const light = new THREE.PointLight(0xffffff, 1, 500);
light.position.set(10, 0, 25);
scene.add(light);
// 设置相机位置
camera.position.y = 1;
camera.position.z = 5;
2.设置射线投射器
- THREE.Raycaster 是 Three.js 中实现射线投射的核心。首先,创建一个 Raycaster 对象和一个用于存储鼠标位置的二维向量。
js
// 初始化射线投射器和鼠标向量
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// 添加鼠标点击事件监听
window.addEventListener('click', onClick, false);
3.转换鼠标位置
- 当用户点击屏幕时,需要将鼠标的屏幕坐标转换为 Three.js 使用的标准化设备坐标(NDC)。这可以在鼠标事件监听器中完成。
js
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
拓展:坐标转换
1.屏幕坐标系
- 首先我们鼠标获取的坐标是基于屏幕坐标系的,屏幕坐标系的原点
(0,0)
在左上角,x轴向右,y轴向下。
2.three.js中的坐标系
在 Three.js 中,渲染几何体到屏幕上的最终步骤涉及将几何体的坐标从世界坐标转换到屏幕坐标。
- 1.世界坐标
首先,几何体在世界坐标系中被定位。这是一个三维空间,几何体的位置和方向是相对于全局原点的。
- 2.视图坐标或摄像机坐标
接下来,几何体的世界坐标被转换为视图坐标。这个过程通过"视图矩阵"(View Matrix,通常由摄像机位置和方向定义)来完成,结果是几何体的位置现在是相对于摄像机的。
- 3.裁剪坐标
视图坐标接着被转换为裁剪坐标。这一步涉及"投影矩阵"(Projection Matrix),它将三维空间投影到一个标准化的立方体内,范围在每个轴上都是 -1 到 1。这个过程决定了哪些部分的几何体会被裁剪掉,因为它们不在摄像机的可视范围内。
- 4.标准化设备坐标(NDC)
裁剪坐标被进一步转换为 NDC。在这个阶段,所有可见的几何体都位于一个立方体内,其边界在每个轴上从 -1 到 1。
- 5.屏幕坐标
最后,NDC 被转换为屏幕坐标。这个过程涉及将 NDC 的范围映射到实际的屏幕尺寸,包括处理像素密度和屏幕的宽高比。这是最终的坐标系,用于在屏幕上渲染几何体。
在整个渲染管线中,Three.js 内部管理这些变换。开发者通常只需要关心世界坐标系中的物体位置,而 Three.js 框架则负责将它们正确地转换和渲染到屏幕上。
3.上面转换式子的意思
这两个计算公式用于将鼠标的屏幕坐标转换为 Three.js 中的标准化设备坐标(Normalized Device Coordinates,NDC)。在 NDC 中,坐标系定义在一个 [-1, 1] x [-1, 1] 的平面上,其中屏幕的中心是 (0, 0)
,左上角是 (-1, 1)
,右下角是 (1, -1)
。这种坐标系统在 3D 渲染中非常有用,因为它独立于实际屏幕尺寸,可以在不同大小和分辨率的屏幕上保持一致性。
1.X
坐标的转换
js
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
-
event.clientX / window.innerWidth
:这部分计算鼠标位置相对于窗口宽度的比例。event.clientX 是鼠标相对于屏幕左边缘的水平位置,window.innerWidth 是视口(viewport)的宽度。这个比例在屏幕最左边是 0,在最右边是 1。 -
* 2
:将这个比例扩展到 0 到 2 的范围。 -
-1
:将范围进一步调整到 -1 到 1,这符合 NDC 的要求。
2.Y
坐标的转换
js
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
-
event.clientY / window.innerHeight
:计算鼠标位置相对于窗口高度的比例。event.clientY
是鼠标相对于屏幕顶部的垂直位置。 -
* 2
和+ 1
:与 X 坐标类似,将比例转换到-1
到1
的范围。 -
-
符号:因为屏幕的 Y 坐标系是从上到下增加的(顶部是0
,底部是window.innerHeight
),这与 NDC 中的 Y 轴(顶部是1
,底部是-1
)相反,所以需要添加负号来反转 Y 轴。
4.检测物体和射线的交集
- 在用户点击事件的处理函数中,使用射线投射器来检测与场景中物体的交集。
js
// 更新射线投射方向
raycaster.setFromCamera(mouse, camera);
// 获取射线和物体的交集
const intersects = raycaster.intersectObject(cube);
if (intersects.length > 0) {
// 改变线框颜色
cubeEdges.material.color.set(0xff0000);
} else {
// 还原线框颜色
cubeEdges.material.color.set(0x00ff00);
}
5.处理选中的物体
- 在
intersects
数组中,你会得到所有与射线相交的物体。这个数组是按距离摄像机从近到远排序的,所以intersects[0]
是最近的物体。你可以在这里添加自己的逻辑来处理这些物体,比如更改它们的颜色、显示信息标签等。
js
if (intersects.length > 0) {
// 改变线框颜色
cubeEdges.material.color.set(0xff0000);
} else {
// 还原线框颜色
cubeEdges.material.color.set(0x00ff00);
}
6.完整可运行代码用例
html
<!DOCTYPE html>
<html>
<head>
<title>Three.js Click Raycasting Example</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script src="https://threejs.org/build/three.js"></script>
<script>
let scene, camera, renderer, cube, cubeEdges, raycaster, mouse;
function init() {
// 创建场景、相机和渲染器
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 添加立方体(用于射线投射检测)和立方体的线框(用于视觉展示)
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, opacity: 0, transparent: true });
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 线框立方体
const edges = new THREE.EdgesGeometry(geometry);
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00 });
cubeEdges = new THREE.LineSegments(edges, lineMaterial);
scene.add(cubeEdges);
// 添加光源
const light = new THREE.PointLight(0xffffff, 1, 500);
light.position.set(10, 0, 25);
scene.add(light);
// 设置相机位置
camera.position.y = 1;
camera.position.z = 5;
// 初始化射线投射器和鼠标向量
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// 添加鼠标点击事件监听
window.addEventListener('click', onClick, false);
}
function onClick(event) {
// 将鼠标位置转换为NDC坐标
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 更新射线投射方向
raycaster.setFromCamera(mouse, camera);
// 获取射线和物体的交集
const intersects = raycaster.intersectObject(cube);
if (intersects.length > 0) {
// 改变线框颜色
cubeEdges.material.color.set(0xff0000);
} else {
// 还原线框颜色
cubeEdges.material.color.set(0x00ff00);
}
}
function animate() {
requestAnimationFrame(animate);
// 渲染场景
renderer.render(scene, camera);
}
init();
animate();
</script>
</body>
</html>
7.场景中有多个物体,怎么选中不同的物体设置不同的颜色?
多个物体的选中跟单个物体的选中逻辑是一样的。
html
<!DOCTYPE html>
<html>
<head>
<title>Three.js Click Raycasting Example</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script src="https://threejs.org/build/three.js"></script>
<script>
let scene, camera, renderer, cubes = [], raycaster, mouse;
const position = [
[-4, 1, 1],
[-2, 1, 1],
[0, 1, 1],
[2, 1, 1],
[4, 1, 1]
]
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 添加多个立方体到场景
for (let i = 0; i < 5; i++) {
let geometry = new THREE.BoxGeometry();
let material = new THREE.MeshBasicMaterial({ opacity: 1, transparent: true, color: 0x00ff00 });
let cube = new THREE.Mesh(geometry, material);
cube.position.set(...position[i]);
scene.add(cube);
// 将线框立方体存储在数组中
cubes.push(cube);
}
// 设置相机位置
camera.position.y = 1;
camera.position.z = 5;
light = new THREE.PointLight(0xffffff, 1, 500);
light.position.set(10, 0, 25);
scene.add(light);
raycaster = new THREE.Raycaster();
raycaster.near = 0.1;
raycaster.far = 500;
mouse = new THREE.Vector2();
window.addEventListener('click', onClick, false);
}
function onClick(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 检测射线与所有立方体的交集
let intersects = raycaster.intersectObjects(cubes);
// 还原所有线框立方体的颜色
cubes.forEach(c => c.material.color.set(0x00ff00));
if (intersects.length > 0) {
// 改变选中线框立方体的颜色
intersects[0].object.material.color.set(0xff0000);
}
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
init();
animate();
</script>
</body>
</html>
4.总结
通过上述的讲解,你应该能够理解并实现在 Three.js 中使用射线投射来选中和交互场景中的物体。这种技术在开发三维应用程序、游戏或交互式可视化时非常有用。多学,多写,多理解,让我们一起变得更强!