Skip to content

Lesson 052|Mask、Graphics、Spine、Particle 的特殊渲染成本 ​

Tags: #Creator2.x #Stage04 #Mask #Graphics #Spine #ParticleDifficulty: ⭐⭐⭐⭐☆


真实问题:活动页只有 30 个节点,为什么比 300 个普通 Sprite 更慢 ​

页面节点不多,却同时包含:

text
圆形头像 Mask
动态 Graphics 折线
两套 Spine 角色
全屏 Particle

节点计数不能描述这些组件的工作量。普通 Sprite 常是固定四边形;特殊组件可能有额外 Pass、动态网格、骨骼求值、大量粒子和状态切换。

🎯 本课目标 ​

这些组件都能“显示东西”,但渲染路径不同。本课建立快速判断能力:

text
组件看起来是什么
≠
组件实际如何渲染

二、Mask ​

以 Stencil Mask 为例,实际不是“超出范围就不画”一句话:

text
绘制 Mask 形状
→ 写入模板标记
→ 切换 Stencil Test
→ 绘制子树内容
→ 退出时恢复上一级模板状态

如果列表中每个头像都有独立 Mask,Renderer 会在普通 UI 之间反复进入和退出模板上下文。即使头像很小,批次也可能被切成许多段。

Mask 通常需要定义裁剪区域,可能使用:

text
Stencil
额外绘制
裁剪几何
独立状态

Mask 嵌套和大面积裁剪会增加状态切换与像素处理。


三、Graphics ​

lineTo、圆弧和贝塞尔曲线只是业务描述,GPU 最终仍需要三角形。描边宽度、连接方式和曲线精度会影响三角化结果:

text
更多路径点
→ 更多线段和连接
→ 更多顶点/索引
→ 更多 CPU 生成与缓冲写入

静态路径只生成一次和每帧 clear → 重画 的视觉结果可以相同,CPU 成本却完全不同。

Graphics 运行时生成线段、矩形、圆和路径:

text
业务命令
    ↓
路径解析
    ↓
顶点和索引生成
    ↓
上传或更新几何

每帧清空并重画复杂路径会造成 CPU 和顶点更新成本。静态图形应尽量缓存或减少重建。


四、Spine ​

Spine 一帧至少可以拆成两段:

text
动画更新
→ 时间线采样、骨骼世界变换、插槽、附件、事件

渲染准备
→ 网格顶点/蒙皮、裁剪、Texture、Blend、绘制顺序

暂停渲染但继续动画,和暂停动画但保留最后一帧渲染,是区分 CPU 动画成本与 Renderer/GPU 成本的有效对照。只把节点移出屏幕不一定停止骨骼更新。

Spine 可能包含:

  • 骨骼层级计算。
  • 动画采样。
  • 插槽和附件切换。
  • 多纹理或多材质。
  • 蒙皮顶点更新。

骨骼动画的 CPU 更新和 Renderer 提交都需要测量,不能只看骨骼数量。


五、Particle ​

粒子成本不能只数“发射了多少个”,还要看:

text
活跃粒子数
× 每粒子顶点数
× 每秒更新频率
→ CPU 与几何规模

活跃粒子数
× 单粒子屏幕面积
× 平均重叠层数
→ Fragment 与 Blend 压力

100 个 16×16 小粒子和 100 个 512×512 半透明粒子,DrawCall 可能相近,GPU 像素成本却相差几个数量级。

粒子系统通常带来:

text
粒子生命周期更新
发射和回收
顶点生成
透明混合
大量屏幕覆盖

粒子数量不一定等于 DrawCall 数量,但会影响 CPU 更新、顶点量和 Overdraw。


六、为什么这些组件容易打断合批 ​

“特殊”不是固定性能标签,而是这些组件更容易改变普通 Sprite 批次所依赖的条件:

text
Mask:改变 Stencil 上下文
Graphics:产生不同或动态几何
Spine:多附件、纹理、Blend 和网格
Particle:特殊顶点、透明材质和大面积覆盖

