新知一下
海量新知
6 2 9 1 4 5 3

Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

Cocos | 让游戏开发更简单 2022/01/28 12:11

今天将基于 Cocos Creator 3.3.1,通过一个 模拟云海效果的 Demo ,一步步编写顶点着色器,实现修改模型的形状,来一起了解一下 Mesh 模型、顶点着色器、片元着色器、噪声 之间的作用。

本文着重于分享顶点着色器「能做的事情」,并不是真的想模拟一个真正的云海,毕竟对比 RAYMarching 体积云之类的效果来说还是有一定差距。

选用这个来作为开篇的理由很简单:

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

图源网络

这里用到的 Mesh 的形状是矩形。显卡只能绘制三角形,那么绘制一个矩形至少要两个三角形拼接起来。如果有非常多的小矩形,组成一个大矩形,其实就相当于有很多的小三角形,组成一个大矩形。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

顶点着色器 只修改了模型的 Y 轴,没有做过多的改变。顶点着色器的变化只是从噪声贴图中获取该怎么变,没有使用复杂的公式计算,所以也很容易想象。

片元着色器 更简单,只是返回了顶点着色器输出的 v_color,顶点着色器输出的值会根据重心坐标进行差值。

噪声 是一个只有黑白灰的图片,所以也很容易理解。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

效果预览

结合我们已知的知识整理一下思路:

  • GPU 渲染的是一个一个的三角形。

  • 一个面只要顶点够多,就能生成一个平滑的曲面 ——因此,在低端机可以让三角形【顶点】少一些。

  • 动态生成一个 Mesh,是一个平面,并且三角形足够的多。

  • 通过一张外部图片【噪声】的信息,来存储云的凹凸信息 ——可以想到,一张图只有黑白灰,越白的地方,让三角形的高度越高,反之亦然。

  • 让这张贴图运动【滚动】起来,随着时间的变化,修改获取 UV 的位置信息 ——这样三角形就可以变化了。

  • 通过读取多张噪声,或者读取同一张噪声不同位置的地方,叠加起来,就可以获得翻涌的感觉。

接下来进入正题,上手实操!

限于篇幅,本文仅展示部分核心代码,完整代码及 Demo 工程请移步论坛讨论帖查看、下载:

https://forum.cocos.org/t/topic/128595

1、准备

首先创建默认的场景、材质和 effect 文件。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

2、编辑 effect 文件

双击打开 effect,获得默认的 Cocos 的 Shader 文件,可以看到一行:

    - vert: general-vs:vert # builtin header

根据后面的注释可知,这里使用了默认的 builtin 的顶点着色器,可参考 Cocos 官方文档。

Effect Syntax · Cocos Creator

https://docs.cocos.com/creator/3.3/manual/zh/material-system/effect-syntax.html

所以这个文件里面缺少了要编写的顶点着色器,因此需要手动补充一个。

找到自带的 chunks 里面的 general-vs,将内容复制出来。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

回到 effect 文件中,补充一个 CCProgram 块 my-vs:

CCProgram my-vs %{

  

  precision highp float;

  #include <input-standard>

  #include <cc-global>

  #include <cc-local-batch>

  #include <input-standard>

  #include <cc-fog-vs>

  #include <cc-shadow-map-vs>

  in vec4 a_color;

  #if HAS_SECOND_UV

    in vec2 a_texCoord1;

  #endif

  out vec3 v_position;

  out vec3 v_normal;

  out vec3 v_tangent;

  out vec3 v_bitangent;

  out vec2 v_uv;

  out vec2 v_uv1;

  out vec4 v_color;

  vec4 vert () {

    StandardVertInput In;

    CCVertInput(In);

    mat4 matWorld, matWorldIT;

    CCGetWorldMatrixFull(matWorld, matWorldIT);

    vec4 pos = matWorld * In.position;

    v_position = pos.xyz;

    v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);

    v_tangent = normalize((matWorld * vec4(In.tangent.xyz, 0.0)).xyz);

    v_bitangent = cross(v_normal, v_tangent) * In.tangent.w; // note the cross order

    v_uv = a_texCoord;

    #if HAS_SECOND_UV

      v_uv1 = a_texCoord1;

    #endif

    v_color = a_color;

    CC_TRANSFER_FOG(pos);

    CC_TRANSFER_SHADOW(pos);

    return cc_matProj * (cc_matView * matWorld) * In.position;

  }

}%

