Unity自定义SRP(十):点光和聚光阴影

https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-shadows/

1 聚光阴影

1.1 阴影混合

​ 修改Shadows.hlsl 中的GetOtherShadowAttenuation,让其和GetDirectionalShadowAttenuation类似:

float GetOtherShadow (
    OtherShadowData other, ShadowData global, Surface surfaceWS
) 
{
    return 1.0;
}

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

global.strength用于决定我们是否跳过采样实时阴影这一步骤。级联只应用于平行阴影,而其它光源有固定位置,它们的阴影贴图不能随观察移动。最好的方法是用相同的方式渐变阴影,因此使用相同的global.strength

​ 我们将设置级联数和距离渐变的代码移至Shadows.Render:

    public void Render () 
    {
        …
        buffer.SetGlobalInt(
            cascadeCountId,
            shadowedDirLightCount > 0 ? settings.directional.cascadeCount : 0
        );
        float f = 1f - settings.directional.cascadeFade;
        buffer.SetGlobalVector(
            shadowDistanceFadeId, new Vector4(
                1f / settings.maxDistance, 1f / settings.distanceFade,
                1f / (1f - f * f)
            )
        );
        buffer.EndSample(bufferName);
        ExecuteBuffer();
    }

    void RenderDirectionalShadows () 
    {
        …

        //buffer.SetGlobalInt(cascadeCountId, settings.directional.cascadeCount);
        buffer.SetGlobalVectorArray(
            cascadeCullingSpheresId, cascadeCullingSpheres
        );
        buffer.SetGlobalVectorArray(cascadeDataId, cascadeData);
        buffer.SetGlobalMatrixArray(dirShadowMatricesId, dirShadowMatrices);
        //float f = 1f - settings.directional.cascadeFade;
        //buffer.SetGlobalVector(
        //  shadowDistanceFadeId, new Vector4(
        //      1f / settings.maxDistance, 1f / settings.distanceFade,
        //      1f / (1f - f * f)
        //  )
        //);
        …
    }

​ 在GetShadowData中确保全局强度不会设置为0:

    if (i == _CascadeCount && _CascadeCount > 0) 
    {
        data.strength = 0.0;
    }

1.2 其它光实时阴影

​ 我们定义最大可产生实时阴影的其它类型光的数量:

    const int maxShadowedDirLightCount = 4, maxShadowedOtherLightCount = 16;
    const int maxCascades = 4;

    …

    int shadowedDirLightCount, shadowedOtherLightCount;
    
    …
    
    public void Setup (…) 
    {
        …
        shadowedDirLightCount = shadowedOtherLightCount = 0;
        useShadowMask = false;
    }

​ 灯光是否可以产生阴影区别于其在可见光列表中的位置,次序低的就不保存阴影数据,但若是有烘培阴影也可以保留。我们可以在ReserveOtherShadow的一开始针对那些没有阴影的灯光返回默认值:

    public Vector4 ReserveOtherShadows (Light light, int visibleLightIndex) 
    {
        if (light.shadows == LightShadows.None || light.shadowStrength <= 0f)
        {
            return new Vector4(0f, 0f, 0f, -1f);
        }

        float maskChannel = -1f;
        //if (light.shadows != LightShadows.None && light.shadowStrength > 0f) {
        LightBakingOutput lightBaking = light.bakingOutput;
        if (
            lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
            lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
        ) {
            
            useShadowMask = true;
            maskChannel = lightBaking.occlusionMaskChannel;
        }
        return new Vector4(
            light.shadowStrength, 0f, 0f,
            maskChannel
        );
            //}
        //}
        //return new Vector4(0f, 0f, 0f, -1f);
    }

​ 在最后的返回值前,判断灯光数量是否达到最大值,或者对于该光是否还有阴影可渲染。如果是的话,返回负数阴影强度,这样就可以恰当地使用烘培阴影:

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

        return new Vector4(
            light.shadowStrength, shadowedOtherLightCount++, 0f,
            maskChannel
        );

1.3 两个图集

​ 在ShadowSettings中,我们为其它光源的阴影创建新的图集:

[System.Serializable]
    public struct Other 
    {

        public MapSize atlasSize;

        public FilterMode filter;
    }

    public Other other = new Other 
    {
        atlasSize = MapSize._1024,
        filter = FilterMode.PCF2x2
    };

