外观
Lesson 047|Blend、Depth、Stencil 与 Render State
Tags: #Creator2.x #Stage04 #Blend #Depth #Stencil #RenderStateDifficulty: ⭐⭐⭐⭐☆
真实问题:透明弹窗顺序正确,为什么颜色还是越叠越黑
界面按正确兄弟顺序绘制三个半透明遮罩。没有节点消失,DrawCall 也不高,但重叠区明显变暗。
这是 Blend 的数学结果,不是排序失效:
text
每一层都把源颜色与目标颜色再次混合
→ 同一像素重复叠加
→ 透明度和颜色逐层累积Render State 决定 GPU 怎样解释绘制结果;相同几何和 Shader,在 Blend、Depth 或 Stencil 不同时可能产生完全不同画面,也不能随意合批。
🎯 本课目标
材质不仅选择 Shader,还会改变 GPU 如何写入像素。本课理解:
- Blend 如何组合前景和背景。
- Depth 如何判断前后遮挡。
- Stencil 如何建立遮罩区域。
- 为什么状态切换会影响合批。
二、Blend:颜色如何叠加
透明 UI 常见的概念是:
text
最终颜色 = 源颜色 × 源因子 + 目标颜色 × 目标因子半透明对象需要读取或组合背景颜色,因此多个透明层会产生 Overdraw 和混合成本。
三、透明对象为什么难以排序
不透明对象可以借助深度测试较容易判断遮挡。透明对象通常需要按一定顺序绘制,因为:
text
A 先画、B 后画与:
text
B 先画、A 后画混合结果可能不同。因此透明 UI、粒子和特效经常受到渲染顺序影响。
四、Depth:谁挡住谁
Depth Buffer 保存深度信息:
text
新片段到来
↓
比较深度
↓
通过则写入颜色和深度
不通过则丢弃2D UI 的深度配置和 3D 世界可能不同。错误的 Depth Test 或 Write 会导致对象穿插、消失或排序异常。
Depth Test 和 Depth Write 必须分开:
text
Depth Test:新片元是否通过深度比较
Depth Write:通过后是否更新深度缓冲一个对象可以测试但不写入。透明对象若过早写入深度,后面的透明层可能直接测试失败,连混合机会都没有;若完全不测试,又可能穿过本应遮挡它的 3D 对象。
所以“是否开启 Depth”不是完整问题,还要问比较函数、是否写入、不透明与透明对象的顺序,以及多台 Camera 是否使用同一个 Depth 目标。
五、Stencil:建立模板区域
Stencil 可以把像素限制在某个区域:
text
先写入模板值
↓
后续绘制只通过匹配区域Mask 常依赖类似机制。它能解决裁剪问题,但通常会增加状态、Pass 或额外绘制。
Stencil Buffer 可以理解为与目标像素对应的一组整数标记。简化 Mask 流程是:
text
1. 绘制 Mask 形状并写入 Stencil 标记
2. 绘制子内容,只允许标记匹配的片元通过
3. 离开 Mask 时恢复父层模板条件嵌套 Mask 不能只有“开/关”两个状态,内外层需要区分当前模板层级。自定义 Material 若覆盖比较值或 Stencil 操作,就可能破坏 Creator 的 Mask 协议,导致内容消失或越界。
六、Render State 为什么影响合批
两个对象即使使用同一张纹理,如果 Blend、Depth、Stencil 或 Shader 状态不同,也可能不能放进同一个批次:
text
状态不同
↓
GPU 需要切换配置
↓
批次被分开合批判断不能只看 Texture。
一次 DrawCall 使用一套确定状态。假设 A 使用普通 Alpha Blend,B 使用加法 Blend:
text
A:srcAlpha / oneMinusSrcAlpha
B:one / oneGPU 不能在一次普通 DrawCall 中让一半三角形使用 A 因子、另一半使用 B 因子,除非引入另一套专门的数据和 Shader 设计。通常 Renderer 必须结束 A 的批次,切换 Blend State,再提交 B。
七、案例:透明面板叠加
text
背景
→ 半透明遮罩
→ 半透明面板
→ 半透明按钮
→ 文字和图标每层都可能读写和混合同一片屏幕区域。优化方向包括:
- 减少无意义的全屏透明层。
- 合并视觉相同的背景。
- 关闭不可见节点。
- 评估 Mask 和特效是否必要。
八、常见误区
误区一:透明只改变一个 alpha 数字
它还会影响混合、排序和像素重复处理。
误区二:Depth 开启就能解决所有遮挡
透明对象、UI 层级和渲染顺序仍可能需要额外规则。
误区三:Stencil 是免费的裁剪
模板写入、测试和额外 Pass 都可能有成本。
误区四:状态切换只影响 GPU,不影响 DrawCall
状态不兼容经常直接表现为批次拆分。
九、深入推导:Blend 必须先明确颜色表示
常见 Alpha 混合概念式:
text
out = srcColor × srcAlpha
+ dstColor × (1 - srcAlpha)若纹理采用预乘 Alpha,RGB 已经乘过 Alpha,混合因子应与之匹配。混用 straight alpha 与 premultiplied alpha 常导致黑边、发白或颜色错误。
不要只记一个 Blend 公式,还要确认:
- 输入纹理是否预乘;
- Material Pass 使用什么因子;
- RenderTexture 是否保留 Alpha;
- 最终背景颜色;
- 多层叠加次数。
用一个数字例子可以看清“越叠越黑”并不是错觉。假设背景亮度为 1,在上面画一层 50% 黑色:
text
第一次:0 × 0.5 + 1 × 0.5 = 0.5
第二次:0 × 0.5 + 0.5 × 0.5 = 0.25
第三次:0 × 0.5 + 0.25 × 0.5 = 0.125三层 50% 黑色不是“仍然 50%”,而是只剩约 12.5% 的背景亮度。每一层都以上一层的结果作为新的目标颜色,所以混合次数和顺序都会改变结果。
Straight Alpha 常在混合时再乘源 Alpha;Premultiplied Alpha 的 RGB 在纹理数据中已经乘过 Alpha。若预乘数据又使用需要再次乘 Alpha 的因子,边缘会被重复变暗;反过来混用也可能发白。排查黑边时必须同时确认纹理表示与 Pass 的 Blend 因子。
十、Depth Test 与 Depth Write 不是一回事
Depth Test 比较片元深度,Depth Write 决定是否更新深度缓冲。透明物体常见难点:
text
需要从后往前混合
但深度写入可能让后续透明层被挡2D UI 主要依赖顺序与层级,但自定义 3D/2D 混合、Camera 和材质仍可能受深度状态影响。
十一、Stencil 是按像素保存的模板条件
Mask 常见概念流程:
text
先绘制 Mask 形状
→ 在 Stencil Buffer 写入标记
→ 子内容只在标记满足时通过
→ 恢复/清理模板层级嵌套 Mask 会增加模板层级和状态切换。Stencil 不是裁剪几何本身,范围外片元是否提前避免执行取决于管线与实现。
十二、Creator 2.4.x 实验:亲手制造三个状态问题
使用 Creator 编辑器配置 Sprite、Mask 和 Material,不直接编辑
.effect、.mtl或场景文本。
实验 A:透明叠加
创建三个 800×800、opacity 约 100 的同色 Sprite,使它们部分重叠。观察单层、双层、三层区域颜色,证明 opacity 为低值不代表多层结果相同。
实验 B:兄弟顺序
使用红、绿两张半透明 Sprite 互换兄弟顺序。预期重叠颜色变化,因为 Alpha 混合通常与顺序有关。
实验 C:Mask
创建:
text
MaskNode/Mask
└── LargeContent/Sprite切换 Mask active,观察裁剪结果和 DrawCall。再增加嵌套 Mask,记录状态变化。
RenderStateProbe.ts
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class RenderStateProbe extends cc.Component {
@property([cc.Node])
overlays: cc.Node[] = [];
toggleOverlay(index: number) {
const node = this.overlays[index];
node.active = !node.active;
}
reverseOrder() {
for (let i = this.overlays.length - 1; i >= 0; i--) {
this.overlays[i].setSiblingIndex(
this.overlays.length - 1 - i
);
}
}
}十三、Render State 为什么切断批次
GPU 一次绘制必须使用一组确定状态:
text
Blend enable / factors
Depth test / write / function
Stencil test / reference / operations
Cull mode
Color write mask相邻对象若状态不同,Renderer 通常必须结束当前批次、切换状态,再提交下一批。即使它们使用同一 Texture,也不再等价。
十四、真实项目故障:嵌套 Mask 后部分 UI 消失
现象
单层 ScrollView 正常,外层弹窗再加 Mask 后,列表中部分元素消失或越界显示。
排查
text
单独启用外 Mask
→ 单独启用内 Mask
→ 记录嵌套层级
→ 检查兄弟顺序与 Mask 子树
→ 检查自定义 Material 的 Stencil State
→ 检查渲染中是否有特殊组件打断模板恢复根因
自定义材质 Pass 覆盖了模板状态,与 Creator Mask 使用的 Stencil 协议不兼容。
修复要让材质遵守当前 Mask/Stencil 规则,而不是继续增加节点遮挡。
十五、故障诊断
透明边缘发黑
检查预乘 Alpha、纹理边缘颜色、Blend 因子、Atlas padding 和压缩。
透明对象前后错误
检查绘制顺序、Depth Test/Write、Camera 和材质队列。透明排序不是只改 z 即可。
Mask 外仍有内容
检查 Mask 类型、节点子树、Stencil 兼容、层级和组件是否真的受该 Mask 管理。
十六、练习、推导答案与版本边界
本课讲通用图形状态并落在 Creator 2.4.x Material/Mask。具体默认 Blend、Stencil 层级和批处理行为以项目版本为准。
- 为什么三层 50% 黑色不等于一层 50%? 每层都与上一层结果再次混合,效果累积且与顺序有关。
- 相同 Texture、不同 Blend 能合批吗? 通常不能,因为一次 DrawCall 只能使用一套兼容 Render State。
- Stencil 与普通矩形裁剪有什么差别? Stencil 用模板缓冲表达按像素条件,可支持复杂形状,但需要额外 Pass/状态管理。
- 透明对象为什么常从后往前画? 常规 Alpha 混合需要先有后方颜色,再把前方透明色混入。
能力验收
你应该能用一次具体颜色计算解释半透明叠加,分别说明 Depth Test 与 Depth Write,画出 Mask 写入和测试 Stencil 的顺序,并能证明同纹理对象为何会因为 Render State 不同而拆批。
本课总结
text
Blend 决定颜色如何组合。
Depth 决定深度测试和写入。
Stencil 决定模板区域。
Render State 差异可能打断合批。
透明和遮罩要同时关注正确性与成本。十一、下一课预告
text
Lesson048|DrawCall 是什么?CPU 在提交什么?