在Unity實現風格化渲染的Shader

Eric Hu
Akatsuki Taiwan Technology
15 min readApr 28, 2022

風格化渲染,也稱為NPR或者Toon shading,可能是在除了Unity內建的standard shader外,Unity的開發者們最常使用的一種渲染風格。前幾個月自己寫了一個風格化渲染的shader,下文將我寫的shader內重點feature大致分為下列幾個項目。

  • 基於漸變貼圖的漫反射光照(Ramp lighting)
  • 風格化的Specular Highlight
  • 邊緣光Rim Lighting)
  • 描邊效果(Outline)
  • 頭髮上的Specular Highlight
  • 半色調效果(Halftone Overlay)
  • 素描效果(Hatching Overlay)
  • 自訂陰影顏色(Custom Shadow Color)
  • 自訂材質編輯器
  • 其他

基於漸變貼圖的漫反射光照(Ramp lighting)

在諸多常見的光照模型中,用於計算漫反射的Lambert模型還有Half-Lambert模型是最常用到的,透過dot(normal, lightDirection)[之後就稱NdotL]的計算結果我們可以讓物體表面的顏色呈現出一個基本的漫反射光照。

Lambert model & Half-Lambert Model

但在做風格化渲染時,我們希望對於光照在物體表面的顏色可以有更多客製的能力,基於漸變貼圖的漫反射光照就是一個解決的方法,原理是不僅僅是將NdotL的值作為光照在物體表面的強度,而是將NdotL當作uv值去採樣一張漸變貼圖(Ramp texture),透過這個步驟我們可以讓原本漫反射光照的結果可以完全由貼圖上的顏色漸層去控制。

4 steps ramp texture, gradient ramp texture and ramp texture on team fortress 2

可以發現, 僅僅是透過更換不同的漸變貼圖,就可以呈現出完全不同的風格。透過拓展Unity的編輯器,我們還可以讓使用者在Unity內利用Gradient編輯器直接拉出不一樣的漸變貼圖,所見即所得的快速試出想要的渲染結果。

最簡單也是網路上最常見的的toon shading是2-step tone,
在shader內可以用 step(_Thredshold, saturate(NdotL))來表示

有需要的話我們還可以混和Unity PBR渲染跟Ramp lighting的結果,以下是直接將PBR diffuse與ramp lighting用lerp作內插的結果

風格化的Specular Highlight

Specular hightlight是當光在物體表面完美的反射時(鏡面反射)所呈現的亮點,常見的算法有Blinn-Phong模型算出的NdotH值

//Source : Unity Graphics Repofloat3 halfVec = normalize(float3(lightDir) + float3(viewDir)); 
half NdotH = saturate(dot(normal, halfVec));
half modifier = pow(NdotH, smoothness);
half3 specularReflection = specular.rgb * modifier;
return lightColor * specularReflection;

複雜一點的則有像是Unity的standard shader基於BRDF的實作對paper有興趣的可以參考這一篇Moving Mobile Graphics — SIGGRAPH 2015 Course

個人很喜歡Unity的BRDF Specular,於是就把這段code拉出來,再把額外的參數放進去這段計算結後對邊緣衰減作一些銳利跟平滑化,可以做出相較於原本真實感渲染更卡通風格一點的specular highlight。

//對衰減做銳利 & 平滑的code
//source : https://github.com/ChiliMilk/URP_Toon/blob/master/Assets/ChiliMilkToonShader/Include/ToonFunction.hlsl
half StepFeatherToon(half Term,half maxTerm,half step,half feather)
{
return saturate((Term/maxTerm-step)/feather)*maxTerm;
}
左至右為原本的PBR specular至調整成完全銳利邊緣的specular

邊緣光(Rim Lighting)

zelda與TF2的角色身上都rim light,zelda的較為銳利

邊緣光也是一個滿常在遊戲中看到的效果,像是薩爾達傳說、絕地要塞二...等遊戲都可以看到。實作的方式我目前知道兩種,一種是用深度圖另一種是用菲涅爾效應(Fresnel Effect),因為習慣用菲涅耳了了這裡就講菲涅爾效應版本的實作。

