【分享】CocosCreator3.x 应用在UI(Sprite) 上的 shader(.effect) 的合批,通过自定义顶点参数

首发于 (一)【前言】 CocosCreator3.x 应用在UI(Sprite) 上的 shader(.effect) 的合批,通过自定义顶点参数 - bakabird1998 - 博客园 (cnblogs.com)


先上效果,用了六个参数不同的 RoateSprite,DC总共为4,分别是:背景 + 文字 + RotateSprite + Sprite。RotateSprite 合批成功!

前言

为啥要合批

减少DC

什么是自定义顶点参数

通过 几何体实例化 特性(GPU Instancing)可使 GPU 批量绘制模型相同且材质相同的渲染对象。如果我们想在不打破这一特性的情况下单独修改某个对象的显示效果,就需要通过自定义几何体实例化属性。

参考文档

UI(Sprite) 怎么你了?

按照文档中的说法,需要用到 MeshRenderer 来设置自定义属性。
但 UI(Sprite) 并没有这个组件,也就无法按照文档来实现自定义 UI shader 的合批。

但事情还没完,根据社区成员的分享,我们有更 hardcore 一点的方式来完成这些事情,接下来是具体的操作步骤。

具体操作步骤

接下来以一个制造旋转效果的 shader 为例子,提供了这些参数的设置:

  1. 旋转速度 float
  2. 旋转中心位置 vec2
  3. 逆时针/顺时针 bool
  4. 扭曲度 float

并在使用的贴图一致的前提下并且参数不同的值都能够合批。

最终项目可以从 GITHUB 获取。
CCC版本:3.8.0

深入了解可以阅读后续的 参考资料 及 源码阅读。

第一步、shader(.effect)

1. 将 builtin-sprite.effect 复制一份出来,重命名为 rotate-sprite.effect.

  • builtin-sprite.effect 是 Sprite 组件默认使用的 shader(.effect)。

在 Assets面板 中搜索 builtin-sprite 即可找到 builtin-sprite.effect。
image

复制一份到项目的 assets 中。
image

重命名为 rotate-sprite.effect。
image

2. 打开 rotate-sprite.effect,在 顶点着色器 sprite-vs 上定义顶点参数,并传递给 片元着色器 sprite-fs。

顶点着色器 sprite-vs 如下,其中如 in vec3 a_position 这类以 a_ 就是使用的顶点参数。

CCProgram sprite-vs %{
  ...
  // 😀 使用的 顶点参数
  in vec3 a_position;
  in vec2 a_texCoord;
  in vec4 a_color;

  // 😀 传递给 片元着色器 的变量
  out vec4 color;
  out vec2 uv0;

  vec4 vert () {
    ...
    // 😀 给变量赋值
    uv0 = a_texCoord;
    #if SAMPLE_FROM_RT
      CC_HANDLE_RT_SAMPLE_FLIP(uv0);
    #endif
    color = a_color;

    return pos;
  }
}%

添加我们要用到的顶点参数后。

CCProgram sprite-vs %{
  ...
  in vec3 a_position;
  in vec2 a_texCoord;
  in vec4 a_color;
  // 😀😀😀 开始 😀😀😀
  // 旋转速度
  in float a_rotateSpeed;
  // 旋转中心
  in vec2 a_rotateCenter;
  // 是否顺时针旋转
  in float a_clockwise;
  // 扭曲度
  in float a_distort;
  // 😀😀😀 结束 😀😀😀
  ...
}%

因为旋转效果要在 片元着色器 sprite-fs 中实现,因此我们要把这些 顶点参数的值 传递给 片元着色器 sprite-fs。

在 顶点着色器 sprite-vs 中定义对应的 out 输出变量。

CCProgram sprite-vs %{
  ...
  out vec4 color;
  out vec2 uv0;
  // 😀😀😀 开始 😀😀😀
  // 旋转速度
  out float rotateSpeed;
  // 旋转中心
  out vec2 rotateCenter;
  // 是否顺时针旋转
  out float clockwise;
  // 扭曲度
  out float distort;
  // 😀😀😀 结束 😀😀😀
  ...
}%

在 顶点着色器 sprite-vs 的函数中完成对 out 输出变量 的赋值。

