02. 渲染顺序与深度测试详解
在 3D 渲染中,"哪个物体挡住哪个物体" 是一个看似简单实则充满陷阱的问题。Three.js 默认帮我们处理了大部分情况,但在处理透明物体、重叠平面或特殊特效时,我们需要手动接管控制权。
1. 深度缓冲 (Depth Buffer) 原理
深度缓冲 (Z-buffer) 是一张与屏幕分辨率相同的灰度图,存储了每个像素点当前的深度值(通常是 0.0 到 1.0,0 代表近裁剪面,1 代表远裁剪面)。
核心逻辑
当 GPU 准备绘制一个像素时,它会执行 深度测试 (Depth Test):
- 比较:将当前片段的深度值与 Depth Buffer 中对应位置的值进行比较。
- 通过:如果当前片段更“近”(值更小,取决于 depthFunc),则通过测试,绘制颜色,并(可选)更新 Depth Buffer。
- 失败:如果当前片段更“远”,则丢弃该片段,不进行绘制。
2. 关键材质属性
Three.js 的 Material 提供了三个关键属性来控制这一过程:
depthTest (Boolean, 默认 true)
是否开启深度测试。
true: 只有当物体比当前缓存的像素更近时才显示。false: 无视深度,永远绘制(像 X 光一样穿透墙壁)。- 应用场景:HUD(平视显示器)、Gizmo(坐标轴)、被遮挡时显示轮廓。
depthWrite (Boolean, 默认 true)
绘制时是否将自己的深度值写入 Depth Buffer。
true: 绘制后,会挡住后面渲染的物体。false: 绘制后,不留下深度记录,后面的物体依然可以根据之前的深度判断是否覆盖它。- 应用场景:透明物体(通常设为 false)、粒子系统、光晕。
depthFunc
比较函数,默认是 LessEqualDepth (<=)。
3. 渲染顺序 (Render Order)
当深度测试无法解决问题(例如透明物体),或者我们需要人为干预绘制顺序时,Three.js 的渲染顺序机制就非常重要。
Three.js 的默认渲染顺序:
- 不透明物体 (Opaque):从前往后排序(Front-to-back)。这是一种优化,先画近的,利用深度测试尽早剔除被遮挡的像素。
- 透明物体 (Transparent):从后往前排序(Back-to-front)。这是必须的,因为透明混合依赖于背景颜色已经存在。
renderOrder 属性
每个 Object3D 都有 renderOrder 属性,默认为 0。
- 规则:数值越小越先画,数值越大越后画。
- 注意:
renderOrder的优先级高于透明/不透明的自动排序,但仅限于同一个 RenderList(渲染列表)内。 - 跨越限制:它不能改变不透明物体先于透明物体绘制的大原则(除非手动清除深度缓存或分层渲染)。
实战技巧: 如果你想让一个物体永远显示在最上层(例如 3D 标记):
material.depthTest = false;mesh.renderOrder = 999;(确保它最后被提交,盖在所有东西上面)
4. 透明物体 (Transparent) 的坑
当 material.transparent = true 时:
- Three.js 会自动将
depthWrite设为false(通常情况下)。 - 物体会按从远到近排序。
常见问题:透明物体互相遮挡错乱 如果两个复杂的透明物体相交,或者一个凹陷的透明物体自己遮挡自己,简单的重心排序就会失效,导致看起来有的面在前,有的面在后,闪烁或穿插。
解决方案:
- Alpha Test (Alpha Cutoff): 如果是类似栅栏、草叶这种“要么全透要么不透”的贴图,不要用
transparent = true,而是用alphaTest。这样依然可以写入深度,避免排序问题。material.transparent = false;
material.alphaTest = 0.5; // 低于0.5的像素直接丢弃 - 多Pass渲染: 极其复杂的透明关系可能需要分层渲染(先画内部,再画外部)。
5. 解决 Z-Fighting (深度冲突)
当两个平面几乎完全重合(例如墙上的海报、地上的贴花)时,由于深度精度的限制,GPU 无法判断谁在前谁在后,会导致画面闪烁。
解决方案:Polygon Offset 让 GPU 在计算深度时,人为地给某个物体加一点点偏移量。
// 让海报稍微“浮”在墙面上
posterMaterial.polygonOffset = true;
posterMaterial.polygonOffsetFactor = -1; // 负值表示拉近摄像机
posterMaterial.polygonOffsetUnits = -1;
polygonOffsetFactor: 基于斜率的缩放因子。polygonOffsetUnits: 恒定的深度偏移单位。
6. 总结表
| 场景 | depthTest | depthWrite | renderOrder | polygonOffset |
|---|---|---|---|---|
| 普通实心物体 | true | true | 0 | false |
| 玻璃/半透明 | true | false | 0 (自动排序) | false |
| 始终置顶的UI/Gizmo | false | false | 999 | false |
| 墙上的贴花 (Decal) | true | true | 1 | true (factor < 0) |
| 粒子特效 | true | false | 0 | false |
思考与练习
- 尝试创建一个场景,包含一个不透明的立方体,包裹着一个半透明的球体,球体内部还有一个不透明的小立方体。调整参数,使得你能透过球体看到里面的小立方体,同时球体也能正确遮挡后面的大立方体背面。
- 为什么在 Three.js 中,不透明物体是从前往后排序渲染的,而透明物体却是从后往前排序渲染的?分别是为了解决什么问题?
- 如果
depthWrite = false,但depthTest = true,这个物体会怎样显示?它会遮挡后面的物体吗?它会被前面的物体遮挡吗? - 如果遇到严重的透明物体自遮挡问题(如一个复杂的玻璃杯),除了
alphaTest之外,还可以尝试哪些进阶方案(如背面与正面分 Pass 渲染)?请简述思路。