美文网首页
Unity自定义SRP(六):阴影遮罩

Unity自定义SRP(六):阴影遮罩

作者: Dragon_boy | 来源:发表于2021-01-16 20:46 被阅读0次

https://catlikecoding.com/unity/tutorials/custom-srp/shadow-masks/

1 烘培阴影

​ 使用光照贴图的优势在于我们不受最大阴影距离的限制,烘培的阴影不会被剔除,但也不会变化。理论上,我们可以在最大阴影距离内使用实时阴影,范围外使用烘培阴影。

1.1 距离相关阴影遮罩

​ 将Mixed Lighting下的Lighting Mode改为ShadowMask

Project Settings下的QualityShadows可以进行阴影遮罩的配置,有Distance ShadowmaskShadowmask。这里设置为distance的。

​ 阴影遮罩贴图包含单个混合平行光的阴影衰减信息,即那些贡献全局光照的静态物体产生的阴影,数据存储在R通道。

1.2 检测阴影遮罩

​ 为了使用阴影遮罩,我们必须检测其是否存在。我们使用关键字来控制其是否使用阴影遮罩:

    static string[] shadowMaskKeywords = 
    {
        "_SHADOW_MASK_DISTANCE"
    };

​ 添加一个布尔变量:

    bool useShadowMask;

    public void Setup (…) 
    {
        …
        useShadowMask = false;
    }

​ 在Render的末尾开启或关闭关键字:

    public void Render () 
    {
        …
        buffer.BeginSample(bufferName);
        SetKeywords(shadowMaskKeywords, useShadowMask ? 0 : -1);
        buffer.EndSample(bufferName);
        ExecuteBuffer();
    }

​ 为了知道是否需要阴影遮罩,我们需要判断是否有灯光使用它。在ReserveDirectionalShadows中进行。

​ 每个灯光包含烘培数据,存储在LightBakingOutput结构体中,可通过Light.bakingOutput属性获得。如果一个灯光的模式设置为混合,并且混合光照模式设置为阴影遮罩,那么就是要使用阴影遮罩:

    public Vector3 ReserveDirectionalShadows (
        Light light, int visibleLightIndex
    ) 
    {
        if (…) 
        {
            LightBakingOutput lightBaking = light.bakingOutput;
            if (
                lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
                lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
            ) 
            {
                useShadowMask = true;
            }

            …
        }
        return Vector3.zero;
    }

​ 在shader中添加multi_compile指令:

            #pragma multi_compile _ _SHADOW_MASK_DISTANCE

1.3 阴影遮罩数据

​ shader中,我们需要知道阴影遮罩是否在使用中,如果在使用的话,那么烘培的阴影数据是什么。添加一个ShadowMask结构体,包含距离和阴影属性:

struct ShadowMask 
{
    bool distance;
    float4 shadows;
};

struct ShadowData 
{
    int cascadeIndex;
    float cascadeBlend;
    float strength;
    ShadowMask shadowMask;
};

​ 在GetShadowData中初始化:

ShadowData GetShadowData (Surface surfaceWS) 
{
    ShadowData data;
    data.shadowMask.distance = false;
    data.shadowMask.shadows = 1.0;
    …
}

​ 阴影遮罩本质上还是烘培光照数据的一部分,因此我们需要在GI中进行:

struct GI 
{
    float3 diffuse;
    ShadowMask shadowMask;
};

…

GI GetGI (float2 lightMapUV, Surface surfaceWS) 
{
    GI gi;
    gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
    gi.shadowMask.distance = false;
    gi.shadowMask.shadows = 1.0;
    return gi;
}

​ 我们可以通过unity_ShadowMask来获取阴影遮罩纹理:

TEXTURE2D(unity_ShadowMask);
SAMPLER(samplerunity_ShadowMask);

​ 然后添加一个采样方法SampleBakedShadow

float4 SampleBakedShadows (float2 lightMapUV) 
{
    #if defined(LIGHTMAP_ON)
        return SAMPLE_TEXTURE2D(
            unity_ShadowMask, samplerunity_ShadowMask, lightMapUV
        );
    #else
        return 1.0;
    #endif
}

​ 修改GetGI,在距离相关阴影遮罩模式后,使用上述方法采样阴影遮罩纹理:

GI GetGI (float2 lightMapUV, Surface surfaceWS) 
{
    GI gi;
    gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
    gi.shadowMask.distance = false;
    gi.shadowMask.shadows = 1.0;

    #if defined(_SHADOW_MASK_DISTANCE)
        gi.shadowMask.distance = true;
        gi.shadowMask.shadows = SampleBakedShadows(lightMapUV);
    #endif
    return gi;
}

​ 在GetLighting中应用阴影遮罩:

float3 GetLighting (Surface surfaceWS, BRDF brdf, GI gi) 
{
    ShadowData shadowData = GetShadowData(surfaceWS);
    shadowData.shadowMask = gi.shadowMask;
    
    …
}

​ 我们还需要让Unity将相关数据送往GPU,配置逐物体数据的属性:

            perObjectData =
                PerObjectData.Lightmaps | PerObjectData.ShadowMask |
                PerObjectData.LightProbe |
                PerObjectData.LightProbeProxyVolume

1.4 遮蔽探针

​ 动态物体使用光照探针,而不是光照贴图,也就没有阴影遮罩数据,不过Unity也将阴影遮罩数据烘培到了光照探针中,即遮蔽探针。通过将unity_ProbesOcclusion添加到UnityPerDraw缓冲,我们可以访问该数据。

​ 在SampleBakedShadows中,我们可以简单的返回该数据:

float4 SampleBakedShadows (float2 lightMapUV) 
{
    #if defined(LIGHTMAP_ON)
        …
    #else
        return unity_ProbesOcclusion;
    #endif
}

​ 同样,设置对应的逐物体数据属性:

            perObjectData =
                PerObjectData.Lightmaps | PerObjectData.ShadowMask |
                PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
                PerObjectData.LightProbeProxyVolume

​ 不过这么做会破坏GPU实例化,只有在定义SHADOWS_SHADOWMASK时Unity才会自动的实例化遮蔽数据,因此在Common.hlsl中添加:

#if defined(_SHADOW_MASK_DISTANCE)
    #define SHADOWS_SHADOWMASK
#endif

1.5 LPPV

​ 阴影遮罩也可以使用光照探针代理体,首先开启标志:

            perObjectData =
                PerObjectData.Lightmaps | PerObjectData.ShadowMask |
                PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
                PerObjectData.LightProbeProxyVolume |
                PerObjectData.OcclusionProbeProxyVolume

​ 获得LPPV光照数据也很简单,使用SampleProbeOcclusion方法:

float4 SampleBakedShadows (float2 lightMapUV, Surface surfaceWS) 
{
    #if defined(LIGHTMAP_ON)
        …
    #else
        if (unity_ProbeVolumeParams.x) 
        {
            return SampleProbeOcclusion(
                TEXTURE3D_ARGS(unity_ProbeVolumeSH, samplerunity_ProbeVolumeSH),
                surfaceWS.position, unity_ProbeVolumeWorldToObject,
                unity_ProbeVolumeParams.y, unity_ProbeVolumeParams.z,
                unity_ProbeVolumeMin.xyz, unity_ProbeVolumeSizeInv.xyz
            );
        }
        else 
        {
            return unity_ProbesOcclusion;
        }
    #endif
}

2 混合阴影

​ 在最大阴影距离外的范围我们使用阴影遮罩。

2.1 在需要时使用烘培阴影

​ 我们将实时的获取级联阴影的代码分离出来构建GetCascadedShadow方法,在GetDirectionalShadowAttenuation中调用:

float GetCascadedShadow (
    DirectionalShadowData directional, ShadowData global, Surface surfaceWS
) 
{
    float3 normalBias = surfaceWS.normal *
        (directional.normalBias * _CascadeData[global.cascadeIndex].y);
    float3 positionSTS = mul(
        _DirectionalShadowMatrices[directional.tileIndex],
        float4(surfaceWS.position + normalBias, 1.0)
    ).xyz;
    float shadow = FilterDirectionalShadow(positionSTS);
    if (global.cascadeBlend < 1.0) 
    {
        normalBias = surfaceWS.normal *
            (directional.normalBias * _CascadeData[global.cascadeIndex + 1].y);
        positionSTS = mul(
            _DirectionalShadowMatrices[directional.tileIndex + 1],
            float4(surfaceWS.position + normalBias, 1.0)
        ).xyz;
        shadow = lerp(
            FilterDirectionalShadow(positionSTS), shadow, global.cascadeBlend
        );
    }
    return shadow;
}

float GetDirectionalShadowAttenuation (
    DirectionalShadowData directional, ShadowData global, Surface surfaceWS
) 
{
    #if !defined(_RECEIVE_SHADOWS)
        return 1.0;
    #endif
    
    float shadow;
    if (directional.strength <= 0.0) 
    {
        shadow = 1.0;
    }
    else 
    {
        shadow = GetCascadedShadow(directional, global, surfaceWS);
        shadow = lerp(1.0, shadow, directional.strength);
    }
    return shadow;
}

​ 然后添加GetBakedShadow方法:

float GetBakedShadow (ShadowMask mask) 
{
    float shadow = 1.0;
    if (mask.distance) 
    {
        shadow = mask.shadows.r;
    }
    return shadow;
}

​ 接着,创建MixBakedAndRealTimeShadows方法,混合实时阴影和烘培阴影:

float MixBakedAndRealtimeShadows (
    ShadowData global, float shadow, float strength
) 
{
    float baked = GetBakedShadow(global.shadowMask);
    if (global.shadowMask.distance) 
    {
        shadow = baked;
    }
    return lerp(1.0, shadow, strength);
}

2.2 过渡到烘培阴影

​ 为了基于深度从实时阴影过渡到烘培阴影,我们需要基于全局阴影强度在两者间插值,之后还需要应用灯光的阴影强度,也就不能直接在GetDirectionalShadowData中混合强度了。

​ 在开启距离控制的阴影遮罩模式后,我们基于全局强度在烘培阴影和实时阴影间插值,然后应用灯光阴影强度:

float MixBakedAndRealtimeShadows (
    ShadowData global, float shadow, float strength
) 
{
    float baked = GetBakedShadow(global.shadowMask);
    if (global.shadowMask.distance) 
    {
        shadow = lerp(baked, shadow, global.strength);
        return lerp(1.0, shadow, strength);
    }
    return lerp(1.0, shadow, strength * global.strength);
}

2.3 只有烘培阴影

​ 如果只想使用烘培阴影而不使用实时阴影的话,我们设置一个GetBakedShadow变体:

float GetBakedShadow (ShadowMask mask, float strength) 
{
    if (mask.distance) 
    {
        return lerp(1.0, GetBakedShadow(mask), strength);
    }
    return 1.0;
}

​ 然后,在GetDirectionalShadowAttenuation中,检查灯光的阴影强度和全局强度的乘积是否小于或等于0,是的话,就表明只有烘培阴影:

    if (directional.strength * global.strength <= 0.0) 
    {
        shadow = GetBakedShadow(global.shadowMask, directional.strength);
    }

​ 除此之外,我们还需要修改ReserveDirectionalShadows,让其不会在没有实时阴影投射物时就跳过该灯光,之后检测是否有实时阴影投射物:

        if (
            shadowedDirLightCount < maxShadowedDirLightCount &&
            light.shadows != LightShadows.None && light.shadowStrength > 0f //&&
            //cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b)
        ) 
        {
            LightBakingOutput lightBaking = light.bakingOutput;
            if (
                lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
                lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
            ) 
            {
                useShadowMask = true;
            }

            if (!cullingResults.GetShadowCasterBounds(
                visibleLightIndex, out Bounds b
            )) 
            {
                return new Vector3(-light.shadowStrength, 0f, 0f);
            }

            …
        }