​ 添加对应的multi_compile指令:

            #pragma multi_compile _ _OTHER_PCF3 _OTHER_PCF5 _OTHER_PCF7

​ 向Shadows.cs添加关键字:

    static string[] otherFilterKeywords = 
    {
        "_OTHER_PCF3",
        "_OTHER_PCF5",
        "_OTHER_PCF7",
    };

​ 定义阴影图集和矩阵:

    static int
        dirShadowAtlasId = Shader.PropertyToID("_DirectionalShadowAtlas"),
        dirShadowMatricesId = Shader.PropertyToID("_DirectionalShadowMatrices"),
        otherShadowAtlasId = Shader.PropertyToID("_OtherShadowAtlas"),
        otherShadowMatricesId = Shader.PropertyToID("_OtherShadowMatrices"),
        …;
        
    …
        
    static Matrix4x4[]
        dirShadowMatrices = new Matrix4x4[maxShadowedDirLightCount * maxCascades],
        otherShadowMatrices = new Matrix4x4[maxShadowedOtherLightCount];

​ 我们定义一个4维向量,xy组件存储平行光阴影图集尺寸,zw组件存储其它光阴影尺寸:

    Vector4 atlasSizes;
    
    …
    
    public void Render () 
    {
        …
        buffer.SetGlobalVector(shadowAtlasSizeId, atlasSizes);
        buffer.EndSample(bufferName);
        ExecuteBuffer();
    }
    
    void RenderDirectionalShadows () 
    {
        int atlasSize = (int)settings.directional.atlasSize;
        atlasSizes.x = atlasSize;
        atlasSizes.y = 1f / atlasSize;
        …
        //buffer.SetGlobalVector(
        //  shadowAtlasSizeId, new Vector4(atlasSize, 1f / atlasSize)
        //);
        buffer.EndSample(bufferName);
        ExecuteBuffer();
    }

​ 添加新的RenderOtherShadow方法:

    void RenderOtherShadows () 
    {
        int atlasSize = (int)settings.other.atlasSize;
        atlasSizes.z = atlasSize;
        atlasSizes.w = 1f / atlasSize;
        buffer.GetTemporaryRT(
            otherShadowAtlasId, atlasSize, atlasSize,
            32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap
        );
        buffer.SetRenderTarget(
            otherShadowAtlasId,
            RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store
        );
        buffer.ClearRenderTarget(true, false, Color.clear);
        buffer.BeginSample(bufferName);
        ExecuteBuffer();

        int tiles = shadowedOtherLightCount;
        int split = tiles <= 1 ? 1 : tiles <= 4 ? 2 : 4;
        int tileSize = atlasSize / split;

        for (int i = 0; i < shadowedOtherLightCount; i++) 
        {
            
        }
        buffer.SetGlobalMatrixArray(otherShadowMatricesId, otherShadowMatrices);
        SetKeywords(
            otherFilterKeywords, (int)settings.other.filter - 1
        );
        buffer.EndSample(bufferName);
        ExecuteBuffer();
    }

​ 接着在Render中渲染两种阴影:

    public void Render () {
        if (shadowedDirLightCount > 0) 
        {
            RenderDirectionalShadows();
        }
        else 
        {
            buffer.GetTemporaryRT(
                dirShadowAtlasId, 1, 1,
                32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap
            );
        }
        if (shadowedOtherLightCount > 0) 
        {
            RenderOtherShadows();
        }
        else 
        {
            buffer.SetGlobalTexture(otherShadowAtlasId, dirShadowAtlasId);
        }
        
        …
    }

​ 在Cleanup中移除其它阴影数据:

    public void Cleanup () 
    {
        buffer.ReleaseTemporaryRT(dirShadowAtlasId);
        if (shadowedOtherLightCount > 0) 
        {
            buffer.ReleaseTemporaryRT(otherShadowAtlasId);
        }
        ExecuteBuffer();
    }

1.4 渲染聚光阴影

​ 为渲染聚光阴影,我们需要知道可见光索引,梯度缩放偏移量和法线偏移量。我们创建一个ShadowedOtherLight结构体:

    struct ShadowedOtherLight {
        public int visibleLightIndex;
        public float slopeScaleBias;
        public float normalBias;
    }

    ShadowedOtherLight[] shadowedOtherLights =
        new ShadowedOtherLight[maxShadowedOtherLightCount];

