渲染系統作為游戲引擎的核心模塊,是引擎畫面表現力的天花板,直接決定了游戲所能輸出給玩家的內容上限。Cocos Creator 3.x 的渲染系統,從架構到設計都是以面向未來、高性能、跨平臺為目標,支持開發者制作出更加精致的游戲畫面。新版本 Cocos Creator 3.1 已發布,歡迎大家前往下載體驗!
近日引擎渲染組使用 Cocos Creator 3.1,參考塞爾達/原神的吉卜力(Ghibli) 卡通渲染風格做了有趣的嘗試,大家先看一段編輯器里的操作視頻:Cocos 塞爾達 3D 渲染風格Cocos的視頻
由宮崎駿開創的吉卜力風格,并不追求對真實世界的高度逼真再現,而是從色彩、明暗、對比度等方面進行風格獨特的藝術表達,在繪畫感上更接近于水彩,并大量使用亮色作為主色調,通過藝術化的顏色來展現世界,而非純粹追求寫實,使畫面更有卡通幻想色彩。
重要的事情寫在前面:
1. 上述視頻里的 demo 的所有素材和源碼都已開源
倉庫地址https://github.com/cocos-creator/cartoon-vegetation
2. 可以掃描下方二維碼直接進入微信小游戲體驗,方便懶得下載編譯的朋友:

這里強調一下,構建發布到小游戲平臺可運行,只是為了嘗試性能的極限情況如何,以及方便大家快速體驗。在性能內存約束最大的小游戲上都能運行的話,PC Web、手機原生的性能就更沒問題了。
小游戲版本的已知問題:- 默認設置下,由于 ShadowMap 精度不夠導致樹葉有些閃動。在高端安卓手機上可以通過游戲內「左上角齒輪」 -> 開啟「High Quality」解決該問題。但 iPhone12 由于沒有 JIT,開啟 High Quality 之后會掉幀到30以下。- 已知在部分安卓手機上由于不支持 WebGL 的 float texture 導致「Bend Glass」,也就是角色行走處的草被壓平效果無法正確出來。該問題在 iPhone 上沒有。(哎,所以引擎的全平臺適配真是個苦力活)
下文將詳細介紹植被渲染的部分以及模型與人物互動,希望對大家有所幫助。
01
工具
引擎版本:Cocos Creator 3.1
DCC 工具:Blender
Demo 中的人物是一個塞爾達愛好者的同人作品,作者提供了很多人物模型來給大家免費使用。地址:https://twitter.com/artstoff
由于下載下來的模型是沒有動作的,可以在以下網站上搜索一些簡單的動作來使用。地址:https://www.mixamo.com/
02
植被渲染
初始狀態
植被初始狀態相當于引擎默認的 unlit 材質,只能設置貼圖和基礎顏色。整個畫面都是綠色,顯得比較平淡,沒有任何氛圍和辨識度。
vec2 uv = mainTiling.xy + mainTiling.zw * v_uv;vec4 col = texture(mainTexture, uv);

添加修改色
我們可以添加草地的修改色,并基于噪聲貼圖得到一個平滑隨機數 0-1,在原有基礎色進行平滑過渡,這樣草地顏色會變得更豐富明亮。
float rand = texture(randMap, worldPos.xz * randMapTiling).r;col.rgb = mix(col.rgb, hue.rgb, rand * hue.a);


