06 颜色写入与绘制顺序:看不见的物体也能挡光

前置知识:第 04 章的深度测试、第 05 章的写入组合、第 02 章的顺序相关混合。
本章示例:不可见遮挡体与叠色 · 源码。
预期画面:左侧初始有方形空洞,但看不到方形物体的颜色;隐藏物体后空洞消失且绘制调用数从 4 变 3。右侧交换透明层绘制顺序时,重叠颜色变化。

先看现象

左侧橙色光斑在后方,前方有一块方形代理几何。代理初始 visible=true, colorWrite=false, depthWrite=true, depthTest=true。代理不改变颜色缓冲,却先留下深度;随后光斑的片元在方形区域被深度测试挡掉,于是背景透出,像一个方形空洞。

把代理的 colorWrite 打开,方形颜色出现,遮挡仍在。只关闭 depthWrite,方形不可见且也不再挡光。把 visible 关闭,代理完全不提交,本例的 renderer.info.render.calls 从 4 降到 3。colorWrite: false 不是对象剔除,也不代表免费。

右侧两片透明层都保持 depthWrite: false。交换 renderOrder 后,重叠区域的颜色发生变化,因为普通 Alpha 混合通常不满足交换律。

1. colorWrite 控制哪一份输出

const proxyMaterial = new THREE.MeshBasicMaterial( {
  colorWrite: false,
  depthTest: true,
  depthWrite: true
} );
const proxy = new THREE.Mesh( proxyGeometry, proxyMaterial );
proxy.renderOrder = - 1;
scene.add( proxy );

colorWrite 对应颜色缓冲的写掩码;设为 false 后,该材质的片元不能写入 RGBA 颜色缓冲,但仍可执行深度测试和写入。材质默认三项分别是 true。这里让代理先于后方透明光斑绘制,才有“留下深度供光斑比较”的效果。

用途包括:用简化代理建立遮挡边界、某些多通道渲染的深度预写、让后续内容只在指定深度关系下出现。它并不自动生成正确代理:代理轮廓、位置与真实目标不匹配时会出现不自然的空洞。若要只在指定区域画内容,模板缓冲(stencil)有时比单纯深度更合适。

2. visible 与 colorWrite 的区别

object.visible = false 让对象不进入本次常规场景渲染提交;该对象既不画颜色,也不写深度。material.colorWrite = false 仍提交对象,仍可写深度,仍有绘制调用与可能的顶点/片元工作。示例的调用数包含左侧光斑与代理、右侧两片透明层,所以是 4;隐藏代理后为 3。这个数只是当前示例中 renderer.info.render.calls 的结果,不是一般场景的固定公式。

它也说明“看不见”有多种原因:颜色写入关闭、Alpha 为 0、被深度挡住、在视锥外、对象不提交,都可能看不见,成本却不相同。第 08 章会专门区分物体遮挡与遮挡剔除。

3. renderOrder 只在适用的绘制组内调整顺序

warm.renderOrder = 2; // 暖色透明层后画
cool.renderOrder = 1;

warm.material.depthWrite = false;
cool.material.depthWrite = false;

在本例中,两层都是透明对象,renderOrder 较小者先提交。两层仍要经过 depthTest;改变顺序不等于关闭测试,也不更改几何 z 坐标。透明混合的结果则取决于先后:例如普通混合 C = aS + (1-a)D 连续执行两次,交换两个不同颜色的源,最终重叠颜色通常不同。

Three.js r174 的 WebGL 渲染列表把不透明、透射、透明对象分组并分别排序,再按组渲染。renderOrder 可以影响同组对象的先后,却不能用一个极小值让透明对象越过不透明组。父级 Group.renderOrder 也可能改变排序分组与子对象的排序结果;排错时检查父级设置。对于一个 THREE.Points,renderOrder 作用于整个对象,不能逐点排序。后续会专门分析大批量透明粒子的排序边界。

为什么代理要先画

本例的代理是不透明组,光斑是透明组。不透明组先渲染,因此代理深度能挡住光斑;代理自身 renderOrder=-1 用来清楚表达它应早于同组其他对象。若把代理做成透明材质,仍需确认它与其他透明对象的相对顺序以及深度状态。已经画进颜色缓冲的东西,不会因为后来的不可见代理写深度而被擦掉。

4. 选择参数的简表

意图 常见组合 要检查的边界
实体表面遮挡后方 测试、深度写入、颜色写入都开启 几何位置与材质透明设置
软烟被墙挡住但彼此叠色 depthTest=true, depthWrite=false, colorWrite=true 透明层顺序及 overdraw
不显示代理颜色、只留下遮挡深度 colorWrite=false, depthWrite=true, depthTest=true 代理必须先提交,轮廓要合理
故意穿墙显示提示 depthTest=false, depthWrite=false 后画内容和覆盖范围
同组透明层指定先后 不同的 renderOrder 只按对象排序,不能逐片元或逐粒子排序

常见问题

renderOrder 能强制烟雾永远正确吗? 不能。它只能指定对象级提交顺序。相交的烟片在不同屏幕位置可能需要不同顺序;单个 Points 内还有多个点。

colorWrite=false 会省下绘制调用吗? 不会。示例中代理仍贡献一次调用;若只是要完全跳过对象,使用 visible=false 或真正的对象级剔除策略。

深度代理等于 Occlusion Culling 吗? 不是。它制造视觉遮挡,后面的对象仍可能被提交、变换和光栅化;对象级遮挡剔除要另行判断哪些对象可以不提交。第 08 章会深入讲。

继续做第 06 章练习。参考:Material.colorWrite、Object3D.renderOrder。