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

​ 由上得阴影拼贴的尺寸为2tan \theta,其中\theta为外角的一半,那么可以得到世界空间的纹素尺寸在距离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
            );
            …
        }

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容