​ 在ReserveOtherShadows中填充:

    public Vector4 ReserveOtherShadows (Light light, int visibleLightIndex) 
    {
        …

        shadowedOtherLights[shadowedOtherLightCount] = new ShadowedOtherLight {
            visibleLightIndex = visibleLightIndex,
            slopeScaleBias = light.shadowBias,
            normalBias = light.shadowNormalBias
        };

        return new Vector4(
            light.shadowStrength, shadowedOtherLightCount++, 0f,
            maskChannel
        );
    }

​ 我们需要确保我们使用的是恰当的可见光索引,因此修改所有的初始化方法:

    void SetupDirectionalLight (
        int index, int visibleIndex, ref VisibleLight visibleLight
    ) 
    {
        …
        dirLightShadowData[index] =
            shadows.ReserveDirectionalShadows(visibleLight.light, visibleIndex);
    }

    void SetupPointLight (
        int index, int visibleIndex, ref VisibleLight visibleLight
    ) 
    {
        …
        otherLightShadowData[index] =
            shadows.ReserveOtherShadows(light, visibleIndex);
    }

    void SetupSpotLight (
        int index, int visibleIndex, ref VisibleLight visibleLight
    ) 
    {
        …
        otherLightShadowData[index] =
            shadows.ReserveOtherShadows(light, visibleIndex);
    }

​ 修改SetupLights让其传入正确的可见光索引:

            switch (visibleLight.lightType) 
            {
                case LightType.Directional:
                    if (dirLightCount < maxDirLightCount) 
                    {
                        SetupDirectionalLight(
                            dirLightCount++, i, ref visibleLight
                        );
                    }
                    break;
                case LightType.Point:
                    if (otherLightCount < maxOtherLightCount) 
                    {
                        newIndex = otherLightCount;
                        SetupPointLight(otherLightCount++, i, ref visibleLight);
                    }
                    break;
                case LightType.Spot:
                    if (otherLightCount < maxOtherLightCount) 
                    {
                        newIndex = otherLightCount;
                        SetupSpotLight(otherLightCount++, i, ref visibleLight);
                    }
                    break;
            }

Shadows.cs中创建一个RenderSpotShadows方法,我们使用CullingResults.ComputeSpotShadowMatricesAndCullingPrimitives来获得view和projection矩阵以及splitData:

    void RenderSpotShadows (int index, int split, int tileSize) 
    {
        ShadowedOtherLight light = shadowedOtherLights[index];
        var shadowSettings =
            new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);
        cullingResults.ComputeSpotShadowMatricesAndCullingPrimitives(
            light.visibleLightIndex, out Matrix4x4 viewMatrix,
            out Matrix4x4 projectionMatrix, out ShadowSplitData splitData
        );
        shadowSettings.splitData = splitData;
        otherShadowMatrices[index] = ConvertToAtlasMatrix(
            projectionMatrix * viewMatrix,
            SetTileViewport(index, split, tileSize), split
        );
        buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
        buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
        ExecuteBuffer();
        context.DrawShadows(ref shadowSettings);
        buffer.SetGlobalDepthBias(0f, 0f);
    }

​ 在RenderOtherShadows的循环中调用:

        for (int i = 0; i < shadowedOtherLightCount; i++) 
        {
            RenderSpotShadows(i, split, tileSize);
        }

1.5 无平坠

​ 注意,阴影平坠只对正交阴影投影有效,也就是针对平行光而言,对于聚光灯这种拥有具体位置的光源来说,其后方的阴影投射物就无用了。由于使用透视投影,将顶点紧靠近平面会让阴影严重变形,因此需要在恰当的时候关闭阴影平坠。

​ 使用一个属性:

    static int
        …
        shadowDistanceFadeId = Shader.PropertyToID("_ShadowDistanceFade"),
        shadowPancakingId = Shader.PropertyToID("_ShadowPancaking");

RenderDirectionalShadows中设为1:

        buffer.ClearRenderTarget(true, false, Color.clear);
        buffer.SetGlobalFloat(shadowPancakingId, 1f);
        buffer.BeginSample(bufferName);

RenderOtherShadows中设为0:

        buffer.ClearRenderTarget(true, false, Color.clear);
        buffer.SetGlobalFloat(shadowPancakingId, 0f);
        buffer.BeginSample(bufferName);

