游戏开发论坛

 找回密码
 立即注册
搜索
查看: 21889|回复: 0

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

[复制链接]

1113

主题

1113

帖子

2853

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2853
发表于 2022-1-28 11:55:29 | 显示全部楼层 |阅读模式
今天将基于 Cocos Creator 3.3.1,通过一个模拟云海效果的 Demo,一步步编写顶点着色器,实现修改模型的形状,来一起了解一下 Mesh 模型、顶点着色器、片元着色器、噪声之间的作用。

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

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

微信图片_20220128113707.jpg
图源网络

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

微信图片_20220128113724.jpg

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

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

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

微信图片_20220128113730.gif
效果预览

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

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

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

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

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

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

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

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

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

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


1、准备

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

微信图片_20220128113736.jpg

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,将内容复制出来。

微信图片_20220128113744.jpg

回到 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: [1, 1, 1, 1], 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 到材质上

微信图片_20220128113752.jpg

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

微信图片_20220128113755.jpg

4、创建 Plane 并应用材质

微信图片_20220128113800.jpg

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

微信图片_20220128113804.jpg

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

微信图片_20220128113808.jpg

5、准备噪声贴图

微信图片_20220128113812.jpg

微信图片_20220128113816.jpg

这里有两张噪声,他们看上去好像并没有区别,但是如果让 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,将两张噪声分别放进材质里,看看会发生什么。

微信图片_20220128113823.jpg

微信图片_20220128113827.jpg

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

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

6、修改顶点着色器

定义 mainTexture。

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

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

微信图片_20220128113833.jpg

并且,由于是黑白灰的噪声,所以 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 变得凹凸不平,并且越黑的地方越低,越白的地方越高。

微信图片_20220128113837.jpg

7、平滑

微信图片_20220128113841.jpg

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

微信图片_20220128113845.jpg

创建一个脚本,叫 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);

}

}

微信图片_20220128113851.jpg

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

微信图片_20220128113854.jpg

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;

}

}%

微信图片_20220128113900.gif

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;

}

}%

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

微信图片_20220128113908.gif

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.0, 1.0, 0.1, 0.1);

vec4 tiling1 = vec4(1.0, 1.0, 0.07, 0.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;

}

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

微信图片_20220128113917.gif

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。

微信图片_20220128113926.gif

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: [1, 1, 1, 1], editor: { type: color } }

c0:      { value: [1, 0, 0, 1], editor: { type: color } }

c1:      { value: [0, 1, 0, 1], editor: { type: color } }

tiling0:   { value: [1.0, 1.0, 0.1, 0.1] }

tiling1:   { value: [1.0, 1.0, 0.07, 0.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 中,查看材质。

微信图片_20220128113933.jpg

13、成品与 Demo

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

微信图片_20220128113939.gif

最终完整的 effect 文件内容这里不再赘述,大家可以点击文末【阅读原文】移步论坛专贴查看完整内容、下载 Demo 工程,欢迎一起讨论交流!

Demo 工程&论坛讨论帖:

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

文/安康
来源:COCOS
原文:https://mp.weixin.qq.com/s/bzfFXRzoT5_LmuhDxofGMQ


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-4-20 04:58

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表