閱讀本文前最好先看一下Unity custom shader中調用內置Lightmap和Light Probes
我們知道,unity中的Lighting Mode分為三種,baked indirect,subtractive和shadowmask三種。
baked indirect模式混合了實時直接光和烘焙間接光,并提供了實時陰影。由于光照比較真實,陰影精確度比較可信,所以非常適合中端硬件。
subtractive模式提供直接和間接光的烘焙,直接實時陰影只受一盞方向光的影響。由于它并不能提供非常真實的光照結果,所以風格化的渲染或者低端硬件比較適合這種模式。
shadowmask模式混合了實時直接光和烘焙間接光,它能烘焙物體的陰影并能自動混合實時陰影。這種模式看起來最為真實但也最消耗資源,不過可以在Quality Settings進行調整。高中端設備適合這種模式。

有了初步的印象,我們來看看具體使用時的不同之處


從生成的文件來看,shadowmask模式比subtractive模式多了一張貼圖,這張貼圖保存了靜態(tài)物體的陰影烘焙信息。那么subtractive模式沒有這張貼圖是不是意味著沒有陰影信息呢?其實不是的,subtractive模式中靜態(tài)物體的陰影也烘焙了,并且保存在了光照貼圖中,也就是說陰影信息其實是shadowmask模式額外多用了一張貼圖來保存,并不是說subtractive模式陰影信息沒了。


