Cocos Creator 卡通水體渲染教程
本篇教程轉(zhuǎn)載自 Cocos 中文社區(qū),作者:regocxy。通過(guò)本篇教程你將學(xué)到如何做風(fēng)格化水體的渲染,包含的知識(shí)點(diǎn)有如何使用天空立方體貼圖作反射,如何巧用噪聲貼圖作紋理擾動(dòng)并順便做出浮沫效果,如何巧用uv做出邊沿霧效果。
水體渲染是游戲中比較有挑戰(zhàn)的一種效果,實(shí)現(xiàn)難度也有深有淺,這里筆者希望使用一種簡(jiǎn)單高效的方法實(shí)現(xiàn)一個(gè)簡(jiǎn)單美觀的風(fēng)格化水體效果,最終實(shí)現(xiàn)效果如下,性能非常優(yōu)秀,對(duì)移動(dòng)設(shè)備非常友好。


下面是實(shí)現(xiàn)過(guò)程:1、天空反射
天空的反射需要:
- 環(huán)境立方體貼圖
- 噪聲貼圖
- 菲涅爾反射
1.1 環(huán)境立方體貼圖
想要做出水體流動(dòng)的感覺有非常多的方法,其中使用 uv 偏移是最簡(jiǎn)單并且性能最好的方法,該方案絕大多數(shù)的做法都是對(duì)一張法線貼圖作 uv 縮放和偏移,并作光影計(jì)算從而表現(xiàn)出流動(dòng)的水面。
該方案的確能做出相當(dāng)不錯(cuò)的風(fēng)格化水體效果,但是筆者這次不想這么做,因?yàn)榉ň€貼圖的采樣還原在筆者看來(lái)還是不夠精簡(jiǎn),甚至水體對(duì)光源的明暗變化筆者也不想計(jì)算。
筆者選擇了直接對(duì)環(huán)境立方體貼圖做采樣,表現(xiàn)一個(gè)簡(jiǎn)單的水面反射效果。
代碼如下:
vec3??v?=?normalize(v_view);
vec3??r?=?-v;
vec3??reflectColor?=?texture(envTexture,?r).rgb;
以上代碼中,筆者對(duì)反射做了一個(gè)計(jì)算優(yōu)化,直接對(duì)視角向量取反,即 r = -v。
常規(guī)做法是 r = reflect(v, n),其中 reflect(v, n) = v - 2.0 * dot(n, v) * n。
由 reflect 表達(dá)式就能看出筆者的寫法效率要遠(yuǎn)高于常規(guī)做法,少了 2 次的乘法計(jì)算和 1 次點(diǎn)成計(jì)算。
而筆者的計(jì)算優(yōu)化成立的原因是對(duì)于天空的反射,如果僅僅讓視覺上看起來(lái)像反射,我們其實(shí)可以不用關(guān)心反射方向的正確性,讀者可以自己作個(gè)圖細(xì)品下。
完整代碼如下:
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);
}
此時(shí)讀者應(yīng)該能得到一個(gè)鏡子一般的水面,毫無(wú)美感,并且絲毫也感受不出這是水。

1.2 噪聲貼圖
表現(xiàn)水體的核心有兩點(diǎn):一個(gè)是流動(dòng)感,另一個(gè)是扭曲感。
而這兩點(diǎn)都可以通過(guò)對(duì)噪聲貼圖進(jìn)行 uv 偏移實(shí)現(xiàn)。
本文使用的噪聲貼圖:

代碼如下:
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 偏移是基于世界坐標(biāo)偏移的,而不是簡(jiǎn)單的 v_uv.xy += cc_time.x * 0.05,這里的原因是基于世界坐標(biāo)作偏移可以隨意調(diào)整水面大小,而不會(huì)拉伸噪聲貼圖,造成失真。
這里還有一點(diǎn)需要注意的是:我們的噪聲貼圖的 wrap mode 需要設(shè)置為 repeat 即重復(fù)模式。

1.3 菲涅爾反射
加入流動(dòng)感和扭曲感后,我們的水體終于看起來(lái)像水了。
目前還存在一個(gè)問(wèn)題:水面任何視角的反射表現(xiàn)都是一樣的,這是不正確的。
這里需要引出一個(gè)現(xiàn)象叫菲涅爾反射(fresnel),簡(jiǎn)單的講,就是視線垂直于表面時(shí),反射較弱,而當(dāng)視線非垂直表面時(shí),夾角越小,反射越明顯。
代碼如下:
float?fresnel?=?mix(0.15,?1.0,?pow(1.0?-?dot(n,?v),?3.0));
常規(guī)的 fresnel 反射公式為 fresnel = pow(1.0 - dot(n, v), x),x 為指數(shù)系數(shù)。
而這里筆者使用了 mix 函數(shù),將 fresnel 的數(shù)值映射到 0.15 到 1.0 之間,確保視角與水面垂直時(shí),也是存在反射的。
mix(x, y, a)是一個(gè)混合函數(shù),等價(jià)于 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);
}
加入菲涅爾反射前后對(duì)比:


2、浮末
目前我們的水面雖然有了些許流動(dòng)感,但還不夠明顯,所以我們需要在水面上制造一些浮沫,突出水的流動(dòng)。
浮沫有幾個(gè)特點(diǎn):
- 位置不固定;
- 大小也不固定。
觀察我們的噪聲貼圖,你會(huì)發(fā)現(xiàn)噪聲貼圖上的一些白色圖案剛好符合我們需求,我們只要想個(gè)辦法將它提取出來(lái)就可以了。
所以我們對(duì)上述代碼做如下改動(dòng):
vec4?frag?()?{
????float?t?=?texture(noiseTexture,?v_uv.xy).r;
????...
????color?=?mix(color,?vec3(1.0),?step(0.9,?t));
????...
}
首先我用 step 函數(shù)對(duì)噪聲作了一次過(guò)濾,將大于 0.9 的噪聲提取了出來(lái),并用 mix 混合函數(shù),將水的顏色和白色(vec3(1.0) 是白色)進(jìn)行混合得到帶有白色浮沫的水面。
step(edge, x)是一個(gè)階躍函數(shù),等價(jià)于x < edge ? 0: 1。
另外大多時(shí)候我們使用 step,提取出來(lái)的圖案,都是有鋸齒感的,所以需要作抗鋸齒,這時(shí)就需要使用 smoothstep 函數(shù)。修改如下:
vec4?frag?()?{
????float?t?=?texture(noiseTexture,?v_uv.xy).r;
????...
????color?=?mix(color,?vec3(1.0),?smoothstep(0.9,?0.91,?t));
????...
}
改動(dòng)非常少,僅僅是將 step(0.9, t)替換為 smoothstep(0.9, 0.91, t)。smoothstep(0.9, 0.91, t) 的作用是將 t 在 [0.9, 0.91] 的范圍內(nèi)作平滑處理,當(dāng) t < 0.9 時(shí),取 0;當(dāng) t > 0.91 時(shí),取1。
smoothstep(edge0, edge1, x)是一個(gè)三次平滑階躍函數(shù),可以將x在[edge0, edge1]之間做一個(gè)平滑過(guò)渡,大多時(shí)候都用來(lái)消除鋸齒。
完整的代碼如下:
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);
}
效果如下:

這里的天空盒沒用skybox,因?yàn)槭褂?skybox,水面的邊沿線消除有些困難,所以筆者使用了一個(gè)球體。
效果如下:


讀者可以看到在水面和天空的交界處有一個(gè)明顯的邊沿線,下面我們就要消除掉這個(gè)邊沿線。
消除這個(gè)邊沿線,我們首先想到的是使用霧,將遠(yuǎn)處的水面與天空盒用霧來(lái)模糊掉。
但是使用常規(guī)的霧效會(huì)帶來(lái)一個(gè)問(wèn)題:當(dāng)相機(jī)進(jìn)行遠(yuǎn)近移動(dòng)時(shí),霧的效果會(huì)產(chǎn)生變化,水面的邊沿線還是沒解決。

所以我們要換個(gè)思路實(shí)現(xiàn)。
其實(shí)消除這個(gè)邊沿線的思路很簡(jiǎn)單,我們只要讓遠(yuǎn)處水面的顏色與天空盒一致就好了。
于是筆者寫了下面這段代碼:
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)));
????...
}
效果如下:

完美解決問(wèn)題,原理是這樣的:我們將與水面中心距離大于一定范圍內(nèi)的區(qū)域顏色設(shè)置成 rimColor(rimColor 的顏色基本與天空盒的顏色一致) 并且用 smoothstep,對(duì)一定范圍內(nèi)的距離值做了平滑處理。
但是在實(shí)際計(jì)算中,筆者作了一個(gè)計(jì)算優(yōu)化,筆者沒有直接使用距離值即 sqrt(dot(d,d)),而是使用了距離的平方值即 dot(d, d),原因是求平方根比較廢性能,如果僅僅是比大小,其實(shí)沒必要開根號(hào)。
完整的代碼如下:
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);
}
最后在相機(jī)前擺上一些粒子烘托下氣氛:

3、結(jié)語(yǔ)非常感謝?regocxy 帶來(lái)的技術(shù)分享,歡迎各位開發(fā)者點(diǎn)擊「閱讀原文」查看原貼,為作者點(diǎn)贊,與作者進(jìn)行交流學(xué)習(xí)!作者會(huì)在社區(qū)中繼續(xù)講解如何制造岸邊泡沫以及浪花喲!
