UI(Sprite) 利用 Property Atlas 合批
依官方文件【2D 渲染组件合批规则说明】,Sprite 一但使用 customMaterial 合批 (batch) 就会被拆分。而同 Shader 不同参数想合批,正统是将参数带入顶点中。详细做法论坛上的 bakabird 大大提供保母级的教程 【分享】CocosCreator3.x 应用在UI(Sprite) 上的 shader(.effect) 的合批,通过自定义顶点参数。
这方法需对 Sprite 的 4 种顶点宣告模式 (SIMPLE、SLICE、TILED、FILLED) 作实现。那…还有其他方法可以不用动到修改顶点格式吗?
Propert Atlas
Peoperty Atlas
的特点在于,不同 Sprite 相同 Shader 效果下,将自己所属的参数储存在同一张PropsTexture (参数贴图)
中,渲染时透过索引于取出所属参数计算,如此就能利用引擎本身的合批规则减少Drawcall。
实践思路
-
对一 Shader 效果准备一张格式 RGBA32 的
PropsTexture (参数贴图)
。 -
每个 Sprite 在同一 Shader 效果下,自有所属唯一的 index。
-
借此 index 在渲染时对
PropsTexture (参数贴图)
取出所属参数并计算。 -
属性贴图的储存格式
PropsTexture (参数贴图)
的 width 决定一次能合批(batch) 多少个 sprite,例如:64 代表最多可以一次合批 (batch) 64 不同参数设定的 Sprite。
上代码
SpDemoEffect.ts
-
继承Sprite Component
@ccclass('SpDemoEffect') export class SpDemoEffect extends Sprite { //...略 }
-
static 参数
@ccclass('SpDemoEffect') export class SpDemoEffect extends Sprite { private static propsTexture: Texture2D | null = null; private static propBuffer: Float32Array | null = null; private static effectUUID: string[] = []; private static mat: Material | null = null; private static isDirty: boolean = false; private instanceID: number = -1; //...略 }
在同一 Shader 效果下,所有 Sprite 共用的参数:
-
propsTexture (参数贴图)
,,用来储存同一个 Shader 效果不同 Sprite 各自的设定参数。 -
propBuffer
,TypeScript 端参数 buffer,暂存参数并于laterUpdate()
检查异动同步PropsTexture (参数贴图)
。 -
effectUUID
与instanceID
,利用 CC 每个 Node 的 uuid 唯一性,给予当下 Sprite 一个唯一的instanceID
。this.instanceID = SpDemoEffect.effectUUID.findIndex((uuid) => uuid === this.node.uuid); if (this.instanceID === -1) { this.instanceID = SpDemoEffect.effectUUID.push(this.node.uuid) - 1; }
-
mat
,同 Sahder 效果共用一个材质。 -
isDirty
,参数异动的旗标。
-
-
建立参数贴图
const PROP_TEXTURE_SIZE = 128; // 定義屬性貼圖 width @ccclass('SpDemoEffect') export class SpDemoEffect extends Sprite { //...略 start() { if (!this.effectAsset) { warn("需指定 effect asset"); return; } // 利用 CC 每個 Node 的 uuid 唯一性,給予當下 Sprite 一個唯一的 `instanceID` this.instanceID = SpDemoEffect.effectUUID.findIndex((uuid) => uuid === this.node.uuid); if (this.instanceID === -1) { this.instanceID = SpDemoEffect.effectUUID.push(this.node.uuid) - 1; } // 利用 Sprite Component 中的 color 將 instanceID 傳入 Shader 效果中 this.color = new Color(this.instanceID % PROP_TEXTURE_SIZE, this.pixelsUsage, PROP_TEXTURE_SIZE, 255); if (SpDemoEffect.mat === null) { // 建立材質與屬性貼圖 PropsTexture (參數貼圖) const w = PROP_TEXTURE_SIZE; const h = this.pixelsUsage; SpDemoEffect.propBuffer = new Float32Array(w * h * 4); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const index = (x + (y * w)) * 4; SpDemoEffect.propBuffer[index] = 1; SpDemoEffect.propBuffer[index + 1] = 0; SpDemoEffect.propBuffer[index + 2] = 1; SpDemoEffect.propBuffer[index + 3] = 1; } } SpDemoEffect.propsTexture = new Texture2D(); SpDemoEffect.propsTexture.setFilters(Texture2D.Filter.NEAREST, Texture2D.Filter.NEAREST); SpDemoEffect.propsTexture.reset({ width: w, height: h, format: Texture2D.PixelFormat.RGBA32F, mipmapLevel: 0 }); SpDemoEffect.propsTexture.uploadData(SpDemoEffect.propBuffer); //...略 } //...略 } }
-
建立材质并绑定
PropsTexture (参数贴图)
,指定给customMaterial
参数const PROP_TEXTURE_SIZE = 128; @ccclass('SpDemoEffect') export class SpDemoEffect extends Sprite { //...略 start() { //...略 // 建立客制材質,綁定 `PropsTexture (參數貼圖)` 指定至 customMaterial 參數 if (SpDemoEffect.mat === null) { //...略 SpDemoEffect.mat = new Material(); SpDemoEffect.mat.initialize( { effectAsset: this.effectAsset, defines: {}, technique: 0 } ); SpDemoEffect.mat.setProperty('propsTexture', SpDemoEffect.propsTexture); } this.customMaterial = SpDemoEffect.mat; this.reflashParams(); } //...略 }
-
laterUpdate
,若有参数有异动时进行更新@ccclass('SpDemoEffect') export class SpDemoEffect extends Sprite { //...略 start() { //...略 } lateUpdate(deltaTime: number) { if (SpDemoEffect.isDirty) { SpDemoEffect.propsTexture!.uploadData(SpDemoEffect.propBuffer!); SpDemoEffect.isDirty = false; } } }
-
每个 Sprite 的
instanceID
利用 Sprite.color 传入 Shader 效果中,在 TypeScript 中编码this.color = new Color(this.instanceID, this.pixelsUsage, PROP_TEXTURE_SIZE, 255);
-
R 通道
this._instanceID
-
G 通道
pixelsUsage
,一个 pixel 有 4 个 float 可以保存参数,代表这个 Shader 效果参数用了几个 4 各 float。 -
B 通道
PROP_TEXTURE_SIZE
,参数贴图 Width。 -
A 通道
255
,设定为预设值不使用。
-
SpDemoEffect.effect
这个 Shader 效果为简单定义一个 effectColor
对原 Sprite 进行颜色相加。
-
代码如下
CCEffect %{ techniques: - name: default passes: - vert: sprite-vs:vert frag: sprite-fs:frag depthStencilState: depthTest: false depthWrite: false blendState: targets: - blend: true blendSrc: src_alpha blendDst: one_minus_src_alpha blendDstAlpha: one_minus_src_alpha rasterizerState: cullMode: none properties: &props alphaThreshold: { value: 0.5 } propsTexture: { value: white, editor: { type: sampler2D }} }% CCProgram sprite-vs %{ precision highp float; #include <cc-global> #if USE_LOCAL #include <cc-local> #endif #if SAMPLE_FROM_RT #include <common/common-define> #endif in vec3 a_position; in vec2 a_texCoord; in vec4 a_color; out vec4 color; out vec2 uv0; vec4 vert () { //...略 } }% CCProgram sprite-fs %{ precision highp float; #include <embedded-alpha> #include <alpha-test> #include <sprite-texture> #include "./chunks/util.chunk" in vec4 color; // 原 Sprite 屬性 color,用來當做 index 參數。 in vec2 uv0; uniform sampler2D propsTexture; vec4 frag () { // [記住] Sprite 原始的 color 屬性已經被拿去當作 index。 vec4 effectColor = getPropFromPropTexture(propsTexture, color, 0); // [小心思] index 編碼避免使用 a,因此會保留為 CC 中上一階 Canvas // 透明 a,讓自定義的效果依然能正常受影響。 effectColor = vec4(effectColor.rgb, effectColor.a * color.a); vec4 o = vec4(1, 1, 1, 1); o = CCSampleWithAlphaSeparated(cc_spriteTexture, uv0); // 顏色與 effectColor 相加 o = vec4(o.rgb + effectColor.rgb, o.a * effectColor.a); ALPHA_TEST(o); return o; } }%
-
在 Shader 中解码 Sprite.color
// propTexture: 參數贴图 // encodeIdx: 參數索引編碼 // idxOfProps: 效果中的參數索引 vec4 getPropFromPropTexture(sampler2D propsTexture, vec4 encodeIdx, int idxOfProps) { vec2 prop_uv = vec2((1.0/(encodeIdx.b * 255.0)) * (encodeIdx.r * 255.0), (1.0/(encodeIdx.g * 255.0)) * float(idxOfProps)); return texture(propsTexture, prop_uv); }
-
PropsTexture (参数贴图)
-
encodeColor
,传入的 Sprite.color,解码后即为instanceID
,贴图座标的u
用来存取PropsTexture (参数贴图)
。 -
idxOfProps
,解法后为 Shader 效果中的第几个参数,贴图座标的v
用来存取PropsTexture (参数贴图)
。
-
范例专案下载
-
范例完整代码在 GitHub CC3.SpriteEffect.DemoProject 。
-
上述概念在 GitHub CC3.SpriteEffect 实现了一个样板库,可依此延伸出各种 Sprite 效果合批方。