Skip to content

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 / one

GPU 不能在一次普通 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 层级和批处理行为以项目版本为准。

  1. 为什么三层 50% 黑色不等于一层 50%? 每层都与上一层结果再次混合,效果累积且与顺序有关。
  2. 相同 Texture、不同 Blend 能合批吗? 通常不能,因为一次 DrawCall 只能使用一套兼容 Render State。
  3. Stencil 与普通矩形裁剪有什么差别? Stencil 用模板缓冲表达按像素条件,可支持复杂形状,但需要额外 Pass/状态管理。
  4. 透明对象为什么常从后往前画? 常规 Alpha 混合需要先有后方颜色,再把前方透明色混入。

能力验收 ​

你应该能用一次具体颜色计算解释半透明叠加,分别说明 Depth Test 与 Depth Write,画出 Mask 写入和测试 Stencil 的顺序,并能证明同纹理对象为何会因为 Render State 不同而拆批。

本课总结 ​

text
Blend 决定颜色如何组合。
Depth 决定深度测试和写入。
Stencil 决定模板区域。
Render State 差异可能打断合批。
透明和遮罩要同时关注正确性与成本。

十一、下一课预告 ​

text
Lesson048|DrawCall 是什么?CPU 在提交什么?