Demo的卡通渲染方案

歡迎參與討論,轉載請註明出處。

前言

本篇文章按理來說在三月便該發佈了,因爲插隊原因延宕至今,不過好飯不怕晚,幹就完了奧利給!閱讀本文最好擁有一定的圖形學知識,當然看個熱鬧也是好的。
  遊戲畫面的風格是一開始便要定下的大事,這在古法2D主要通過素材本身及後期調色決定,沒有太多文章可作。而在現代遊戲(尤其是3D)則會通過Shader在原本的元素上進行加料,如通過基於物理的渲染(PBR)將模型凸顯出金屬、石頭、布料等材質傾向。而在早期爲了凸顯3D模型的立體感,一般會採用經驗總結出來的馮氏光照模型(Blinn-Phong),這也是許多3D軟件的默認方案,那將會讓我們的模型長成這樣:
在這裏插入圖片描述
  嗯,這有夠雕塑風的,讓我想起了當初名震一時的猴賽雷,有着異曲同工之妙:
在這裏插入圖片描述
  由此可見,對於講究卡通風格的遊戲,這種通用的光照模型肯定得槍斃,於是本文才會誕生。對於這類非寫實方向的渲染方案,業界稱之爲NPR。而往下細分則是日式卡通渲染,其中佼佼者當屬《罪惡裝備》系列,而《崩壞3rd》也是不少人在這方面的啓蒙者。當然美術這一塊沒有絕對的風格一致,渲染也不例外,所以Demo裏的卡通渲染方案乃是個人的方案,不代表業界的標準實現與效果。
  Demo基於Unity2019.3開發,渲染管線爲URP7.3.1,採用直接編寫Shader的方式(HLSL),將一一介紹其中要點。本文所謂的卡通效果以日式2D賽璐璐風格爲準,不論厚塗之類的風格。

着色

首先我們先拋開一切:馮氏光照不好那咱們就是了。直接把貼圖顯示了,什麼料都不要加。

// 根據uv座標獲取對應貼圖上的顏色

half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);

在這裏插入圖片描述
  嗯,雖然很原始,但好歹沒那股噁心感了,把投影也加上:

// 根據渲染管線提供的shadowCoord獲取光照信息,並計算出投影顏色

Light mainLight = GetMainLight(shadowCoord);
color *= mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation);

在這裏插入圖片描述
  哎呀,有了投影瞬間立體起來了,開始有《塞爾達傳說:風之杖》內味了:
在這裏插入圖片描述
  要加投影記得添加Pass:ShadowCaster,並且獲取光照信息也需要開啓一定的宏,這些並非本文重點,詳情請查閱URP的Shader實現。
  但只是如此還不夠:顏色太鮮豔了,看久了會累。那麼有兩種方案:調色與着色,調色則是進行總體的顏色調節,使之不要這麼鮮豔,着色則是根據模型面對光的吸收度決定明暗。這裏還是選擇着色:它將會增強模型的立體感。
  這裏說的着色其實就是馮氏光照中的漫反射(Diffuse):當光照射到非平面的物體上,將根據與光的夾角決定吸收度(越是與光垂直的面越亮)。而在3D模型中,每個模型面都會往上發射一條射線,也就事實上構成了一條垂直於平面向量,這在數學中稱之爲法線(Normal)。我們可以使用**向量點積(Dot)**獲取法線與光照方向之間的夾角,以此決定模型面的光亮程度。
在這裏插入圖片描述

half NDotL = dot(normal, lightDir); // 計算法線與光照方向的夾角係數
NDotL = saturate(NDotL); // 保證係數在0-1
color *= NDotL;

在這裏插入圖片描述
  啊這,這不是跟一開始差不多麼?這是當然的,因爲一開始便是馮氏光照的方案。其漫反射的思想其實並無問題,但原罪在於過渡太豐富了,每個模型面與光的夾角都不同,導致顏色都不同。整個模型看起來就過於立體,以至於產生了雕塑感。
  而在日式2D卡通的世界裏(尤其是賽璐璐),着色並不會有太詳細的過渡,只是到了某個角度統一塗暗,反之爲亮,最多在兩者之間加點過渡而已。那麼便基於此思想進行改造即可:

half NDotL = dot(normal, lightDir); // 計算法線與光照方向的夾角係數

// 根據_DiffuseRange約束係數,輸出0-1的值
// 由於使用了smoothstep,在接近_DiffuseRange上下限時會做柔滑處理,使之產生過渡感
// _DiffuseRange的參考值爲0.5, 0.7
half v = smoothstep(_DiffuseRange[0], _DiffuseRange[1], NDotL);

// 根據根據v的值決定輸出_LightRange範圍內的值
// _LightRange的參考值爲0.9, 1
v = lerp(_LightRange[0], _LightRange[1], v);

color *= v;

在這裏插入圖片描述
  還不錯,這下便爲模型劃分了明暗,並在兩者之間做了過渡,這種方式稱之爲二值化。着色並沒有採用很明顯的暗色,只是想凸顯一點立體感,以及讓畫面更柔和,不那麼刺眼罷了。當然目前可以說是非常不明顯了,這是有原因的,且待後續調色。

描邊

接下來需要補上日式2D卡通不可或缺的一部分:描邊(Outline),描邊有助於劃分物體,明確空間上的層次,並提供一定的風味。
  關於描邊的實現方式,業界主要有模型多畫一遍並將邊緣外擴以及屏幕後處理的方案。前者方案在日式遊戲較爲流行,優點在於實現簡單,性能也還算過得去,缺點是必須開抗鋸齒不然沒眼看。後者實現方式多樣,並且根據實現方式能達到不一樣的效果(如一定程度的內描邊),但有些更適合搭配延遲渲染(Deferred Rendering),而這代表着對顯卡帶寬與光照方案有要求。
  另外在顯示方案上也有區別,有追求任何縮放下描邊大小不變的,也有自然派的。有讓描邊純色的,也有要讓描邊根據貼圖顏色決定的。本人採用的是模型外擴、自然縮放、根據貼圖顏色決定的描邊方案。
  多顯示一遍模型在Unity增加一個Pass即可,並且開啓正面剔除(只顯示背面,不然會干擾到正常模型)。並且在頂點着色器對模型頂點進行外擴,外擴的方向由所在模型面的法線決定。而顏色方面則在片元着色器根據貼圖顏色進行置暗顯示即可:

// 頂點着色器

half4 viewPos = mul(UNITY_MATRIX_MV, input.positionOS); // 將頂點從模型空間轉爲觀察空間
half3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, input.normalOS); // 同上,將法線轉爲觀察空間
viewPos += float4(normalize(normal), 0) * 0.0075; // 頂點沿法線外擴
output.positionCS = mul(UNITY_MATRIX_P, viewPos); // 將頂點從觀察空間轉爲裁剪空間

output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap); // 提取uv

return output;
// 片源着色器
color *= SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); // 提取貼圖顏色

return color * 0.3; // 壓暗

在這裏插入圖片描述

果不其然,沒有抗鋸齒的話就很搓,跟早期的跑跑卡丁車似的。安排一波MSAA8x:
在這裏插入圖片描述
  哎,這就舒服多了,當然實際上由於小泥人的關係,4x和8x實際上看不出區別,而2x也算可以接受的效果,這麼看來能耗也還好。當然關於描邊實際上還有內描邊這個大題,但小泥人不需要這麼豐富的細節,這就很舒服。

發光

目前模型的顯示還欠缺一些發光的元素,如一般頭髮和武器會有一些高光效果。這在馮氏光照稱之爲鏡面光照(Specular):本質上與漫反射一樣,只是由視角方向與光照方向相加,並與法線做點積獲得兩者的夾角係數,如此便可實現根據攝像機與光照運動結合決定模型高光的位置。
  當然僅此而已是不夠的,顯而易見僅此而已的話將會如漫反射一般範圍很大,而高光實際上只需要一點即可。實際上會將之範圍縮小:

