美文网首页
unity中混合光照与阴影的探索

unity中混合光照与阴影的探索

作者: 上善若水_2019 | 来源:发表于2021-01-09 21:05 被阅读0次

    阅读本文前最好先看一下Unity custom shader中调用内置Lightmap和Light Probes

    我们知道,unity中的Lighting Mode分为三种,baked indirect,subtractive和shadowmask三种。

    baked indirect模式混合了实时直接光和烘焙间接光,并提供了实时阴影。由于光照比较真实,阴影精确度比较可信,所以非常适合中端硬件。
    subtractive模式提供直接和间接光的烘焙,直接实时阴影只受一盏方向光的影响。由于它并不能提供非常真实的光照结果,所以风格化的渲染或者低端硬件比较适合这种模式。
    shadowmask模式混合了实时直接光和烘焙间接光,它能烘焙物体的阴影并能自动混合实时阴影。这种模式看起来最为真实但也最消耗资源,不过可以在Quality Settings进行调整。高中端设备适合这种模式。

    有了初步的印象,我们来看看具体使用时的不同之处

    subtractive shadowmask

    从生成的文件来看,shadowmask模式比subtractive模式多了一张贴图,这张贴图保存了静态物体的阴影烘焙信息。那么subtractive模式没有这张贴图是不是意味着没有阴影信息呢?其实不是的,subtractive模式中静态物体的阴影也烘焙了,并且保存在了光照贴图中,也就是说阴影信息其实是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
    

    最后,我们来明确下各个模式下对动静态物体的影响。

    shadowmask

    distance shadowmask模式

    • 动态物体被混合光照到会接受到:
      • 实时直接光
      • 烘焙间接光,通过光照探针
      • 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
      • 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
      • 超出阴影距离后使用了光照探针的静态物体所产生的烘焙阴影
    • 静态物体被混合光照到会接受到:
      • 实时直接光
      • 烘焙间接光,通过光照贴图
      • 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
      • 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
      • 超出阴影距离后静态物体所产生的烘焙阴影,通过shadowmask贴图实现

    shadowmask模式

    • 动态物体被混合光照到会接受到:
      • 实时直接光
      • 烘焙间接光,通过光照探针
      • 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
      • 超出阴影距离后使用了光照探针的静态物体所产生的烘焙阴影
    • 静态物体被混合光照到会接受到:
      • 实时直接光
      • 烘焙间接光,通过光照贴图
      • 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
      • 超出阴影距离后使用了阴影贴图的静态物体所产生的烘焙阴影
    subtractive

    subtractive模式

    • 动态物体被混合光照到会接受到:
      • 实时直接光
      • 烘焙间接光,通过光照探针
      • 在阴影距离内动态物体由主光源投出的实时阴影,阴影源自shadowmap技术
      • 使用了光照探针的静态物体所产生的阴影
    • 静态物体被混合光照到会接受到:
      • 烘焙直接光,通过光照贴图
      • 烘焙间接光,通过光照贴图
      • 静态物体产生的烘焙阴影,通过光照贴图
      • 在阴影距离内动态物体由主光源投出的实时阴影,阴影源自shadowmap技术
    baked indirect

    baked indirect模式

    • 动态物体被混合光照到会接受到:
      • 实时直接光
      • 烘焙间接光,通过光照探针
      • 在阴影距离内动态物体投出的阴影,阴影源自shadowmap技术
      • 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
    • 静态物体被混合光照到会接受到:
      • 实时直接光
      • 烘焙间接光,通过光照贴图
      • 静态物体产生的实时阴影,阴影源自shadowmap技术
      • 在阴影距离内动态物体产生的实时阴影,阴影源自shadowmap技术

    项目地址

    参考
    unity中UnityGlobalIllumination.cginc的分析

    Mixed Lighting Lightmap & Shader In Unity | Unity中混合光照Lightmap研究

    Rendering 17 Mixed Lighting

    Unity 阴影淡入淡出效果中Shader常量 unity_ShadowFadeCenterAndType和_LightShadowData的问题

    相关文章

      网友评论

          本文标题:unity中混合光照与阴影的探索

          本文链接:https://www.haomeiwen.com/subject/ksycaktx.html