菲涅爾反射:當你站在湖邊的時候,往正下方看會發現湖水很清澈,但當你看向遠方的湖面,會發現水面就向一面鏡子一樣,很難看到水下的景象,這種現象就是菲涅爾效應,

Tenaya Lake, Yosemite National Park

在shader內這個菲涅爾反射的強度可以用view direction跟該頂點normal direction做內積來求得。當這兩個向量夾角越大時,菲涅爾反射就越強
(EX : 當我們看著遠方的湖面時,湖面的一點到我們眼睛的向量與湖面法線夾角就比較大)。

//source : Unity Shader Graph Documentfresnel = pow((1.0 — saturate(dot(normalize(Normal), normalize(ViewDir)))), Power); 

透過跟NdotL做內插我們還可以控制邊緣光面光, 背光的強度。
也可以再利用smoothstep對邊緣的衰減做銳利或平滑化。

左、中 - 調整邊緣的平滑化。右 - 將強度與背光的方向做lerp

描邊效果(Outline)

outline實做方法也是很多:用第二個pass重畫一次物件時做normal extrude(法向量外推)並且剔除掉front face 、用邊緣偵測,用本村線,甚至直接畫在貼圖上。

左 - 荒野亂鬥(使用2pass) ,中 - 二之國(使用邊緣偵測),右 - Guilty Gears(使用2pass+本村線)

由於被PS4二之國的描邊效果深深吸引,我原本是用邊緣偵測(camera深度圖 + vertex color + camera normal texture),但嘗試+參考了一堆網路上的文章,覺得用邊緣偵測要有完美的outline感覺單純地用這三個貼圖是不夠的,控制邊緣偵測的outline可能需要更多額外的貼圖或vertex data。於是我退而求其次選擇用2 pass + normal extrude的方法了,在不同的空間做extrude效果也不太一樣(我是在clip space做外推的動作)。

用額外的pass在Vertex shader內將當前object的position稍微外推, 在pixel shader內將目前object畫為全黑(或者邊線顏色), 替除掉front face之後疊加在原本的object上

如果是Built-In render pipeline應該是直接在sub shader內多加一個pass就可以,URP的話必須寫一個renderer feature去插入一個custom pass,透過shadertagId來找到那個額外寫的outline pass,就可以在指定的render時機渲染所有帶有該outline pass的renderer。

更多outline的實作方式可以參考 5 ways to draw an outline

頭髮上的Specular Highlight

頭髮上的specular highlight跟光滑平面上的specular highlight是不一樣的,米哈游曾經在Unite 2018 | 《崩坏3》:在Unity中实现高品质的卡通渲染(下)分享過這部分的實作,使用的是Kajiya-kay Model,這是一種各向異性光照模型,出處跟詳細說明可以看這篇2004年的paper

一般的specular是用NdotH來計算,而Kajiya-kay Model則是將光照計算的法向量改為切線向量,並且假設物體表面的法向量會是在切線(Tangent)與View direction構成的平面上. 算出的結果再搭配noise貼圖就可以製造出髮絲上的specular light了.

source : Hair rendering and shading(2004)float3 ShiftTangentHair(float3 Tangent, float3 N, float shift)
{
float3 shiftedT = Tangent+ (shift * N);
return normalize(shiftedT);
}
float3 StrandSpecular(float3 T, float3 V, float3 L, float exponent)
{
float3 H = normalize(L + V);
float dotTH = dot(T,H);
float sintTH = sqrt(1.0-dotTH*dotTH);
float dirAtten = smoothstep(-1.0,0.0,dot(T,H));
return dirAtten * pow(sintTH,exponent);
}
float3 AnistropicColor(float3 tangent, float3 normal, float3 viewVec, float3 lightVec, float2 uv)
{
float shiftValue = tex2d(tSpecShift, uv).r;
float3 t1 = ShiftTangentHair(tangent, normal, shiftValue);
float spec = StrandSpecular(t1, viewVec, lightVec, power);
return _SpecularColor.rgb * spec;
}

