Unity自定义SRP(九):点光和聚光

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

1 点光

1.1 其它灯光数据

​ 除平行光外,我们至多模拟64个其它类型的光源。Lighting.cs中:

    const int maxDirLightCount = 4, maxOtherLightCount = 64;

​ 我们需要将其它光源的数量、颜色和位置等信息送往GPU:

    static int
        otherLightCountId = Shader.PropertyToID("_OtherLightCount"),
        otherLightColorsId = Shader.PropertyToID("_OtherLightColors"),
        otherLightPositionsId = Shader.PropertyToID("_OtherLightPositions");

    static Vector4[]
        otherLightColors = new Vector4[maxOtherLightCount],
        otherLightPositions = new Vector4[maxOtherLightCount];

​ 在SetupLights中,确保只有有光源的时候才将数据送往GPU:

    void SetupLights () 
    {
        NativeArray visibleLights = cullingResults.visibleLights;
        int dirLightCount = 0, otherLightCount = 0;
        for (int i = 0; i < visibleLights.Length; i++) 
        {
            …
        }

        buffer.SetGlobalInt(dirLightCountId, dirLightCount);
        if (dirLightCount > 0) 
        {
            buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
            buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
            buffer.SetGlobalVectorArray(dirLightShadowDataId, dirLightShadowData);
        }

        buffer.SetGlobalInt(otherLightCountId, otherLightCount);
        if (otherLightCount > 0) 
        {
            buffer.SetGlobalVectorArray(otherLightColorsId, otherLightColors);
            buffer.SetGlobalVectorArray(
                otherLightPositionsId, otherLightPositions
            );
        }
    }

​ shader层面,在Light.hlsl中定义相应的变量:

#define MAX_DIRECTIONAL_LIGHT_COUNT 4
#define MAX_OTHER_LIGHT_COUNT 64

CBUFFER_START(_CustomLight)
    int _DirectionalLightCount;
    float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
    float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
    float4 _DirectionalLightShadowData[MAX_DIRECTIONAL_LIGHT_COUNT];

    int _OtherLightCount;
    float4 _OtherLightColors[MAX_OTHER_LIGHT_COUNT];
    float4 _OtherLightPositions[MAX_OTHER_LIGHT_COUNT];
CBUFFER_END

​ 定义一个获取其它灯光数量的方法:

int GetOtherLightCount () 
{
    return _OtherLightCount;
}

1.2 初始化点光

​ 在Lighting.cs中创建一个SetupPointLight方法,初始化一个点光。其位置可由局部到世界转换矩阵的第4列得到:

    void SetupPointLight (int index, ref VisibleLight visibleLight) 
    {
        otherLightColors[index] = visibleLight.finalColor;
        otherLightPositions[index] = visibleLight.localToWorldMatrix.GetColumn(3);
    }

​ 现在,我们需要修改SetupLights中的循环,根据光源的类型进行不同的选择:

        for (int i = 0; i < visibleLights.Length; i++) 
        {
            VisibleLight visibleLight = visibleLights[i];
            //if (visibleLight.lightType == LightType.Directional) 
            //  {
            //  SetupDirectionalLight(dirLightCount++, ref visibleLight);
            //  if (dirLightCount >= maxDirLightCount) 
            //  {
            //      break;
            //  }
            //}
            switch (visibleLight.lightType) 
            {
                case LightType.Directional:
                    if (dirLightCount < maxDirLightCount) 
                    {
                        SetupDirectionalLight(dirLightCount++, ref visibleLight);
                    }
                    break;
                case LightType.Point:
                    if (otherLightCount < maxOtherLightCount) 
                    {
                        SetupPointLight(otherLightCount++, ref visibleLight);
                    }
                    break;
            }
        }

1.3 着色

​ 在Light.hlsl中创建一个获取其它光数据的GetOtherLight方法:

Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData) 
{
    Light light;
    light.color = _OtherLightColors[index].rgb;
    float3 ray = _OtherLightPositions[index].xyz - surfaceWS.position;
    light.direction = normalize(ray);
    light.attenuation = 1.0;
    return light;
}

​ 在GetLighting中添加其它光的循环:

float3 GetLighting (Surface surfaceWS, BRDF brdf, GI gi) 
{
    ShadowData shadowData = GetShadowData(surfaceWS);
    shadowData.shadowMask = gi.shadowMask;
    
    float3 color = IndirectBRDF(surfaceWS, brdf, gi.diffuse, gi.specular);
    for (int i = 0; i < GetDirectionalLightCount(); i++) 
    {
        Light light = GetDirectionalLight(i, surfaceWS, shadowData);
        color += GetLighting(surfaceWS, brdf, light);
    }

    for (int j = 0; j < GetOtherLightCount(); j++) 
    {
        Light light = GetOtherLight(j, surfaceWS, shadowData);
        color += GetLighting(surfaceWS, brdf, light);
    }
    return color;
}

1.4 基于距离的衰减

​ 这里我们进行尝试进行平方衰减,,其中是强度,是距离:

    float distanceSqr = max(dot(ray, ray), 0.00001);
    light.attenuation = 1.0 / distanceSqr;

1.5 灯光范围

​ 灯光衰减到一定距离就很难察觉,这时可直接将衰减置为0,也就是说我们需要一个最大可影响范围,但在到达范围时不能立即设置为0。Unity的URP使用来得到范围衰减,其中是灯光的范围(球半径)。

​ 我们在灯光位置的第四个组件处存储灯光范围:

    void SetupPointLight (int index, ref VisibleLight visibleLight) 
    {
        otherLightColors[index] = visibleLight.finalColor;
        Vector4 position = visibleLight.localToWorldMatrix.GetColumn(3);
        position.w =
            1f / Mathf.Max(visibleLight.range * visibleLight.range, 0.00001f);
        otherLightPositions[index] = position;
    }

​ 在GetOtherLight中重新计算衰减:

    float distanceSqr = max(dot(ray, ray), 0.00001);
    float rangeAttenuation = Square(
        saturate(1.0 - Square(distanceSqr * _OtherLightPositions[index].w))
    );
    light.attenuation = rangeAttenuation / distanceSqr;

2 聚光

2.1 方向

​ 聚光除位置等外,还需要方向:

    static int
        otherLightCountId = Shader.PropertyToID("_OtherLightCount"),
        otherLightColorsId = Shader.PropertyToID("_OtherLightColors"),
        otherLightPositionsId = Shader.PropertyToID("_OtherLightPositions"),
        otherLightDirectionsId = Shader.PropertyToID("_OtherLightDirections");

    static Vector4[]
        otherLightColors = new Vector4[maxOtherLightCount],
        otherLightPositions = new Vector4[maxOtherLightCount],
        otherLightDirections = new Vector4[maxOtherLightCount];

​ 我们需要将相应的数据送往GPU。

​ 创建一个SetupSpotLight方法,灯光的方向由局部到世界矩阵的第三列取反得到:

    void SetupSpotLight (int index, ref VisibleLight visibleLight) 
    {
        otherLightColors[index] = visibleLight.finalColor;
        Vector4 position = visibleLight.localToWorldMatrix.GetColumn(3);
        position.w =
            1f / Mathf.Max(visibleLight.range * visibleLight.range, 0.00001f);
        otherLightPositions[index] = position;
        otherLightDirections[index] =
            -visibleLight.localToWorldMatrix.GetColumn(2);
    }

​ 在SetupLights的循环中加入聚光支持:

                case LightType.Spot:
                    if (otherLightCount < maxOtherLightCount) 
                    {
                        SetupSpotLight(otherLightCount++, ref visibleLight);
                    }
                    break;

​ shader方面,加入新数据:

    float4 _OtherLightDirections[MAX_OTHER_LIGHT_COUNT];

​ 衰减方面,我们暂时先用聚光方向和光源到片元方向的点积来模拟:

    float spotAttenuation =
        saturate(dot(_OtherLightDirections[index].xyz, light.direction));
    light.attenuation = spotAttenuation * rangeAttenuation / distanceSqr;

2.2 聚光角

​ 锥形光,外角控制范围,内角来控制灯光衰减的开始角度。URP在使用saturate前先缩放点积并加上一些东西,然后对结果开方,,其中是点积,,,和是内角和外角。

​ 我们可以提前计算好和,先声明变量:

    static int
        otherLightCountId = Shader.PropertyToID("_OtherLightCount"),
        otherLightColorsId = Shader.PropertyToID("_OtherLightColors"),
        otherLightPositionsId = Shader.PropertyToID("_OtherLightPositions"),
        otherLightDirectionsId = Shader.PropertyToID("_OtherLightDirections"),
        otherLightSpotAnglesId = Shader.PropertyToID("_OtherLightSpotAngles");

    static Vector4[]
        otherLightColors = new Vector4[maxOtherLightCount],
        otherLightPositions = new Vector4[maxOtherLightCount],
        otherLightDirections = new Vector4[maxOtherLightCount],
        otherLightSpotAngles = new Vector4[maxOtherLightCount];

