Unity Deferred Shader

Created by miccall (转载请注明出处 miccall.tech)

我们给shader 加一个 Pass

            Pass {
                Tags {
                    "LightMode" = "Deferred"
                }
                CGPROGRAM
                #pragma target 3.0

                #pragma exclude_renderers nomrt
                #pragma shader_feature _ _RENDERING_CUTOUT
                #pragma shader_feature _METALLIC_MAP
                #pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
                #pragma shader_feature _NORMAL_MAP
                #pragma shader_feature _OCCLUSION_MAP
                #pragma shader_feature _EMISSION_MAP
                #pragma shader_feature _DETAIL_MASK
                #pragma shader_feature _DETAIL_ALBEDO_MAP
                #pragma shader_feature _DETAIL_NORMAL_MAP
                #pragma multi_compile _ UNITY_HDR_ON


                #pragma vertex MyVertexProgram
                #pragma fragment MyFragmentProgram

                #define DEFERRED_PASS
                #include "My Lighting.cginc"

                ENDCG
            }

它最终将着色结果写入G缓冲区,就是几何缓冲 ,而不是 颜色缓冲 :

    struct FragmentOutput {
        #if defined(DEFERRED_PASS)
            float4 gBuffer0 : SV_Target0;
            float4 gBuffer1 : SV_Target1;
            float4 gBuffer2 : SV_Target2;
            float4 gBuffer3 : SV_Target3;
        #else
            float4 color : SV_Target;
        #endif
    };

在 MyFragmentProgram 中 ,我们计算返回这个 :

    FragmentOutput MyFragmentProgram (Interpolators i) {

       FragmentOutput output;

      #if defined(DEFERRED_PASS)
            #if !defined(UNITY_HDR_ON)
                color.rgb = exp2(-color.rgb);
            #endif
            output.gBuffer0.rgb = albedo;
            output.gBuffer0.a = GetOcclusion(i);
            output.gBuffer1.rgb = specularTint;
            output.gBuffer1.a = GetSmoothness(i);
            output.gBuffer2 = float4(i.normal * 0.5 + 0.5, 1);
            output.gBuffer3 = color;
        #else
            output.color = color;
        #endif

            return output ;
    }

第一个G缓冲区用于存储漫 Albedo 和 AO。
这是一个ARGB32纹理,就像常规的帧缓冲一样。Albedo存储在RGB通道中,AO 存储在A通道中。
我们知道此时的 Albedo 颜色,我们可以使用它GetOcclusion来访问遮挡值。

第二个G缓冲区用于存储 specular 颜色以及 Smoothness 。它也是ARGB32纹理。

第三个G缓冲区存储世界空间法线向量。它们存储在 ARGB2101010 纹理的 RGB通道中。这意味着每个坐标使用十位而不是通常的八位存储,这使得它们更精确。A通道只有两位 - 所以总数也是32位 - 但它没有被使用,所以我们只需将其设置为1.法线的编码就像常规法线贴图一样

最后的G缓冲区用于累积场景的光照。其格式取决于相机是设置为LDR还是HDR。
在LDR的情况下,它是ARGB2101010纹理,就像法线的缓冲区一样。
启用HDR时,格式为ARGBHalf,每个通道存储一个16位浮点值,总共64位。

因此HDR版本是其他缓冲区的两倍。仅使用RGB通道,因此可以再次将A通道设置为1。

灯光 :

我们现在使用的颜色是完全阴影,好像有一个方向灯,这是不正确的。我们可以通过使用针对延迟传递的黑色虚拟灯来消除所有直接光计算。


    UnityLight CreateLight (Interpolators i) {
        UnityLight light;


        #if defined(DEFERRED_PASS)
            light.dir = float3(0, 1, 0);
            light.color = 0;



        #else
            #if defined(POINT) || defined(POINT_COOKIE) || defined(SPOT)
                light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
            #else
                light.dir = _WorldSpaceLightPos0.xyz;
            #endif
            UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);

            light.color = _LightColor0.rgb * attenuation;
        #endif
        return light;
    }