CCProgram sprite-vs %{
  ...
  vec4 vert () {
    ...
    uv0 = a_texCoord;
    #if SAMPLE_FROM_RT
      CC_HANDLE_RT_SAMPLE_FLIP(uv0);
    #endif
    color = a_color;
    // 😀😀😀 开始 😀😀😀
    rotateSpeed = a_rotateSpeed;
    rotateCenter = a_rotateCenter;
    clockwise = a_clockwise;
    distort = a_distort;
    // 😀😀😀 结束 😀😀😀

    return pos;
  }
}%

以上步骤,就完成了在 顶点着色器 sprite-vs 上的需要编辑内容。

3. 在 片元着色器 sprite-fs 上接收从 顶点着色器 sprite-vs 传递过来的顶点参数。

片元着色器 sprite-fs 如下。其中 in vec4 color 这样的 in 输入变量,从 顶点着色器 sprite-vs 中接收了对应变量。

CCProgram sprite-fs %{
  precision highp float;
  #include <builtin/internal/embedded-alpha>
  #include <builtin/internal/alpha-test>

  // 😀 从 sprite-vs 中接收的变量 part1
  in vec4 color;

  #if USE_TEXTURE
    // 😀 从 sprite-vs 中接收的变量 part2
    in vec2 uv0;
    #pragma builtin(local)
    layout(set = 2, binding = 12) uniform sampler2D cc_spriteTexture;
  #endif

  vec4 frag () {
    vec4 o = vec4(1, 1, 1, 1);

    #if USE_TEXTURE
      o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0);
      #if IS_GRAY
        float gray  = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b;
        o.r = o.g = o.b = gray;
      #endif
    #endif

    o *= color;
    ALPHA_TEST(o);
    return o;
  }
}%

增加对应我们新增顶点参数的 in 输入变量。

CCProgram sprite-fs %{
  ...
  in vec4 color;
  // 😀😀😀 开始 😀😀😀
  // 旋转速度
  in float rotateSpeed;
  // 旋转中心
  in vec2 rotateCenter;
  // 是否顺时针旋转
  in float clockwise;
  // 扭曲度
  in float distort;
  // 😀😀😀 结束 😀😀😀

  #if USE_TEXTURE
    in vec2 uv0;
    #pragma builtin(local)
    layout(set = 2, binding = 12) uniform sampler2D cc_spriteTexture;
  #endif
  ...
}%

4. 使用参数并实现效果。

如何实现不是本文关注点,这里直接给出完成后的 片元着色器 sprite-fs 的代码。

