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 (二)