Unity3D CustomSRP[译].6.阴影遮罩[Shadow Masks]

Shadow Masks(阴影遮罩)

——烘焙直接光遮罩


本节内容

  • 烘焙静态阴影
  • 合并实时光照和烘焙阴影
  • 混合实时和烘焙阴影
  • 支持四个光源的阴影遮罩



这是关于如何创建Custom SRP的系列教程的第六个部分,它使用阴影遮罩来烘焙阴影,同时仍然计算实时照明。

这个教程使用的是Unity版本是2019.2.6f1.

近处实时阴影,远处烘焙阴影

(ps:文章总被吞…最后偶然看到可能会被吞的一些词儿…尝试改了点但有些意思感觉不到位~)


1. 烘焙阴影(Baking Shadows)

使用光照贴图的优势在于,我们不受最大阴影距离的限制。烘焙的阴影不会被剔除,但它们也不会发生改变。理想情况下,我们结合使用有着最大阴影距离的实时阴影和烘焙阴影。 Unity的阴影遮罩混合照明模式使这成为可能。


1.1 距离阴影遮罩(Distance Shadow Mask)

让我们考虑与之前教程相同的场景,但减少最大阴影距离,这样房间的内部不会产生阴影。 这使得实时阴影的结束位置变得非常清晰。 我们从一个简单的光源开始。

烘焙间接混合光照,最大距离11

切换混合照明模式到Shadow Mask。 这将使照明数据失效,所以需要再次烘焙。

阴影遮罩混合光照模式

有两种方法使用阴影遮罩混合照明,可以通过Quality项目设置进行配置。 我们将使用Distance Shadowmask模式。 另一种模式被称为Shadowmask,我们将在后面介绍。

阴影遮罩模式设置为 Distance Shadowmask

两种阴影遮罩模式使用相同的烘焙光照数据。 在这两种情况下,光照贴图最终都包含了间接照明,与烘焙间接混合照明模式完全相同。 不同的是,现在也有一个烘焙阴影遮罩贴图,你可以通过烘焙光照地图预览窗口查看。

烘焙间接光和阴影遮罩

阴影遮罩贴图包含了我们的混合平行光的阴影衰减,代表了所有静态物体投射的阴影,这有助于全局照明。 数据存储在红色通道中,所以贴图是黑红色的。

就像烘焙的间接照明一样,烘焙阴影在运行时不能改变。 然而,无论光的强度或颜色如何,阴影都是有效的。 但是光不应该旋转,否则它的阴影就没有意义了。 此外,如果间接照明被烘焙了,你不应该将光源改变太多。 例如,如果一盏灯关闭后,间接照明仍然存在,这显然是不合适的。 如果光源变化很大,那么你可以将它的间接倍率设置为零,这样就不会有间接光照被烘焙。


1.2 检测阴影遮罩(Detecting a Shadow Mask)

要使用阴影遮罩,我们的管线必须知道它的存在。 因为这都是关于阴影,这是我们的Shadows类的工作。 我们将使用Shader关键字来控制是否使用阴影遮罩。 由于有两种模式,我们将引入另一个静态关键字数组,尽管它目前只包含一个关键字_SHADOW_MASK_DISTANCE

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