CCProgram sprite-fs %{
  precision highp float;
  #include <builtin/uniforms/cc-global>
  #include <builtin/internal/embedded-alpha>
  #include <builtin/internal/alpha-test>

  in vec4 color;
  // 旋转速度
  in float rotateSpeed;
  // 旋转中心
  in vec2 rotateCenter;
  // 是否顺时针旋转
  in float clockwise;
  // 扭曲度
  in float distort;

  #define PI 3.1415926535897932384626433832795

  #if USE_TEXTURE
    in vec2 uv0;
    #pragma builtin(local)
    layout(set = 2, binding = 12) uniform sampler2D cc_spriteTexture;
  #endif

  float yOflineOnX(float k, float b, float x) {
    return k * x + b;
  }

  float xOflineOnY(float k, float b, float y) {
    return (y - b) / k;
  }

  bool isBetween(float value, float min, float max) {
    return value >= min && value <= max;
  }

  vec2 findFarthestFittingPoint(vec2 dir, vec2 rotateCenter) {
    vec2 farFitPoint = vec2(0.0);
    float len4fit = 0.0;
    float xSign = sign(dir.x);
    float slope = dir.y / (xSign * max(abs(dir.x), 0.00000001));
    slope = clamp(slope, -9999999999.9, 9999999999.9);
    float yIntercept = rotateCenter.y - slope * rotateCenter.x;
    yIntercept = clamp(yIntercept, -9999999999.9, 9999999999.9);
    
    vec2 checkVal = vec2(0.0, yOflineOnX(slope, yIntercept, 0.0));
    vec2 check2center = checkVal - rotateCenter;
    if (isBetween(checkVal.y, 0.0, 1.0) && dot(dir, check2center) > 0.0) {
      farFitPoint = checkVal;
      len4fit = length(check2center);
    }
    checkVal = vec2(1.0, yOflineOnX(slope, yIntercept, 1.0));
    check2center = checkVal - rotateCenter;
    float len4check = length(check2center);
    if (isBetween(checkVal.y, 0.0, 1.0) && dot(dir, check2center) > 0.0 && len4check > len4fit) {
      farFitPoint = checkVal;
      len4fit = len4check;
    }
    checkVal = vec2(xOflineOnY(slope, yIntercept, 0.0), 0.0);
    check2center = checkVal - rotateCenter;
    len4check = length(check2center);
    if (isBetween(checkVal.x, 0.0, 1.0) && dot(dir, check2center) > 0.0 && len4check > len4fit) {
      farFitPoint = checkVal;
      len4fit = len4check;
    }
    checkVal = vec2(xOflineOnY(slope, yIntercept, 1.0), 1.0);
    check2center = checkVal - rotateCenter;
    len4check = length(check2center);
    if (isBetween(checkVal.x, 0.0, 1.0) && dot(dir, check2center) > 0.0 && len4check > len4fit) {
      farFitPoint = checkVal;
      len4fit = len4check;
    }
    return farFitPoint;
  }

  vec2 rotateVector(vec2 vec, float angle) {
    return vec2(
      vec.x * cos(angle) - vec.y * sin(angle),
      vec.x * sin(angle) + vec.y * cos(angle)
    );
  }

  float easeOutBounce(float x){
    float n1 = 7.5625 * distort;
    float d1 = 2.75;

    if (x < 1.0 / d1) {
        return n1 * x * x;
    } else if (x < 2.0 / d1) {
        return n1 * (x -= 1.5 / d1) * x + 0.75;
    } else if (x < 2.5 / d1) {
        return n1 * (x -= 2.25 / d1) * x + 0.9375;
    } else {
        return n1 * (x -= 2.625 / d1) * x + 0.984375;
    }
  }

  float easeInCirc(float x) {
    return 1.0 - sqrt(1.0 - pow(x, 2.0 * distort));
  }

  vec4 frag () {
    vec4 o = vec4(1.0);

    #if USE_TEXTURE
      float rotateRad = sign(clockwise) * cc_time.x * PI * rotateSpeed;

      // 通过 uv转换 来实现旋转
      vec2 dir = uv0 - rotateCenter;
      vec2 farFitPoint = findFarthestFittingPoint(dir, rotateCenter);
      float percent = length(dir) / length(farFitPoint - rotateCenter);
      vec2 dirRotated = rotateVector(dir, rotateRad);
      farFitPoint = findFarthestFittingPoint(dirRotated, rotateCenter);
      vec2 uvRotated = rotateCenter + (farFitPoint - rotateCenter) * easeInCirc(percent);
      o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uvRotated);
      #if IS_GRAY
        float gray  = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b;
        o.r = o.g = o.b = gray;
      #endif
    #endif

    o *= color;
    ALPHA_TEST(o);
    return o;
  }
}%

5. 创建 rotate-sprite.mat,并使用 rotate-sprite.effect。

创建新的 material。

重命名为 rotate-sprite.mat。
image

修改材质所使用的 shader(.effect),选中 rotate-sprite.effect。

并激活 “USE TEXTURE”,然后保存设置即可。

第二步、编写 RotateSprite.ts (一)

1. 新建一个 ts 脚本,命名为 RotateSprite.ts。
image

2. 删掉 start 和 update,并其继承 Sprite
image

3. 编写顶点参数相关逻辑

回顾 shader(.effect) 定义的顶点参数。
image

对应列出如下表,其中 gfx.Format 的值可以 查表 得。

字段 glsl类型 gfx.Format
in vec3 a_position vec3 RGB32F
in vec2 a_texCoord vec2 RG32F
in vec4 a_color vec4 RGBA32F
in float a_rotateSpeed float R32F
in vec2 a_rotateCenter vec2 RG32F
in float a_clockwise float R32F
in float a_distort float R32F

:watermelon:
参考源码,对照该表,复写 requestRenderData。(请按需 引入 import)