自发光:


    float3 GetEmission (Interpolators i) {

        #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)

            #if defined(_EMISSION_MAP)
                return tex2D(_EmissionMap, i.uv.xy) * _Emission;
            #else
                return _Emission;
            #endif
        #else
            return 0;
        #endif
    }

间接光 :


    UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
        UnityIndirect indirectLight;
        indirectLight.diffuse = 0;
        indirectLight.specular = 0;
        #if defined(VERTEXLIGHT_ON)
            indirectLight.diffuse = i.vertexLightColor;
        #endif





        #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)




            indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
            float3 reflectionDir = reflect(-viewDir, i.normal);
            Unity_GlossyEnvironmentData envData;
            envData.roughness = 1 - GetSmoothness(i);
            envData.reflUVW = BoxProjection(
                reflectionDir, i.worldPos,
                unity_SpecCube0_ProbePosition,
                unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax
            );
            float3 probe0 = Unity_GlossyEnvironment(
                UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData
            );
            envData.reflUVW = BoxProjection(
                reflectionDir, i.worldPos,
                unity_SpecCube1_ProbePosition,
                unity_SpecCube1_BoxMin, unity_SpecCube1_BoxMax
            );
            #if UNITY_SPECCUBE_BLENDING
                float interpolator = unity_SpecCube0_BoxMin.w;
                UNITY_BRANCH
                if (interpolator < 0.99999) {
                    float3 probe1 = Unity_GlossyEnvironment(
                        UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1, unity_SpecCube0),
                        unity_SpecCube0_HDR, envData
                    );
                    indirectLight.specular = lerp(probe1, probe0, interpolator);
                }
                else {
                    indirectLight.specular = probe0;
                }
            #else
                indirectLight.specular = probe0;
            #endif
            float occlusion = GetOcclusion(i);
            indirectLight.diffuse *= occlusion;
            indirectLight.specular *= occlusion;
        #endif
        return indirectLight;
    }


HDR和LDR :



    #pragma multi_compile _ UNITY_HDR_ON


     #if !defined(UNITY_HDR_ON)
                color.rgb = exp2(-color.rgb);
     #endif


延迟渲染 :


vertex shader :
    Interpolators VertexProgram (VertexData v) {
        Interpolators i;
        i.pos = UnityObjectToClipPos(v.vertex);
        i.uv = ComputeScreenPos(i.pos);
        i.ray = lerp(
            UnityObjectToViewPos(v.vertex) * float3(-1, -1, 1),
            v.normal,
            _LightAsQuad
        );
        return i;
    }
  1. 计算UV
    i.uv = ComputeScreenPos(i.pos);

uv是一个 float4 类型的

        struct VertexData {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
        };

        struct Interpolators {
              float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
                float3 ray : TEXCOORD1;
        };

fragment shader 计算最终的2D坐标


        float2 uv = i.uv.xy / i.uv.w;


        float4 FragmentProgram (Interpolators i) : SV_Target {
            float2 uv = i.uv.xy / i.uv.w;
            float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
            depth = Linear01Depth(depth);
            float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;
            float3 viewPos = rayToFarPlane * depth;
            float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
            float3 viewDir = normalize(_WorldSpaceCameraPos - worldPos);
            float3 albedo = tex2D(_CameraGBufferTexture0, uv).rgb;
            float3 specularTint = tex2D(_CameraGBufferTexture1, uv).rgb;
            float3 smoothness = tex2D(_CameraGBufferTexture1, uv).a;
            float3 normal = tex2D(_CameraGBufferTexture2, uv).rgb * 2 - 1;
            float oneMinusReflectivity = 1 - SpecularStrength(specularTint);
            UnityLight light = CreateLight(uv, worldPos, viewPos.z);
            UnityIndirect indirectLight;
            indirectLight.diffuse = 0;
            indirectLight.specular = 0;
            float4 color = UNITY_BRDF_PBS(
                albedo, specularTint, oneMinusReflectivity, smoothness,
                normal, viewDir, light, indirectLight
            );
            #if !defined(UNITY_HDR_ON)
                color = exp2(-color);
            #endif
            return color;
        }
  1. 世界位置