​ 在ShadowCasterpass中有选择地使用平坠:

bool _ShadowPancaking;

Varyings ShadowCasterPassVertex (Attributes input) 
{
    …

    if (_ShadowPancaking) 
    {
        #if UNITY_REVERSED_Z
            output.positionCS.z = min(
                output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE
            );
        #else
            output.positionCS.z = max(
                output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE
            );
        #endif
    }

    output.baseUV = TransformBaseUV(input.baseUV);
    return output;
}

1.6 采样聚光阴影

​ 定义宏以及声明变量:

#if defined(_OTHER_PCF3)
    #define OTHER_FILTER_SAMPLES 4
    #define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_3x3
#elif defined(_OTHER_PCF5)
    #define OTHER_FILTER_SAMPLES 9
    #define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_5x5
#elif defined(_OTHER_PCF7)
    #define OTHER_FILTER_SAMPLES 16
    #define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_7x7
#endif

#define MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT 4
#define MAX_SHADOWED_OTHER_LIGHT_COUNT 16
#define MAX_CASCADE_COUNT 4

TEXTURE2D_SHADOW(_DirectionalShadowAtlas);
TEXTURE2D_SHADOW(_OtherShadowAtlas);
#define SHADOW_SAMPLER sampler_linear_clamp_compare
SAMPLER_CMP(SHADOW_SAMPLER);

CBUFFER_START(_CustomShadows)
    …
    float4x4 _DirectionalShadowMatrices
        [MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT * MAX_CASCADE_COUNT];
    float4x4 _OtherShadowMatrices[MAX_SHADOWED_OTHER_LIGHT_COUNT];
    …
CBUFFER_END

​ 定义SampleOtherShadowAtlasFilterOtherShadow方法:

float SampleOtherShadowAtlas (float3 positionSTS) 
{
    return SAMPLE_TEXTURE2D_SHADOW(
        _OtherShadowAtlas, SHADOW_SAMPLER, positionSTS
    );
}

float FilterOtherShadow (float3 positionSTS) 
{
    #if defined(OTHER_FILTER_SETUP)
        real weights[OTHER_FILTER_SAMPLES];
        real2 positions[OTHER_FILTER_SAMPLES];
        float4 size = _ShadowAtlasSize.wwzz;
        OTHER_FILTER_SETUP(size, positionSTS.xy, weights, positions);
        float shadow = 0;
        for (int i = 0; i < OTHER_FILTER_SAMPLES; i++) 
        {
            shadow += weights[i] * SampleOtherShadowAtlas(
                float3(positions[i].xy, positionSTS.z)
            );
        }
        return shadow;
    #else
        return SampleOtherShadowAtlas(positionSTS);
    #endif
}

​ 在OtherShadowData中添加拼贴索引系数:

struct OtherShadowData 
{
    float strength;
    int tileIndex;
    int shadowMaskChannel;
};

​ 在GetOtherShadowData中设置:

OtherShadowData GetOtherShadowData (int lightIndex) 
{
    OtherShadowData data;
    data.strength = _OtherLightShadowData[lightIndex].x;
    data.tileIndex = _OtherLightShadowData[lightIndex].y;
    data.shadowMaskChannel = _OtherLightShadowData[lightIndex].w;
    return data;
}

​ 在GetOtherShadow中我们获取阴影。由于是透视投影,我们在传入FilterOtherShadow前将对应坐标进行透视除法:

float GetOtherShadow (
    OtherShadowData other, ShadowData global, Surface surfaceWS
) 
{
    float3 normalBias = surfaceWS.interpolatedNormal * 0.0;
    float4 positionSTS = mul(
        _OtherShadowMatrices[other.tileIndex],
        float4(surfaceWS.position + normalBias, 1.0)
    );
    return FilterOtherShadow(positionSTS.xyz / positionSTS.w);
}

1.7 法线偏移

​ 聚光阴影也有痤疮效果,但由于使用透视投影,纹素尺寸不是连续的,痤疮也不是连续的,离灯光越远越大。

​ 纹素尺寸从灯光所在平面开始随距离线性增长,该平面将空间分为在灯光前和在灯光后两部分,我们可以计算在距离为1处的纹理尺寸和法线偏移,以此来缩放到合适的尺寸。