某个具体组件是否昂贵,仍取决于资源、更新频率、屏幕面积和平台。一个静态小 Graphics 可能完全可接受,一百个每帧重画的复杂路径则是另一回事。

text
特殊顶点格式
不同 Shader
不同纹理
Blend / Stencil
独立更新阶段

因此特殊组件应在性能分析中单独归类,而不是和普通 Sprite 混在一起平均计算。


七、案例:滚动列表里的 Mask ​

text
ScrollView
└── View
    └── Content
        └── 多个 Item

优化方向:

  • 减少嵌套 Mask。
  • 控制可见 Item 数量。
  • 复用 Item,避免不断创建。
  • 评估是否可用边界裁剪替代复杂 Mask。
  • 真机测量模板、填充和 DrawCall。

八、案例:粒子特效覆盖 UI ​

粒子可能使用透明材质覆盖大面积屏幕:

text
粒子数量不多
但每个粒子很大
    ↓
大量 Fragment 和混合
    ↓
GPU 仍然超时

这类问题要看 Overdraw 和 GPU 时间,不要只数粒子对象。


九、常见误区 ​

误区一:Mask 只是限制显示区域,不会增加绘制 ​

实现裁剪通常需要额外状态或处理。

误区二:Graphics 画一条线很便宜,重复画也一样 ​

每帧重建大量路径会带来 CPU 和顶点成本。

误区三:Spine 只需要播放一张图片 ​

它可能持续计算骨骼、附件和蒙皮数据。

误区四:粒子卡顿只看粒子数量 ​

粒子面积、透明层数、Shader 和更新频率同样重要。



十、深入推导:四类成本必须拆开看 ​

Mask ​

可能需要先绘制遮罩形状、写 Stencil,再绘制子内容并恢复状态。嵌套层级进一步增加状态复杂度。

Graphics ​

路径、描边、连接和填充需要在 CPU 侧三角化。每帧 clear 后重画复杂路径,会反复生成几何。

Spine ​

每帧进行动画采样、骨骼/插槽更新、网格变形,再按贴图与 Blend 组织渲染。剪裁、网格附件、多纹理和复杂混合会增加成本。

Particle ​

包含发射、生命周期、位置/颜色/尺寸更新和大量 Billboard/顶点;大粒子还会制造 Overdraw。

它们分别可能压到 CPU、DrawCall、顶点或 Fragment 阶段。

不要用“这个组件贵不贵”作为最终问题,要拆成四个轴:

组件CPU 更新几何/顶点提交与状态片元覆盖
Mask层级与裁剪管理遮罩几何Stencil 进入/退出遮罩与内容覆盖
Graphics路径解析、三角化随路径复杂度变化材质与批次边界取决于填充面积
Spine动画、骨骼、蒙皮网格附件变化纹理、Blend、剪裁切换取决于角色面积与重叠
Particle发射与生命周期模拟活跃粒子顶点Texture、Blend、批次粒子尺寸和层叠常主导

同一种组件在不同项目里可能落在不同瓶颈。例如静态小 Mask 主要是状态边界,而全屏复杂 Mask 还可能带来明显像素成本;必须先关掉更新、再关掉渲染做对照。

十一、Creator 2.4.x 实验:四组独立开关 ​

组件和资源通过 Creator 编辑器创建/绑定,不手工修改动画、材质、Prefab 或 Effect 序列化文件。

场景 ​

text
MaskGroup
GraphicsGroup
SpineGroup(若项目已有合法测试资源)
ParticleGroup
ControlPanel

没有 Spine 测试资源时,不新造或伪造资源;只完成其他三组,并在已有正式项目资源中测 Spine。

SpecialRenderProbe.ts ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class SpecialRenderProbe extends cc.Component {
    @property([cc.Node])
    groups: cc.Node[] = [];

    setOnly(index: number) {
        for (let i = 0; i < this.groups.length; i++) {
            this.groups[i].active = i === index;
        }
    }

    setAll(active: boolean) {
        for (const group of this.groups) {
            group.active = active;
        }
    }
}

