Table of contents
🌐 Translate this post:

之前心血來潮做的風格化渲染Shader,已經上架一段時間,順便發一篇筆記來幫助自己整理一下腦袋。

關於Unity內自訂光照的shader在中文的網路資源中較多停留在Built-in render pipeline時代,SRP家族的Universal Render Pipeline則比較少,不過受惠於SRP開源的性質,trace一下原始碼可以很快就搞懂整個lit shader的渲染流程。

這篇文章我會寫一個自訂光照的shader,包括cast/receive陰影與支援多個光源,會順便爬一下com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl 的程式碼,從而幫助我們了解Unity的lit/simple lit shader在做Forward rendering時的一些細節(Deffered rendering之後再講)。

自訂義光照的Shader

Unity官方文件有提供基礎的Unlit shader的範例,但要製作一個有完整的shader我們還需要在shader內補上其他帶有不同pass tag的pass。 這份程式碼是一個簡單的template。(太長,貼link就好)

將這個shader掛上材質球後,assign給場景上的mesh renderer,如下圖。

fragment shader目前只有return _BaseColor,掛上材質球在場景上

在shader code的最下方可以看到好幾個use pass,原因是渲染管線會針對不同的LightMode tag在不同的時機抓出這些pass進行渲染 :

  • ShadowCaster會在管線渲染shadow map的時候,將這個物件的深度從光源視角畫進shadow map內,唯有補上這個pass,才能讓物體cast出陰影。 —

  • Depth Only則是從當前相機視角將物件深度畫進depth texture內。URP的depth prepass也會使用到。

  • Meta Pass用於光照烘培,Unity在build專案的過程會自動把這個pass抽掉。

  • Universal2D用於URP的2D renderer以及2d lighting,不過我沒有深入研究過。

這裡用use pass是偷懶,SRP batcher會因為其他shader的cbuffer不一致失效,實際操作時應該要自己寫對應各自pass tag的pass,不過code會很長。

這些pass都補上之後,一個物件在專案場景渲染中必要的元素(深度、寫入陰影貼圖、烘培),就只差物體本身的顏色&光照了,Template最上方,LightModeUniversalForward的pass就是主要需要自己寫的部分。

目前這個shader只會渲染出單一顏色,計算光照模型前需要先拿到main light跟世界空間下的normal,

在vertex shader內將object normal轉換為world space normal後傳至fragment shader

有了main light的資訊後,就可以寫出最簡單的lambert光照模型(NdotL),將NdotL的值乘上light color與 albedo color,就完成一個lamber光照了。

(左)三個使用目前shaderl的物件 (右)這三個物件目前沒有接收來自其他物體的陰影

目前shader內的計算還沒包含陰影,上方的程式碼中,在呼叫*GetMainLight()時我們還有傳一個shadowcoord參數進去,當有傳shadowcoord進去的時候,unity會透過MainLightRealtimeShadow()*來採樣當前shadow map,我們可以從light struct中的shadowAttenuation獲得採樣的結果。接著在光照計算的最後多乘上shadowAttenuation就可以完成物體被陰影遮住的部分。

float lambert = ndotl * mainLight.color * mainLight.shadowAttenuation;

在計算光照模型時也考慮到shadow attenuation

自訂的shader渲染結果有接收陰影,但是陰影相較於內建的shader很明顯的不一樣(純黑)

關於shadowmap — Unity的shadow map有分單張的shadowmap跟CSM(Cascade shadow map),在陰影距離有限的條件下,為了能提升陰影繪製距離又能讓近景有高品質的陰影,現在的遊戲通常都會把CSM打開,CSM詳細的說明與實做可以參考https://learnopengl.com/Guest-Articles/2021/CSM

回到場景上,shader現在已經可以接收陰影,但陰影的部分相較於內建的lit shader(地板部分)特別的黑,這時候可以參考URP的simple lit shader來看看有甚麼差異。

simple lit計算光照使用ShaderLibrary/Lighting.hlsl內的UniversalFragmentBlinnPhong函式

Unity的simple lit是用blinn-phong光照模型,lambert的計算跟目前我自訂的shader內容是一樣的,但仔細看上方diffuseColor變數,可以發現simple lit在計算lambert光照之前,有再加上一個inputData.bakedGI的值,目前我自訂的shader內缺少bakedGI的值,因此呈上陰影的output最終就會是0(純黑)。

這個bakeGI的值包含了來自skybox的環境光以及lightprobe的間接光,Unity的lit/sample lit shader會在各自的fragment shader內透過SAMPLE_GI這個巨集內來設定bakeGI的值。

inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS);

為了讓code單純一點,不考慮lightmap的情況我們可以直接呼叫SampleSH來做為GI的顏色。

在lambert的計算結果之前加上bakedGI的值

SAMPLE_GI內部其實是對skybox還有indirect light(lightprobe)做Spheric harmonic 的光照計算。 原理牽扯到很多數學的推導,可以參考https://huailiang.github.io/blog/2019/harmonics/ https://en.wikipedia.org/wiki/Spherical_harmonic_lighting

再回到場景上,目前自訂義的光照結果已經與simple lit幾乎是一樣了(不過沒有specular)。接收到陰影的部分因為有SampleSH的顏色當底色也不再是單純的純黑色。

到這邊,其實已經可以嘗試不同的光照模型(Oren-Nayer, Blinn-Phong, PBR, Toon shading),但在套用這些模型之前,還有additional light(多光源)的情況需要考慮,目前shader內唯一計算的光源只有Main Light。

在場景上放四個point light,只有unity內建的lit shader有顯示這些point light的顏色,自訂的shader並沒有。

為了計算額外的光源(point light, spot light…etc),我們一樣需要獲得這些光源的資訊(light color, direction, attenuation),一樣參考lighting.hlsl 內的code,可以找到這段 -

UniversalFragmentBlinnPhong函式 — 計算多光源的部分

URP預設的forward rendereing,每個URP的物件最多可以拿到8個additional light的資料(所以在forward rendering中,對每一個計算受光照物件的fragment來說,這個for迴圈最多會跑8次),

把上面的get additonal light的部份抽到目前的Shader後,在目前的pass補上#pragma multi_compile _ _ADDITIONAL_LIGHTS keyword後,將迴圈內的每個additional light都做一次光照的計算,並將計算結果疊加到diffuseColor上。

將多光源的code補進自訂的shader內

最終可以渲染出支援多光源的物體。

最後寫完的template code可以參考這裡

嘗試不同的光照模型

將ndotl的結果做一個step(0.5, ndotl),可以做出一個簡單的toon shading, 把每一行saturate(dot(normalWS, mainLight.direction))都改成 step(0.5, saturate(dot(normalWS, mainLight.direction)))之後回場景上看結果 -

step(0.5, ndotl)的toon shader, base color設為綠色(左) 與白色(右)

我們就在URP上做出一個,可以cast shadow、可以recive shadow,又支援多光源的toon shader了。

最後列一下重點

  • 要製造出陰影必須有shadowcaster pass

  • depthOnly pass要記得補,才會把物體的深度寫入進_CameraDepthTexture,很多shader渲染的效果都會用到。

  • 要接收陰影必須在get light的時候把Shadow coord傳進去,才能讓light.shadowattenation有採樣shadow map的值

  • 在Unity內寫光照shader還要考慮到間接光與環境光,這部分大部分是在SAMPLE_GI內完成。

  • 多光源的部分可以參考ShaderLibrary/Lighting.hlsl

URP的ShaderLibrary/Lighting.hlsl 中還有像是BRDF的specular light與一般ndoth的specular light的相關程式碼,在寫自己寫光照時都可以多多參考,