image

​ 由上得阴影拼贴的尺寸为,其中为外角的一半,那么可以得到世界空间的纹素尺寸在距离1处等于2除以投影缩放系数,该系数可由投影矩阵的左上值得到:

        float texelSize = 2f / (tileSize * projectionMatrix.m00);
        float filterSize = texelSize * ((float)settings.other.filter + 1f);
        float bias = light.normalBias * filterSize * 1.4142136f;
        otherShadowMatrices[index] = ConvertToAtlasMatrix(
            projectionMatrix * viewMatrix,
            SetTileViewport(index, split, tileSize), tileScale
        );

​ 我们需要将偏移传至GPU,设置拼贴数属性:

    static int
        …
        otherShadowMatricesId = Shader.PropertyToID("_OtherShadowMatrices"),
        otherShadowTilesId = Shader.PropertyToID("_OtherShadowTiles"),
        …;

    static Vector4[]
        cascadeCullingSpheres = new Vector4[maxCascades],
        cascadeData = new Vector4[maxCascades],
        otherShadowTiles = new Vector4[maxShadowedOtherLightCount];
    
    …
    
    void RenderOtherShadows () 
    {
        …

        buffer.SetGlobalMatrixArray(otherShadowMatricesId, otherShadowMatrices);
        buffer.SetGlobalVectorArray(otherShadowTilesId, otherShadowTiles);
        …
    }

​ 创建SetOtherTileData方法:

    void SetOtherTileData (int index, float bias) 
    {
        Vector4 data = Vector4.zero;
        data.w = bias;
        otherShadowTiles[index] = data;
    }

​ 在RenderSpotShadows中调用:

        float bias = light.normalBias * filterSize * 1.4142136f;
        SetOtherTileData(index, bias);

​ shader层面,加入拼贴数据:

CBUFFER_START(_CustomShadows)
    …
    float4x4 _OtherShadowMatrices[MAX_SHADOWED_OTHER_LIGHT_COUNT];
    float4 _OtherShadowTiles[MAX_SHADOWED_OTHER_LIGHT_COUNT];
    float4 _ShadowAtlasSize;
    float4 _ShadowDistanceFade;
CBUFFER_END

…

float GetOtherShadow (
    OtherShadowData other, ShadowData global, Surface surfaceWS
) 
{
    float4 tileData = _OtherShadowTiles[other.tileIndex];
    float3 normalBias = surfaceWS.interpolatedNormal * tileData.w;
    …
}

​ 为缩放法线偏移,我们需要世界空间灯光位置和聚光方向:

struct OtherShadowData 
{
    float strength;
    int tileIndex;
    int shadowMaskChannel;
    float3 lightPositionWS;
    float3 spotDirectionWS;
};

​ 在GetOtherLight获取:

OtherShadowData GetOtherShadowData (int lightIndex) 
{
    …
    data.lightPositionWS = 0.0;
    data.spotDirectionWS = 0.0;
    return data;
}

Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData) 
{
    Light light;
    light.color = _OtherLightColors[index].rgb;
    float3 position = _OtherLightPositions[index].xyz;
    float3 ray = position - surfaceWS.position;
    …
    float3 spotDirection = _OtherLightDirections[index].xyz;
    float spotAttenuation = Square(
        saturate(dot(spotDirection, light.direction) *
        spotAngles.x + spotAngles.y)
    );
    OtherShadowData otherShadowData = GetOtherShadowData(index);
    otherShadowData.lightPositionWS = position;
    otherShadowData.spotDirectionWS = spotDirection;
    …
}

​ 计算到灯光平面的距离:

    float4 tileData = _OtherShadowTiles[other.tileIndex];
    float3 surfaceToLight = other.lightPositionWS - surfaceWS.position;
    float distanceToLightPlane = dot(surfaceToLight, other.spotDirectionWS);
    float3 normalBias =
        surfaceWS.interpolatedNormal * (distanceToLightPlane * tileData.w);

1.8 限制采样

​ 使用法线偏移的话会在拼贴范围外采样。我们可以手动将采样限制在拼贴边界内,不过可能会拉伸边缘的阴影。