​ 设为负就不会在阴影强度大于0时采样阴影贴图。然后在GetDirectionalShadowAttenuation中调用GetBakedShadow:

        shadow = GetBakedShadow(global.shadowMask, abs(directional.strength));

2.4 总是使用阴影遮罩

​ 还有一种阴影模式是Shadowmask,该模式下Unity会为灯光省略哪些静态阴影投射物。阴影遮罩到处可以使用,我们可以让所有的静态物体使用,速度快,不过阴影质量可能不佳。

​ 添加_SHADOW_MASK_ALWAYS关键字,检查QualitySettings.shadowmaskMode属性来看看使用哪个关键字:

    static string[] shadowMaskKeywords = 
    {
        "_SHADOW_MASK_ALWAYS",
        "_SHADOW_MASK_DISTANCE"
    };
    
    …
    
    public void Render () 
    {
        …
        buffer.BeginSample(bufferName);
        SetKeywords(shadowMaskKeywords, useShadowMask ?
            QualitySettings.shadowmaskMode == ShadowmaskMode.Shadowmask ? 0 : 1 :
            -1
        );
        buffer.EndSample(bufferName);
        ExecuteBuffer();
    }

​ 添加对应的multi_compile指令:

        #pragma multi_compile _ _SHADOW_MASK_ALWAYS _SHADOW_MASK_DISTANCE

​ 在Common.hlsl中:

#if defined(_SHADOW_MASK_ALWAYS) || defined(_SHADOW_MASK_DISTANCE)
    #define SHADOWS_SHADOWMASK
#endif

​ 为ShadowMask结构体添加新的属性:

struct ShadowMask 
{
    bool always;
    bool distance;
    float4 shadows;
};

…

ShadowData GetShadowData (Surface surfaceWS) 
{
    ShadowData data;
    data.shadowMask.always = false;
    …
}

​ 在GetGI中,若开启对应的关键字,获取对应的阴影数据:

GI GetGI (float2 lightMapUV, Surface surfaceWS) 
{
    GI gi;
    gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
    gi.shadowMask.always = false;
    gi.shadowMask.distance = false;
    gi.shadowMask.shadows = 1.0;

    #if defined(_SHADOW_MASK_ALWAYS)
        gi.shadowMask.always = true;
        gi.shadowMask.shadows = SampleBakedShadows(lightMapUV, surfaceWS);
    #elif defined(_SHADOW_MASK_DISTANCE)
        gi.shadowMask.distance = true;
        gi.shadowMask.shadows = SampleBakedShadows(lightMapUV, surfaceWS);
    #endif
    return gi;
}

​ 两个GetBakedShadow都添加模式检查:

float GetBakedShadow (ShadowMask mask) 
{
    float shadow = 1.0;
    if (mask.always || mask.distance) 
    {
        shadow = mask.shadows.r;
    }
    return shadow;
}

float GetBakedShadow (ShadowMask mask, float strength) 
{
    if (mask.always || mask.distance) 
    {
        return lerp(1.0, GetBakedShadow(mask), strength);
    }
    return 1.0;
}

​ 最后,在MixedBakedAndReatimeShadows中,对于总是开启阴影遮招的选项,使用不同的混合方式。首先,实时阴影通过全局强度调节,基于深度渐变,然后在烘培阴影和实时阴影间选择最小值:

float MixBakedAndRealtimeShadows (
    ShadowData global, float shadow, float strength
) 
{
    float baked = GetBakedShadow(global.shadowMask);
    if (global.shadowMask.always) 
    {
        shadow = lerp(1.0, shadow, global.strength);
        shadow = min(baked, shadow);
        return lerp(1.0, shadow, strength);
    }
    if (global.shadowMask.distance) 
    {
        shadow = lerp(baked, shadow, global.strength);
        return lerp(1.0, shadow, strength);
    }
    return lerp(1.0, shadow, strength * global.strength);
}

