3D渲染技术分享:一种高效的卡通水体渲染

今天玉兔更新了一个关于卡通水面渲染的视频,视频中提到了 炫烨 大佬的文章。

【玉兔 | 图形学与游戏开发】超简单的卡通风格水体渲染Shader | 新手入门友好

抱着蹭一波热度的心态,将文章搬运了过来,并微调了格式。
接下来,有请大家重温一下炫烨大佬的卡通水体渲染,感受大佬对细节的掌控力。

通过本篇教程你将学到如何做风格化水体的渲染,包含的知识点有:

  • 如何使用天空立方体贴图作反射
  • 如何巧用噪声贴图作纹理扰动并顺便做出浮沫效果
  • 如何巧用 uv 做出边沿雾效果。

水体渲染是游戏中比较有挑战的一种效果,实现难度也有深有浅。

这里笔者希望使用一种简单高效的方法实现一个简单美观的风格化水体效果,性能好,对移动设备非常友好。

下面是实现过程。

1、天空反射

天空的反射需要用到两个东西,分别是

  • 环境立方体贴图
  • 噪声贴图
  • 菲涅尔反射

1.1、环境立方体贴图

想要做出水体流动的感觉有非常多的方法,其中使用 uv 偏移是最简单并且性能最好的方法。

该方案绝大多数的做法都是对一张法线贴图作 uv 缩放和偏移,并作光影计算从而表现出流动的水面。

这样的确能做出相当不错的风格化水体效果,但是笔者这次不想这么做。

因为法线贴图的采样还原在笔者看来还是不够精简,甚至水体对光源的明暗变化笔者也不想计算。

于是笔者选择了直接对环境立方体贴图做采样,表现一个简单的水面反射效果。代码如下:

vec3  v = normalize(v_view);
vec3  r = -v;
vec3  reflectColor = texture(envTexture, r).rgb;

以上代码中,笔者对反射做了一个计算优化,即直接对视角向量取反(r = -v)。

常规做法是 r = reflect(v, n),其中 reflect(v, n) = v - 2.0 * dot(n, v) * n

reflect 表达式就能看出笔者的写法效率要远高于常规做法,少了 2 次的乘法计算和 1 次点乘运算。

对于天空的反射,如果仅仅让视觉上看起来像反射,我们其实可以不用关心反射方向的正确性,读者可以自己作个图细品下。

完整代码如下:

vec4 frag () {
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v;
      
    vec3  reflectColor = texture(envTexture, r).rgb;
    return vec4(reflectColor, mainColor.a);
}

此时读者应该能得到一个镜子一般的水面,毫无美感,并且丝毫也感受不出这是水。

1.2、噪声贴图

表现水体的核心有两点,一个是流动感,另一个是扭曲感。而这两点都可以通过对噪声贴图进行 uv 偏移实现。

本文使用的噪声贴图如下:

噪声图相关代码如下:

vec4 vert() {
    StandardVertInput In;
    CCVertInput(In);
    mat4 matWorld, matWorldIT;
    CCGetWorldMatrixFull(matWorld, matWorldIT);
    vec4 worldPos = matWorld * In.position;
    v_uv.xy = worldPos.xz * 0.1 + cc_time.x * 0.05;
    ...
}

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v + t * 0.03;
        
    vec3  reflectColor = texture(envTexture, r).rgb;
    return vec4(reflectColor, mainColor.a);
}

需要注意一点,在以上代码中笔者对 uv 偏移是基于世界坐标偏移的,而不是简单的 v_uv.xy += cc_time.x * 0.05

这是因为基于世界坐标作偏移可以随意调整水面大小,而不会拉伸噪声贴图,造成失真。

这里还有一点需要注意的是我们的噪声贴图的 wrap mode 需要设置为重复模式(repeat)。

到这一步,我们可以看到流动的水波效果,如下所示:

1.3、菲涅尔反射

加入流动感和扭曲感后,我们的水体终于看起来像水了。

但目前水面任何视角的反射表现都是一样的,这个效果是不正确的。

这里需要引出一个现象叫菲涅尔反射(fresnel),简单的讲就是:视线垂直于表面时,反射较弱,而当视线非垂直表面时,夹角越小,反射越明显。

代码如下:

float fresnel = mix(0.15, 1.0, pow(1.0 - dot(n, v), 3.0));

常规的 fresnel 反射公式为: fresnel = pow(1.0 - dot(n, v), x)

x 为指数系数,而这里笔者使用了 mix 函数,将 fresnel 的数值映射到 0.15 到 1.0 之间,确保视角与水面垂直时,也是存在反射的。

mix(x, y, a) 是一个插值函数,等价于 x(1−a)+y*a*。

完整的代码如下:

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v + t * 0.03;
     
    vec3  reflectColor = texture(envTexture, r).rgb;
    float fresnel = mix(0.15, 1.0, pow(1.0 - dot(n, v), 3.0));
    vec3  color = mix(mainColor.rgb, reflectColor, fresnel);
    return vec4(color, mainColor.a);
}

加入菲涅尔反射前后对比:

2、 浮沫

目前我们的水面虽然有了些许流动感,但还不够明显,所以我们需要在水面上制造一些浮沫,突出水的流动。

浮沫有两个特点:

  • 1、位置不固定
  • 2、大小也不固定