​ 修改SetOtherTileData方法,让其计算和存储拼贴边界,我们存储拼贴最小的纹理坐标作为偏移的基准,并存储缩放。注意将边界缩小一点,确保采样不会超出边界:

    void SetOtherTileData (int index, Vector2 offset, float scale, float bias) 
    {
        float border = atlasSizes.w * 0.5f;
        Vector4 data;
        data.x = offset.x * scale + border;
        data.y = offset.y * scale + border;
        data.z = scale - border - border;
        data.w = bias;
        otherShadowTiles[index] = data;
    }

​ 在RenderSpotShadows中调用:

        Vector2 offset = SetTileViewport(index, split, tileSize);
        SetOtherTileData(index, offset, 1f / split, bias);
        otherShadowMatrices[index] = ConvertToAtlasMatrix(
            projectionMatrix * viewMatrix, offset, split
        );

​ shader中,在SampleOtherShadowAtlas中,我们使用边界限制范围:

float SampleOtherShadowAtlas (float3 positionSTS, float3 bounds) 
{
    positionSTS.xy = clamp(positionSTS.xy, bounds.xy, bounds.xy + bounds.z);
    return SAMPLE_TEXTURE2D_SHADOW(
        _OtherShadowAtlas, SHADOW_SAMPLER, positionSTS
    );
}

2 点光阴影

2.1 6个拼贴

​ 首先,我们要确保是否是渲染点光阴影:

    struct ShadowedOtherLight 
    {
        …
        public bool isPoint;
    }

​ 在ReserveOtherShadows中,判断我们是否有点光,如果是,数量加6:

    public Vector4 ReserveOtherShadows (Light light, int visibleLightIndex) 
    {
        …

        bool isPoint = light.type == LightType.Point;
        int newLightCount = shadowedOtherLightCount + (isPoint ? 6 : 1);
        if (
            newLightCount > maxShadowedOtherLightCount ||
            !cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b)
        ) 
        {
            return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
        }

        shadowedOtherLights[shadowedOtherLightCount] = new ShadowedOtherLight {
            visibleLightIndex = visibleLightIndex,
            slopeScaleBias = light.shadowBias,
            normalBias = light.shadowNormalBias,
            isPoint = isPoint
        };

        Vector4 data = new Vector4(
            light.shadowStrength, shadowedOtherLightCount,
            isPoint ? 1f : 0f, maskChannel
        );
        shadowedOtherLightCount = newLightCount;
        return data;
    }

2.2 渲染点光阴影

​ 修改RenderOtherShadows,循环中先调用新的RenderPointShadows:

        for (int i = 0; i < shadowedOtherLightCount;) 
        {
            if (shadowedOtherLights[i].isPoint) {
                RenderPointShadows(i, split, tileSize);
                i += 6;
            }
            else {
                RenderSpotShadows(i, split, tileSize);
                i += 1;
            }
        }

​ 建立RenderPointShadows方法,每个灯光要渲染六次。CubemapFace包含面索引:

    void RenderPointShadows (int index, int split, int tileSize) 
    {
        ShadowedOtherLight light = shadowedOtherLights[index];
        var shadowSettings =
            new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);
        for (int i = 0; i < 6; i++) 
        {
            cullingResults.ComputePointShadowMatricesAndCullingPrimitives(
                light.visibleLightIndex, (CubemapFace)i, 0f,
                out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,
                out ShadowSplitData splitData
            );
            shadowSettings.splitData = splitData;
            int tileIndex = index + i;
            float texelSize = 2f / (tileSize * projectionMatrix.m00);
            float filterSize = texelSize * ((float)settings.other.filter + 1f);
            float bias = light.normalBias * filterSize * 1.4142136f;
            Vector2 offset = SetTileViewport(tileIndex, split, tileSize);
            float tileScale = 1f / split;
            SetOtherTileData(tileIndex, offset, tileScale, bias);
            otherShadowMatrices[tileIndex] = ConvertToAtlasMatrix(
                projectionMatrix * viewMatrix, offset, tileScale
            );

            buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
            buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
            ExecuteBuffer();
            context.DrawShadows(ref shadowSettings);
            buffer.SetGlobalDepthBias(0f, 0f);
        }
    }