3 多个灯光

​ 因为阴影遮罩贴图有4个通道,而我们又至多支持4个混合平行光,那么这四个通道就可以利用起来。

3.1 阴影遮罩通道

​ 为了应用所有的通道,我们需要将灯光的通道索引送往GPU。

​ 在ReserveDirectionalShadows中,通过LightBakingOutput.occlusionMaskChannel来获得通道索引。并且该方法返回Vector4,第四个组件存储通道索引:

    public Vector4 ReserveDirectionalShadows (
        Light light, int visibleLightIndex
    ) 
    {
        if (
            shadowedDirLightCount < maxShadowedDirLightCount &&
            light.shadows != LightShadows.None && light.shadowStrength > 0f
        ) 
        {
            float maskChannel = -1;
            LightBakingOutput lightBaking = light.bakingOutput;
            if (
                lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
                lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
            ) 
            {
                useShadowMask = true;
                maskChannel = lightBaking.occlusionMaskChannel;
            }

            if (!cullingResults.GetShadowCasterBounds(
                visibleLightIndex, out Bounds b
            )) 
            {
                return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
            }

            shadowedDirectionalLights[shadowedDirLightCount] =
                new ShadowedDirectionalLight {
                    visibleLightIndex = visibleLightIndex,
                    slopeScaleBias = light.shadowBias,
                    nearPlaneOffset = light.shadowNearPlane
                };
            return new Vector4(
                light.shadowStrength,
                settings.directional.cascadeCount * shadowedDirLightCount++,
                light.shadowNormalBias, maskChannel
            );
        }
        return new Vector4(0f, 0f, 0f, -1f);
    }

3.2 选择合适的通道

​ 在DirectionalShadowData中添加通道索引属性:

struct DirectionalShadowData 
{
    float strength;
    int tileIndex;
    float normalBias;
    int shadowMaskChannel;
};

​ 在GI的GetDirectionalShadowData中获取通道索引:

DirectionalShadowData GetDirectionalShadowData (
    int lightIndex, ShadowData shadowData
) 
{
    …
    data.shadowMaskChannel = _DirectionalLightShadowData[lightIndex].w;
    return data;
}

​ 对两个版本的GetBakedShadow添加通道:

float GetBakedShadow (ShadowMask mask, int channel) 
{
    float shadow = 1.0;
    if (mask.always || mask.distance) 
    {
        if (channel >= 0) 
        {
            shadow = mask.shadows[channel];
        }
    }
    return shadow;
}

float GetBakedShadow (ShadowMask mask, int channel, float strength) 
{
    if (mask.always || mask.distance) 
    {
        return lerp(1.0, GetBakedShadow(mask, channel), strength);
    }
    return 1.0;
}

​ 在MixBakedAndRealtimeShadows中添加:

float MixBakedAndRealtimeShadows (
    ShadowData global, float shadow, int shadowMaskChannel, float strength
) 
{
    float baked = GetBakedShadow(global.shadowMask, shadowMaskChannel);
    …
}

​ 最后,在GetDirectionalShadowAttenuation中使用:

float GetDirectionalShadowAttenuation (
    DirectionalShadowData directional, ShadowData global, Surface surfaceWS
) 
{
    #if !defined(_RECEIVE_SHADOWS)
        return 1.0;
    #endif
    
    float shadow;
    if (directional.strength * global.strength <= 0.0) 
    {
        shadow = GetBakedShadow(
            global.shadowMask, directional.shadowMaskChannel,
            abs(directional.strength)
        );
    }
    else 
    {
        shadow = GetCascadedShadow(directional, global, surfaceWS);
        shadow = MixBakedAndRealtimeShadows(
            global, shadow, directional.shadowMaskChannel, directional.strength
        );
    }
    return shadow;
}

相关文章

网友评论

      本文标题:Unity自定义SRP(六):阴影遮罩

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