仔细观察我们的噪声贴图,你会发现噪声贴图上的一些白色图案刚好符合我们需求,我们只要想个办法将它提取出来就可以了,所以我们对上述代码做如下改动:

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    ...
    color = mix(color, vec3(1.0), step(0.9, t));
    ...
}

首先用 step 函数对噪声作了一次过滤,将大于 0.9 的噪声提取了出来。
接着用 mix 混合函数,将水的颜色和白色(vec3(1.0))进行混合得到带有白色浮沫的水面。

step(edge, x) 是一个阶跃函数,等价于 x < edge ? 0: 1。

大多时候我们使用 step 提取出来的图案都是有锯齿感的,所以需要做抗锯齿处理,这时就需要使用 smoothstep 函数。修改如下:

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    ...
    color = mix(color, vec3(1.0), smoothstep(0.9, 0.91, t));
    ...
}

改动非常少,仅仅是将 step(0.9, t) 替换为 smoothstep(0.9, 0.91, t)

smoothstep(0.9, 0.91, t) 的作用是将 t 在 [0.9, 0.91] 的范围内作平滑处理,当 t < 0.9 时,取 0.0,当 t > 0.91 时,取1.0。

smoothstep(edge0, edge1, x) 是一个三次平滑阶跃函数,可以将 x 在[edge0, edge1]之间做一个平滑过渡,大多时候都用来消除锯齿。

完整的代码如下:

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v + t * 0.03;
     
    vec3  reflectColor = texture(envTexture, r).rgb;
    float fresnel = mix(0.15, 1.0, pow(1.0 - dot(n, v), 3.0));
    vec3  color = mix(mainColor.rgb, reflectColor, fresnel);
    color = mix(color, vec3(1.0), smoothstep(0.9, 0.901, t));
    return vec4(color, mainColor.a);
}

效果如下:

3、天空盒

这里的天空盒没用引擎的 skybox,因为使用 skybox 水面的边沿线消除有些困难,所以笔者使用了一个球体。效果如下:

将水面与天空盒结合后,效果如下所示:

眼尖的读者肯定可以看到,在水面和天空的交界处有一个明显的边沿线。

要消除这个边沿线,我们首先想到的是使用雾,将远处的水面与天空盒用雾来模糊掉。

但是使用常规的雾效会带来一个问题,当相机进行远近移动时,雾的效果会产生变化,水面的边沿线还是没解决。如下所示:

所以我们要换个思路实现。

我们只要让远处水面的颜色与天空盒一致就好了,于是笔者写了下面这段代码:

vec4 vert() {
    ...
    v_uv.zw = a_texCoord;
    ...
}

vec4 frag() {
    ...
    vec2 d = v_uv.zw - vec2(0.5, 0.5);
    color = mix(color, rimColor.rgb, rimColor.a * smoothstep(0.0, 0.27, dot(d,d)));
    ...
}

完美解决问题,效果如下:

我们将与水面中心距离大于一定范围内的区域颜色设置成 rimColor(rimColor 的颜色基本与天空盒的颜色一致,并且用 smoothstep 对一定范围内的距离值做了平滑处理。

但是在实际计算中,笔者作了一个计算优化,笔者没有直接使用距离值即sqrt(dot(d,d)),而是使用了距离的平方值即 dot(d, d),求平方根比较费性能。如果仅仅是比大小,其实没必要开根号。

完整的代码如下:

vec4 vert() {
    StandardVertInput In;
    CCVertInput(In);
    mat4 matWorld, matWorldIT;
    CCGetWorldMatrixFull(matWorld, matWorldIT);
    vec4 worldPos = matWorld * In.position;
    v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);
    v_view = cc_cameraPos.xyz - worldPos.xyz;
    v_uv.xy = worldPos.xz * 0.1 + cc_time.x * 0.05;
    v_uv.zw = a_texCoord;
    return cc_matProj * cc_matView * worldPos;
}

vec4 frag () {
    float t = texture(noiseTexture, v_uv.xy).r;
    vec3  n = normalize(v_normal);
    vec3  v = normalize(v_view);
    vec3  r = -v + t * 0.03;
     
    vec3  reflectColor = texture(envTexture, r).rgb;
    float fresnel = mix(0.15, 1.0, pow(1.0 - dot(n, v), 3.0));
    vec3  color = mix(mainColor.rgb, reflectColor, fresnel);
    color = mix(color, vec3(1.0), smoothstep(0.9, 0.91, t));
    vec2 d = v_uv.zw - vec2(0.5, 0.5);
    color = mix(color, rimColor.rgb, rimColor.a * smoothstep(0.0, 0.27, dot(d,d)));
    return vec4(color, mainColor.a);
}

最后在相机前摆上一些粒子烘托下气氛:

至此,咱们的教程结束,你学废了吗?


4、写在最后

由于炫烨大佬当初太忙,没有准备DEMO,论坛里除了高喊 大佬666 却什么都不能做。

在这里特别感谢玉兔,替大家实现了文章中的DEMO效果并制作了视频教程。

**获得方法1:**打开Dashboard->商城,搜索 玉兔

获得方法2: 打开 store.cocos.com,搜索 玉兔

同时,借此机会推荐一个神器。

名称:Cinestation

作者:炫烨

用途:3D游戏镜头制作

价格:免费

更多3D图形渲染相关内容
请关注 麒麟子

猜你喜欢

转载自blog.csdn.net/qq_36720848/article/details/123191845