添加一个布尔字段来跟踪我们是否使用了阴影遮罩。 我们每一帧都重新计算这个值,所以在Setup中将它初始化为false

    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](https://docs.unity3d.com/ScriptReference/Light.html).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关键字。 在Lit着色器的CustomLit通道中添加一个相应的多编译指令。

            #pragma multi_compile _ _CASCADE_BLEND_SOFT _CASCADE_BLEND_DITHER
            #pragma multi_compile _ _SHADOW_MASK_DISTANCE
            #pragma multi_compile _ LIGHTMAP_ON


1.3 阴影遮罩数据(Shadow Mask Data)

在着色器方面,我们必须知道是否使用了阴影遮罩,如果使用了,烘焙的阴影是什么。 让我们在Shadows中添加一个ShadowMask结构来跟踪这两个。然后将这个结构体作为一个字段添加到全局ShadowData结构体中。

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的责任。 所以给GI结构添加一个阴影遮罩字段,并将它在GetGI中初始化为不使用的。

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通过unity_ShadowMask纹理和相应的采样状态使阴影遮罩映射对着色器可用。 在GI.hlsl中定义这些。

TEXTURE2D(unity_Lightmap);
SAMPLER(samplerunity_Lightmap);

TEXTURE2D(unity_ShadowMask);
SAMPLER(samplerunity_ShadowMask);

然后添加一个SampleBakedShadows函数,使用光照贴图UV坐标对贴图进行采样。 就像常规的光照映射一样,这只对光映射几何有意义,所以应该是在定义LIGHTMAP_ON时有效,否则就没有烘焙阴影,衰减总是1。

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

现在我们可以调整GetGI,使其启用距离阴影遮罩模式,并在定义了_SHADOW_MASK_DISTANCE的情况下对烘焙的阴影进行采样。 注意,这使得distance布尔值成为一个编译时常量,因此它的使用不会导致动态分支。

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;
}

这取决于LightingGIShadowData复制阴影遮罩数据。 在这一点,我们也可以通过直接返回它作为最终的照明颜色来调试阴影遮罩数据。

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

一开始它似乎不起作用,因为所有的东西最后都是白色的。 我们必须指示Unity将相关的数据发送给GPU,就像我们在之前的教程中在CameraRenderer.DrawVisibleGeometry中为光照映射和探针所做的一样。 在本例中,我们必须添加PerObjectData.ShadowMask到逐对象的数据。

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

采集阴影遮罩


为什么每次改变着色器代码时Unity都会自动烘焙光照?
.
当我们更改元通道引用的HLSL文件时,就会发生这种情况。 暂时禁用Auto Generate”功能,可以防止不必要的烘焙。


1.4 遮挡探针(Occlusion Probes)

我们可以看到阴影遮罩被正确地应用到光照映射的物体上。 我们还看到,动态对象没有阴影遮罩数据,正如预期的那样。 他们使用光照探针而不是光照贴图。然而,Unity也烘焙阴影遮罩数据到光照探针,称为遮挡探针。 我们可以通过在UnityInputUnityPerDraw缓冲区中添加unity_ProbesOcclusion向量来访问这些数据。

    real4 unity_WorldTransformParams;

    float4 unity_ProbesOcclusion;

    float4 unity_LightmapST;

现在我们可以简单地在动态对象的SampleBakedShadows中返回那个向量。

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

再次,我们必须指示Unity将这些数据发送给GPU,这一次是通过启用PerObjectData.OcclusionProbe标记。

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

采样遮挡探针

阴影遮罩未使用的为了探针的通道被设置为白色,所以动态对象最终在完全照明时是白色的,在完全被遮挡时是青色的,而不是红色和黑色。

虽然这足以让阴影遮罩通过探针工作,但它破坏了GPU-Instancing。 遮挡数据可以自动得到实例化,但UnityInstancing只在SHADOWS_SHADOWMASK被定义时这样做。 所以在Common中需要的时候定义它,在引用UnityInstancing之前。

#if defined(_SHADOW_MASK_DISTANCE)
    #define SHADOWS_SHADOWMASK
#endif

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"


1.5 光照探针代理体(LPPVs)

LPPV也可以与阴影遮罩一起工作。 同样,我们必须通过设置一个标记来启用它,这次是PerObjectData.OcclusionProbeProxyVolume

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

检索LPPV遮挡数据的工作原理与检索它的光照数据相同,只是我们必须调用SampleProbeOcclusion而不是SampleProbeVolumeSH4。它存储在相同的纹理中,需要相同的参数,唯一的例外是不需要法向量。添加一个分支函数到SampleBakedShadows,以及一个Surface参数。

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
}

在调用GetGI中的函数时添加新的Surface参数。

        gi.shadowMask.shadows = SampleBakedShadows(lightMapUV, surfaceWS);
采集LPPV遮挡


1.5 网格球(Mesh Ball)

如果我们的网格球使用LPPV,它已经支持阴影遮罩,但是当它插入光探针本身时,我们必须在MeshBall.Update中添加遮挡探针数据。 这是通过为CalculateInterpolatedLightAndOcclusionProbes的最后一个参数使用一个临时的Vector4数组来完成的,并通过CopyProbeOcclusionArrayFrom方法将它传递给属性块。

                var lightProbes = new SphericalHarmonicsL2[1023];
                var occlusionProbes = new Vector4[1023];
                LightProbes.CalculateInterpolatedLightAndOcclusionProbes(
                    positions, lightProbes, occlusionProbes
                );
                block.CopySHCoefficientArraysFrom(lightProbes);
                block.CopyProbeOcclusionArrayFrom(occlusionProbes);

在验证阴影遮罩数据正确发送到着色器后,我们可以从GetLighting中移除它的调试可视化。

    //return gi.shadowMask.shadows.rgb;


2. 混合阴影(Mixing Shadows)

现在我们有了可用的阴影遮罩,下一步是在实时阴影不存在的时候使用它,当一个片元最终超过最大阴影距离的情况下。


2.1 使用有效的烘焙阴影(Use Baked when Available)

混合烘焙阴影和实时阴影将使GetDirectionalShadowAttenuation的工作更加复杂。 让我们从隔离所有实时阴影采样的代码开始,将它移动到Shadows中的一个新的GetCascadedShadow函数。

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函数,它返回给定阴影遮罩的烘焙阴影衰减。 如果遮罩的距离模式是启用的,那么我们需要它的阴影向量的第一个分量,否则就没有衰减,结果是1。

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);
}

GetDirectionalShadowAttenuation使用该函数。

        shadow = GetCascadedShadow(directional, global, surfaceWS);
        shadow = MixBakedAndRealtimeShadows(global, shadow, directional.strength);

淡出的烘焙阴影

结果是我们现在总是使用阴影遮罩,所以我们可以看到它的效果。 然而,烘焙后的阴影和实时阴影一样会随着距离而消失。


2.2 过渡到烘焙阴影(Transitioning to Baked)

为了根据深度从实时阴影过渡到烘烤阴影,我们必须在它们之间基于全局阴影强度进行插值。 然而,我们还必须应用光的阴影强度,这是我们在插值之后必须做的。 所以我们不能在GetDirectionalShadowData中立即结合两个强度。

    data.strength = _DirectionalLightShadowData[lightIndex].x; // * shadowData.strength;

MixBakedAndRealtimeShadows中,基于全局强度在烘焙和实时之间进行插值,然后应用光的阴影强度。 但是当没有阴影遮罩的时候,只将合并的强度应用到实时阴影上,就像我们之前做的那样。

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 仅有烘焙阴影(Only Baked Shadows)

目前我们的方法只在有实时阴影需要渲染时有效。 如果没有,那么阴影遮罩也会消失。 这可以通过缩小场景视图来验证,直到所有东西都超出最大阴影距离。

既没有实时阴影也没有烘焙阴影

我们必须支持有阴影遮罩但没有实时阴影的情况。 让我们从创建一个GetBakedShadow函数变体开始,它也有一个强度参数,这样我们就可以方便地得到一个通过强度调制的烘焙阴影。

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

接下来,在GetDirectionalShadowAttenuation检查合并的强度是否最终为零或更小。如果是这样,比起总是返回1,改为只返回调整的烘焙阴影,仍然跳过实时阴影采样。

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

除此之外,我们还得改变Shadows.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);
            }

            …
        }

但是当阴影强度大于零时,着色器将对阴影贴图进行采样,即使这是不正确的。 我们可以通过削弱阴影的强度来实现。

                return new Vector3(-light.shadowStrength, 0f, 0f);

然后,当我们跳过实时阴影时,在GetDirectionalShadowAttenuation中传递绝对强度给GetBakedShadow。 这样,当没有实时阴影投射者以及当我们超出最大阴影距离时,它都可以工作。

shadow = GetBakedShadow(global.shadowMask, abs(directional.strength));
仅有烘焙阴影


2.4 总是使用阴影遮罩(Always use the Shadow Mask)

还有另一种阴影遮罩模式,简称为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();
    }