Graphics 动态路径探针 ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export class GraphicsProbe extends cc.Component {
    @property(cc.Graphics)
    graphics: cc.Graphics = null;

    private dynamic = false;
    private time = 0;

    toggleDynamic() {
        this.dynamic = !this.dynamic;
    }

    update(dt: number) {
        if (!this.dynamic) {
            return;
        }
        this.time += dt;
        const g = this.graphics;
        g.clear();
        g.moveTo(-300, 0);
        for (let i = 0; i < 100; i++) {
            g.lineTo(
                -300 + i * 6,
                Math.sin(this.time * 3 + i * 0.2) * 80
            );
        }
        g.stroke();
    }
}

比较静态绘制一次与每帧重绘,记录 CPU 差异。

实验记录 ​

组CPU FrameDrawCall视觉覆盖特殊变量
Mask实测实测小/中嵌套层
Graphics实测实测小路径点数/更新频率
Spine实测实测中骨骼/插槽/贴图
Particle实测实测大活跃粒子/尺寸

十二、真实项目案例:滚动列表里每个头像一个 Mask ​

100 个 Item 均带圆形 Mask,即使屏幕只看到 12 个,项目仍保留全部节点并参与部分更新。

改进组合:

  • 使用虚拟列表只创建可见 Item;
  • 头像源图提供圆形透明边缘;
  • 评估统一 shader 裁剪方案;
  • 对不可见 Item 停止动画和粒子;
  • 只在确有动态形状需求时保留 Mask;
  • 验证替代方案的 Overdraw 和边缘质量。

十三、真实项目案例:Spine 换装后 DrawCall 激增 ​

多个插槽引用不同 Atlas/材质与 Blend Mode,绘制顺序不断切换。优化需要从资源和骨骼内容入手:

  • 合并兼容纹理;
  • 减少不需要的插槽与透明附件;
  • 控制剪裁附件;
  • 远处/不可见角色降低更新频率;
  • 不播放时暂停动画更新;
  • 以目标 Spine 运行时版本验证。

十四、故障诊断 ​

Graphics 静止仍高 CPU ​

检查是否每帧 clear/stroke、路径数据是否变化、脚本是否重复构造点数组。静态图形应绘制一次或缓存为资源。

Particle DrawCall 不高但 GPU 慢 ​

检查粒子尺寸、数量、重叠、纹理透明留白和 Fragment Shader。

Spine CPU 高 ​

分别关动画更新和渲染,区分骨骼求值与 GPU;检查活跃实例、骨骼/网格复杂度和监听回调。

Mask 引发批次碎片 ​

检查嵌套层级、Mask 子树范围和相邻普通 Sprite 是否被反复切开。

十五、练习、推导答案与版本边界 ​

Spine、Particle、Graphics 和 Mask 的内部实现随 Creator 2.4.x 小版本与原生运行时变化。必须固定资源和设备实测。

  1. 为什么节点少不代表便宜? 一个节点可生成大量几何、多个 Pass、骨骼计算或海量片元。
  2. 静态 Graphics 应怎样优化? 避免每帧重建,可绘制一次、缓存或预制为 Sprite 资源。
  3. Particle 优化为什么同时看 CPU/GPU? CPU 模拟粒子,GPU 处理顶点和重叠片元,两端都可能瓶颈。
  4. Mask 替换为透明 PNG 一定更快吗? 不一定,可能增加透明像素 Overdraw 和纹理内存;要按目标设备测量。

能力验收 ​

你应该能把四类组件分别放到 CPU 更新、几何生成、提交状态和片元覆盖四个维度中分析;能用独立开关区分成本,并避免用节点数或 DrawCall 单一指标替代证据。

本课总结 ​

text
Mask 可能增加裁剪和状态成本。
Graphics 的动态路径会产生几何更新。
Spine 有骨骼、附件和蒙皮成本。
Particle 同时影响更新、顶点和像素覆盖。
特殊组件应单独测量。

十一、下一课预告 ​

text
Lesson053|Overdraw 与 Fill Rate:DrawCall 不高为什么仍然会卡?