​ 在SetupSpotLight中,外角可由VisibleLight.spotAngle获得,而内角可由VisibleLight.light.innerSpotAngle获得:

    void SetupSpotLight (int index, ref VisibleLight visibleLight) 
    {
        …

        Light light = visibleLight.light;
        float innerCos = Mathf.Cos(Mathf.Deg2Rad * 0.5f * light.innerSpotAngle);
        float outerCos = Mathf.Cos(Mathf.Deg2Rad * 0.5f * visibleLight.spotAngle);
        float angleRangeInv = 1f / Mathf.Max(innerCos - outerCos, 0.001f);
        otherLightSpots[index] = new Vector4(
            angleRangeInv, -outerCos * angleRangeInv
        );
    }

​ shader层面,我们应用等式得到聚光衰减:

    float4 spotAngles = _OtherLightSpotAngles[index];
    float spotAttenuation = Square(
        saturate(dot(_OtherLightDirections[index].xyz, light.direction) *
        spotAngles.x + spotAngles.y)
    );
    light.attenuation = spotAttenuation * rangeAttenuation / distanceSqr;

​ 为确保点光不会被聚光衰减影响,我们将聚光角分别设置为0和1:

    void SetupPointLight (int index, ref VisibleLight visibleLight) 
    {
        …
        otherLightSpotAngles[index] = new Vector4(0f, 1f);
    }

2.3 配置内角

​ Unity默认的灯光组件不能配置聚光内角,为此,我们可以自定义灯光编辑器,新的类需继承LightEditor。加上[CanEditMultipleObjects]确保可修改多个灯光。加上[CustomEditorForRenderPipeline]属性,第一个参数是灯光的类型,第二个参数必须是自定义RP资产的类型:

using UnityEngine;
using UnityEditor;

[CanEditMultipleObjects]
[CustomEditorForRenderPipeline(typeof(Light), typeof(CustomRenderPipelineAsset))]
public class CustomLightEditor : LightEditor {}

​ 然后重写OnInspectorGUI方法,先调用默认的GUI:

    public override void OnInspectorGUI() 
    {
        base.OnInspectorGUI();
    }

​ 我们可通过settings来获得相应的属性,如果只是聚光灯我们就调整内角和外角:

        base.OnInspectorGUI();
        if (
            !settings.lightType.hasMultipleDifferentValues &&
            (LightType)settings.lightType.enumValueIndex == LightType.Spot
        )
        {
            settings.DrawInnerAndOuterSpotAngle();
            settings.ApplyModifiedProperties();
        }

3 烘培光和阴影

3.1 完全烘培

​ 烘培点光和聚光的话会发现比实时光亮不少,这是因为Unity默认使用的光衰减不正确。

3.2 灯光代理

​ 我们可以让Unity使用不同的光衰减值,在Unity于编辑器内执行lightmapping前为本该调用的方法提供一个代理值,为此,我们将CustomRenderPipeline变为一个partial类,构造方法中调用编辑器专属的InitializeForEditor方法:

public partial class CustomRenderPipeline : RenderPipeline 
{

    …

    public CustomRenderPipeline (
        bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher,
        ShadowSettings shadowSettings
    ) 
    {
        …
        InitializeForEditor();
    }

    …
}

​ 定义另一个编辑器版本的partial类:

using Unity.Collections;
using UnityEngine;
using UnityEngine.Experimental.GlobalIllumination;
using LightType = UnityEngine.LightType;

public partial class CustomRenderPipeline 
{

    partial void InitializeForEditor ();
}

​ 我们重写Lightmapper初始化灯光数据的方法,提供一个方法代理,将来自输入Light数组的数据传输到NativeArray输出中,代理类型为Lightmapping.RequestLightsDelegate,这里我们使用lambda表达式来定义方法:

partial void InitializeForEditor ();
    
#if UNITY_EDITOR

    static Lightmapping.RequestLightsDelegate lightsDelegate =
        (Light[] lights, NativeArray output) => {};

#endif

​ 我们需要为每个灯光配置LightDataGI结构体,并添加到输出中,对每种灯光类型,我们进行不同的操作,默认情况下不烘培光:

    static Lightmapping.RequestLightsDelegate lightsDelegate =
        (Light[] lights, NativeArray output) => {
            var lightData = new LightDataGI();
            for (int i = 0; i < lights.Length; i++) 
            {
                Light light = lights[i];
                switch (light.type) 
                {
                    default:
                        lightData.InitNoBake(light.GetInstanceID());
                        break;
                }
                output[i] = lightData;
            }
        };

