(二)【具体操作步骤】CocosCreator3.x 应用在UI(Sprite) 上的 shader(.effect) 的合批,通过自定义顶点参数
具体操作步骤
接下来以一个制造旋转效果的 shader 为例子,提供了这些参数的设置:
- 旋转速度 float
- 旋转中心位置 vec2
- 逆时针/顺时针 bool
- 扭曲度 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;
}
}%
以上步骤,就完成了在 顶点着色器 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。

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

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

第二步、编写 RotateSprite.ts (一)
1. 新建一个 ts 脚本,命名为 RotateSprite.ts。

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

3. 编写顶点参数相关逻辑
回顾 shader(.effect) 定义的顶点参数。

对应列出如下表,其中 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 })
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 文件夹。

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

将文件复制到项目 assets 中。

重命名为 rotateAssembler.ts。

2. 修正代码报错。
首先将变量名从 simple 修改为 rotateAssembler

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

然后增加如下 引入 import
import { DynamicAtlasManager, IAssembler, IRenderData, RenderData, Sprite } from "cc";
🍉🍉
针对 dynamicAtlasManager.packToDynamicAtlas(sprite, frame); 的报错。
将其改为 DynamicAtlasManager.instance.packToDynamicAtlas(sprite, frame); 即可。

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


至此应该就没有报错了。
3. 按照 rotate-sprite.effect 的 顶点参数定义 补充代码。
再次回顾 shader(.effect) 定义的顶点参数。

对应列出表。
| 字段 | 占位 | 偏移 |
|---|---|---|
| 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 在设置顶点参数值时写死了偏移值。

那么我们需要修改 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) 完成对 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;
}
}
🍉🍉🍉
然后是 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;
}
},
🍉🍉🍉🍉
然后是 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;
}
},
🍉🍉🍉🍉🍉
然后是 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) 定义的顶点参数。

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 对象。

删掉原有的 Sprite 组件。

添加 RotateSprite 组件。

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

自定义参数对外接口。

能在 Scene 中看到这个。

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

3. 玩起来吧!
旋转速度2


旋转速度3


调整扰乱度


4. DC测试
搭了一个这样的环境

其中文字的 CacheMode 用了 CHAR

测试,用了六个参数不同的 RoateSprite,DC总共为4,合批成功!
背景占用 1 DC,
文字占用 1 DC,
RotateSprite占用 1 DC,
Sprite占用 1 DC。


浙公网安备 33010602011771号