在定向光的情况下,四边形的四个顶点的光线作为法向矢量提供。所以我们可以将它们传递给顶点程序并插入它们。

通过对 CameraDepthTexture 纹理进行采样并将其线性化来找到 fragment 中的深度值

            UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);    
            float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
            depth = Linear01Depth(depth);

对它们进行缩放,以便我们得到 远裁剪平面 的光线

        float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;

按深度值缩放此光线可为我们提供一个位置。提供的光线在视图空间中定义,视图空间是摄像机的本地空间。所以我们最终得到了片段在视图空间中的位置。从该空间到世界空间的转换是使用unity_CameraToWorld矩阵完成的,该矩阵在ShaderVariables中定义 。

            float3 viewPos = rayToFarPlane * depth;
            float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
  1. 读取G缓冲区数据
    缓冲区通过三个_CameraGBufferTexture变量提供
    sampler2D _CameraGBufferTexture0;
    sampler2D _CameraGBufferTexture1;
    sampler2D _CameraGBufferTexture2;

我们需要反照率,镜面色调,光滑度和法线。

            float3 albedo = tex2D(_CameraGBufferTexture0, uv).rgb;
            float3 specularTint = tex2D(_CameraGBufferTexture1, uv).rgb;
            float3 smoothness = tex2D(_CameraGBufferTexture1, uv).a;
            float3 normal = tex2D(_CameraGBufferTexture2, uv).rgb * 2 - 1;
  1. BRDF

BRDF函数在UnityPBSLighting中定义,因此我们必须包含该文件
计算BRDF 需要 viewdir ,反射率 , 光的数据 :

        float3 viewDir = normalize(_WorldSpaceCameraPos - worldPos);
        float oneMinusReflectivity = 1 - SpecularStrength(specularTint);

            UnityLight light = CreateLight(uv, worldPos, viewPos.z);
            UnityIndirect indirectLight;
            indirectLight.diffuse = 0;
            indirectLight.specular = 0;

于是,我们就可以计算 BRDF 了

            float4 color = UNITY_BRDF_PBS(
                albedo, specularTint, oneMinusReflectivity, smoothness,
                normal, viewDir, light, indirectLight
            );
  1. 配置灯 :

间接光不适用于此,因此它仍然是黑色的。但必须配置直射光,使其与当前正在渲染的光相匹配。对于定向光,我们需要颜色和方向。这些是通过_LightColor和_LightDir变量提供的

    float4 _LightColor, _LightDir, _LightPos;

        UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
            UnityLight light;
            float attenuation = 1;
            float shadowAttenuation = 1;
            bool shadowed = false;
            #if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
                light.dir = -_LightDir;
                #if defined(DIRECTIONAL_COOKIE)
                    float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
                    attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
                #endif
                #if defined(SHADOWS_SCREEN)
                    shadowed = true;
                    shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
                #endif
            #else
                float3 lightVec = _LightPos.xyz - worldPos;
                light.dir = normalize(lightVec);
                attenuation *= tex2D(
                    _LightTextureB0,
                    (dot(lightVec, lightVec) * _LightPos.w).rr
                ).UNITY_ATTEN_CHANNEL;
                #if defined(SPOT)
                    float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
                    uvCookie.xy /= uvCookie.w;
                    attenuation *=
                        tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
                    attenuation *= uvCookie.w < 0;
                    #if defined(SHADOWS_DEPTH)
                        shadowed = true;
                        shadowAttenuation = UnitySampleShadowmap(
                            mul(unity_WorldToShadow[0], float4(worldPos, 1))
                        );
                    #endif
                #else
                    #if defined(POINT_COOKIE)
                        float3 uvCookie =
                            mul(unity_WorldToLight, float4(worldPos, 1)).xyz;
                        attenuation *=
                            texCUBEbias(_LightTexture0, float4(uvCookie, -8)).w;
                    #endif

                    #if defined(SHADOWS_CUBE)
                        shadowed = true;
                        shadowAttenuation = UnitySampleShadowmap(-lightVec);
                    #endif
                #endif
            #endif
            if (shadowed) {
                float shadowFadeDistance =
                    UnityComputeShadowFadeDistance(worldPos, viewZ);
                float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
                shadowAttenuation = saturate(shadowAttenuation + shadowFade);
                #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
                    UNITY_BRANCH
                    if (shadowFade > 0.99) {
                        shadowAttenuation = 1;
                    }
                #endif
            }
            light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
            return light;
        }