​ 对每种类型的光,我们调用LightmapperUtils.Extract方法获得灯光结构体数据,然后调用灯光数据的Init方法初始化:

                switch (light.type) 
                {
                    case LightType.Directional:
                        var directionalLight = new DirectionalLight();
                        LightmapperUtils.Extract(light, ref directionalLight);
                        lightData.Init(ref directionalLight);
                        break;
                    case LightType.Point:
                        var pointLight = new PointLight();
                        LightmapperUtils.Extract(light, ref pointLight);
                        lightData.Init(ref pointLight);
                        break;
                    case LightType.Spot:
                        var spotLight = new SpotLight();
                        LightmapperUtils.Extract(light, ref spotLight);
                        lightData.Init(ref spotLight);
                        break;
                    case LightType.Area:
                        var rectangleLight = new RectangleLight();
                        LightmapperUtils.Extract(light, ref rectangleLight);
                        rectangleLight.mode = LightMode.Baked;
                        lightData.Init(ref rectangleLight);
                        break;
                    default:
                        lightData.InitNoBake(light.GetInstanceID());
                        break;
                }

​ 最后,我们修改衰减类型:

                lightData.falloff = FalloffType.InverseSquared;
                output[i] = lightData;

​ 在InitializeForForEditor中我们设置灯光代理:

    partial void InitializeForEditor ();
    
#if UNITY_EDITOR

    partial void InitializeForEditor () 
{
        Lightmapping.SetDelegate(lightsDelegate);
    }

​ 我们还需要重写Dispose方法:

    protected override void Dispose (bool disposing) 
    {
        base.Dispose(disposing);
        Lightmapping.ResetDelegate();
    }

3.3 阴影遮罩

​ 我们添加ReserveOtherShadow方法,目前只支持阴影这招模式,且只考虑配置阴影强度和遮罩通道:

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

​ 在SetupPointLightSetupSpotLight中配置数据:

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

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

​ shader层面,在Shadows.hlsl中创建OtherShadowData结构体和GetOtherShadowAttenuation方法,目前先使用和平行光阴影相同的处理过程:

struct OtherShadowData 
{
    float strength;
    int shadowMaskChannel;
};

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

Light.hlsl中,加入GetOtherShadowData方法,并在GetOtherLight中配置:

CBUFFER_START(_CustomLight)
    …
    float4 _OtherLightShadowData[MAX_OTHER_LIGHT_COUNT];
CBUFFER_END

…           

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

Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData) 
{
    …
    
    OtherShadowData otherShadowData = GetOtherShadowData(index);
    light.attenuation =
        GetOtherShadowAttenuation(otherShadowData, shadowData, surfaceWS) *
        spotAttenuation * rangeAttenuation / distanceSqr;
    return light;
}

4 逐物体灯光

​ 目前的光照计算都是逐片元的,这对于平行光来说可以接受,但对于其它类型的灯光来说就消耗太大了,毕竟数量很多,而且大部分片元不会受某一光源影响。因此,我们需要减少哪些逐片元计算的光源数量,最简单的方法是使用Unity的逐物体灯光索引。

​ Unity决定哪些灯光影响每个物体,然后将信息送往GPU,这样对于每个物体我们就只用考虑几个光源,这对于小物体来说很不错,但对于较大的物体来说,会丢失一些光源影响。

4.1 逐物体灯光数据

​ 在CameraRenderer.DrawVisibleGeometry中,添加一个布尔参数来指示是否使用逐物体灯光模式。如果是则开启PerObejctData.LightDataPerObjectData.LightIndices:

    void DrawVisibleGeometry (
        bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject
    ) 
    {
        PerObjectData lightsPerObjectFlags = useLightsPerObject ?
            PerObjectData.LightData | PerObjectData.LightIndices :
            PerObjectData.None;
        var sortingSettings = new SortingSettings(camera) {
            criteria = SortingCriteria.CommonOpaque
        };
        var drawingSettings = new DrawingSettings(
            unlitShaderTagId, sortingSettings
        ) {
            enableDynamicBatching = useDynamicBatching,
            enableInstancing = useGPUInstancing,
            perObjectData =
                PerObjectData.ReflectionProbes |
                PerObjectData.Lightmaps | PerObjectData.ShadowMask |
                PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
                PerObjectData.LightProbeProxyVolume |
                PerObjectData.OcclusionProbeProxyVolume |
                lightsPerObjectFlags
        };
        …
    }