模擬 AO
一般草地的根部或者樹葉根部受光度比較少,因此顏色應該比葉尖處更暗一些,我們可以簡單利用 uv 值來得到草地根部到頂端的明暗度差,基于這個數據來模擬計算 AO 值,或者把明暗信息在建模時預先存儲在頂點顏色中。
float getMask () { #if Mask_Type == Mask_Channel_Color return a_color.r; #elif Mask_Type == Mask_Channel_Uv return 1. - a_texCoord.y; #else return 0.; #endif}
圖中使用 AO mask 作為顏色輸出,可以明顯看到 AO mask 的值從頂部到根部逐漸變小。

float mask = getMask();float ao = mix(col.a, col.a * mask, ambientOcclusion);col.rgb *= ao;
把 ambientOcclusion 暴露為材質參數,結合之前顏色輸出的效果如下圖:

擺動草地
草地顏色已經比較豐富了,現在可以賦予草地生命力,讓它隨風擺動起來。
先簡單讓它按照 Sine(正弦) 曲線隨時間擺動,然后通過 windStrength 和 windSpeed 調節參數。
需要注意的是能被吹動的只有葉尖,根部是不需要擺動的。這和我們之前得到是 AO Mask 明暗度的過度是一樣的。所以我們可以利用之前計算好的 Mask 值乘上 Sine,來做簡單模擬。
vec4 offset = vec4(0.);
float strength = windStrength;float sine = sin(windSpeed * (cc_time.x ));sine = sine * mask * strength;
// 計算 xz 平面偏移offset.xz = vec2(sine);
// 計算 y 方向彎曲度float windWeight = length(offset.xz) + 0.0001;windWeight = pow(windWeight, 1.5);offset.y = windWeight * mask;

我們發現草左右來回大幅度擺動看起來是比較奇怪的,正常情況下草地被吹動后回擺的幅度是小而自然的。
因此需要把 Sine 計算結果的范圍由 (-1, 1) 重新映射到 (0, 1) 上,并通過參數 windSwinging 來調節擺動幅度。
sine = mix(sine * 0.5 + 0.5, sine, windSwinging);

現在看上去好一些了,但是擺動太規整沒有層次感。所以我們需要給每個頂點計算不一樣的擺動值,這里簡單使用模型坐標系下的坐標值計算頂點強度。
float f = length(positionOS.xz) * windRandVertex;// 重新計算 sin 值,rand 為之前用 noise 貼圖獲取的噪聲值,rand隨機范圍0-1float sine = sin(s.speed * (cc_time.x + rand + f));

陣風效果
風不是一直連續刮過來的,一般是一股一股風吹過來。下圖來自現實中陣風吹過草地的效果,可以看到草地在局部中會形成明暗變化。

我們可以使用噪聲貼圖來模擬這一效果。
vec2 gustUV = (worldPos.xz * windGustFrequency * windSpeed) + (cc_time.x * windSpeed * windGustFrequency) * -windDirection。xy;
float gust = texture(windMap, gustUV).r;
gust *= windGustStrength * mask;
// col 為之前計算的結果
col.rgb += (gust * v_color.a * windGustTint);

霧效與天空盒
到現在草地的效果已經有了,不過玩家還感受不出場景的縱深感,并且場景的背景還是默認色。我們可以利用引擎內置的霧效和天空盒來增加畫面細節。
天空盒對于增強畫面感是非常重要的,當處于玩家視角的情況下,大概有四分之一到一半的畫面是由天空盒來填充的,選擇一個合適的天空盒非常重要。
這個場景我選了一個偏淡的天空盒,搭配偏白色的霧效可以使場景盡頭和天空盒的色調比較接近。

陰影
這次風格化渲染的實現中沒有使用光照模型來計算光照效果,而是使用陰影計算的結果來增加畫面細節的。
// getShadowAttenuation 參考了引擎內部計算陰影的邏輯來獲取陰影強度
float getShadowAttenuation () {
float shadowAttenuation = 0.0;
#if CC_RECEIVE_SHADOW
// cc_shadowInfo 的定義可以在引擎中 cc-shadow.chunk 文件中找到, 其中的數據格式為 :
// x -> width; y -> height; z -> pcf; w -> bais;
// z -> pcf 對應的是場景中設置 pcf 選項的值
float pcf = cc_shadowInfo.z + 0.001;
// CCGetShadowFactorXX 為引擎內部 PCF 陰影計算方法, 后面的數字表示采樣 shadowmap 的次數,采樣次數越多獲得的陰影模糊效果越好,表現得越柔軟,消耗的性能也越高
if (pcf > 3.0) {
shadowAttenuation = CCGetShadowFactorX25();
}
else if (3.0 > pcf && pcf > 2.0) {
shadowAttenuation = CCGetShadowFactorX9();
}
else if (2.0 > pcf && pcf > 1.0) {
shadowAttenuation = CCGetShadowFactorX5();
}
else {
shadowAttenuation = CCGetShadowFactorX1();
}
#endif
return shadowAttenuation;
}
float shadowAttenuation = getShadowAttenuation();
**PCF** 陰影設置:

// 獲取陰影強度,并暴露參數 shadowIntensity 自由調節陰影強度
shadowAttenuation = 1. - min(shadowAttenuation, shadowIntensity);
col.rgb *= shadowAttenuation;

半透明效果
這里說的半透明效果不是指玻璃那種能透過物體看到其他物體的效果,而是指陽光能穿過樹葉繼續照射的效果。當我們的視線向著陽光的方向看樹葉的時候,會發現樹葉顯得更加明亮。

按照上面的描述我們可以概括為當樹葉到攝像機的方向,與陽光到樹葉方向一致的時候樹葉應該顯得更加明亮,使用 overlay 疊加效果可以簡單模擬明亮的效果。
// 主光源也就是太陽光的方向
vec3 ld = normalize(cc_mainLitDir.xyz);
// viewDirectionWS 為樹葉到攝像機的方向
vec3 viewDirectionWS = normalize(cc_cameraPos.xyz - worldPos.xyz);
// translucency 暴露為材質參數方便調整效果
float VdotL = max(0., dot(viewDirectionWS, ld)) * translucency;
VdotL = pow(VdotL, 4.) * 8.;
float tMask = VdotL * shadowAttenuation;
vec3 tColor = col.rgb + BlendOverlay(cc_mainLitColor.rgb * cc_mainLitColor.w, color);
col.rgb = mix(col.rgb, tColor, tMask);


與植被的互動
當玩家在草地上移動時,草地受到玩家碰撞擠壓,應該是會明顯向周圍彎曲的。
要做到這一點,我們需要將希望產生交互的物體繪制到一張高度貼圖上,貼圖中的信息包括物體的高度、物體在 XZ 軸上擠壓的方向、擠壓的力度。
渲染到高度圖中的信息:
float mask = -v_normal.y * heightStrength;// * v_color.r;
float height = (v_position.y + heightOffset);
// 將值從 (-1, 1) 重新映射到 (0, 1)vec2 dir = (v_normal.xz * extendStrength) * 0.5 + 0.5;
vec4 heightMapInfo = vec4(dir.x, height, dir.y, mask);
生成高度圖的方法是用一個垂直向下的攝像機拍攝所有需要互動的物體,在 Demo 中攝像機會一直跟隨主角移動,可以隨時修改攝像機拍攝的范圍。

當渲染物體到高度圖上的時候,我們并不需要把原有主角整個完整渲染上去,因為主角的面數一般會比較多,為了節約一些性能,可以用一個大小相近但是面數比較少的物體來做近似渲染。
參考下圖,使用一個圓柱體來代替主角渲染到高度圖上,并且我們可以自由改變圓柱體大小來控制渲染范圍。

下面再看下在草地材質中如何獲取到高度圖里面的信息:
// cc_grass_bend_uv 是在自定義管線里面計算的結果
// cc_grass_bend_uv.xy 為高度圖攝像機的世界坐標
// cc_grass_bend_uv.z 為高度圖攝像機拍攝的范圍
// 使用像素的世界坐標值減去高度圖攝像機的世界坐標再除以范圍就可以得到高度圖中的 uv 坐標
vec2 getBendMapUV(in vec3 wPos) {
vec2 uv = (wPos.xz - cc_grass_bend_uv.xy) / cc_grass_bend_uv.z + 0.5;
return uv;
}
// cc_grass_bend_map 就是高度貼圖了
vec4 getBendVector(vec3 wPos) {
vec2 uv = getBendMapUV(wPos);
vec4 v = texture(cc_grass_bend_map, uv);
//Remap from 0.1 to -1.1
v.x = v.x * 2.0 - 1.0;
v.z = v.z * 2.0 - 1.0;
return v;
}
以上就是 Cocos 引擎渲染組為大家獻上的吉卜力卡通風格渲染的全部分享,視頻、源碼、小游戲體驗、實現步驟講解都已奉上。
評論美三代,轉發富一生。如果您喜歡 Cocos 渲染組的這期分享,歡迎關注微信公眾號【COCOS】了解更多信息,最后希望大家多留言、多轉發哦!