@ccclass('RotateSprite')
export class RotateSprite extends Sprite {
    public requestRenderData(drawInfoType?: __private._cocos_2d_renderer_render_draw_info__RenderDrawInfoType): RenderData {
        // 😀😀😀 开始 😀😀😀
        const data = RenderData.add([
            new gfx.Attribute(gfx.AttributeName.ATTR_POSITION, gfx.Format.RGB32F),
            new gfx.Attribute(gfx.AttributeName.ATTR_TEX_COORD, gfx.Format.RG32F),
            new gfx.Attribute(gfx.AttributeName.ATTR_COLOR, gfx.Format.RGBA32F),
            new gfx.Attribute("a_rotateSpeed", gfx.Format.R32F),
            new gfx.Attribute("a_rotateCenter", gfx.Format.RG32F),
            new gfx.Attribute("a_clockwise", gfx.Format.R32F),
            new gfx.Attribute("a_distort", gfx.Format.R32F),
        ]);
        // 😀😀😀 结束 😀😀😀
        data.initRenderDrawInfo(this, drawInfoType);
        this._renderData = data;
        return data;
    }
}

:watermelon::watermelon:

增加 顶点参数 对应的成员和属性。(请按需 引入 import)

@ccclass('RotateSprite')
export class RotateSprite extends Sprite {
    @property({ type: CCFloat })
    private _rotateSpeed: number = 1;
    @property({ type: Vec2 })
    private _rotateCenter: Vec2 = new Vec2(0.5, 0.5);
    @property({ type: CCBoolean })
    private _isClockWise: boolean = true;
    @property({ type: CCFloat })
    private _distort: number = 1;

    public get rotateSpeed(): number {
        return this._rotateSpeed;
    }

    @property({ type: CCFloat })
    public set rotateSpeed(value: number) {
        if (this._rotateSpeed == value) return;
        this._rotateSpeed = value;
    }

    public get rotateCenter(): Vec2 {
        return this._rotateCenter;
    }

    @property({ type: Vec2 })
    public set rotateCenter(value: Vec2) {
        if (this._rotateCenter.equals(value)) return;
        this._rotateCenter.set(value);
    }

    public get isClockWise(): boolean {
        return this._isClockWise;
    }

    @property({ type: CCBoolean })
    public set isClockWise(value: boolean) {
        if (this._isClockWise == value) return;
        this._isClockWise = value;
    }

    public get distort(): number {
        return this._distort;
    }

    @property({ type: CCFloat })
    public set distort(value: number) {
        if (this._distort == value) return;
        this._distort = value;
    }
    ...
}

到此为止,针对 RotateSprite 的编写暂告一段落,待我们完成组装器 rotateAssembler.ts 的编写后再来补充后续。

第三步、编写 rotateAssembler.ts

1. 将 simple Assembler 复制出来。

打开 CCC 源码文件夹。

在编辑器中打开 resources 文件夹。
image

准确导航到 Sprite 使用的 simple.ts。注意参考下图确定路径。

将文件复制到项目 assets 中。
image

重命名为 rotateAssembler.ts。
image

2. 修正代码报错。

首先将变量名从 simple 修改为 rotateAssembler
image

:watermelon:

首先将原本的 引入 import 全部注释掉

然后增加如下 引入 import

import { DynamicAtlasManager, IAssembler, IRenderData, RenderData, Sprite } from "cc";

:watermelon::watermelon:

针对 dynamicAtlasManager.packToDynamicAtlas(sprite, frame); 的报错。
将其改为 DynamicAtlasManager.instance.packToDynamicAtlas(sprite, frame); 即可。

:watermelon::watermelon::watermelon:

针对这种 类型定义 的报错,直接把它们改成 any

image

image

至此应该就没有报错了。

3. 按照 rotate-sprite.effect 的 顶点参数定义 补充代码。

再次回顾 shader(.effect) 定义的顶点参数。
image

对应列出表。

字段 占位 偏移
in vec3 a_position 3 0
in vec2 a_texCoord 2 3
in vec4 a_color 4 5
in float a_rotateSpeed 1 9
in vec2 a_rotateCenter 2 10
in float a_clockwise 1 12
in float a_distort 1 13

