開篇胡扯
? ? ? 之前在學(xué)習(xí)入門精要的時(shí)候,看到陰影部分各種名字超長(zhǎng)的內(nèi)置宏和看不明白的采樣方式就想直接跳過(guò)這一章了(當(dāng)時(shí)想的是,反正用到的時(shí)候把這些內(nèi)置宏復(fù)制一遍出來(lái)就可以了)。直到前段時(shí)間面試被問(wèn)到unity內(nèi)部陰影到底是怎么實(shí)現(xiàn)的,才發(fā)現(xiàn)自己對(duì)陰影一無(wú)所知,剛好最近有時(shí)間,準(zhǔn)備再次認(rèn)真的學(xué)習(xí)一下陰影相關(guān)的知識(shí)。
1.陰影必須的三要素
? ? ? 想要產(chǎn)生陰影,至少需要三個(gè)物體的支持:
- 光源(廢話)
- 陰影產(chǎn)生(投射?)者
- 陰影接收者

? ? ? 陰影的產(chǎn)生與消失與這三個(gè)物體息息相關(guān),所以當(dāng)場(chǎng)景中陰影消失時(shí)要先查找一下是不是這三個(gè)物體沒(méi)有打開對(duì)應(yīng)的開關(guān)。

? ? ? light組件中的mode和shadow type選項(xiàng)都會(huì)影響陰影的產(chǎn)生,mesh組件重的cast shadows表示是否向其他物體投射陰影,receive shadows表示這個(gè)mesh是否接收其他物體的陰影。
2.陰影是怎么產(chǎn)生的
? ? ? 我們知道,陰影是由于物體遮住了光的傳播,不能穿過(guò)不透明物體而形成的較暗區(qū)域。
? ? ? 而在unity中,陰影使用的是一種Shadow Map的技術(shù)。大概意思就是把相機(jī)放在光源位置,相機(jī)看不到的地方,就是這個(gè)光源的陰影區(qū)域。
3.陰影產(chǎn)生著(投射者)
? ? ? 上面提到過(guò)unity使用的是Shadow Map技術(shù),unity要把相機(jī)放在光源位置,然后計(jì)算一張?jiān)擖c(diǎn)的深度圖。如果按照正常流程來(lái)說(shuō),我們要把物體的渲染流程全都走一遍來(lái)寫入深度,得到shadowmap,但是這么做無(wú)疑會(huì)做很多多余的計(jì)算(很多和深度無(wú)關(guān)的計(jì)算如光照模型等)。
? ? ? unity使用了一個(gè)額外的pass來(lái)專門更新光源的shadowmap:這個(gè)pass就是LightMode標(biāo)簽被設(shè)置為ShadowCaster的pass。所以,在unity中,如果沒(méi)有這個(gè)pass,并且沒(méi)有指定Fallback或指定的Fallback中沒(méi)有LightMode標(biāo)簽為ShadowCaster的pass時(shí),該物體就無(wú)法向其他物體投射陰影。
Tags { "LightMode" = "ShadowCaster" }

? ? ? 對(duì)于不透明物體,我們一般可以使用unity中寫好的shader作為Fallback,這樣在渲染的時(shí)候unity會(huì)自己去Fallback中尋找ShadowCaster的pass來(lái)渲染陰影。但是對(duì)于透明物體或者是使用了透明度測(cè)試的材質(zhì),使用默認(rèn)的ShadowCaster就會(huì)得到一些錯(cuò)誤的結(jié)果,這個(gè)時(shí)候就要我們根據(jù)自己的需要來(lái)實(shí)現(xiàn)ShadowCaster的pass,比如在片元著色器進(jìn)行透明度測(cè)試等。
4.陰影接收者
? ? ? 在傳統(tǒng)的陰影映射紋理中,我們會(huì)在正常渲染的pass中把頂點(diǎn)轉(zhuǎn)換到光源空間下,然后對(duì)光源的shadowmap進(jìn)行采樣,再把采樣結(jié)果與頂點(diǎn)的深度進(jìn)行對(duì)比,如果頂點(diǎn)深度大于采樣結(jié)果,那么說(shuō)明該頂點(diǎn)在陰影內(nèi)。
? ? ? unity使用了不同的陰影采樣技術(shù):屏幕空間的陰影映射技術(shù)。但是不是所有平臺(tái)unity都會(huì)使用這種技術(shù),因?yàn)檫@種技術(shù)需要顯卡支持MRT。那么屏幕空間的陰影映射技術(shù)到底做了什么呢?
? ? ? unity首先會(huì)調(diào)用LightMode為ShadowCaster的pass得到光源的陰影映射紋理(shadowmap)和攝像機(jī)的深度紋理。然后根據(jù)陰影映射紋理和相機(jī)的深度紋理得到屏幕空間的陰影圖。如果攝像機(jī)的深度圖中記錄的深度大于轉(zhuǎn)換到陰影映射紋理中的深度值,就說(shuō)明該表面可見但是處于陰影中。
? ? ? 如果我們想要某個(gè)物體接受來(lái)自其他物體的陰影,只需要在shader中對(duì)這張屏幕空間的陰影圖進(jìn)行采樣就可以了。因?yàn)殛幱皥D是基于屏幕空間的,所以我們?cè)诓蓸拥臅r(shí)候要把表面坐標(biāo)從模型空間變換到屏幕坐標(biāo)空間。


