背景
- 系列文章会分为两篇,本篇是第一篇。
-
第一篇说明如何向shader传入自定义顶点格式。
-
第二篇模仿江南百景图的效果,只是模仿某些方面,地址是https://forum.cocos.org/t/topic/166464。不是百分百还原。具体来说,笔者要做的效果是,一开始图片以灰度显示一段时间,然后随着时间的流逝,在垂直方向慢慢的向中间填充彩色。当然,江南百景图的shader复杂很多,这里只是模仿了很少的部分。如下。
- 论坛之前已经有一篇文章介绍怎么自定义顶点格式了,不过是基于2.x引擎。如下。
【分享】自定义渲染合批之自定义顶点格式(附 Demo 和引擎源码解读)https://forum.cocos.org/t/topic/95087
- 本篇基于3.8.4,因此上面的写法不是很适合,但是原理是一样的。为什么要用自定义顶点格式,在上面的那篇文章也有论述。
自定义顶点数据的原理
-
Sprite是渲染组件,它的渲染数据,由它当前的Assembler组织数据,和提交数据。Sprite的的Type,共有SIMPLE,SLICED,TILED,FILLED,不同的Type对应不同的Assembler。
-
查看Sprite和它的几种Assembler的源码,要实现笔者上面所说的效果,比较简单的方法是借用Sprite和它其中之一的Assembler(simple)。具体来说,是新建一个CustomSprite类重写它的某些方法,然后再新建一个CustomAssembler继承SIMPLE,也重写它的一些方法。
自定义顶点数据的格式
- Spite的renderData的顶点格式vfmtPosUvColor,即x,y,z,u,v,r,g,b,a,共9个数据。这里让我比较惊讶的是color,也占用了4个float值,其实只需要一个int就可以了。以下是vfmtPosUvColor的顶点数据格式。
export const vfmtPosUvColor = [
new Attribute(AttributeName.ATTR_POSITION, Format.RGB32F),
new Attribute(AttributeName.ATTR_TEX_COORD, Format.RG32F),
/** color这里用了4个float值,其实只需要一个32位的int就可以了。*/
new Attribute(AttributeName.ATTR_COLOR, Format.RGBA32F),
];
- 由于只是演示功能,为了简单,笔者现在设计的顶点数据格式,就在现有的vfmtPosUvColor基础上增加三个数据,一个是时间参数,一个是纹理的上边界,一个是纹理的下边界。因为最终的效果,是要随着时间流逝,在垂直的方向上慢慢向中间填充彩色。片段着色器知道自己当前采样的uv坐标的,那么如果坐标uv.y大于或小于一定的数值(计算方法将在下一篇讨论),就显示彩色,否则显示灰色。
// 定义自定义顶点属性
const customAttributes: gfx.Attribute[] = [
new Attribute(AttributeName.ATTR_POSITION, Format.RGB32F),
new Attribute(AttributeName.ATTR_TEX_COORD, Format.RG32F),
new Attribute(AttributeName.ATTR_COLOR, Format.RGBA32F),
/** 新增的顶点属性,一个是时间参数,一个是纹理的上边界,一个是纹理的下边界*/
new Attribute("a_customData", Format.RGB32F),
];
具体的实现方法
-
新的Assembler-CustomAssembler。
- 本来想直接继承Sprite的Simple那个Assembler的,但最终发现它只是一个对象,不是类。所以这里直接把它的内容复制过来,然后只需要修改updateUVs方法,再增加setCustomData方法和getCustomData方法就可以了。这里要吐槽一下它原来的写法,写的太死了,如果用到步长的话,就不需要更改了。
updateUVs(sprite: Sprite) {
if (!sprite.spriteFrame) return;
const renderData = sprite.renderData!;
const vData = renderData.chunk.vb;
const uv = sprite.spriteFrame.uv;
/**原来的写法 */
// vData[3] = uv[0];
// vData[4] = uv[1];
// vData[12] = uv[2];
// vData[13] = uv[3];
// vData[21] = uv[4];
// vData[22] = uv[5];
// vData[30] = uv[6];
// vData[31] = uv[7];
/**现在的写法 */
let offset = 3;
for (let i = 0; i < 8; i += 2, offset += renderData.floatStride) {
vData[offset] = uv[i];
vData[offset + 1] = uv[i + 1];
}
},
/**
* 要同时设置四个顶点
* @param sprite
* @param x 时间参数
* @param y 图片资源SpriteFrame的uv的上边界
* @param z 图片资源SpriteFrame的uv的下边界
*/
setCustomData(sprite: Sprite, x: number, y: number, z: number) {
const renderData = sprite.renderData!;
const vData = renderData.chunk.vb;
let offset = 9;
for (let i = 0; i < 4; i++, offset += renderData.floatStride) {
vData[offset] = x;
vData[offset + 1] = y;
vData[offset + 2] = z;
}
},
customData: { x: 0, y: 0, z: 0 },
/**只需要取第一个顶点的数据就行了 */
getCustomData(sprite: Sprite): { x: number, y: number, z: number } {
const renderData = sprite.renderData!;
const vData = renderData.chunk.vb;
let offSet = 9;
this.customData.x = vData[offSet];
this.customData.y = vData[offSet + 1];
this.customData.z = vData[offSet + 2];
return this.customData;
}
-
新的Sprite-CustomSprite
- 需要用新的顶点格式构建renderData,并且将自己的assembler赋值给新的CustomAssembler。这其实就只涉及到两个方法,requestRenderData方法和_flushAssembler
/**
* @en Request new render data object.
* @zh 请求新的渲染数据对象。
* @return The new render data
* RenderDrawInfoType.COMP = 0
*/
public requestRenderData(drawInfoType = 0) {
// const data = RenderData.add(); //Sprite原有的定义顶点格式
/** 新的定义顶点格式 只需要将新的顶点格式传入即可*/
const data = RenderData.add(customAttributes);
data.initRenderDrawInfo(this, drawInfoType);
this._renderData = data;
return data;
}
_flushAssembler(): void {
/**关键代码 将CustomAssembler赋值给_assembler*/
const assembler = CustomAssembler;
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();
}
}
}
- 如何设置uv的上下边界,如下:
let sp = node.getComponent(CustomSprite);
let x = Math.min(time + 0.05, 1)
let sframe = sp.spriteFrame;
let y = sframe.uv[1];
let z = sframe.uv[5];
sp.setCustomData(x, y, z);
总结
- 总共分了3步
-
定义自己想要的顶点数据格式。
-
定义自己的Assembler,笔者这里定义的顶点数据格式比较简单,所以改的东西不多。一般可能需要重写updateVertexData,updateUVs和updateColor三个方法。
-
定义自己的Sprite,一般只需要重写requestRenderData方法和_flushAssembler即可。
另外,不妨使用color作为载体传到你shader里面,这样就不需要重新定义顶点数据格式了
- 原有的数据格式是vfmtPosUvColor,所以其实如果额外的数据不超过4个,而color又没有用到的话(color一般没有用到),不妨直接使用color,作为你传到shader的载体。
作者qq/wx:408293635
ps,笔者正在寻找base广州的机会,如果合适的话,请加我wx
完整的项目代码,在商城:
https://store.cocos.com/app/detail/5440