LightDir设置为光线行进的方向。对于我们的计算,我们需要从表面到光的方向,所以相反

        #if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
            light.dir = -_LightDir;
      #else
  1. 阴影

通过 ShadowMapTexture 变量访问阴影贴图

    #if defined (SHADOWS_SCREEN)
        sampler2D _ShadowMapTexture;
    #endif

需要对阴影纹理进行采样并使用它来衰减光色

        bool shadowed = false;
        float shadowAttenuation = 1;

                #if defined(SHADOWS_SCREEN)
                    shadowed = true;
                    shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
                #endif

仅在定向灯启用阴影时才有效。如果不是,则阴影衰减始终为1

                    #if defined(SHADOWS_DEPTH)
                        shadowed = true;
                        shadowAttenuation = UnitySampleShadowmap(
                            mul(unity_WorldToShadow[0], float4(worldPos, 1))
                        );
                    #endif

当聚光灯有阴影时,SHADOWS_DEPTH定义关键字
聚光灯和方向灯使用相同的变量来对阴影贴图进行采样。在聚光灯的情况下,我们可以UnitySampleShadowmap用来处理硬或阴影采样的细节。我们必须在阴影空间中提供片段位置。unity_WorldToShadow数组中的第一个矩阵可用于从世界转换为阴影空间

点光源的阴影存储在立方体贴图中。UnitySampleShadowmap为我们处理抽样。在这种情况下,我们必须为它提供从光到表面的矢量,以便对立方体贴图进行采样。这与光矢量相反


                    #if defined(POINT_COOKIE)
                        float3 uvCookie =
                            mul(unity_WorldToLight, float4(worldPos, 1)).xyz;
                        attenuation *=
                            texCUBEbias(_LightTexture0, float4(uvCookie, -8)).w;
                    #endif

                    #if defined(SHADOWS_CUBE)
                        shadowed = true;
                        shadowAttenuation = UnitySampleShadowmap(-lightVec);
                    #endif
  1. 阴影范围

阴影贴图是有限的。它无法覆盖整个世界。它覆盖的区域越大,阴影的分辨率越低。Unity具有最大距离,可以绘制阴影。除此之外,没有实时阴影。当阴影接近这个距离时,它们会淡出。至少,这就是Unity的着色器所做的。因为我们手动对阴影贴图进行采样,所以当到达地图边缘时,我们的阴影会被截断。结果是阴影被急剧切断或者在淡入淡出距离之外丢失。

    UnityComputeShadowFadeDistance  功能可以为我们找出正确的指标
            if (shadowed) {
                float shadowFadeDistance =
                    UnityComputeShadowFadeDistance(worldPos, viewZ);
                float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
                shadowAttenuation = saturate(shadowAttenuation + shadowFade);
                #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
                    UNITY_BRANCH
                    if (shadowFade > 0.99) {
                        shadowAttenuation = 1;
                    }
                #endif
            }

当阴影接近渐变距离时,阴影应该开始消失,一旦达到它就完全消失
视图深度是视图空间中片段位置的Z分量 。

    UnityLight light = CreateLight(uv, worldPos, viewPos.z);

最终超出阴影渐变距离的碎片将不会被遮蔽。但是,我们仍在对它们的阴影进行采样,这可能很昂贵。我们可以通过基于阴影衰落因子的分支来避免这种情况。它接近1,然后我们可以完全跳过阴影衰减

                #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
                    UNITY_BRANCH
                    if (shadowFade > 0.99) {
                        shadowAttenuation = 1;
                    }
                #endif

