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

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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容