UI(Sprite) 利用 Property Atlas 合批

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 (参数贴图) 取出所属参数并计算。

  • 属性贴图的储存格式

    explain_props_texture_formate

    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 (参数贴图)

    • effectUUIDinstanceID,利用 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 (参数贴图)

范例专案下载

参考文献

15赞

可以,好久没看到一些新思路了

1赞

2.x也实现一个

1赞

强无敌。先收藏一个

1赞

在我的收藏夹吃灰吧!

1赞

感谢大老们的支持,小弟也才刚使用CC半年的时间,有很多地方也都是拜读版上的大老们的分享。

用到的shader 都要传入PropsTexture还是说所有的共用一个

nice 学习新思路了。以前都是用扩展顶点属性

1赞

每種 Shader 效果都自己一組的靜態變數。

这就是滤镜系统吗?

不算吧,只是一種 Sprite 的可合批客製 Effect 方法之一。

强,mark一波

1赞