分支机构本身可能很昂贵。这只是一个改进,因为这是一个连贯的分支。除了在阴影区域的边缘附近,所有碎片都落在它的内部或外部。但这只有GPU可以利用这一点才有意义
HLSLSupport定义UNITY_FAST_COHERENT_DYNAMIC_BRANCHING宏时应该是这种情况
用SHADOWS_SOFT关键字表示。定向阴影总是需要单个纹理样本,因此便宜

  1. cookie
    cookie纹理可通过_LightTextureB0 。unity_WorldToLight矩阵变量可以实现从世界转换为光照空间
    #if defined(POINT_COOKIE)
        samplerCUBE _LightTexture0;
    #else
        sampler2D _LightTexture0;
    #endif

    float4x4 unity_WorldToLight;

使用矩阵将世界位置转换为光空间坐标。然后使用它们来采样cookie纹理。让我们使用一个单独的attenuation变量来跟踪cookie的衰减

                float attenuation = 1;

                #if defined(DIRECTIONAL_COOKIE)
                    float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
                    attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
                #endif

            light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
            return light;

cookie坐标之间存在较大差异时,会出现这些伪影。Unity使用的解决方案是在采样mip贴图时应用偏差,因此我们也会这样做:

           attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;

点光饼干也可以通过_LightTexture0。但是,在这种情况下,我们需要一个立方体贴图而不是常规纹理。

        #if defined(POINT_COOKIE)
            samplerCUBE _LightTexture0;
        #else
            sampler2D _LightTexture0;
        #endif

对cookie进行采样,请将片段的世界位置转换为光照空间,然后使用它来对立方体贴图进行采样

                    #if defined(POINT_COOKIE)
                        float3 uvCookie =
                            mul(unity_WorldToLight, float4(worldPos, 1)).xyz;
                        attenuation *=
                            texCUBEbias(_LightTexture0, float4(uvCookie, -8)).w;
                    #endif

  1. LDR

编码的LDR颜色必须乘以光缓冲区而不是加。我们可以通过将着色器的混合模式更改为。但是,如果我们这样做,那么HDR渲染就会出错。相反,我们必须使混合模式变量

                Blend [_SrcBlend] [_DstBlend]
                ZWrite Off

计算完PBS之后:


            #if !defined(UNITY_HDR_ON)
                color = exp2(-color);
            #endif
  1. 聚光灯

非定向的灯具有位置。它是 LightPos 。可以确定聚光灯的光矢量和光线方向

        float3 lightVec = _LightPos.xyz - worldPos;
        light.dir = normalize(lightVec);

LightAsQuad :光线方向 通过将点转换为视图空间来完成的

            i.ray = lerp(
                UnityObjectToViewPos(v.vertex) * float3(-1, -1, 1),
                v.normal,
                _LightAsQuad
            );

Cookie衰减,聚光灯的圆锥衰减是通过cookie纹理创建的


                #if defined(DIRECTIONAL_COOKIE)
                    float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
                    attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
                #endif

                #if defined(SPOT)
                    float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
                    uvCookie.xy /= uvCookie.w;
                    attenuation *=
                        tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
                    attenuation *= uvCookie.w < 0;
                #else

这实际上导致两个光锥,一个向前,一个向后。向后锥体通常最终在渲染区域之外,但这不能保证。我们只需要前锥,它对应于负W坐标 。

         attenuation *= uvCookie.w < 0;
  1. 距离衰减

来自聚光灯的光也会根据距离衰减。该衰减存储在查找纹理中,该纹理可通过_LightTextureB0
应该使用哪个纹理通道因平台而异,并由UNITY_ATTEN_CHANNEL宏定义

                float3 lightVec = _LightPos.xyz - worldPos;
                light.dir = normalize(lightVec);
                attenuation *= tex2D(
                    _LightTextureB0,
                    (dot(lightVec, lightVec) * _LightPos.w).rr
                ).UNITY_ATTEN_CHANNEL;
  1. 点光源

    点光源使用与聚光灯相同的光矢量,方向和距离衰减。所以他们可以共享该代码。只有在SPOT定义关键字时才能使用其余的聚光灯代码。