CameraRenderer.RenderCustomRenderPipeline中我们都加上对应的参数。

​ 在CustomRenderPipelineAsset中我们加上对应的选项:

    [SerializeField]
    bool
        useDynamicBatching = true,
        useGPUInstancing = true,
        useSRPBatcher = true,
        useLightsPerObject = true;

    [SerializeField]
    ShadowSettings shadows = default;

    protected override RenderPipeline CreatePipeline () 
    {
        return new CustomRenderPipeline(
            useDynamicBatching, useGPUInstancing, useSRPBatcher,
            useLightsPerObject, shadows
        );
    }

4.2 优化灯光索引

​ Unity为每个物体创建一个灯光列表,按照重要性排序,列表中不管可不可见都会包含,并且包含平行光,因此我们要进行优化,只留下那些可见的非平行光。在SetupLights中进行。在进入循环前,先调用GetLightIndexMap获得灯光索引表:

        NativeArray indexMap = useLightsPerObject ?
            cullingResults.GetLightIndexMap(Allocator.Temp) : default;
        NativeArray visibleLights = cullingResults.visibleLights;

​ 我们只需要点光和聚光的索引,不要的灯光的索引设为-1,并修改剩余灯光的索引:

        for (int i = 0; i < visibleLights.Length; i++) 
        {
            int newIndex = -1;
            VisibleLight visibleLight = visibleLights[i];
            switch (visibleLight.lightType) {
                …
                case LightType.Point:
                    if (otherLightCount < maxOtherLightCount) 
                    {
                        newIndex = otherLightCount;
                        SetupPointLight(otherLightCount++, ref visibleLight);
                    }
                    break;
                case LightType.Spot:
                    if (otherLightCount < maxOtherLightCount) 
                    {
                        newIndex = otherLightCount;
                        SetupSpotLight(otherLightCount++, ref visibleLight);
                    }
                    break;
            }
            if (useLightsPerObject) 
            {
                indexMap[i] = newIndex;
            }
        }

​ 不可见光的索引也剔除,在第一次循环后进行(从未重新设置的索引处开始):

        if (useLightsPerObject) 
        {
            for (; i < indexMap.Length; i++) 
            {
                indexMap[i] = -1;
            }
        }

​ 完成后,我们要修改索引表,然后删除不再使用的表:

        if (useLightsPerObject) 
        {
            for (; i < indexMap.Length; i++) 
            {
                indexMap[i] = -1;
            }
            cullingResults.SetLightIndexMap(indexMap);
            indexMap.Dispose();
        }

​ 记得设置对应的关键字:

    static string lightsPerObjectKeyword = "_LIGHTS_PER_OBJECT";
    
    …
    
    void SetupLights (bool useLightsPerObject) 
    {
        …

        if (useLightsPerObject) 
        {
            for (; i < indexMap.Length; i++) 
            {
                indexMap[i] = -1;
            }
            cullingResults.SetLightIndexMap(indexMap);
            indexMap.Dispose();
            Shader.EnableKeyword(lightsPerObjectKeyword);
        }
        else 
        {
            Shader.DisableKeyword(lightsPerObjectKeyword);
        }
        
        …
    }

4.3 使用索引

​ 使用multi_compile:

            #pragma multi_compile _ _LIGHTS_PER_OBJECT

​ 在UnityPerDraw缓冲中定义灯光数据和灯光索引,unity_LightData的y组件包含灯光数量,unity_LightIndices的两个vector的每个通道包含一个灯光索引:

    real4 unity_WorldTransformParams;

    real4 unity_LightData;
    real4 unity_LightIndices[2];

​ 在GetLighting中,如果定义了_LIGHTS_PER_OBJECT,遍历所有的逐物体光:

    #if defined(_LIGHTS_PER_OBJECT)
        for (int j = 0; j < min(unity_LightData.y, 8); j++) 
        {
            int lightIndex = unity_LightIndices[j / 4][j % 4];
            Light light = GetOtherLight(lightIndex, surfaceWS, shadowData);
            color += GetLighting(surfaceWS, brdf, light);
        }
    #else
        for (int j = 0; j < GetOtherLightCount(); j++) 
        {
            Light light = GetOtherLight(j, surfaceWS, shadowData);
            color += GetLighting(surfaceWS, brdf, light);
        }
    #endif

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