那么在shadowmask模式下有了這張陰影貼圖后我們該如何去使用呢?看下面的代碼
float directAtten = SHADOW_ATTENUATION(i);
float viewZ = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
float shadowFadeDistance = UnityComputeShadowFadeDistance(i.worldPos, viewZ);
float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
float bakedAtten = UnitySampleBakedOcclusion(i.uvLM,i.worldPos);
float atten = UnityMixRealtimeAndBakedShadows(directAtten, bakedAtten, shadowFade);
col.rgb *= atten;
col.rgb += lm;
首先我們需要UnitySampleBakedOcclusion這個方法來獲取烘焙陰影,它需要光照貼圖的uv和世界坐標來進行采樣,采樣完畢所得到的陰影需要再扔進UnityMixRealtimeAndBakedShadows這個方法,同時扔進實時陰影和一個距離來進行混合,使得烘焙和實時的陰影不至于涇渭分明。
現在我們來詳細看看這兩個方法在干什么,UnitySampleBakedOcclusion的源碼如下
fixed UnitySampleBakedOcclusion (float2 lightmapUV, float3 worldPos)
{
#if defined (SHADOWS_SHADOWMASK)
#if defined(LIGHTMAP_ON)
fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
fixed4 rawOcclusionMask = fixed4(1.0, 1.0, 1.0, 1.0);
#if UNITY_LIGHT_PROBE_PROXY_VOLUME
if (unity_ProbeVolumeParams.x == 1.0)
rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
else
rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#endif
#endif
return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
#else
//In forward dynamic objects can only get baked occlusion from LPPV, light probe occlusion is done on the CPU by attenuating the light color.
fixed atten = 1.0f;
#if defined(UNITY_INSTANCING_ENABLED) && defined(UNITY_USE_SHCOEFFS_ARRAYS)
// ...unless we are doing instancing, and the attenuation is packed into SHC array's .w component.
atten = unity_SHC.w;
#endif
#if UNITY_LIGHT_PROBE_PROXY_VOLUME && !defined(LIGHTMAP_ON) && !UNITY_STANDARD_SIMPLE
fixed4 rawOcclusionMask = atten.xxxx;
if (unity_ProbeVolumeParams.x == 1.0)
rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
#endif
return atten;
#endif
}
首先可以知道,我們要用UnitySampleBakedOcclusion不得不聲明一下SHADOWS_SHADOWMASK,否則進入else邏輯就不讀取陰影貼圖了(這時的陰影就要去讀取光照探針中的數據了,傳入的世界坐標worldPos這時就有用了)。。。 然后主要的代碼就涉及到如果有光照貼圖的話,去采樣unity_ShadowMask貼圖。采樣出來的rawOcclusionMask記錄的是該像素,被燈光影響的情況,比如采樣的是(1,1,0,1)那么表示這個像素,被0號和1、3號燈照射到了,但是2號燈,不能照射到,這是原始的遮擋信息。 unity_OcclusionMaskSelector就是說,unity會自動的進行篩選最強的燈(像是(0,1,0,0),不可能出現(1,1,0,0)這種有最強的兩盞燈),這個unity_OcclusionMaskSelector就記錄對應的燈的編號而已。dot就是過濾出來真正需要的信息,比如說現在rawOcclusionMask是(0.5,0.1,0,0.8),unity_OcclusionMaskSelector是(0,1,0,0),那么過濾出來就是0.1,rawOcclusionMask的g通道的值被保留下來了。
UnityMixRealtimeAndBakedShadows的源碼如下
half UnityMixRealtimeAndBakedShadows(half realtimeShadowAttenuation, half bakedShadowAttenuation, half fade)
{
// -- Static objects --
// FWD BASE PASS
// ShadowMask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK
// Subtractive mode = LIGHTMAP_ON + LIGHTMAP_SHADOW_MIXING
// Pure realtime direct lit = LIGHTMAP_ON
// FWD ADD PASS
// ShadowMask mode = SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = SHADOWS_SHADOWMASK
// Pure realtime direct lit = LIGHTMAP_ON
// DEFERRED LIGHTING PASS
// ShadowMask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK
// Pure realtime direct lit = LIGHTMAP_ON
// -- Dynamic objects --
// FWD BASE PASS + FWD ADD ASS
// ShadowMask mode = LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = N/A
// Subtractive mode = LIGHTMAP_SHADOW_MIXING (only matter for LPPV. Light probes occlusion being done on CPU)
// Pure realtime direct lit = N/A
// DEFERRED LIGHTING PASS
// ShadowMask mode = SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = SHADOWS_SHADOWMASK
// Pure realtime direct lit = N/A
#if !defined(SHADOWS_DEPTH) && !defined(SHADOWS_SCREEN) && !defined(SHADOWS_CUBE)
#if defined(LIGHTMAP_ON) && defined (LIGHTMAP_SHADOW_MIXING) && !defined (SHADOWS_SHADOWMASK)
//In subtractive mode when there is no shadow we kill the light contribution as direct as been baked in the lightmap.
return 0.0;
#else
return bakedShadowAttenuation;
#endif
#endif
#if (SHADER_TARGET <= 20) || UNITY_STANDARD_SIMPLE
//no fading nor blending on SM 2.0 because of instruction count limit.
#if defined(SHADOWS_SHADOWMASK) || defined(LIGHTMAP_SHADOW_MIXING)
return min(realtimeShadowAttenuation, bakedShadowAttenuation);
#else
return realtimeShadowAttenuation;
#endif
#endif
#if defined(LIGHTMAP_SHADOW_MIXING)
//Subtractive or shadowmask mode
realtimeShadowAttenuation = saturate(realtimeShadowAttenuation + fade);
return min(realtimeShadowAttenuation, bakedShadowAttenuation);
#endif
//In distance shadowmask or realtime shadow fadeout we lerp toward the baked shadows (bakedShadowAttenuation will be 1 if no baked shadows)
return lerp(realtimeShadowAttenuation, bakedShadowAttenuation, fade);
}
相對于上一個方法,這個理解起來比較簡單,根據不同情況返回實時陰影或者烘焙陰影,或者通過距離(點到攝像頭的距離)來進行烘焙陰影與實時陰影的混合。
現在來看這個方法里的fade參數,通過代碼可以看出我們需要調用UnityComputeShadowFadeDistance和UnityComputeShadowFade來計算得到最終的fade,那么就來看看這兩個方法在干什么吧。
UnityComputeShadowFadeDistance源碼如下
float UnityComputeShadowFadeDistance(float3 wpos, float z)
{
float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
}
這里unity_ShadowFadeCenterAndType包含了陰影中心位置(xyz分量保存)和陰影的類型(w分量保存),由世界坐標wpos計算得到與陰影中心位置的距離sphereDist,按照陰影類型在視線距離z和sphereDist之間做lerp操作來得到陰影fade的距離。不過我谷歌了一下,這個陰影類型w分量不是0就是1,也就是說最終結果返回的不是z就是sphereDist。
UnityComputeShadowFade源碼如下
half UnityComputeShadowFade(float fadeDist)
{
return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
}
上面計算得到的陰影fade的距離還需要進一步的被矯正,所以需要把計算得到的距離扔進UnityComputeShadowFade方法,而_LightShadowData的z分量包含著scale信息,w分量包含著offset信息,計算一下后將結果限制在0-1之間,就是最終陰影fade的距離了。
最后,我們把UnityMixRealtimeAndBakedShadows計算得出的陰影乘上輸出的顏色,再加上(shadowmask模式下烘焙出來的光照貼圖比較暗,加比較能還原真實結果。而subtractive模式下是乘上去比較能還原結果,個人覺得這里是加還是乘以美術導向為準)光照貼圖就好了。
這里要注意,shadowmask有兩種模式,distance shadowmask和shadowmask。Distance shadowmask模式使用時第一眼看上去和baked indirect模式一樣,陰影都是實時計算的。然而,還是會有一張陰影貼圖生成,這張貼圖中的陰影是當超出場景設置中的陰影距離時使用的,而在這個距離內都會使用實時陰影,所以這個模式會是最為昂貴的模式。我們之前代碼里已經用上了UnityMixRealtimeAndBakedShadows這個方法,所以可以提供實時和烘焙陰影的過渡。
PS. 只有直接光的情況下UNITY_LIGHT_ATTENUATION就是UNITY_SHADOW_ATTENUATION,而UNITY_SHADOW_ATTENUATION就是SHADOW_ATTENUATION。
# define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#ifdef DIRECTIONAL
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif
最后,我們來明確下各個模式下對動靜態(tài)物體的影響。