:watermelon:

首先,如果 updateUVs 在设置顶点参数值时写死了偏移值。

image

那么我们需要修改 updateUVs 成下面的样子。

    updateUVs(sprite: Sprite) {
        if (!sprite.spriteFrame) return;
        const renderData = sprite.renderData!;
        const vData = renderData.chunk.vb;
        const uv = sprite.spriteFrame.uv;
        // 😀😀😀 开始 😀😀😀
        let offset = 3;
        let count = 0;
        for (let i = 0; i < 4; i++, offset += renderData.floatStride) {
            vData[offset] = uv[count++];
            vData[offset + 1] = uv[count++];
        }
        // 😀😀😀 结束 😀😀😀
    },

:watermelon::watermelon:

然后是 in float a_rotateSpeed 旋转速度,占位 1,偏移位 9。
增加对应函数 updateRotateSpeed(sprite: RotateSprite) 完成对 a_rotateSpeed 的赋值。

    updateRotateSpeed(sprite: RotateSprite) {
        const renderData = sprite.renderData!;
        const vData = renderData.chunk.vb;
        let offset = 9;
        for (let i = 0; i < 4; i++, offset += renderData.floatStride) {
            vData[offset] = sprite.rotateSpeed;
        }
    }

:watermelon::watermelon::watermelon:

然后是 in vec2 a_rotateCenter 旋转中心,占位 2,偏移位 10。
增加对应函数 updateRotateCenter(sprite: RotateSprite) 完成对 a_rotateCenter 的赋值。

    updateRotateCenter(sprite: RotateSprite) {
        const renderData = sprite.renderData!;
        const vData = renderData.chunk.vb;
        let offset = 10;
        for (let i = 0; i < 4; i++, offset += renderData.floatStride) {
            vData[offset] = sprite.rotateCenter.x;
            vData[offset + 1] = sprite.rotateCenter.y;
        }
    },

:watermelon::watermelon::watermelon::watermelon:

然后是 in float a_clockwise 是否顺时针旋转,占位 1,偏移位 12。
增加对应函数 updateaClockwise(sprite: RotateSprite) 完成对 a_clockwise 的赋值。

    updateaClockwise(sprite: RotateSprite) {
        const renderData = sprite.renderData!;
        const vData = renderData.chunk.vb;
        let offset = 12;
        for (let i = 0; i < 4; i++, offset += renderData.floatStride) {
            vData[offset] = sprite.isClockWise ? 1 : -1;
        }
    },

:watermelon::watermelon::watermelon::watermelon::watermelon:

然后是 in float a_distort 扰乱程度,占位 1,偏移位 13。
增加对应函数 updateaDistort(sprite: RotateSprite) 完成对 a_distort 的赋值。

    updateaDistort(sprite: RotateSprite) {
        const renderData = sprite.renderData!;
        const vData = renderData.chunk.vb;
        let offset = 13;
        for (let i = 0; i < 4; i++, offset += renderData.floatStride) {
            vData[offset] = sprite.distort;
        }
    },

4. 增加 updateCustomVertexData 方法

export const rotateAssembler: IAssembler = {
  ...
  updateCustomVertexData(sprite: Sprite) {
    this.updateRotateSpeed(sprite);
    this.updateRotateCenter(sprite);
    this.updateaClockwise(sprite);
    this.updateaDistort(sprite);
  },
  ...
}

5. 修改 updateRenderData 方法 和 fillBuffers 方法

export const rotateAssembler: IAssembler = {
  ...
  updateRenderData(sprite: Sprite) {
      const frame = sprite.spriteFrame;

      // dynamicAtlasManager.packToDynamicAtlas(sprite, frame);
      DynamicAtlasManager.instance.packToDynamicAtlas(sprite, frame);
      this.updateUVs(sprite);// dirty need
      //this.updateColor(sprite);// dirty need

      const renderData = sprite.renderData;
      if (renderData && frame) {
          if (renderData.vertDirty) {
              this.updateVertexData(sprite);
              // 😀😀😀 开始 😀😀😀
              this.updateCustomVertexData(sprite);
              // 😀😀😀 结束 😀😀😀
          }
          renderData.updateRenderData(sprite, frame);
      }
  },
  ...
  fillBuffers(sprite: Sprite, renderer: any) {
    if (sprite === null) {
        return;
    }

    const renderData = sprite.renderData!;
    const chunk = renderData.chunk;
    if (sprite.node.hasChangedFlags || renderData.vertDirty) {
        // const vb = chunk.vertexAccessor.getVertexBuffer(chunk.bufferId);
        this.updateWorldVerts(sprite, chunk);
        // 😀😀😀 开始 😀😀😀
        this.updateCustomVertexData(sprite);
        // 😀😀😀 结束 😀😀😀
        renderData.vertDirty = false;
    }
    ...
  }
  ...
}

