通过本篇教程你将学到如何做风格化水体的渲染,包含的知识点有如何使用天空立方体贴图作反射,如何巧用噪声贴图作纹理扰动并顺便做出浮沫效果,如何巧用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,当t > 0.91时,取1。
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);
}
最后在相机前摆上一些粒子烘托下气氛: