CocosCreator3.x 应用在UI(Sprite) 上的 shader(.effect) 的合批,通过自定义顶点参数(二)、具体操作步骤

发布时间 2023-10-26 14:03:36作者: bakabird1998

具体操作步骤

接下来以一个制造旋转效果的 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。

复制一份到项目的 assets 中。

重命名为 rotate-sprite.effect。

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;
  }
}%

最终 shader(.effect) 的 顶点着色器 sprite-vs 会是这样。

CCProgram sprite-vs %{
  precision highp float;
  #include <builtin/uniforms/cc-global>
  #if USE_LOCAL
    #include <builtin/uniforms/cc-local>
  #endif
  #if SAMPLE_FROM_RT
    #include <common/common-define>
  #endif
  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;

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

  vec4 vert () {
    vec4 pos = vec4(a_position, 1);

    #if USE_LOCAL
      pos = cc_matWorld * pos;
    #endif

    #if USE_PIXEL_ALIGNMENT
      pos = cc_matView * pos;
      pos.xyz = floor(pos.xyz);
      pos = cc_matProj * pos;
    #else
      pos = cc_matViewProj * pos;
    #endif

    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;
  }
}%

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>

  in vec4 color;

  #if USE_TEXTURE
    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。

修改材质所使用的 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

?

对应该表复写 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;
    }
}

??

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

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

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

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

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

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

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

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

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

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

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

第三步、编写 rotateAssembler.ts

1. 将 simple Assembler 复制出来。

打开 CCC 源码文件夹。
image

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

准确导航到 Sprite 使用的 simple.ts。
image

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

重命名为 rotateAssembler.ts。
image

2. 修正代码报错。

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

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

然后增加如下 引入 import

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

❤❤

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

❤❤❤

针对这种 类型定义 的报错,直接把它们改成 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

?

首先,如果 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++];
        }
    },

??

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

    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;
        }
    }

???

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

    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;
        }
    },

????

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

    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;
        }
    },

?????

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

    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. 修改 updateRenderData 方法

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