到此为止,针对 rotateAssembler.ts 的编写结束,接下来我们继续回到 RotateSprite 进行编写。

第四步、编写 RotateSprite.ts (二)

再次回顾 shader(.effect) 定义的顶点参数。
image

1. 增加自定义顶点参数的对应更新方法。

@ccclass('RotateSprite')
export class RotateSprite extends Sprite {
    @property({ type: CCFloat })
    public set rotateSpeed(value: number) {
        if (this._rotateSpeed == value) return;
        this._rotateSpeed = value;
        // 😀😀😀 开始 😀😀😀
        this._updateRotateSpeed();
        // 😀😀😀 结束 😀😀😀
    }
    ...
    @property({ type: Vec2 })
    public set rotateCenter(value: Vec2) {
        if (this._rotateCenter.equals(value)) return;
        this._rotateCenter.set(value);
        // 😀😀😀 开始 😀😀😀
        this._updateRotateCenter();
        // 😀😀😀 结束 😀😀😀
    }
    ...
    @property({ type: CCBoolean })
    public set isClockWise(value: boolean) {
        if (this._isClockWise == value) return;
        this._isClockWise = value;
        // 😀😀😀 开始 😀😀😀
        this._updateaClockwise();
        // 😀😀😀 结束 😀😀😀
    }
    ...
    @property({ type: CCFloat })
    public set distort(value: number) {
        if (this._distort == value) return;
        this._distort = value;
        // 😀😀😀 开始 😀😀😀
        this._updateDistort();
        // 😀😀😀 结束 😀😀😀
    }
    ...
    private _updateRotateSpeed() {
        if (this._assembler) {
            this._assembler.updateRotateSpeed(this);
            this.markForUpdateRenderData();
        }
    }

    private _updateRotateCenter() {
        if (this._assembler) {
            this._assembler.updateRotateCenter(this);
            this.markForUpdateRenderData();
        }
    }

    private _updateaClockwise() {
        if (this._assembler) {
            this._assembler.updateaClockwise(this);
            this.markForUpdateRenderData();
        }
    }

    private _updateDistort() {
        if (this._assembler) {
            this._assembler.updateaDistort(this);
            this.markForUpdateRenderData();
        }
    }
}

2. 增加 _updateCustomVertexData 方法。

@ccclass('RotateSprite')
export class RotateSprite extends Sprite {
    ...
    private _updateCustomVertexData() {
        this._updateRotateSpeed();
        this._updateRotateCenter();
        this._updateaClockwise();
        this._updateDistort();
    }
    ...
}

3. 复写 _flushAssembler 方法。

参考源码,对照该表,复写 _flushAssembler。(请按需 引入 import)

@ccclass('RotateSprite')
export class RotateSprite extends Sprite {
    ...
    protected _flushAssembler() {
        // 😀😀😀 开始 😀😀😀
        const assembler = rotateAssembler;
        // 😀😀😀 结束 😀😀😀

        if (this._assembler !== assembler) {
            this.destroyRenderData();
            this._assembler = assembler;
        }

        if (!this._renderData) {
            if (this._assembler && this._assembler.createData) {
                this._renderData = this._assembler.createData(this);
                this._renderData!.material = this.getRenderMaterial(0);
                this.markForUpdateRenderData();
                if (this.spriteFrame) {
                    this._assembler.updateUVs(this);
                }
                this._updateColor();
                // 😀😀😀 开始 😀😀😀
                this._updateCustomVertexData();
                // 😀😀😀 结束 😀😀😀
            }
        }

        // 😀😀😀 注释掉下面几行 😀😀😀
        // // Only Sliced type need update uv when sprite frame insets changed
        // if (this._spriteFrame) {
        //     if (this._type === SpriteType.SLICED) {
        //         this._spriteFrame.on(SpriteFrame.EVENT_UV_UPDATED, this._updateUVs, this);
        //     } else {
        //         this._spriteFrame.off(SpriteFrame.EVENT_UV_UPDATED, this._updateUVs, this);
        //     }
        // }
    }
    ...
}