并且将最上面的 CCEffect 的 vert 部分定义修改成:my-vs:vert

CCEffect %{

  techniques:

  - name: opaque

    passes:

    - vert: my-vs:vert # builtin header

      frag: unlit-fs:frag

      properties: &props

        mainTexture:    { value: white }

        mainColor:      { value: [1111], editor: { type: color } }

  - name: transparent

    passes:

    - vert: general-vs:vert # builtin header

      frag: unlit-fs:frag

      blendState:

        targets:

        - blend: true

          blendSrc: src_alpha

          blendDst: one_minus_src_alpha

          blendSrcAlpha: src_alpha

          blendDstAlpha: one_minus_src_alpha

      properties: *props

}%

3、绑定 effect 到材质上

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

选中材质,选择 Effect,选中刚刚新建的 effect 文件,最后不要忘记点击右上角的箭头,保存一下。正确的话会预览出一个纯白的方块。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

4、创建 Plane 并应用材质

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

场景中创建 3D 对象,Plane。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

选中 Plane 节点,将 material 拖拽覆盖原本的 default-material 材质,最终可以得到一个纯白的 Plane。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

5、准备噪声贴图

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

这里有两张噪声,他们看上去好像并没有区别,但是如果 让 UV 偏移0.5 的话就会发生奇怪的问题。现在我们来测试一下。

先简单修改下片元着色器,也就是 frag 块:

CCProgram unlit-fs %{

  precision highp float;

  #include <output>

  #include <cc-fog-fs>

  in vec2 v_uv;

  uniform sampler2D mainTexture;

  uniform Constant {

    vec4 mainColor;

  };

  vec4 frag () {

    vec4 col = mainColor * texture(mainTexture, v_uv + 0.5);

    CC_APPLY_FOG(col);

    return CCFragOutput(col);

  }

}%

注意:这里修改了 UV 的取值,将 v_uv 增加了0.5。

回到 Cocos Creator,将两张噪声分别放进材质里,看看会发生什么。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

可以很明显地发现噪声在偏移之后,中间并不平滑。所以这里使用的噪声贴图有一个条件: 无缝噪声。

测试完记得把 +0.5 删掉!!

6、修改顶点着色器

定义 mainTexture。

定义 p = In.position,并且用 p 代替后续代码中的 In.position。

将噪声图映射在矩形上面,矩形上各个三角形对应的顶点,判断颜色是更黑还是更白,根据颜色值的深浅来决定这个顶点在 y 值上的高低。在着色器中,颜色的取值范围是 0~1,所以现在每个顶点的 y 有了高度信息,即取值范围 0~1。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

并且,由于是黑白灰的噪声,所以 r=g=b,直接将 r 的颜色赋值给 p.y。

uniform sampler2D mainTexture;

  vec4 vert () {

    StandardVertInput In;

    CCVertInput(In);

    mat4 matWorld, matWorldIT;

    CCGetWorldMatrixFull(matWorld, matWorldIT);

    vec4 p = In.position;

    float y = texture(mainTexture, a_texCoord).x;

    p.y = y;

    vec4 pos = matWorld * p;

    v_position = pos.xyz;

    v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);

    v_tangent = normalize((matWorld * vec4(In.tangent.xyz, 0.0)).xyz);

    v_bitangent = cross(v_normal, v_tangent) * In.tangent.w; // note the cross order

    v_uv = a_texCoord;

    #if HAS_SECOND_UV

      v_uv1 = a_texCoord1;

    #endif

    v_color = a_color;

    CC_TRANSFER_FOG(pos);

    CC_TRANSFER_SHADOW(pos);

    return cc_matProj * (cc_matView * matWorld) * p;

  }

回到 Cocos Creator 就可以发现 Plane 变得凹凸不平,并且越黑的地方越低,越白的地方越高。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

7、平滑

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

默认的 Plane 面数比较少,所以会变得比较不平滑。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

创建一个脚本,叫 my-mesh,用来替换 plane 的默认 mesh:

import { _decorator, Component, utils, primitives, MeshRenderer } from 'cc';

const { ccclass, property } = _decorator;

 

@ccclass('MyMesh')

export class MyMesh extends Component {

    start () {

        const renderer = this.node.getComponent(MeshRenderer);

        if(!renderer){

            return;

        }

        const plane: primitives.IGeometry = primitives.plane({

            width: 10,

            length: 10

            widthSegments: 100,

            lengthSegments: 100,

        });

        renderer.mesh = utils.createMesh(plane);

    }

}

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

回到 Cocos Creator,将脚本和 Node 绑定起来,并且运行。可以看到,相对编辑器中的已经平滑了许多,并且很容易的区分出高低的颜色。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

8、运动

引入时间戳(单位:s),根据时间的不同,获取不同位置的 UV 信息,就可以让画面滚动起来。

引入 #incloud cc-global。

修改 UV 的获取,a_texCoord 值加上 cc_time.x 并且 *一个速度系数0.1。

uniform sampler2D mainTexture;

  #include <cc-global>

  vec4 vert () {

    StandardVertInput In;

    CCVertInput(In);

    mat4 matWorld, matWorldIT;

    CCGetWorldMatrixFull(matWorld, matWorldIT);

    vec4 p = In.position;

    float y = texture(mainTexture, a_texCoord + cc_time.x * 0.1).x;

    p.y = y;

    vec4 pos = matWorld * p;

    v_position = pos.xyz;

    v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);

    v_tangent = normalize((matWorld * vec4(In.tangent.xyz, 0.0)).xyz);

    v_bitangent = cross(v_normal, v_tangent) * In.tangent.w; // note the cross order

    v_uv = a_texCoord;

    #if HAS_SECOND_UV

      v_uv1 = a_texCoord1;

    #endif

    v_color = a_color;

    CC_TRANSFER_FOG(pos);

    CC_TRANSFER_SHADOW(pos);

    return cc_matProj * (cc_matView * matWorld) * p;

  }

}%

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

9、颜色

形状改变了,但是颜色好像并没有重新发生变化。

修改顶点着色器,将 texture 函数获取到的颜色直接丢给 v_color。

修改片元着色器,直接将 v_color 颜色返回(记得先声明 in vec4 v_color)。

CCProgram my-vs %{

  

  precision highp float;

  #include <input-standard>

  #include <cc-global>

  #include <cc-local-batch>

  #include <input-standard>

  #include <cc-fog-vs>

  #include <cc-shadow-map-vs>

  in vec4 a_color;

  #if HAS_SECOND_UV

    in vec2 a_texCoord1;

  #endif

  out vec3 v_position;

  out vec3 v_normal;

  out vec3 v_tangent;

  out vec3 v_bitangent;

  out vec2 v_uv;

  out vec2 v_uv1;

  out vec4 v_color;

  uniform sampler2D mainTexture;

  #include <cc-global>

  vec4 vert () {

    StandardVertInput In;

    CCVertInput(In);

    mat4 matWorld, matWorldIT;

    CCGetWorldMatrixFull(matWorld, matWorldIT);

    vec4 p = In.position;

    vec4 baseColor0 = texture(mainTexture, a_texCoord + cc_time.x * 0.1);

    p.y = baseColor0.x;

    vec4 pos = matWorld * p;

    v_position = pos.xyz;

    v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);

    v_tangent = normalize((matWorld * vec4(In.tangent.xyz, 0.0)).xyz);

    v_bitangent = cross(v_normal, v_tangent) * In.tangent.w; // note the cross order

    v_uv = a_texCoord;

    #if HAS_SECOND_UV

      v_uv1 = a_texCoord1;

    #endif

    v_color = baseColor0;

    CC_TRANSFER_FOG(pos);

    CC_TRANSFER_SHADOW(pos);

    return cc_matProj * (cc_matView * matWorld) * p;

  }

}%

CCProgram unlit-fs %{

  precision highp float;

  #include <output>

  #include <cc-fog-fs>

  in vec2 v_uv;

  in vec4 v_color;

  uniform sampler2D mainTexture;

  uniform Constant {

    vec4 mainColor;

  };

  vec4 frag () {

    return v_color;

  }

}%

可以发现没有刚刚的问题了,回到越白的地方越高,越黑的地方越暗了。

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海

10、噪声叠加-翻涌

噪声可以用多张,也可以读取多次,只要读取的位置不一样,并且叠加起来,那么就可以得到翻涌的感觉了。

定义了 tiling0 和 tiling1,其中,xy 用来控制 UV 的倍率,zw 用来控制 UV 移动的方向。

texture 采样两次,分别为 baseColor0 和 baseColor1,并且两个颜色的红色加起来 *0.5,赋值给 p.y。

