unity中混合光照與陰影的探索

閱讀本文前最好先看一下Unity custom shader中調用內置Lightmap和Light Probes

我們知道,unity中的Lighting Mode分為三種,baked indirect,subtractive和shadowmask三種。

baked indirect模式混合了實時直接光和烘焙間接光,并提供了實時陰影。由于光照比較真實,陰影精確度比較可信,所以非常適合中端硬件。
subtractive模式提供直接和間接光的烘焙,直接實時陰影只受一盞方向光的影響。由于它并不能提供非常真實的光照結果,所以風格化的渲染或者低端硬件比較適合這種模式。
shadowmask模式混合了實時直接光和烘焙間接光,它能烘焙物體的陰影并能自動混合實時陰影。這種模式看起來最為真實但也最消耗資源,不過可以在Quality Settings進行調整。高中端設備適合這種模式。

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

subtractive
shadowmask

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

subtractive
shadowmask

那么在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參數,通過代碼可以看出我們需要調用UnityComputeShadowFadeDistanceUnityComputeShadowFade來計算得到最終的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,按照陰影類型在視線距離zsphereDist之間做lerp操作來得到陰影fade的距離。不過我谷歌了一下,這個陰影類型w分量不是0就是1,也就是說最終結果返回的不是z就是sphereDist。

UnityComputeShadowFade源碼如下

half UnityComputeShadowFade(float fadeDist)
{
    return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
}

上面計算得到的陰影fade的距離還需要進一步的被矯正,所以需要把計算得到的距離扔進UnityComputeShadowFade方法,而_LightShadowDataz分量包含著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)物體的影響。

shadowmask

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

subtractive模式

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

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的問題

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容