distance shadowmask模式
- 動態(tài)物體被混合光照到會接受到:
- 實時直接光
- 烘焙間接光,通過光照探針
- 在陰影距離內動態(tài)物體投出的實時陰影,陰影源自shadowmap技術
- 在陰影距離內靜態(tài)物體投出的實時陰影,陰影源自shadowmap技術
- 超出陰影距離后使用了光照探針的靜態(tài)物體所產生的烘焙陰影
- 靜態(tài)物體被混合光照到會接受到:
- 實時直接光
- 烘焙間接光,通過光照貼圖
- 在陰影距離內動態(tài)物體投出的實時陰影,陰影源自shadowmap技術
- 在陰影距離內靜態(tài)物體投出的實時陰影,陰影源自shadowmap技術
- 超出陰影距離后靜態(tài)物體所產生的烘焙陰影,通過shadowmask貼圖實現
shadowmask模式
- 動態(tài)物體被混合光照到會接受到:
- 實時直接光
- 烘焙間接光,通過光照探針
- 在陰影距離內動態(tài)物體投出的實時陰影,陰影源自shadowmap技術
- 超出陰影距離后使用了光照探針的靜態(tài)物體所產生的烘焙陰影
- 靜態(tài)物體被混合光照到會接受到:
- 實時直接光
- 烘焙間接光,通過光照貼圖
- 在陰影距離內動態(tài)物體投出的實時陰影,陰影源自shadowmap技術
- 超出陰影距離后使用了陰影貼圖的靜態(tài)物體所產生的烘焙陰影

subtractive模式
- 動態(tài)物體被混合光照到會接受到:
- 實時直接光
- 烘焙間接光,通過光照探針
- 在陰影距離內動態(tài)物體由主光源投出的實時陰影,陰影源自shadowmap技術
- 使用了光照探針的靜態(tài)物體所產生的陰影
- 靜態(tài)物體被混合光照到會接受到:
- 烘焙直接光,通過光照貼圖
- 烘焙間接光,通過光照貼圖
- 靜態(tài)物體產生的烘焙陰影,通過光照貼圖
- 在陰影距離內動態(tài)物體由主光源投出的實時陰影,陰影源自shadowmap技術

baked indirect模式
- 動態(tài)物體被混合光照到會接受到:
- 實時直接光
- 烘焙間接光,通過光照探針
- 在陰影距離內動態(tài)物體投出的陰影,陰影源自shadowmap技術
- 在陰影距離內靜態(tài)物體投出的實時陰影,陰影源自shadowmap技術
- 靜態(tài)物體被混合光照到會接受到:
- 實時直接光
- 烘焙間接光,通過光照貼圖
- 靜態(tài)物體產生的實時陰影,陰影源自shadowmap技術
- 在陰影距離內動態(tài)物體產生的實時陰影,陰影源自shadowmap技術
參考
unity中UnityGlobalIllumination.cginc的分析
Mixed Lighting Lightmap & Shader In Unity | Unity中混合光照Lightmap研究
Rendering 17 Mixed Lighting
Unity 陰影淡入淡出效果中Shader常量 unity_ShadowFadeCenterAndType和_LightShadowData的問題