4. 增加一个小处理,否则在移动端上会显示错误。

@ccclass('RotateSprite')
export class RotateSprite extends Sprite {
    ...
    public start(): void {
        this.scheduleOnce(() => {
            this._updateCustomVertexData();
        }, 0);
    }
    ...
}

第五步、测试

1. 使用 RotateSprite

新建一个 Sprite 对象。
image

删掉原有的 Sprite 组件。
image

添加 RotateSprite 组件。
image

将 CustomMaterial 设置为 rotate-sprite.mtl;
并将 SpriteFrame 改成要用的图片,这里我使用一个叫 door 的图片。
image

自定义参数对外接口。
image

能在 Scene 中看到这个。
1663727-20231027100738352-294359472

2. 关闭所用图片的 Packable

关掉这个 RotateSprite 所用图片的 Packable。
否则针对 uv 的转换会因为合图逻辑变得异常。
image

3. 玩起来吧!

旋转速度2

image
1663727-20231027101150866-1390134852

旋转速度3

image
1663727-20231027101241733-314154023

调整扰乱度

image
1663727-20231027101403025-2016079366

4. DC测试

搭了一个这样的环境

其中文字的 CacheMode 用了 CHAR
image

测试,用了六个参数不同的 RoateSprite,DC总共为4,分别是:背景 + 文字 + RotateSprite + Sprite(原图)。RotateSprite 合批成功!


参考资料

资料1

来源:动态生成图片的需求
用户:homym(tkhoi01281)

3.x 版自定参数我是利用createMesh方法去生成ui,因为createMesh就有自定义顶点参数的方法

这个改动其实是可以弄一个新sprite 来继承老spirte, 然后把引擎里的simple.ts,splice.ts 等assembler 拷一份出来, 放到自己项目, 然后新sprite 复写一下assembler的初始化改为取你魔改的simple.ts,splice.ts就能弄一个新的sprite跟老的sprite一起用又不用改动引擎, 唯一缺点是新sprite和老sprite不能共同一个material

针对提问 “您好, 我也是想在3.x中使用自定义顶点合批。你是使用的 utils.createMesh 方法吗。”
不是,3.x版以sprite为例,需先继续Sprite,然后复写函数requestRenderData,这个父函数可以查看引擎源码的ui-renderer.ts ,其中RenderData.add([添加你想要的参数]) 比如:
RenderData.add([
new gfx.Attribute(gfx.AttributeName.ATTR_POSITION, gfx.Format.RGB32F),
new gfx.Attribute(gfx.AttributeName.ATTR_TEX_COORD, gfx.Format.RG32F),
new gfx.Attribute(gfx.AttributeName.ATTR_COLOR, gfx.Format.RGBA32F)
])
最后就是我上面回复的处理,把需要值塞到buffer里。

资料2

来源:【分享】自定义渲染合批之自定义顶点格式(附 Demo 和引擎源码解读)
用户:GT(caogtaa)

介绍了在 CocosCreator2.x 中怎么完成如题需求。


更多请阅读:
(四)【源码阅读】CocosCreator3.x 应用在UI(Sprite) 上的 shader(.effect) 的合批,通过自定义顶点参数 - bakabird1998 - 博客园 (cnblogs.com)

18赞

真腿哥,第一次看到这么清楚的

很强。mark一下

真●保姆级!!!!!教程!感谢分享

新年快乐~

mark mark

你干的好哇,my Friend :yum:

試過哥的方法,工作上需要客製化 Effect 但又需要考慮合批,對CC要更深入理解這篇真的很有幫助

1赞

确实是保姆级的教程了,安排的妥妥当当的 :kissing_heart: