外观
Lesson 042|Assembler 如何生成顶点、UV、颜色和索引数据?
Tags: #Creator2.x #Stage04 #Assembler #Vertex #UVDifficulty: ⭐⭐⭐⭐☆
真实问题:圆形进度条每帧变化,为什么比移动节点贵
技能冷却使用 Filled Sprite,每帧更新 fillRange。另一组图标只改变父节点位置。节点数相同,前者的 CPU 数据准备成本却可能更高。
移动节点主要改变变换结果;Filled 几何的可见扇区发生变化,Assembler 可能需要重新计算顶点位置、UV 和索引范围。
Assembler 可以理解成:
把组件语义翻译成 GPU 能消费的几何数据。
🎯 本课目标
Renderer 不能直接理解“这是一张头像”。它需要结构化的几何数据。本课解释:
- Assembler 的职责。
- 顶点、UV、颜色和索引分别是什么。
- 为什么 Sprite 类型会影响几何生成。
- 顶点数据变化和材质变化有什么区别。
二、Assembler 是什么
可以把 Assembler 理解为:
把渲染组件的业务属性转换成 Renderer 能提交的几何数据。
输入:
text
SpriteFrame
节点尺寸和 Anchor
颜色和透明度
Fill / Sliced / Tiled 参数
世界变换输出:
text
顶点
UV
颜色
索引三、四边形需要哪些数据
一个普通 Sprite 可以用四个顶点表示:
text
v0 ───── v1
│ │
│ │
v3 ───── v2两个三角形可以通过索引连接:
text
0, 1, 2
0, 2, 3每个顶点通常还带有:
text
position
uv
color四、顶点位置
顶点位置通常先在 Sprite 的本地空间生成:
text
左下:(-width * anchor.x, -height * anchor.y)
右上:( width * (1-anchor.x), height * (1-anchor.y))之后再由世界矩阵和 Camera 变换到渲染空间。
这样同一套本地几何可以被不同 Node Transform 复用。
五、UV 坐标
UV 告诉 Shader:
从纹理的哪个位置采样像素。
text
纹理左下:uv = (0, 0)
纹理右上:uv = (1, 1)SpriteFrame 如果只是图集中的一块区域,Assembler 需要把区域矩形换算成对应 UV,而不是总是使用完整纹理范围。
图像上下方向、纹理旋转和平台坐标约定都可能影响 UV 计算。
六、顶点颜色
顶点颜色可以携带:
text
Node.color
Opacity
组件颜色
渐变或局部颜色Shader 通常会把顶点颜色和采样到的纹理颜色相乘或按材质规则组合。
因此改颜色不一定要重新上传一张纹理,但可能需要更新顶点缓冲数据。
七、索引为什么存在
索引告诉 GPU 如何复用顶点组成三角形:
text
顶点数组:v0、v1、v2、v3
索引:0,1,2, 0,2,3复用顶点可以减少重复数据。复杂的 Filled、Tiled 和 Mask 几何会产生更多顶点和索引。
八、不同 Sprite 类型的生成差异
text
Simple:固定矩形
Sliced:按边界拆分多个区域
Tiled:重复 UV 或生成重复几何
Filled:根据比例裁剪几何Assembler 不只是复制一份四边形,它需要根据组件属性生成对应形状。
九、更新成本从哪里来
可能触发 Assembler 更新:
- 尺寸改变。
- Anchor 改变。
- SpriteFrame 改变。
- Fill 参数改变。
- 顶点颜色改变。
- 自定义渲染属性改变。
仅仅移动 Node 通常主要影响变换,但具体是否重建几何还要看组件和引擎缓存策略。
十、案例:Filled 进度条
ts
this.progressBar.fillRange = value;可能发生:
text
Fill 参数改变
↓
Assembler 重新计算可见区域
↓
更新顶点或索引
↓
Renderer 使用新的几何如果进度条数量很大且每帧都更新,需要评估更新频率和显示数量。
十一、常见误区
误区一:顶点就是屏幕像素
顶点还要经过矩阵、光栅化和 Fragment Shader 才形成像素。
误区二:UV 表示世界坐标
UV 是纹理采样坐标。
误区三:改变颜色一定重新生成完整几何
可能只更新顶点颜色或材质输入,具体看实现。
误区四:DrawCall 数量等于顶点数量
一个 DrawCall 可以提交很多顶点,二者是不同指标。
十二、深入推导:用一个四边形理解顶点与索引
假设一个宽 100、高 50、Anchor 在中心的 Simple Sprite,不考虑旋转缩放,它的局部顶点可以写成:
text
v0 = (-50, -25)
v1 = ( 50, -25)
v2 = ( 50, 25)
v3 = (-50, 25)如果 Frame 占据独立 Texture 的完整区域,概念 UV 是:
text
uv0 = (0, 0)
uv1 = (1, 0)
uv2 = (1, 1)
uv3 = (0, 1)两个三角形可以使用索引:
text
0, 1, 2
0, 2, 3索引中的 0 和 2 被复用,所以不必为第二个三角形再复制两个完全相同的顶点。实际顶点顺序、坐标原点和 UV 方向以 Creator 2.4.x 当前 Assembler 为准,这个例子用于理解数据关系,而不是要求硬编码内部数组。
如果 Anchor 改成左下角,局部顶点范围会从 [-50, 50] × [-25, 25] 变为接近 [0, 100] × [0, 50]。Node Position 没变,几何相对 Position 的分布却变了。
Simple Sprite 通常可用四个顶点和两个三角形表达:
text
v3 ----- v2
| / |
| / |
| / |
v0 ----- v1一种索引排列:
text
triangle 1: 0, 1, 2
triangle 2: 0, 2, 3每个顶点至少需要关联:
text
position:画在哪里
uv:采样纹理哪里
color:怎样调制颜色/透明度具体顶点格式、分量和内存布局以 Creator 2.4.x 当前渲染实现为准。
十三、为什么索引可以复用顶点
如果两个三角形各自保存三个顶点,需要六份顶点;索引让它们共享对角线端点,只保存四份顶点和六个较小索引。
对复杂网格:
text
顶点缓冲:属性数据
索引缓冲:三角形如何引用顶点减少重复只是一个收益;索引还明确拓扑与绘制顺序。
十四、Creator 2.4.x 实验:用 Sprite Type 观察几何变化
实验不访问私有 Assembler 字段,只通过公开组件行为和 Profiler 推断数据更新。
场景
创建三组各 100 个 Sprite:
text
SimpleGroup
SlicedGroup
FilledGroup三组使用同一 Atlas/材质,尺寸相近。
AssemblerCostProbe.ts
ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class AssemblerCostProbe extends cc.Component {
@property([cc.Sprite])
filledSprites: cc.Sprite[] = [];
@property(cc.Node)
movingRoot: cc.Node = null;
private animateFill = false;
private animateMove = false;
private time = 0;
toggleFill() {
this.animateFill = !this.animateFill;
}
toggleMove() {
this.animateMove = !this.animateMove;
}
update(dt: number) {
this.time += dt;
if (this.animateFill) {
const value = (Math.sin(this.time) + 1) * 0.5;
for (const sprite of this.filledSprites) {
sprite.fillRange = value;
}
}
if (this.animateMove) {
this.movingRoot.x = Math.sin(this.time) * 50;
}
}
}对照方法
- 全静止,记录基线。
- 只动画一个父节点位置。
- 只动画 100 个 Filled Sprite。
- 降低 Filled 更新频率,再比较视觉与帧时间。
- 保持 DrawCall 相近,观察 CPU 数据准备是否不同。
实验不能推出所有设备固定倍率;它用于证明“同 DrawCall、同节点数也可能有不同几何更新成本”。
十五、Sliced、Tiled、Filled 怎样改变几何
三者解决的问题不同:
text
Sliced
→ 保持四角不拉伸,中间区域伸缩
→ 需要把矩形划成九个区域
Tiled
→ 按原块重复铺满目标尺寸
→ 尺寸越大,重复单元和几何可能越多
Filled
→ 只显示一定比例或角度
→ fillStart / fillRange 改变可见边界这就是为什么“所有 Sprite 都是四个顶点”不成立。Simple 常能用四顶点表达,Sliced、Tiled 和 Filled 可能根据类型和参数生成不同几何。
圆形 Filled 每帧更新时,问题也不只是赋值:
text
fillRange 改变
→ 重新求圆弧边界
→ 更新顶点/UV/索引
→ 写入批次缓冲如果 200 个冷却图标同时更新,即使 DrawCall 仍然很少,CPU 几何更新也可能成为瓶颈。
Sliced
九宫格把矩形拆成多个区域,角不拉伸、边单轴拉伸、中间拉伸。需要比 Simple 更多顶点和索引。
Tiled
按区域重复纹理,节点越大、tile 越小,可能需要更多格子和几何;实际实现和限制以版本为准。
Filled
根据 Horizontal、Vertical 或 Radial 等模式改变可见几何/UV。Radial 的边界穿过哪些角会影响生成结果。
这解释了组件语义如何进入 Assembler,而不是“所有 Sprite 都只是四个点”。
十六、顶点颜色如何进入最终像素
四个顶点可以携带不同颜色。Vertex Shader 把颜色传给光栅化阶段,三角形内部的颜色被插值,再交给 Fragment Shader 与纹理采样结果组合:
text
顶点颜色
→ 光栅化插值
→ 每个片元得到颜色
→ textureColor × vertexColor
→ Blend 到目标因此多个 Sprite 颜色不同,不必然为每个对象复制 Material。如果颜色可以进入兼容的顶点格式,它们仍可能共享 Shader、Texture 和 Render State。
Node/Sprite 颜色通常被编码到顶点属性,Fragment Shader 再与纹理采样相乘:
text
finalColor ≈ textureColor × vertexColor实际公式还受材质、预乘 Alpha、颜色空间和 shader 影响。
大量节点每帧改颜色,可能更新顶点数据;这与只在 shader 中使用统一参数又是不同成本模型。
十七、真实项目故障:200 个圆形冷却同时更新
现象
团战界面有 200 个单位技能图标,每帧设置 Radial Filled fillRange。DrawCall 不高,CPU 却明显上升。
根因
每个图标都持续要求几何/UV 更新,即使剩余时间在两帧之间的视觉差异几乎不可见。
修复
- 冷却显示以 10~20 Hz 更新,而不是强制每渲染帧;
- 只更新屏幕内或重要单位;
- 数值未跨显示阈值时不写属性;
- 评估 shader 方案,但同时考虑材质实例与合批;
- 对比 CPU、DrawCall 和视觉误差。
十八、三种错误优化
直接改私有顶点数组
版本升级或组件刷新会覆盖,且绕过 dirty/批次协议。
认为减少顶点一定减少 DrawCall
顶点数与批次数是两个维度。大量小批次即使每批四顶点,CPU 提交仍可能高。
为避免重建永远缓存所有几何
缓存会占内存,并且在尺寸、Frame、颜色或材质变化时需要失效策略。
十九、练习、推导答案与版本边界
Assembler 是 Creator 2.4.x 渲染内部扩展点,课程主线只讲职责。自定义 Assembler 必须固定引擎版本并阅读对应源码,不能把私有字段当跨版本 API。
- 四边形为何通常用六个索引? 两个三角形各需要三个索引,并共享四个顶点。
- UV 与 position 分别解决什么? position 决定几何位置,UV 决定该顶点对应 Texture 的采样位置。
- Filled 每帧更新为何可能比移动父节点贵? Filled 可能重建每个组件的局部几何/UV,父移动更多是共享变换链更新。
- 顶点少能否保证 GPU 快? 不能。还受片元覆盖、shader、纹理带宽、状态切换和 DrawCall 影响。
能力验收
你应该能手写一个四边形的四个顶点与六个索引,解释位置、UV、颜色如何进入 Shader,并通过 Sprite Type 与更新频率实验区分几何重建成本和 DrawCall。
本课总结
text
Assembler 是组件和 Renderer 之间的翻译层。
顶点描述几何位置。
UV 描述纹理采样位置。
颜色携带顶点表现参数。
索引描述三角形连接关系。十四、下一课预告
text
Lesson043|Label 为什么通常比 Sprite 更复杂?