04 深度测试:谁能占据这个像素

前置知识:第 01 章的片元与深度缓冲、第 02 章的透明混合。
本章示例:墙与光斑 · 源码。
预期画面:默认状态下墙遮住后方光斑;关闭 depthTest 后,光斑覆盖墙面。

先看现象

示例中相机位于正 z 方向,墙在 z=0,光斑初始在 z=-0.35。墙属于不透明绘制组,先写入颜色和深度;光斑是透明平面,随后绘制。默认 depthTest: true,墙内的光斑片元不能通过测试,但墙外没有更近的墙深度,因此还能看到光晕。关闭测试,整个光斑就像贴在墙前。

把光斑移到 z=+0.35,即使保留测试,它也能通过墙面区域。这里比较的是每个候选片元的深度,不是“整件物体可见/不可见”的开关。一个平面可以只有一部分被挡。

1. 深度缓冲与测试的时机

每个屏幕采样位置保存一个已有深度。通常清除深度缓冲后,远平面的值为 1;投影后靠近相机的片元一般有更小的窗口深度。渲染器提交一个片元时,用它的深度和已有值按 depthFunc 比较;测试失败就不能更新该采样位置的颜色和深度。透明混合只发生在片元通过相关测试之后。

这解释了为什么“先画”不必意味着“最后看见”:不透明墙即使早画,也能用留下的深度挡住后来绘制的透明光斑。反过来,禁用测试会让后来绘制的对象无视这个记录。

Three.js 中对应的最小设置是:

const glow = new THREE.Mesh(
  new THREE.PlaneGeometry( 3.2, 3.2 ),
  new THREE.MeshBasicMaterial( {
    map: glowTexture,
    transparent: true,
    depthTest: true,
    depthWrite: false
  } )
);
glow.position.z = - 0.35;
scene.add( glow );

这段代码运行在 JavaScript 场景初始化处。z 是世界坐标,示例相机朝负 z 方向看,所以负 z 位于墙后。depthWrite: false 让光斑不改写深度,详情见第 05 章。

2. depthFunc 究竟比较什么

Material.depthFunc 默认为 THREE.LessEqualDepth,意为候选深度小于或等于已有深度时通过。示例还提供:

函数 规则 在本例中的结果
LessEqualDepth 候选 ≤ 已有 墙后光斑在墙内被挡,墙前光斑可见
AlwaysDepth 不比较,始终通过 只要光斑提交,就可能画在墙上;但深度测试仍处于开启状态
GreaterDepth 候选 > 已有 墙后光斑可能只在墙的投影内出现;墙外已有值通常是清除值 1,候选值很难比它更大

GreaterDepth 的效果不是“倒转整个场景遮挡”的通用按钮。它依赖缓冲区里原先有什么值;若使用反向深度、不同清除值或其他先画对象,结果会变化。本例只改变光斑材质的比较函数,不改变墙和深度缓冲的约定。

关闭 depthTest 时,depthFunc 没有机会发挥作用。Three.js r174 的材质文档还明确指出:禁用深度测试时,深度写入也被隐式禁用;单把 depthWrite 设为 true 无法让它在测试关闭时留下深度。第 05 章会用烟片验证两者的组合。

3. 参数变化与实际用途

正确遮挡的烟、火、雨雪。 通常保留 depthTest: true,这样粒子可以被墙和地形挡住。烟雾与地面相交处可能仍有硬边;后续软粒子阶段再处理相交过渡。

屏幕提示或透视指示。 有些游戏需要“即使在墙后也能看见”的轮廓或标记,可有意识地关闭测试,或在专门渲染通道中使用 AlwaysDepth。此时应把它当成视觉设计,而非几何遮挡的修复方法。

已知深度区域的特效。 使用非默认 depthFunc 能让片元只在某些深度关系下显示,但要确保相关物体已经写入深度,并明确清除值、相机及绘制组。真实项目常配合深度纹理或自定义通道,而不是只切换一个枚举值。

常见问题

depthTest: false 后为什么还受前后顺序影响? 它只是忽略深度;多个通过的片元仍按提交顺序写颜色或混合。renderOrder 与透明排序见第 06 章。

为什么物体明明在墙后,墙外部分仍可见? 墙只覆盖一部分屏幕。墙外采样位置没有墙留下的深度,候选片元可通过默认比较。

为什么不能用 depthTest: false 修烟雾裁切? 它会让烟雾穿过所有不透明物体。透明烟片相互裁切通常需要检查 depthWrite、透明排序和贴图 Alpha;见下一章。

继续做第 04 章练习。参考:Material 深度属性。