​ 一个立方体面的视角永远是90°,那么在距离为1处的世界空间拼贴大小就永远是2,也就只用计算依次:

        float texelSize = 2f / tileSize;
        float filterSize = texelSize * ((float)settings.other.filter + 1f);
        float bias = light.normalBias * filterSize * 1.4142136f;
        float tileScale = 1f / split;
        
        for (int i = 0; i < 6; i++) 
        {
            …
            //float texelSize = 2f / (tileSize * projectionMatrix.m00);
            //float filterSize = texelSize * ((float)settings.other.filter + 1f);
            //float bias = light.normalBias * filterSize * 1.4142136f;
            Vector2 offset = SetTileViewport(tileIndex, split, tileSize);
            //float tileScale = 1f / split;
            …
        }

2.3 采样点阴影

​ 采样立方体纹理,我们需要一个光到面的方向:

struct OtherShadowData 
{
    float strength;
    int tileIndex;
    bool isPoint;
    int shadowMaskChannel;
    float3 lightPositionWS;
    float3 lightDirectionWS;
    float3 spotDirectionWS;
};

​ 在GetOtherShadowDataGetOtherLight中设置:

OtherShadowData GetOtherShadowData (int lightIndex) 
{
    …
    data.isPoint = _OtherLightShadowData[lightIndex].z == 1.0;
    data.lightPositionWS = 0.0;
    data.lightDirectionWS = 0.0;
    data.spotDirectionWS = 0.0;
    return data;
}

Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
    …
    otherShadowData.lightPositionWS = position;
    otherShadowData.lightDirectionWS = light.direction;
    otherShadowData.spotDirectionWS = spotDirection;
    …
}

​ 在GetOtherShadow中,若是点光,使用CubeMapFaceID来获得面偏移:

float GetOtherShadow (
    OtherShadowData other, ShadowData global, Surface surfaceWS
) 
{
    float tileIndex = other.tileIndex;
    float3 lightPlane = other.spotDirectionWS;
    if (other.isPoint) {
        float faceOffset = CubeMapFaceID(-other.lightDirectionWS);
        tileIndex += faceOffset;
    }
    …
}

​ 我们需要一个匹配面朝向的灯光平面,创建一个对应的朝向数组:

static const float3 pointShadowPlanes[6] = 
{
    float3(-1.0, 0.0, 0.0),
    float3(1.0, 0.0, 0.0),
    float3(0.0, -1.0, 0.0),
    float3(0.0, 1.0, 0.0),
    float3(0.0, 0.0, -1.0),
    float3(0.0, 0.0, 1.0)
};

float GetOtherShadow (
    OtherShadowData other, ShadowData global, Surface surfaceWS
) 
{
    float tileIndex = other.tileIndex;
    float3 plane = other.spotDirectionWS;
    if (other.isPoint) 
    {
        float faceOffset = CubeMapFaceID(-other.lightDirectionWS);
        tileIndex += faceOffset;
        lightPlane = pointShadowPlanes[faceOffset];
    }
    …
}

2.4 绘制正确的面

​ 此时会发现点阴影绘制不正确,这是因为Unity针对点阴影,会颠倒顺序绘制,即反转三角形的顶点绘制顺序,即从点光观察的反面会被绘制,这可以有效避免阴影痤疮问题,但会造成物体和阴影间的空白问题。我们可以将view矩阵的第二行反转来还原:

            cullingResults.ComputePointShadowMatricesAndCullingPrimitives(
                light.visibleLightIndex, (CubemapFace)i, fovBias*0,
                out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,
                out ShadowSplitData splitData
            );
            viewMatrix.m11 = -viewMatrix.m11;
            viewMatrix.m12 = -viewMatrix.m12;
            viewMatrix.m13 = -viewMatrix.m13;

2.5 视野偏移

​ 立方体纹理的面之间的连接往往不是连续的,点阴影会因此出现问题。我们可以提升FOV来改善这一问题,即不采样超出拼贴边的范围

image
        float fovBias =
            Mathf.Atan(1f + bias + filterSize) * Mathf.Rad2Deg * 2f - 90f;
        for (int i = 0; i < 6; i++) 
        {
            cullingResults.ComputePointShadowMatricesAndCullingPrimitives(
                light.visibleLightIndex, (CubemapFace)i, fovBias,
                out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,
                out ShadowSplitData splitData
            );
            …
        }

​ 该方法并不完美,因为也会提升纹素大小,因此滤波范围和发现偏移也必须提升,但问题又会回去了。

你可能感兴趣的:(Unity自定义SRP(十):点光和聚光阴影)