美文网首页
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