进阶知识
在掌握了基础的场景搭建后,Web3D 真正的商业价值往往体现在以下进阶交互和特效上:
1. 光线投射 (Raycaster) 与鼠标拾取
在 3D 世界中,用户的鼠标点击是在 2D 的屏幕屏幕坐标系上发生的,如何判断鼠标点击到了哪个 3D 物体? 这就需要用到光线投射 (Raycaster):
- 原理与底层源码机制:将鼠标的屏幕 2D 坐标(clientX, clientY)转换为归一化设备坐标 (NDC)。然后根据相机的位置和方向,从相机发出一条射线,穿过这个 NDC 坐标。在 Three.js 底层源码(如
Mesh.prototype.raycast)中,相交检测分为“粗筛”和“精算”两步:- 粗筛(Bounding Volume 剔除):首先将射线与物体的包围球 (BoundingSphere) 进行相交测试。如果不相交,直接剔除(Early Return);如果相交,再测试包围盒 (BoundingBox)。这一步以极低的计算成本过滤了大部分不可能相交的物体。
- 精算(Ray-Triangle Intersection):对于通过粗筛的物体,遍历其几何体(BufferGeometry)的顶点数据,提取出每一个三角形面。底层调用
Ray.intersectTriangle,使用经典的 Möller–Trumbore 算法 计算射线与三维三角形的交点(通过计算重心坐标系来判断交点是否在三角形内部)。 - 数据封装:计算出交点后,根据重心坐标插值计算出碰撞点的 UV 坐标、法线(Normal),并按距离相机的远近排序返回给开发者。
- 性能问题:Raycaster 是在 CPU 中通过遍历几何体的顶点来计算交叉的。如果场景极其庞大(几百万个顶点),调用一次 Raycaster 可能会严重卡死主线程。
- 优化方案:利用 BVH (Bounding Volume Hierarchy 层次包围盒) 预先建立空间索引,将 O(N) 的遍历复杂度降低为 O(logN);或者利用 GPU 颜色拾取(为每个物体赋予唯一的颜色并渲染到离屏缓冲区,通过读取点击位置的像素颜色来反推物体,效率极高)。
代码示例:基础射线拾取
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener("click", (event) => {
// 1. 将鼠标坐标转化为 NDC 标准设备坐标 [-1, 1]
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // 注意 Y 轴是反的
// 2. 通过相机和鼠标位置更新射线
raycaster.setFromCamera(mouse, camera);
// 3. 计算相交物体(参数 true 表示递归检测子节点)
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
// 拿到的第一个就是离相机最近的物体
const firstObject = intersects[0].object;
firstObject.material.color.set(0xff0000); // 变红
}
});
2. 后期处理 (Post-Processing)
如果你觉得原生渲染出来的场景干瘪、不真实,这时候就需要加上后期处理(类似用 PS 给照片加滤镜)。
- 原理:不把场景直接渲染到屏幕上,而是渲染到一个离屏的 帧缓冲区 (Framebuffer),将其作为一张 2D 纹理。然后通过编写自定义的 Shader,对这张纹理进行二次处理,最后再绘制到屏幕上。
- 常见特效:
- Bloom (辉光/泛光):让发光的物体产生周围的光晕,赛博朋克风格必备。
- SSAO (屏幕空间环境光遮蔽):通过深度图计算暗角的阴影,极大增强模型的立体感。
- FXAA/SMAA (抗锯齿):通过图像边缘检测算法平滑锯齿。
- DOF (景深):模拟单反相机的近实远虚效果。
代码示例:添加 Bloom 辉光特效
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
// 1. 初始化 Composer
const composer = new EffectComposer(renderer);
// 2. 添加基础渲染通道(把场景原本的样子画下来)
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 3. 添加 Bloom 辉光通道
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // 强度
0.4, // 半径
0.85, // 阈值(亮度超过这个值的物体才会发光)
);
composer.addPass(bloomPass);
// 4. 替换原有的 renderer.render
function animate() {
requestAnimationFrame(animate);
// renderer.render(scene, camera); // 注释掉原生渲染
composer.render(); // 使用 Composer 渲染
}
3. 粒子系统 (Particle System)
用于模拟雨雪、烟雾、火焰、星空、爆炸等极其细碎且数量庞大的效果。
- Points 材质:Three.js 提供了
THREE.Points和THREE.PointsMaterial。在 WebGL 底层,它利用了gl.POINTS原语,每一个顶点都会被渲染成一个始终朝向相机的正方形精灵图 (Sprite)。 - 性能瓶颈:如果用 CPU 在
requestAnimationFrame每一帧里去更新 100 万个粒子的position,绝对会卡死。 - 优化:将粒子的初始状态(位置、速度、颜色、生命周期)写入 BufferAttribute,然后在 Vertex Shader (顶点着色器) 中利用内置的
time变量来计算它们当前的运动轨迹,彻底解放 CPU。
代码示例:生成 10 万个星空粒子
// 1. 创建空的缓冲几何体
const geometry = new THREE.BufferGeometry();
const particleCount = 100000;
// 2. 准备一维数组存储坐标 (x, y, z)
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount * 3; i++) {
// 在 -500 到 500 的空间内随机散布
positions[i] = (Math.random() - 0.5) * 1000;
}
// 3. 将数据作为 position 属性写入几何体
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
// 4. 创建点材质
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 2, // 粒子大小
sizeAttenuation: true, // 近大远小
});
// 5. 组装为 Points 并加入场景
const stars = new THREE.Points(geometry, material);
scene.add(stars);