将这个关键字添加到我们的shader中的多编译指令中。

            #pragma multi_compile _ _SHADOW_MASK_ALWAYS _SHADOW_MASK_DISTANCE

当定义SHADOWS_SHADOWMASK时,也要在Common中检查它。

#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;
}

最后,当阴影遮罩始终是活动的,MixBakedAndRealtimeShadows现在必须使用不同的方法。 首先,实时阴影必须通过全局强度进行调整,以基于深度使其淡出。 然后通过取其最小值,将烘焙阴影和实时阴影结合起来。 之后,光的阴影强度被应用到合并的阴影。

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. 多光源(Multiple Lights)

因为阴影遮罩贴图有四个通道,它可以支持多达四个混合灯光。 烘焙过程中最重要的光得到红色通道,第二个光得到绿色通道,以此类推。 让我们通过复制我们的单方向光来尝试一下,旋转它一点,降低它的强度,这样新的光最终会使用绿色通道。


当有超过四个混合模式的光源时会发生什么?
.
Unity将转换把前四个混合模式的光转换为完全烘焙模式。 这是假设所有的光源都是平行光,这是我们目前唯一支持的光源类型。 其他类型的光有一个有限的影响区域,可能会多个光使用同一通道。


两个光源共享同一个烘焙阴影

第二个光的实时阴影效果和预期的一样,但是它最终使用了第一个光的遮罩来处理烘焙阴影,这显然是错误的。 当使用always-shadow-mask模式时,这是最容易看到的。

3.1 阴影遮罩通道(Shadow Mask Channels)

检查阴影贴图可以发现阴影是正确的。 只被第一个光源照亮的区域是红色的,只被第二个光源照亮的区域是绿色的,同时被两个光源照亮的区域是黄色的。 这最多适用于四个光源,尽管第四个光源在预览中不可见,因为alpha通道没有显示。

两个光源的烘焙阴影

这两个光源使用相同的烘焙阴影,因为我们总是使用红色通道。 为了实现这一点,我们必须将光的通道索引发送到GPU。 我们不能依赖于灯光的顺序,因为它可以在运行时变化,因为灯光可以改变,甚至禁用。

我们可以在Shadows.ReserveDirectionalShadows中通过LightBakingOutput.occlusionMaskChannel属性检索光的遮罩通道索引。 当我们向GPU发送一个4D向量时,我们可以将它存储在我们返回的向量的第四个通道中,将返回类型改为Vector4。 当光不使用阴影遮罩时,我们通过将其索引设置为−1来表示。

    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 选择合适的通道(Selecting the Appropriate Channel)

Shadows中定义的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;
}


点积不是比索引渠道更好吗?
.
是的,但是shader编译器会为我们处理。 它将使用通道来索引向量的静态缓冲区,并将适当的组件设置为1,然后它将执行该组件与掩码的点积来过滤它。 我们也可以将点积发送给GPU以跳过查找步骤,但这将需要发送一个额外的向量数组,而这个数组无论如何都必须被索引。


调整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;
}

两个光源使用自己的通道


那么减法混合照明模式呢?
.
减法照明是一种结合烘焙照明和阴影的替代方法,只使用一个光照贴图。 这个想法是,你完全烘焙一个光源,但也使用它的实时照明。 然后计算该光的实时漫反射照明,采样实时阴影,并使用它来确定有多少漫反射光被遮挡,这是从漫反射GI中减去的。
·
所以你最终会得到使用烘焙照明的静态物体——尽管漫反射实时照明是为它们计算的——可以接收实时阴影。动态对象必须依靠遮挡探针来接收静态阴影。
·
这是一种严重受限的预算方法。 它只适用于单一方向的光,不能改变。所有间接照明或任何其他烘焙光照产生不正确的结果,这是通过一个可配置的阴影颜色限制暗化缓解,它应该匹配场景的平均间接GI颜色。
·
在本系列中,我不会包括对减法模式的支持。 如果你有一个阴影遮罩贴图的空间,那么使用阴影遮罩模式优于减法模式。 如果没有,那么考虑完全烘焙,这允许一个更复杂的照明设置。


下一个章节是 多细节层次和反射(LOD and Reflections)

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

推荐阅读更多精彩内容