模仿江南百景图shader效果之一 Cocos creator 3.x如何向shader传递自定义顶点数据

背景

  • 系列文章会分为两篇,本篇是第一篇。
  1. 第一篇说明如何向shader传入自定义顶点格式。

  2. 第二篇模仿江南百景图的效果,只是模仿某些方面,地址是https://forum.cocos.org/t/topic/166464。不是百分百还原。具体来说,笔者要做的效果是,一开始图片以灰度显示一段时间,然后随着时间的流逝,在垂直方向慢慢的向中间填充彩色。当然,江南百景图的shader复杂很多,这里只是模仿了很少的部分。如下。

彩色2

  • 论坛之前已经有一篇文章介绍怎么自定义顶点格式了,不过是基于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步
  1. 定义自己想要的顶点数据格式。

  2. 定义自己的Assembler,笔者这里定义的顶点数据格式比较简单,所以改的东西不多。一般可能需要重写updateVertexData,updateUVs和updateColor三个方法。

  3. 定义自己的Sprite,一般只需要重写requestRenderData方法和_flushAssembler即可。

另外,不妨使用color作为载体传到你shader里面,这样就不需要重新定义顶点数据格式了

  • 原有的数据格式是vfmtPosUvColor,所以其实如果额外的数据不超过4个,而color又没有用到的话(color一般没有用到),不妨直接使用color,作为你传到shader的载体。

作者qq/wx:408293635

ps,笔者正在寻找base广州的机会,如果合适的话,请加我wx

完整的项目代码,在商城:
https://store.cocos.com/app/detail/5440

3赞

非常棒,可以的