? ? ? 那么怎么對(duì)陰影圖進(jìn)行采樣呢?unity其實(shí)已經(jīng)幫我們封裝好了采樣的函數(shù)。我們可以直接使用三個(gè)宏指令,就可以完成對(duì)陰影的采樣。三個(gè)宏指令分別是:SHADOW_COORDS(用在頂點(diǎn)輸出結(jié)構(gòu)體內(nèi))、TRANSFER_SHADOW(用在頂點(diǎn)著色器)、SHADOW_ATTENUATION(用在片元著色器)。這三個(gè)指令都是在AutoLight.cginc中定義的,所以我們?cè)谑褂们耙浀锰砑觟nclude。
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
//參數(shù)為下一個(gè)插值寄存器的索引
SHADOW_COORDS(2)
};
v2f vert(a2v v) {
v2f o;
//你的定點(diǎn)著色器邏輯
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
//片元著色器邏輯
fixed shadow = SHADOW_ATTENUATION(i);
//結(jié)果計(jì)算
}
? ? ? AutoLight中定義的宏指令:
// ---- Screen space direction light shadows helpers (any version)
#if defined (SHADOWS_SCREEN)
#if defined(UNITY_NO_SCREENSPACE_SHADOWS)
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
#if defined(SHADOWS_NATIVE)
fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz);
shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r);
return shadow;
#else
unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, shadowCoord.xy);
// tegra is confused if we use _LightShadowData.x directly
// with "ambiguous overloaded function reference max(mediump float, float)"
unityShadowCoord lightShadowDataX = _LightShadowData.x;
unityShadowCoord threshold = shadowCoord.z;
return max(dist > threshold, lightShadowDataX);
#endif
}
#else // UNITY_NO_SCREENSPACE_SHADOWS
UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
return shadow;
}
#endif
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
#endif
? ? ? 由于這些宏會(huì)使用上下文變量來(lái)進(jìn)行相關(guān)計(jì)算,我們?cè)诰帉憇hader時(shí)需要保證自己定義的變量名與宏使用的名稱相匹配:a2v結(jié)構(gòu)體(頂點(diǎn)輸入結(jié)構(gòu)體)的頂點(diǎn)坐標(biāo)變量名必須是vertex,頂點(diǎn)著色器中a2v(頂點(diǎn)輸入)結(jié)構(gòu)體的名字必須是v,且v2f(頂點(diǎn)輸出結(jié)構(gòu)體)的頂點(diǎn)位置必須為pos。
5.統(tǒng)一管理陰影與光照衰減
? ? ? 在片元著色器中我們使用了SHADOW_ATTENUATION宏進(jìn)行陰影處理(對(duì)屏幕空間陰影圖進(jìn)行采樣)。unity還封裝了一個(gè)宏,可以進(jìn)行統(tǒng)一的光照衰減計(jì)算與陰影計(jì)算,那就是UNITY_LIGHT_ATTENUATION宏,這個(gè)宏需要三個(gè)參數(shù),第一個(gè)是光照的衰減atten,這個(gè)參數(shù)我們不用在外部聲明,宏內(nèi)部會(huì)自己聲明該變量并填充衰減值后傳出,第二個(gè)參數(shù)是我們?cè)谄髦心玫降捻旤c(diǎn)輸出結(jié)構(gòu)體,第三個(gè)參數(shù)是世界空間的坐標(biāo),這個(gè)坐標(biāo)用來(lái)計(jì)算光源空間下的坐標(biāo)。這個(gè)宏針對(duì)不同類型的光源和情況聲明了多個(gè)版本,所以我們?cè)谑褂玫臅r(shí)候不需要在Additional Pass判斷光源類型,代碼也得以統(tǒng)一。
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
總結(jié)
? ? ? 又看了一遍書中關(guān)于陰影的部分,確實(shí)收益良多。但是還有很多東西現(xiàn)在搞不清楚,比如為什么在生成光源陰影映射紋理的時(shí)候要繪制很多次同樣的物體,而且不能合批,是不是陰影映射紋理也默認(rèn)進(jìn)行了LOD處理呢?
補(bǔ)充:
? ? ? 在渲染陰影的時(shí)候,會(huì)繪制四次renderjobdir,這里的陰影也是使用了一種類似于LOD的手法進(jìn)行處理,生成四份光源空間下的深度圖。在游戲運(yùn)行時(shí)根據(jù)相機(jī)距離來(lái)決定最終要采樣哪一種質(zhì)量的陰影貼圖,這樣可以在游戲執(zhí)行時(shí)動(dòng)態(tài)的優(yōu)化效率。但是同樣付出的代價(jià)就是要多繪制幾次,也就是多一些DC,同樣用來(lái)存儲(chǔ)的空間也對(duì)應(yīng)的要增大一些。

? ? ? 那么這個(gè)LOD的設(shè)置在哪呢?在quality setting中我們可以看到有相關(guān)的shadows的設(shè)置信息,在這里就可以設(shè)置shadow cascades數(shù)量啦。