再透過smothstep對計算的結果做衰減的銳利/平滑化就可以調出風格化的頭髮specular light了。

米哈游的實作內還有用curve來控制反射光的粗細跟抖動幅度以及用一額外的map去讓specular不連續,我有嘗試去實作不過因為最後不再我目前的需求內就沒放進專案了,實作的方式是把Unity內的animation curve的結果輸出為一張貼圖。在計算頭髮specular時去採樣這張貼圖的值作粗細的控制。

半色調效果 Halftone Overlay

半色調效果最早用在報紙上,是個歷史悠久的印刷效果,是指為了模擬出連續調影像(色階)的視覺感覺,一般用墨點(半色調網點)的大小或頻率的改變,來模擬明暗的變化。

後來又想到蜘蛛人-新宇宙這部電影內的人物身上好像蓋了一層類似的效果,就想來實作看看,實作的方法滿簡單的可以利用Signed distance field圖當成墨點的形狀,接著只要根據NdotL的強弱去控制每個墨點的大小就可以了(因為是SDF圖,所以大小很好控制,還自帶AA),如果不想用SDF圖也可以在pixel shader內去算出一些procedural的SDF(圓形、方形、矩形)。

SDF圖的採樣有兩種方式,用mesh自己的UV或者用screen position,用mesh的uv會讓haltone的形狀也跟著物體本身uv一起變形(簡單來說就是會跟著物體形狀憶起變形)

用screen space的話,效果就會像是漫畫網點或報紙的半色調一樣,但比較麻煩的則是 - 因為是screen space,物體遠離鏡頭的時候,表面的halftone大小並不會跟著變化,我處理的方式是利用鏡頭跟mesh root的距離在作一次scaling,讓物體靠近/遠離鏡頭的時候,採樣的screen space uv也會跟著放大縮小,確保在物體靠近遠離相機時,物體表面的halftone密度不變。

用mesh uv與screen position採樣出的halftone形狀差異

素描效果Hatching Overlay

常在各種NPR的shader練習裡面看見的一種效果,準備N張tone art maps(TAM),每個TAM是密度不同的素描筆觸貼圖,根據NdotL的值去選擇對應的TAM去作採樣,(EX: 光照弱的地方就選擇筆觸密度高的TAM)。

傳統的作法可以參考這篇paper還有這個Repo,因為我是超級懶鬼有點不想準備太多貼圖,就用一種近似於傳統hatching的作法,準備一張noise貼圖,將他拉伸之後丟到for迴圈內(假設原本有8張TAM,就loop 8次),每次loop的時候都把採樣noise圖的uv跟scale作一點偏移,將結果疊加在一起,這樣可以在runtime僅透過一張拉伸的noise圖就模仿出那N張密度不同的筆觸貼圖(loop到最後的結果就是密度最高的筆觸貼圖),然後根據NdotL的結果去控制跳出loop的層數。

自訂材質編輯器

在給自己的shader塞了一堆properties之後,材質球的編輯器畫面應該會很凌亂,新增一個editor script,繼承BaseShaderGUI來自訂自己的材質編輯器UI,寫的時候參考滿多Unity的LitShader跟SimpleLitShader的editor script內的程式碼,裡面有handle滿多類型的property的EditorGUI,能用的就直接沿用,有缺的類型再自己寫materal drawer。

自訂material drawer可以參考Unity Material Property Drawer — 客製化材質編輯器

其他

一些渲染出的成果

真的想認真寫一個可以work的Unity shader發現還有滿多眉眉角角需要注意的,比如說除了即時的光照計算也要考慮到bakeGI跟light probe影響的結果。Unity官方的shader(這裡指URP)在這部分的code也是每三個月就改一次,讓人追到有點無力。

這篇只是列出我寫的shader有用到的功能,如果是更專注於動漫風的Shader還有更多細節沒有提到,像是眉毛、眼睛、腮紅,以及卡通渲染時臉部的陰影(需要特別處理)...等等,學習的路還很長,只能繼續邊做邊學了。

--

--