美文网首页
Unity自定义SRP(三):平行光

Unity自定义SRP(三):平行光

作者: Dragon_boy | 来源:发表于2020-12-15 16:31 被阅读0次

    https://catlikecoding.com/unity/tutorials/custom-srp/directional-lights/

    光照

    Lit Shader

    这里新建一个可接受光照的shader。新建LitPass.hlsl文件,内容和UnlitPass.hlsl一致,修改函数名:

    #ifndef CUSTOM_LIT_PASS_INCLUDED
    #define CUSTOM_LIT_PASS_INCLUDED
    
    ...
    
    Varyings LitPassVertex(Attributes input)
    {
    }
    
    float4 LitPassFragment(Varyings input) : SV_TARGET
    {
    }
    #endif
    

    Lit.shader:

    Shader "Custom RP/Lit" {
        
        Properties {
            _BaseMap("Texture", 2D) = "white" {}
            _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1.0)
            ...
        }
        
        SubShader {
            Pass {
                HLSLPROGRAM
                ...
                #pragma vertex LitPassVertex
                #pragma fragment LitPassFragment
                #include "LitPass.hlsl"
                ENDHLSL
            }
    }
    

    我们将使用自定义的光照模式,加上标签:

            Pass {
                Tags {
                    "LightMode" = "CustomLit"
                }
    

    CameraRenderer中加入该pass:

        static ShaderTagId
            unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"),
            litShaderTagId = new ShaderTagId("CustomLit");
    

    接着添加到DrawVisibleGeometry中:

            var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
            {
                enableDynamicBatching = useDynamicBatching,
                enableInstancing = useGPUInstancing
            };
            drawingSettings.SetShaderPassName(1, litShaderTagId);
    

    法线

    为使用光照模型,我们需要法线。在结构体中声明:

    struct Attributes
    {
        float3 positionOS : POSITION;
        float3 normalOS : NORMAL;
        float2 baseUV : TEXCOORD0;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
    
    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        float3 positionWS : VAR_POSITION;
        float3 normalWS : VAR_NORMAL;
        float2 baseUV : VAR_BASE_UV;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
    

    在顶点着色器中我们使用TransformObjectToWorldNormal来完成法线从模型空间到世界空间的变换:

        float3 positionWS = TransformObjectToWorld(input.positionOS);
        output.positionCS = TransformWorldToHClip(positionWS);
        output.normalWS = TransformObjectToWorldNormal(input.normalOS);
    

    我们可以在片元着色器中用颜色显示法线:

        base.rgb = input.normalWS;
        return base;
    

    注意,小于0的值会取为0。

    法线插值

    尽管法线在顶点着色器中是单位长度的,但经过沿三角形插值后其长度会变化,因此在片元着色器中要进行标准化:

        base.rgb = normalize(input.normalWS);
    

    表面属性

    shader中的光照是模拟光线遇到表面后的反应的,因此我们需要跟踪表面的属性。我们新建一个Surface.hlsl文件,置于ShaderLibrary文件夹下:

    #ifndef CUSTOM_SURFACE_INCLUDED
    #define CUSTOM_SURFACE_INCLUDED
    
    struct Surface
    {
        float3 normal;
        float3 color;
        float alpha;
    };
    
    #endif
    

    LitPass中包含:

    #include "../ShaderLibrary/Common.hlsl"
    #include "../ShaderLibrary/Surface.hlsl"
    

    在片元着色器中定义一个surface变量,并填充:

        Surface surface;
        surface.normal = normalize(input.normalWS);
        surface.color = base.rgb;
        surface.alpha = base.a;
    
        return float4(surface.color, surface.alpha);
    

    计算光照

    新建一个Lighting.hlsl文件,存放光照计算函数:

    #ifndef CUSTOM_LIGHTING_INCLUDED
    #define CUSTOM_LIGHTING_INCLUDED
    
    float3 GetLighting(Surface surface)
    {
        return surface.normal.y * surface.color;
    }
    
    #endif
    

    LitPass中包含:

    #include "../ShaderLibrary/Common.hlsl"
    #include "../ShaderLibrary/Surface.hlsl"
    #include "../ShaderLibrary/Lighting.hlsl"
    

    在片元着色器中可以使用GetLighting获取光照:

        float3 color = GetLighting(surface);
        return float4(color, surface.alpha);
    

    灯光

    这里我们只模拟拥有方向的平行光。

    灯光结构体

    新建Light.hlsl文件,定义一个平行光结构体,同时定义一个GetDirectionalLight函数来返回一个配置好的平行光:

    #ifndef CUSTOM_LIGHT_INCLUDED
    #define CUSTOM_LIGHT_INCLUDED
    
    struct Light
    {
        float3 color;
        float3 direction;
        float attenuation;
    };
    
    Light GetDirectionalLight(int index, Surface surfaceWS, ShadowData shadowData)
    {
        Light light;
        light.color = 1.0;
        light.direction = float3(0.0, 1.0, 0.0);
        return light;
    }
    
    #endif
    

    LitPass中包含:

    #include "../ShaderLibrary/Common.hlsl"
    #include "../ShaderLibrary/Surface.hlsl"
    #include "../ShaderLibrary/Light.hlsl"
    #include "../ShaderLibrary/Lighting.hlsl"
    

    光照函数

    在文件中新增一个IncomingLight函数,用于计算光照程度,这里使用兰伯特模型:

    float3 IncomingLight(Surface surface, Light light)
    {
        return saturate(dot(surface.normal, light.direction)* light.color;
    }
    

    同时添加一个重载函数GetLighting,用于计算每个灯光的光照:

    float3 GetLighting(Surface surface, Light light)
    {
        return IncomingLight(surface, light) * surface.color;
    }
    

    修改另一个GetLighting,计算总的光照:

    float3 GetLighting(Surface surface)
    {
        return GetLighting(surface, light);
    }
    

    将灯光数据送往GPU

    为了在shader中可以得到灯光数据,我们定义几个uniform变量,放于_CustomLight缓冲中:

    CBUFFER_START(_CustomLight)
        float3 _DirectionalLightColors;
        float3 _DirectionalLightDirection;
    CBUFFER_END
    

    GetDirectionalLight中使用:

    Light GetDirectionalLight()
    {
        Light light;
        light.color = _DirectionalLightColors;
        light.direction = _DirectionalLightDirections;
        return light;
    }
    

    现在我们将数据送往GPU。创建一个新的类Lighting,类似于CameraRenderer

    public class Lighting
    {
    
        const string bufferName = "Lighting";
    
        CommandBuffer buffer = new CommandBuffer
        {
            name = bufferName
        };
    
        public void Setup(ScriptableRenderContext context)
        {
            buffer.BeginSample(bufferName);
            SetupDirectionalLight();
            buffer.EndSample(bufferName);
            context.ExecuteCommandBuffer(buffer);
            buffer.Clear();
        }
    
        void SetupDirectionalLight()
        {
        }
    }
    

    同时跟踪两个shader属性:

        static int
            dirLightColorsId = Shader.PropertyToID("_DirectionalLightColor"),
            dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirection");
    

    我们可以使用RenderSetting.sun来获取场景中的主光源:

        void SetupDirectionalLight(int index, ref VisibleLight visibleLight)
        {
            Light light = RenderSetting.sun;
            buffer.SetGlobalVector(dirLightColorId, light.color.linear * light.intensity);
            buffer.SetGlobalVector(dirLightDirectionalId, -light.transform.forward);
        }
    

    CameraRenderer中设置光照:

        Lighting lighting = new Lighting();
    
        public void Render(ScriptableRenderContext context, Camera camera, bool useDynamicBatching, bool useGPUInstancing)
        {
            Setup();
            lighting.Setup(context);
            DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);
            DrawUnsupportedShaders();
            DrawGizmos();
            lighting.Cleanup();
            Submit();
        }
    

    可见光

    在进行剔除时,Unity也会判断那些灯光会影响摄像机的可见范围。为此,Lighting需要获取剔除结果,在Setup中添加一个参数,同时使用SetupLights进行替换:

        public void Setup(ScriptableRenderContext context, CullingResults cullingResults)
        {
            this.cullingResults = cullingResults;
            buffer.BeginSample(bufferName);
            SetupLights();
            ...
        }
    
        void SetupLights()
        {
        }
    

    CameraRenderer.Render中添加参数:

            lighting.Setup(context, cullingResults);
    

    使用NativeArray设置可见光:

    using Unity.Collections;
    
    public class Lighting
    {
        ...
        void SetupLights()
        {
            NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
        }
        ...
    }
    

    多个平行光

    使用可见光数据的话,可以支持多个平行光源,但我们需要将这些数据送往GPU。首先定义平行光源的最大数量,接着定义相应的颜色和方向数组,记得声明相应的材质属性ID:

        const int maxDirLightCount = 4;
    
        static int
            dirLightCountId = Shader.PropertyToID("_DirectionalLightCount"),
            dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors"),
            dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections");
    
        static Vector4[]
            dirLightColors = new Vector4[maxDirLightCount],
            dirLightDirections = new Vector4[maxDirLightCount];
    
    

    SetupDirectionalLight函数添加一个索引参数,可以根据索引获得相应光源的属性:

        void SetupDirectionalLight(int index, ref VisibleLight visibleLight)
        {
            dirLightColors[index] = visibleLight.finalColor;
            dirLightDirections[index] = -visibleLight.localToWorldMatrix.GetColumn(2);
        }
    

    Unity默认不使用线性空间的灯光强度:

        public CustomRenderPipeline(bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher)
        {
            this.useDynamicBatching = useDynamicBatching;
            this.useGPUInstancing = useGPUInstancing;
            GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
            GraphicsSettings.lightsUseLinearIntensity = true;
        }
    

    接着我们遍历可见光,并设置灯光属性:

        void SetupLights()
        {
            NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
            for (int i = 0; i < visibleLights.Length; i++)
            {
                VisibleLight visibleLight = visibleLights[i];
                SetupDirectionalLight(i, visibleLight);
            }
    
            buffer.SetGlobalInt(dirLightCountId, dirLightCount);
            buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
            buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
        }
    

    注意,我们目前只支持至多4个平行光:

        void SetupLights()
        {
            NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
            int dirLightCount = 0;
            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;
                    }
                }
            }
    
            buffer.SetGlobalInt(dirLightCountId, dirLightCount);
            buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
            buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
        }
    

    Shader循环

    修改Light.hlsl中的_CustomLight缓冲:

    #define MAX_DIRECTIONAL_LIGHT_COUNT 4
    
    CBUFFER_START(_CustomLight)
        int _DirectionalLightCount;
        float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
        float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
    CBUFFER_END
    

    同时添加一个获取灯光数量的方法:

    int GetDirectionalLightCount()
    {
        return _DirectionalLightCount;
    }
    
    Light GetDirectionalLight(int index)
    {
        Light light;
        light.color = _DirectionalLightColors[index].rgb;
        light.direction = _DirectionalLightDirections[index].xyz;
        return light;
    }
    

    接着修改针对一个表面的GetLighting函数:

    float3 GetLighting(Surface surface)
    {
        float3 color = 0.0;
        for (int i = 0; i < GetDirectionalLightCount(); i++)
        {
            Light light = GetDirectionalLight(i);
            color += GetLighting(surface, light);
        }
        return color;
    }
    

    BRDF

    BRDF不过多介绍

    表面属性

    这里使用金属流程,我们在Surface中添加金属度和光滑度的属性,在shader中也添加对应的属性:

            _Metallic ("Metallic", Range(0, 1)) = 0
            _Smoothness ("Smoothness", Range(0, 1)) = 0.5
    
    struct Surface
    {
        float3 normal;
        float3 color;
        float alpha;
        float metallic;
        float smoothness;
    };
    
    UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
        UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
        UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
        UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
        UNITY_DEFINE_INSTANCED_PROP(float, _Metallic)
        UNITY_DEFINE_INSTANCED_PROP(float, _Smoothness)
    UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
    

    在片元着色器中填充相应属性:

        surface.metallic = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
        surface.smoothness = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);
    

    BRDF属性

    BRDF是漫反射和高光反射的结合,除了将表面颜色分为这两块外,我们还需要知道表面的粗糙度。新建BRDF.hlsl,加入结构体:

    #ifndef CUSTOM_BRDF_INCLUDED
    #define CUSTOM_BRDF_INCLUDED
    
    struct BRDF
    {
        float3 diffuse;
        float3 specular;
        float roughness;
    };
    
    #endif
    

    添加GetBRDF方法,返回某一表面的BRDF数据:

    BRDF GetBRDF(inout Surface surface)
    {
        BRDF brdf;
        brdf.diffuse = surface.color;
        brdf.specular = 0.0;
        brdf.roughness = 1.0;
        return brdf;
    }
    

    LitPass中包含相应文件:

    #include "../ShaderLibrary/Common.hlsl"
    #include "../ShaderLibrary/Surface.hlsl"
    #include "../ShaderLibrary/Light.hlsl"
    #include "../ShaderLibrary/BRDF.hlsl"
    #include "../ShaderLibrary/Lighting.hlsl"
    

    在所有的GetLighting函数中加入brdf参数,使用其diffuse属性替换:

    float3 GetLighting(Surface surface, BRDF brdf, Light light)
    {
        return IncomingLight(surface, light) * brdf.diffuse;
    }
    
    float3 GetLighting(Surface surface, BRDF brdf)
    {
        float3 color = 0.0;
        for (int i = 0; i < GetDirectionalLightCount(); i++)
        {
            Light light = GetDirectionalLight(i, surface);
            color += GetLighting(surface, brdf, light);
        }
        return color;
    }
    

    在片元着色器末尾调用:

        BRDF brdf = GetBRDF(surface);
        float3 color = GetLighting(surface, brdf);
    

    反射率

    这里我们假设金属材质进行完全高光反射,反射率等价为表面的金属度,以此计算漫反射:

        float oneMinusReflectivity = surface.metallic;
        brdf.diffuse = surface.color * oneMinusReflectivity;
    

    不过实际中,非金属也会有光的反射,即高光,它们的反射率的平均值大约为0.04。我们添加一个函数来将1-反射率的范围调整到0-0.96(即URP采用的方法):

    #define MIN_REFLECTIVITY 0.04
    
    float OneMinusReflectivity(float metallic)
    {
        float range = 1.0 - MIN_REFLECTIVITY;
        return range - metallic * range;
    }
    

    使用该函数计算漫反射:

        float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);
        brdf.diffuse = surface.color * oneMinusReflectivity;
    

    高光

    根据能量守恒公式,反射的出射光的能量不会大部入射光的能量,也就是说直接相减就能得到高光颜色:

        brdf.diffuse = surface.color * oneMinusReflectivity;
        brdf.specular = surface.color - brdf.diffuse;
    

    不过这样就忽略了一点,金属材质会影响高光颜色,而非金属不会,非金属的高光应该是白色的,我们可以将表面颜色与最小的反射率根据表面的金属度插值得到:

        brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);
    

    粗糙度

    Core RP Library有一个方法PerceptualSmoothnessToPerceptualRoughness,可以根据表面的光滑程度获得粗糙度:

    real PerceptualSmoothnessToPerceptualRoughness(real perceptualSmoothness)
    {
        return (1.0 - perceptualSmoothness);
    }
    

    perceptual即感知到的。

    真正的粗糙度可以使用PerceptualRoughnessToRoughness得到:

    real RoughnessToPerceptualRoughness(real roughness)
    {
        return sqrt(roughness);
    }
    

    这些方法在CommonMaterial.hlsl文件中:

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
    #include "UnityInput.hlsl"
    

    使用这些函数获得粗糙度:

        float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);
        brdf.roughness = PerceptualRoughnessToRoughness(perceptualRoughness);
    

    观察方向

    我们需要摄像机的位置,在UnityInput中定义:

    float3 _WorldSpaceCameraPos;
    

    同时相关光照计算我们需要顶点的世界坐标:

    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        float3 positionWS : VAR_POSITION;
        ...
    };
    
    Varyings LitPassVertex(Attributes input)
    {
        ...
        output.positionWS = TransformObjectToWorld(input.positionOS);
        output.positionCS = TransformWorldToHClip(output.positionWS);
        ...
    }
    

    我们可以将观察方向作为表面属性的一部分:

    struct Surface
    {
        float3 normal;
        float3 viewDirection;
        float3 color;
        float alpha;
        float metallic;
        float smoothness;
    };
    

    接着在片元着色器中计算:

        surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);
    

    高光强度

    这里的高光强度计算使用简化CookTorrance BRDF的一种变体,也是URP使用的。因为需要平方计算,我们在Common.hlsl添加相应的方法:

    float Square(float v)
    {
        return v * v;
    }
    

    上述BRDF的计算公式为:\frac {r^2} {d^2max(0.1, (L \cdot H)^2)n},其中d = (N \cdot H)^2(r^2 - 1) + 1.0001 ,r是粗糙度,N是法线,L是入射光方向,H是中间矢量,n = 4r + 2

    float SpecularStrength(Surface surface, BRDF brdf, Light light)
    {
        float3 h = SafeNormalize(light.direction + surface.viewDirection);
        float nh2 = Square(saturate(dot(surface.normal, h)));
        float lh2 = Square(saturate(dot(light.direction, h)));
        float r2 = Square(brdf.roughness);
        float d2 = Square(nh2 * (r2 - 1) + 1.0001);
        float normalization = brdf.roughness * 4.0 + 2.0;
        return r2 / (d2 * max(0.1, lh2) * normalization);
    }
    

    接着添加一个计算最后颜色的函数:

    float3 DirectBRDF(Surface surface, BRDF brdf, Light light)
    {
        return SpecularStrength(surface, brdf, light) * brdf.specular + brdf.diffuse;
    }
    

    在针对单个光源的GetLighting中应用:

    float3 GetLighting(Surface surface, BRDF brdf, Light light)
    {
        return IncomingLight(surface, light) * DirectBRDF(surface, brdf, light);
    }
    

    透明

    如果应用带有透明度的纹理进行透明度混合的话,会发现反射光会变暗:



    这并不合理,漫发射的确会变暗,但高光反射就说不通了(比如玻璃)。

    预乘Alpha

    我们只想让漫发射衰减,而高光反射不变。为了能够不应用衰减,我们将混合模式的Source改为1。不过这样的话,漫反射也不会衰减了,因此我们在GetBRDF中提前乘上alpha值:

        brdf.diffuse = surface.color * oneMinusReflectivity;
        brdf.diffuse *= surface.alpha;
    

    预乘选项

    我们想同时支持预乘和一般情况,因此加上一个参数:

    BRDF GetBRDF(inout Surface surface, bool applyAlphaToDiffuse = false)
    {
        ...
        brdf.diffuse = surface.color * oneMinusReflectivity;
        if (applyAlphaToDiffuse)
        {
            brdf.diffuse *= surface.alpha;
        }
        ...
    }
    

    在片元着色器中,我们可以使用_PREMULTIPLY_ALPHA关键字来决定是否使用预乘:

    #if defined(_PREMULTIPLY_ALPHA)
        BRDF brdf = GetBRDF(surface, true);
    #else
        BRDF brdf = GetBRDF(surface);
    #endif
        float3 color = GetLighting(surface, brdf);
        return float4(color, surface.alpha);
    

    同时添加相应的着色器变体和属性配置:

                #pragma shader_feature _PREMULTIPLY_ALPHA
    
            [Toggle(_PREMULTIPLY_ALPHA)] _PremulAlpha ("Premultiply Alpha", Float) = 0
    

    相关文章

      网友评论

          本文标题:Unity自定义SRP(三):平行光

          本文链接:https://www.haomeiwen.com/subject/jqkbgktx.html