half3 halfVec = normalize(lightDir + viewDir); // 將攝像頭方向與光照方向相加
half NdotH = dot(normal, halfVec); // 與法線點積,獲取夾角係數
NdotH = saturate(NdotH); // 保證在0-1

half v = pow(NdotH, _Smoothness); // 縮小夾角係數的值,由於NdotH在0-1,所以pow後會變得更小,_Smoothness參考值爲8-64
color += color * v; // 在原有顏色的基礎上疊加

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-c5JP9lox-1589122401873)(https://musoucrow.github.io/images/lbbn_shading/10.png)]
  與之前一樣,這樣的高光過渡太強了,不夠卡通,將之二值化:

half v = pow(NdotH, _Smoothness); // 縮小夾角係數的值,由於NdotH在0-1,所以pow後會變得更小,_Smoothness參考值爲8-64
v = step(_SpecularRamp, v); // 小於_SpecularRamp的值將爲0,反之爲1
v = v * _SpecularStrength; // 定義高光強度,參考值爲0.2

color += color * v; // 在原有顏色的基礎上疊加

在這裏插入圖片描述
  這樣的高光就更有手繪的感覺了,牛屎一塊。但很顯然對於頭髮而言光是一塊牛屎高光是不夠的,讓美術自由的進行創作顯然是更好的方案。於是引入了發光貼圖(Emission),其本身很簡單:就是在最後把發光貼圖的內容顯示出來即可。而之所以要單獨劃分貼圖而不是畫死在原貼圖,在於要自由的控制透明度甚至曝光,以及讓發光參與單獨的光照運算(與高光類似的方法,攝像機視角與光照方向相加後與法線點積)。
在這裏插入圖片描述

到了目前仍缺一個日式2D卡通的一個特性:邊緣光(Rim),一般爲了表達物體處於光亮的環境下,屬於光溢出的一種表達,有助於提升畫面的層次感。實現原理也很簡單:視角方向與法線點積,根據夾角係數取得當前視角下的模型邊緣部分,爲之加光即可。

half VDotN = dot(viewDir, normal); // 視角方向與法線點積,獲取夾角係數
VDotN = 1 - saturate(VDotN); // 取反,方便計算

half v = smoothstep(_RimRange[0], _RimRange[1], VDotN); // 與漫反射部分類似,做二值化,參考值爲0.4-1
v = step(0.5, v); // 小於0.5的部分都不要了
v = v * _RimStrength; // 設定邊緣光強度,參考值爲0.1

color += color * v; // 疊加

在這裏插入圖片描述
  發光的構成大致如此,目前也許看起來不夠明顯,實是尚未調色所致,且看下文。

調色

先來看看目前的效果:
在這裏插入圖片描述
  首先是整體顏色風格不符合主題,這個場景屬於有着岩漿的密室,應該符合昏暗以及灼熱的色調,使用Split Toning進行調色:
在這裏插入圖片描述
  嗯,至少色調上像樣了,但還是缺乏灼熱的感覺,上Bloom看看:
在這裏插入圖片描述
  哎呀,看着只是稍微亮了點的樣子,那是因爲Bloom需要配合HDR使用,將顏色突破0-1的限制下進行運算,才能做到光溢出的效果:
在這裏插入圖片描述
  唔……這溢出的實在是有限,因爲目前還處於Linear顏色空間,顯示器對於顏色會進行處理,使得顏色之間的區間變小(明暗不明顯),需要轉成Gamma才能抵消之:
在這裏插入圖片描述
  成了,如此便得出了昏暗且灼熱的場景風格,高對比度(亮者更亮、暗者更暗)的畫面。

後記

這算是本人進入圖形渲染的一個里程碑,感覺這的確是個美術活。技術不過是讓你能進入賽道罷了,真正決定效果的還得看美術的理念。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章