07 透明对象排序:一个顺序无法照顾每个像素
前置知识:第 02 章的普通与加法混合、第 05 章的透明深度写入、第 06 章的
renderOrder。
本章示例:相交烟幕 · 源码。
预期画面:红、蓝烟幕在空间中交叉;旋转相机或强制交换对象顺序时,重叠区颜色变化。开启深度写入又会出现硬裁。
先看现象
打开示例,保持“自动:按对象中心”和普通 Alpha 混合。把相机方位角从负值拖到正值,红、蓝烟幕的中心前后关系会变化,重叠区颜色也会变化。再选择“强制红色后画”或“强制蓝色后画”:你能控制整片烟幕谁最后混合,却不能让红色在交叉线一侧先画、另一侧后画。
这不是参数遗漏。两个平面相交时,可能在屏幕左边红色片元更近,右边蓝色片元更近;单一对象顺序不可能同时满足两处。示例的细边框用于辨认两个平面,软圆贴图由本地代码生成,不依赖外部图片。
1. 为什么普通透明混合在意顺序
对一个通过深度测试的片元,普通 Alpha 混合可简化为:
新颜色 = a × 当前片元颜色 + (1 − a) × 已有颜色
先混合红、再混合蓝,与先蓝、再红的结果通常不同。后提交的颜色权重更直接。因此常见策略是由远到近绘制透明对象:远层先混入背景,近层再混入结果。对于不相交、可按整体深度排序的层,这通常有效。
不透明对象通常先画并写深度;透明对象常设置 depthTest: true, depthWrite: false,让它们受不透明场景遮挡,却不会因第一片透明层先写深度而把后续层硬裁。代价是多个透明层都可能通过测试,颜色结果依赖提交顺序。depthWrite: false 解决了第 05 章的深度硬裁,不会自动完成颜色排序。
2. Three.js r174 排序的单位与规则
WebGLRenderer.sortObjects 默认开启。r174 的 WebGL 渲染列表把不透明、透射、透明对象分组,透明组默认大致按对象包围球中心投影后的深度由远到近排序;排序还考虑 Group.renderOrder 和对象 renderOrder。本章示例的两个烟幕各是一个 Mesh,所以每片各有一个用于排序的中心深度。
const smoke = new THREE.Mesh( planeGeometry, new THREE.MeshBasicMaterial( {
map: smokeTexture,
transparent: true,
blending: THREE.NormalBlending,
depthTest: true,
depthWrite: false,
side: THREE.DoubleSide,
forceSinglePass: true
} ) );
scene.add( smoke );
这里的 map 是带 Alpha 的软圆贴图;transparent: true 使网格进入透明组并进行混合。DoubleSide 让相机绕到另一侧时仍可看到平面;forceSinglePass: true 避免双面透明材质在 r174 中默认的背面、正面双次提交,以便把注意力放在两个对象的顺序上。它不是透明排序的通用修复。
对一个 THREE.Points,排序单位仍是整个 Points 对象;内部数千个点不会由 WebGLRenderer 按点逐一重新排序。把粒子分成更多对象能获得更细的对象排序,但会增加绘制调用和 CPU 管理成本。一个 InstancedMesh 内的实例也不能靠设置一次 renderOrder 得到逐实例排序。
3. renderOrder 的作用与边界
本例“强制红色后画”对应:
red.renderOrder = 2;
blue.renderOrder = 1;
两者都在透明组,较小值先提交。这个设置在相机角度变化时仍固定对象先后;它可以稳定一些设计好的叠层效果,例如始终压在底层烟上的光晕。但两个平面相交后,局部前后关系随屏幕位置不同,固定顺序总有区域不正确。
renderOrder 也不能让透明组跑到不透明组之前,不能改变 depthTest 的结果,不能给一个 Points 中的每个点各设一份顺序。若父级 Group 有 renderOrder,也要把它纳入排查。关闭 renderer.sortObjects 只会改成更依赖提交顺序的行为,并不会产生逐片元排序。
4. 参数变化:加法混合与深度写入
切换“加法混合”:光效颜色近似相加,红蓝互换时颜色顺序通常不再像普通 Alpha 混合那样显著。但加法会越叠越亮,不能表现浓烟吸收和遮挡背景的质感,也不能修正真实的几何前后关系。它适合星光、火花、能量束等发光效果;烟雾通常仍需普通混合或其他近似。
勾选“烟幕 depthWrite”:先画烟幕的方形片元留下深度,后画烟幕某些片元被拒绝,出现不自然硬裁。软贴图 Alpha 接近 0 也不意味着这些片元自动不写深度。这个开关故意重现上一章问题;不要把硬裁误判为“透明排序已正确”。
5. 实际项目怎样取舍
| 场景 | 可用策略 | 主要代价或边界 |
|---|---|---|
| 少量互不相交的透明层 | 保留对象级自动排序;必要时设 renderOrder |
相交、包围盒中心不代表局部深度 |
| 星光、火花等发光粒子 | 加法混合并关闭透明深度写入 | 易过曝,不能表现烟的吸收 |
| 烟雾粒子团 | 按深度分桶或按粒子中心从远到近重排 CPU 数据 | 每帧排序和缓冲上传有成本;交叉精度仍有限 |
| 硬轮廓碎片、叶片 | alphaTest/alphaHash 后参与深度写入 |
软边损失或出现噪点 |
| 严格要求相交透明面 | 分割几何、深度剥离或顺序无关透明技术 | 实现、显存和多次绘制成本更高,非基础材质开关 |
无论选哪种,先写清楚需要的是“视觉上足够自然”还是“几何上每个采样都严格正确”。粒子系统通常优先稳定的近似和预算可控的工作量。
常见问题
为什么相机一转就突然换颜色? 对象中心深度顺序改变时,整个对象的提交先后可能一次性翻转;平面内部的局部远近关系没有被单独排序。
关掉 depthTest 能消除排序问题吗? 不能。两个片元依旧按提交顺序混合,还会失去墙和地面对烟雾的遮挡。
加法混合是不是完全与顺序无关? 理想的未裁剪、未饱和相加满足交换律;实际渲染还受深度测试、写入、颜色钳制、色彩空间及其他处理影响。示例只说明它比普通 Alpha 混合对交换层次不敏感。
完成第 07 章练习,再阅读第 08 章。参考:Object3D.renderOrder、WebGLRenderer.sortObjects。