p.y 最后还 -0.5,是因为 y 的值原本在 0~1 之间,希望最后在 -0.5~0.5 之间分布,所以整体 -0.5。

将 v_color = baseColor0 改成 v_color = (baseColor0 + baseColor1)* 0.5。

vec4 vert () {

    StandardVertInput In;

    CCVertInput(In);

    mat4 matWorld, matWorldIT;

    CCGetWorldMatrixFull(matWorld, matWorldIT);

    vec4 p = In.position;

    vec4 tiling0 = vec4(1.01.00.10.1);

    vec4 tiling1 = vec4(1.01.00.070.07);

    vec4 baseColor0 = texture(mainTexture, a_texCoord * tiling0.xy + cc_time.x * tiling0.zw);

    vec4 baseColor1 = texture(mainTexture, a_texCoord * tiling1.xy + cc_time.x * tiling1.zw);

    p.y = (baseColor0.x + baseColor1.x) * 0.5 - 0.5;

    vec4 pos = matWorld * p;

    v_position = pos.xyz;

    v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);

    v_tangent = normalize((matWorld * vec4(In.tangent.xyz, 0.0)).xyz);

    v_bitangent = cross(v_normal, v_tangent) * In.tangent.w; // note the cross order

    v_uv = a_texCoord;

    #if HAS_SECOND_UV

      v_uv1 = a_texCoord1;

    #endif

    v_color = (baseColor0 + baseColor1)* 0.5;

    CC_TRANSFER_FOG(pos);

    CC_TRANSFER_SHADOW(pos);

    return cc_matProj * (cc_matView * matWorld) * p;

  }

可以发现运动不再和上面一样只是单一运动,而是带上了起伏的感觉。

11、颜色过渡

黑白灰毕竟不好看,所以我们自定义两个颜色(c0 和 c1)来重新定义高低。

vec4 c0 = vec4(1.0, 0.0, 0.0, 1.0);

vec4 c1 = vec4(0.0, 1.0, 0.0, 1.0);

    v_color = (p.y + 0.5) * (c0 - c1) + c1;

c0 表示最高处的颜色;

c1 表示最低处的颜色;

c0 - c1 = 两个颜色的差距;

p.y + 0.5 得到一个 0~1 之间的值,用来表示当前 y 的高度;

(p.y + 0.5) * (c0 - c1) 得到一个 y 高度变化中的过渡值;

过渡值 +c1,表示过渡值 + 基础值 = 最终的颜色;

c0 - c1 等于两个颜色分量的差,用差 *(y + 0.5)得到变化值,最后再加上 c1。

这样就得到了一个自定义颜色的 Shader。

12、将定义的数据暴露给材质面板

目前位置,这里定义了两个 tiling,两个颜色 c0 和 c1:

CCEffect %{

  techniques:

  - name: opaque

    passes:

    - vert: my-vs:vert # builtin header

      frag: unlit-fs:frag

      properties: &props

        mainTexture:    { value: white }

        mainColor:      { value: [1111], editor: { type: color } }

        c0:      { value: [1001], editor: { type: color } }

        c1:      { value: [0101], editor: { type: color } }

        tiling0:   { value: [1.01.00.10.1] }

        tiling1:   { value: [1.01.00.070.07] }

  - name: transparent

    passes:

    - vert: general-vs:vert # builtin header

      frag: unlit-fs:frag

      blendState:

        targets:

        - blend: true

          blendSrc: src_alpha

          blendDst: one_minus_src_alpha

          blendSrcAlpha: src_alpha

          blendDstAlpha: one_minus_src_alpha

      properties: *props

}%

将 c0,c1,tiling0,tiling1 定义到 properties 里面,原来的参数这里先不做任何删改,保留处理。

给顶点着色器和片元着色器都加上 uniform 声明定义块:

uniform MyVec4 {

    vec4 c0;

    vec4 c1;

    vec4 tiling0;

    vec4 tiling1;

  };

然后移除原本代码里面定义的 c0,c1,tiling0 和tiling1,用 uniform 来代替。

完成后回到 Cocos Creator 中,查看材质。

13、成品与 Demo

最后调整一下摄像机、材质的参数,即得到成品:

新知达人, Shader 之顶点着色器能做什么?Cocos Creator 实现模拟云海


更多“Cocos Creator”相关内容

更多“Cocos Creator”相关内容

新知精选

更多新知精选