06. 数学工具与空间交互
在 3D 开发中,数学不仅仅是矩阵变换,更是解决复杂空间问题和交互优化的钥匙。
1. 深入四元数 (Quaternion)
你可能习惯用欧拉角 (rotation.x/y/z) 来控制旋转,但在进阶开发(尤其是智驾/飞行模拟)中,四元数是必须跨过的坎。
为什么要用四元数?
- 避免万向节死锁 (Gimbal Lock):当一个轴旋转 90 度后,另外两个轴会重合,导致失去一个自由度。
- 平滑插值 (Slerp):在两个旋转状态之间做动画,欧拉角的插值路径往往很诡异,而四元数的球面插值 (
slerp) 是沿着最短路径平滑过渡的。
实战:车辆姿态控制
假设后端发来一个车辆的旋转矩阵或四元数数据,如何应用?
// 1. 设置四元数
const quaternion = new THREE.Quaternion(x, y, z, w);
carMesh.quaternion.copy(quaternion);
// 2. 旋转插值 (平滑过渡)
// 比如这一帧的目标是 targetQuaternion
carMesh.quaternion.slerp(targetQuaternion, 0.1); // 0.1 是 t (0~1)
常用操作
q1.multiply(q2): 组合旋转(先做 q2,再做 q1)。q.setFromAxisAngle(axis, angle): 绕任意轴旋转。q.setFromUnitVectors(vFrom, vTo): 计算将向量 vFrom 转到 vTo 所需的旋转(非常有用,例如让飞船头部永远朝向目标)。
2. 射线检测 (Raycasting) 优化
Raycaster 是拾取物体、点击交互的核心。但遍历场景中所有物体进行碰撞检测极其消耗 CPU。
性能瓶颈
raycaster.intersectObjects(scene.children) 会:
- 遍历所有子对象。
- 对每个对象计算包围盒相交。
- (如果是 Mesh) 遍历所有三角形面片进行数学求交。
如果场景有 1 万个物体,或者一个物体有 10 万个面,每一帧做射线检测会让 FPS 暴跌。
优化方案:BVH (Bounding Volume Hierarchy)
BVH 是一种空间索引结构,它将几何体递归地分割成层级包围盒。
- 查询时,如果射线没碰到大包围盒,就根本不需要去检测里面的几千个小三角形。
工具库:three-mesh-bvh 这是 Three.js 社区事实标准的优化库。
import {
computeBoundsTree,
disposeBoundsTree,
acceleratedRaycast,
} from "three-mesh-bvh";
// 1. 扩展 BufferGeometry 原型
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
THREE.Mesh.prototype.raycast = acceleratedRaycast; // 替换原生 raycast
// 2. 为复杂几何体生成 BVH 索引
complexGeometry.computeBoundsTree();
// 3. 正常使用 Raycaster
// 速度提升可达 100x - 1000x
const intersects = raycaster.intersectObject(complexMesh);
3. 屏幕坐标转世界坐标
在智驾地图中,常需要把鼠标点击的屏幕位置转换到地图平面上。
// 标准转换流程
function getPointOnGround(event, camera, groundPlaneZ = 0) {
const vec = new THREE.Vector3();
const pos = new THREE.Vector3();
// 1. 归一化设备坐标 (NDC)
vec.set(
(event.clientX / window.innerWidth) * 2 - 1,
-(event.clientY / window.innerHeight) * 2 + 1,
0.5,
);
// 2. 反投影
vec.unproject(camera);
// 3. 射线与平面求交 (数学解法,比 Raycaster 快)
// 射线方程: P = origin + t * direction
vec.sub(camera.position).normalize(); // 得到方向向量
const distance = (groundPlaneZ - camera.position.y) / vec.y;
pos.copy(camera.position).add(vec.multiplyScalar(distance));
return pos;
}
总结
- 旋转尽量用 Quaternion。
- 复杂几何体交互必上 BVH。
- 简单的平面点击计算直接用数学公式,别用 Raycaster。
思考与练习
- 在什么情况下,使用欧拉角(Euler Angles)进行旋转插值会出现“异常的抖动翻转”?四元数(Quaternion)的
slerp方法是如何解决这个问题的? - 如果你要实现一个 FPS 游戏中的子弹射击判定,场景里有极多复杂的建筑,不使用 BVH 可能会导致怎样的性能问题?简述 BVH 的核心加速原理。
- 把屏幕点击位置(2D)转换为 3D 世界中地面上的点(Z=0),如果不使用
Raycaster射线检测,纯靠数学几何公式应该如何推导计算?