外观
Lesson 054|阶段复习:一次 UI 修改如何经过 Renderer 到达 GPU
Tags: #Creator2.x #Stage04 #Renderer #Batching #GPUDifficulty: ⭐⭐⭐⭐☆
阶段挑战:解释“点击按钮后变红”背后的全部工作
业务代码只有:
ts
buttonNode.color = cc.Color.RED;完整链路却至少涉及:
text
输入命中与回调
→ Node 颜色状态改变
→ Sprite 显示数据失效
→ Assembler 更新顶点颜色
→ Renderer 收集 Camera 可见组件
→ 与相邻对象比较批次状态
→ 写入顶点/索引缓冲
→ 提交 DrawCall
→ Vertex Shader 处理位置与颜色
→ Fragment Shader 采样 Texture
→ Blend 到 FrameBuffer本课要求你不仅能背流程,还能通过实验判断链路中的哪一层导致了错误或性能变化。
🎯 本课目标
本课串联 Lesson037~Lesson053,完成 Renderer 阶段闭环。
综合问题是:
把一个按钮的文字颜色改掉,最终为什么会影响屏幕像素、DrawCall 和 GPU 时间?
一、先把 Renderer 看成一条证据链
text
Transform / Camera
↓
Sprite / Label / Graphics
↓
RenderData / Assembler
↓
Material / Effect / Shader
↓
Texture / Sampler / Render State
↓
排序 / 合批 / DrawCall
↓
Vertex Shader / Fragment Shader
↓
Blend / Depth / Stencil
↓
像素写入这张知识树可以进一步分成四个取证层:
text
第一层:业务与场景状态
Node / Component / Transform / active / Camera
↓
第二层:CPU 渲染数据
RenderData / Assembler / 顶点 / UV / 颜色 / 索引
↓
第三层:提交与批次
Material / Effect / Pass / Texture / Render State / DrawCall
↓
第四层:GPU 像素
Vertex Shader / Rasterization / Fragment Shader / Blend / FrameBuffer这四层不只是知识分类,也是排错顺序。例如按钮没有变红时,应依次证明:
text
Node 的颜色值真的改变了吗?
→ RenderData 是否带上新颜色?
→ 当前批次是否提交了新数据?
→ Shader 是否使用该颜色并写入目标?上游证据尚未成立,就没有理由跳到下游猜 Shader。
二、贯穿案例一:修改按钮颜色
代码:
ts
this.startButton.color = cc.Color.YELLOW;高层数据流:
text
Node / Render Component 颜色状态改变
↓
标记顶点颜色或渲染参数需要更新
↓
Assembler 更新颜色数据
↓
Renderer 读取新的 RenderData
↓
检查 Material、Texture、Blend 是否仍兼容
↓
合批或拆分 DrawCall
↓
Vertex Shader 接收颜色
↓
Fragment Shader 与纹理颜色组合
↓
Blend 写入目标
↓
屏幕像素改变具体是否重建完整几何,要以组件实现为准。稳定结论是“业务状态先改变,后续渲染阶段消费新状态”,而不是“设置颜色会立即让 GPU 画一次”。
如果同一帧连续把按钮设为红、绿、蓝,屏幕通常只在后续渲染时看到最终蓝色。三次脚本写入不等于同一帧出现三张画面,也不等于产生三个 DrawCall。
这一课始终区分三个容易混淆的概念:
text
修改状态 ≠ 立即绘制
对象数量 ≠ DrawCall 数量
DrawCall 数量 ≠ GPU 工作量三、贯穿案例二:移动按钮
ts
this.startButton.x += 10;text
修改 Local Transform
↓
Dirty 传播
↓
计算 World Matrix
↓
Camera / Viewport 转换
↓
RenderData 使用新变换
↓
Renderer 重新提交顶点位置如果按钮位于复杂父节点下,父子矩阵传播和 UI Layout 也可能增加 CPU 成本。
四、贯穿案例三:为什么 DrawCall 突然增加
按钮修改后 DrawCall 变多,不要直接归因于“改颜色”:
text
检查是否换了 SpriteFrame / Texture
检查是否换了 Material / Shader
检查是否启用 Mask / Stencil
检查是否跨了 Camera 或 Render Layer
检查是否改变了排序
检查是否只是对象和顶点数量增加要通过对比实验找出真正的状态边界。
五、贯穿案例四:DrawCall 不高但低端机卡
text
DrawCall 低
↓
继续检查 GPU Frame Time
↓
Overdraw 可视化
↓
全屏透明、粒子、Mask、Shader 采样
↓
定位 Fill Rate 或 Fragment 瓶颈这时减少 DrawCall 可能不是正确方向。
六、先按症状选择证据,不要按熟悉程度猜
画面不对
text
Node active / activeInHierarchy
Transform 和坐标系
Camera 和渲染层
Material / Texture
Blend / Depth / Stencil
排序和遮挡DrawCall 高
text
Texture 是否分散
Material / Shader 是否分散
Render State 是否变化
Mask / 特殊组件是否插入
图集和 Dynamic Atlas 是否合理GPU 卡
text
Overdraw
Fill Rate
透明覆盖面积
Fragment Shader
纹理采样和带宽
分辨率和 RenderTarget七、第一次闭卷推导:先回答,再进入实验
场景
text
Canvas
└── Dialog
├── SemiTransparentMask
├── PanelBackground
├── Icon
├── MessageLabel
└── ConfirmButton问题:
- 修改 MessageLabel.string 可能触发哪些工作?
- 为什么 Dialog 的位置变化可能影响多个对象?
- 为什么 Mask 可能让批次变多?
- 如果 DrawCall 不高但 GPU 时间高,应先查什么?
- 如果 ConfirmButton 换了一张不同图集的图,可能发生什么?
完成五道题后再展开参考答案
参考答案
- 文本测量、排版、字形缓存、RenderData 更新和纹理/批次变化。
- 父节点 Transform 变化会传播到子树。
- Mask 可能需要 Stencil、额外 Pass 和状态边界。
- Overdraw、透明覆盖、Shader、纹理采样、分辨率和 Fill Rate。
- 纹理绑定可能改变,合批机会下降,DrawCall 可能增加。
八、带着这些问题完成 RendererLab
- Node 为什么不是渲染对象?
- Dirty Flag 和 Renderer 有什么关系?
- SpriteFrame 和 Texture 的区别是什么?
- Assembler 生成了哪些数据?
- Material 和 Shader 的区别是什么?
- DrawCall 和 Overdraw 分别衡量什么?
- 为什么两个 Sprite 同图集仍可能无法合批?
- 为什么 Label 往往比 Sprite 更容易产生更新成本?
- Mask 和 Particle 为什么需要单独分析?
- 如何定位一个低 DrawCall 但高 GPU 时间的问题?
九、阶段综合实验:RendererLab
所有场景、材质、图片、Mask 和组件都通过 Creator 2.4.x 编辑器创建与配置。不手工修改
.scene、.prefab、.meta、.effect、.mtl或生成目录。
场景结构
text
Canvas
├── StaticLayer
│ └── 100 个同 Atlas Sprite
├── DynamicLayer
│ ├── 100 个 Filled Sprite
│ └── 50 个 Dynamic Label
├── SpecialLayer
│ ├── Mask
│ ├── Graphics
│ └── Particle
├── OverdrawLayer
│ └── 6 个全屏透明 Sprite
└── ControlPanel每个 Layer 独立开关,保证控制变量。
RendererLab.ts
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class RendererLab extends cc.Component {
@property(cc.Node)
staticLayer: cc.Node = null;
@property(cc.Node)
dynamicLayer: cc.Node = null;
@property(cc.Node)
specialLayer: cc.Node = null;
@property(cc.Node)
overdrawLayer: cc.Node = null;
@property([cc.Sprite])
filledSprites: cc.Sprite[] = [];
@property([cc.Label])
labels: cc.Label[] = [];
private animate = false;
private time = 0;
private lastText = '';
showOnly(index: number) {
const layers = [
this.staticLayer,
this.dynamicLayer,
this.specialLayer,
this.overdrawLayer,
];
for (let i = 0; i < layers.length; i++) {
layers[i].active = i === index;
}
}
toggleAnimation() {
this.animate = !this.animate;
}
update(dt: number) {
if (!this.animate) {
return;
}
this.time += dt;
const fill = (Math.sin(this.time) + 1) * 0.5;
for (const sprite of this.filledSprites) {
sprite.fillRange = fill;
}
const text = String(Math.floor(this.time));
if (text !== this.lastText) {
this.lastText = text;
for (const label of this.labels) {
label.string = text;
}
}
}
}基线实验
- 只开 StaticLayer。
- 记录 FPS、Frame Time、DrawCall 和目标设备。
- 确认 100 个 Sprite 使用的底层 Texture/Material。
- 不做任何优化,保存基线截图和数据。
Transform 实验
text
只移动 StaticLayer 父节点
对比
每帧移动 100 个子节点解释 Dirty 范围和数据更新差异。
RenderData 实验
开启 DynamicLayer,比较:
text
静态 Filled/Label
→ 每帧 Filled
→ 每秒 Label
→ 每帧 Label记录节点数相同但 CPU 工作不同。
批次实验
给第 50 个 Sprite 换独立 Texture/Material,再把它移动到序列开头、中间、末尾。解释状态隔断为什么产生不同批次分段。
特殊组件实验
分别启用 Mask、Graphics 动态路径和 Particle,记录它们影响 CPU、DrawCall 还是覆盖像素。
Overdraw 实验
启用 1、3、6 层全屏透明 Sprite,再降低预览分辨率或缩小覆盖面积。若 DrawCall近似而帧时间随像素量变化,形成像素瓶颈证据。
十、一张 Renderer 证据表
| 改动 | 先看 CPU | 先看 DrawCall | 先看 GPU/像素 |
|---|---|---|---|
| 大量节点移动 | Transform/遍历 | 可能不变 | 通常次要 |
| Label 每帧改字 | 排版/字形/顶点 | 字体页可能变化 | 字形采样 |
| 换独立 Texture | 资源/数据更新 | 纹理切批 | 上传/带宽 |
| 独立 Material | 参数与状态 | 常切批 | Shader/Pass |
| Mask 嵌套 | 组件/几何 | Stencil 切换 | 模板与覆盖 |
| 大粒子 | 模拟 | 可能少 | Overdraw |
| 全屏模糊 | 提交少 | 可能 1~几次 | 采样/带宽主导 |
表格是优先检查方向,不是固定结论。
十一、阶段真实故障:排行榜打开时卡一帧,滚动时持续掉帧
分成两个问题
打开卡一帧:
text
动态加载 Atlas
→ Texture 解码/上传
→ 200 个 Item instantiate
→ Label 首次字形
→ Layout 全量排版滚动持续掉帧:
text
全部 200 Item 常驻
→ Content Transform 影响后代
→ 每帧世界包围盒判断
→ Label 动态排名动画
→ 每 Item 圆形 Mask修复组合
- 虚拟列表只保留可见 Item;
- 分批创建,首屏优先;
- 字体/资源按实际字符集预热;
- 固定 Label 与 Item 尺寸,减少 Layout;
- 移除每 Item Mask 或选择成本更可控方案;
- 排名动画降低更新频率;
- 对打开峰值与滚动稳定帧分别验收。
“降低 DrawCall”无法单独解决这两个阶段的所有问题。
十二、Stage04 故障定位树
text
画面错误?
├── 不显示
│ ├── active/component/resource
│ ├── Camera group/cullingMask
│ ├── coordinate/viewport
│ └── material/depth/stencil
├── 顺序错误
│ ├── sibling/order
│ ├── Camera depth/clear
│ ├── transparency/depth
│ └── Mask context
└── 颜色/边缘错误
├── Blend/premultiplied alpha
├── Texture filter/wrap
├── Atlas padding/UV
└── Shader/platform
性能错误?
├── CPU
│ ├── Transform Dirty
│ ├── Label/Layout
│ ├── Assembler/Graphics
│ └── Spine/Particle simulation
├── Submit
│ ├── Texture/Material
│ ├── Render State
│ ├── Mask/Special component
│ └── incompatible order
└── GPU
├── vertex count
├── fragment complexity
├── Overdraw/Fill Rate
└── texture bandwidth/upload十三、阶段能力验收
你应该能够:
- 画出 Node 属性到 GPU 像素的数据流。
- 解释 Local 与 World Matrix 及父 Dirty 传播。
- 完成 World → Screen → UI Local 坐标链。
- 用 Camera Group/Mask 证明节点为何不可见。
- 区分 SpriteFrame、RenderData 与 Assembler。
- 解释 Material、Pass、Shader 和 Render State。
- 用控制变量找出一个合批隔断。
- 计算纹理未压缩内存数量级。
- 证明一个 DrawCall 低但 Overdraw 高的场景。
- 对特殊组件分别判断 CPU、Submit 和 GPU 成本。
十四、综合练习与推导答案
练习一
100 个 Sprite 同 Atlas,但每个之间插一个动态 Label。如何判断重排是否值得?
答案: 先记录批次与 CPU;确认透明遮挡允许把同类层连续组织;评估重排对组件复用和数据绑定的成本;再对比 DrawCall、Label 排版和视觉。不能只凭 Atlas 下结论。
练习二
全屏特效只有两个三角形,为什么可能 GPU-bound?
答案: 顶点少,但覆盖每个屏幕像素;多采样、多 Pass、Blend 和高分辨率让 Fragment/带宽主导。
练习三
父节点动画导致大量 Transform 更新,是否应把所有子节点改成世界坐标独立节点?
答案: 不应直接这样做。先缩小动画子树、避免无关节点成为后代、减少世界查询;扁平化会增加管理复杂度并破坏布局/层级语义,需实测。
练习四
Dynamic Atlas 让稳定 DrawCall 降低,但首帧更卡,怎样决策?
答案: 同时量化首次合图成本和稳定收益;可将稳定资源改静态 Atlas、分批预热、限制候选;若页面很少停留,首帧代价可能不值得。
练习五
一个 UI 不显示,怎样避免从 Shader 开始盲查?
答案: 按 active/组件/资源、Transform/尺寸、Camera mask/viewport、顺序/Mask、Material/Shader 的顺序逐层证明,先检查高概率且便宜的条件。
十五、版本边界与验证入口
Stage04 固定以 Creator 2.4.x 渲染体系为主。内部 RenderFlow、Assembler 和批处理实现会随小版本变化;业务与实验不访问私有字段。
十、Stage04 总结
text
Node 提供 Transform 和结构。
渲染组件生成 RenderData。
Assembler 把组件属性转换成几何数据。
Material / Shader / Texture / Render State 决定绘制方式。
Renderer 负责排序、合批和 DrawCall。
GPU 负责顶点、片段和像素写入。
DrawCall、Overdraw、Frame Time 必须结合分析。最终能力是:
能从一次业务属性修改出发,沿 Node、RenderData、Material、Batch、GPU 逐层定位画面问题和性能问题。
十一、下一阶段预告
Stage04 完成,下一阶段进入:
text
Stage05|Performance
Lesson055|性能问题的四个维度:CPU、GPU、Memory、IO