zoukankan      html  css  js  c++  java
  • Unity的Deferred Shading

    什么是Deferred Shading

    Unity自身除了支持前向渲染之外,还支持延迟渲染。Unity的rendering path可以通过Edit/Project Settings中的Graphics进行全局设置:

    除此之外,我们还可以在Main Camera中进行覆盖设置:

    需要注意的是,Unity的延迟渲染不支持MSAA。具体原因可以参考[2]。

    延迟渲染主要是为了解决前向渲染在多光源场景下效率低的问题,这里的延迟指的是将光照部分延迟到后面再进行计算。在前向渲染中,为了计算每个pixel的最终颜色,多个光源要跑多次light pass,将每个光源计算的结果进行混合。每个light pass都会重复计算一遍pixel的几何信息,比如normal,diffuse,specular等,这实际上是没有必要的,只要计算一遍,缓存起来就可以了。除此之外,在不考虑early-z的情况下,深度测试是在fragment shader之后进行的,那么必定存在大量不可见的pixel,都跑了一遍复杂的light pass计算。延迟渲染的实现,就是预先多一个geometry pass,利用深度测试,将不可见的pixel剔除,同时使用MRT(nultiple render targets),将pixel的几何信息,分别存储到不同的G-Buffer中,这样在light pass的时候,直接采样G-Buffer就可以进行光照计算了。

    说了这么多,不如来对比一下同一个场景下前向渲染和延迟渲染的draw call数量:

    如图所示,这个场景包含两个平行光源。先看前向渲染:

    总共有457个draw call,首先为了绘制平行光阴影的screen space shadow map,需要对场景跑一遍depth pass,然后对两个平行光源,依次绘制shadow map,进行阴影收集,最后对场景中受光照影响的物体,分别跑一遍forward base的light pass和forward add的light pass。

    那么再看下延迟渲染:

    可以发现此时只有329个draw call了,Unity首先对场景跑了一遍geometry pass,绘制G-Buffer,然后将该阶段的深度缓存拷贝到depth buffer中,再经过一个reflections相关的pass,绘制反射信息,就到了light pass阶段。light pass中的绘制shadow map的过程与前向渲染类似,先绘制再collect,只不过少了depth pass,这是因为我们在geometry pass之后,已经有了depth buffer了。 可以看到,真正负责绘制光源着色信息的只有2个draw call,一个光源各一个。

    G-Buffer

    现在,让我们以一个拥有1个平行光源,和3个反射探针的场景为例,来深入其中,一探究竟:

    要想让我们自定义的shader支持延迟渲染,就必须要设置LightMode为Deferred,而且只有GPU支持MRT(multiple render targets)时延迟渲染才有效。另外它不能是transparent的,transparent的物体会被Unity强行走前向渲染的流程。

    		Pass {
    			Tags {
    				"LightMode" = "Deferred"
    			}
    
    			CGPROGRAM
    
    			#pragma target 3.0
    			#pragma exclude_renderers nomrt
    
    			...
    
    			ENDCG
    		}
    

    那问题来了,如果没有这个LightMode的pass会怎么样?Unity将不会对这些物体执行geometry pass,还是会走正常的前向渲染的流程,并且还会在geometry pass之后,为这些物体跑一遍depth pass,如图所示:

    Unity的延迟渲染需要4个G-buffer。因此geometry pass的fragment shader的输出需要定义如下:

    struct FragmentOutput {
    		float4 gBuffer0 : SV_Target0;
    		float4 gBuffer1 : SV_Target1;
    		float4 gBuffer2 : SV_Target2;
    		float4 gBuffer3 : SV_Target3;
    };
    

    gBuffer0是ARGB32格式的texture,rgb通道存储的是diffuse信息,a通道存储的是occlusion信息;

    gBuffer1是ARGB32格式的texture,rgb通道存储的是specular信息,a通道存储的是roughness信息;

    gBuffer2是ARGB2101010格式的texture,rgb通道各占10位,a通道只占2位,它的rgb通道存储的是normal信息,a通道未被使用;

    gBuffer3根据是否开启HDR,有不同的格式,在未开启HDR时,是ARGB2101010格式的texture,而在开启HDR时,是ARGBHalf格式的texture,即每个通道占16位。这个buffer就是用来存储场景中的各种光照信息。这里的光照信息主要是自发光,间接的环境光,而不包括场景中光源的直接光照,毕竟光源的光照计算是延迟到后面再去做的。另外还有一点要注意的是,在未开启HDR时,gBuffer3的信息要以对数的形式进行存储,意味着我们要在代码中进行判断并转换:

    #pragma multi_compile _ UNITY_HDR_ON
    
    FragmentOutput MyFragmentProgram (Interpolators i) {
        	...
        	FragmentOutput output;
    		#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;
    		return output;
    }
    

    有了Deferred的shader之后,我们再看下Frame Debug:

    我们注意到,除了常规的blend设置和深度设置之外,geometry pass还开启了模板测试。由于Stencil Comp设置为Always,因此模板测试总是成功的,Stencil Pass设置的是Replace,意味着测试成功时,将把Stencil Ref写入到模板缓存中。写入时会通过Stencil WriteMask掩码操作,只写入mask通过的位。那么综上所述,geometry pass除了绘制了一份深度信息外,还记录了模板信息,所有在场景中的可见物体对应pixel的模板值均为192 & 207 = 192。用RenderDoc截帧,得到geometry pass绘制的4个G-Buffer如图所示:

    depth buffer如图所示,这里分别展示了buffer此时记录的深度信息和模板信息:

    首先我们发现texture是上下颠倒的,这是DirectX纹理坐标系的原因。其次,场景中的金属反射球,在gBuffer0中全黑,而gBuffer1中全白,这是因为反射球的材质将Metallic属性调到了1,故而只有specular而没有diffuse。gBuffer3除了skybox全黑的原因是因为场景中没有间接关照和自发光信息。模板信息是符合预期的,即只有出现可见物体的地方保存了模板信息,其值为192。这里得到的depth buffer,会通过RenderDeferred.CopyDepth拷贝一份到名为Deferred Depth的buffer中去,给后面reflections相关的pass使用,这些pass会以不同的方式去修改depth buffer,尤其是模板信息,因此需要保留一份原始的场景深度信息,也就是Deferred Depth这个buffer。

    Deferred Reflections-Skybox

    那么,我们现在来看下reflections相关的pass。我们知道,在前向渲染中,Unity使用反射探针来实现反射的效果,并且每个物体可以混合不同的反射探针。而在延迟渲染中,不同的反射探针是基于pixel进行混合的,从Frame Debug中可知Unity使用了一个名为DeferredReflections的shader来做这件事:

    Unity会先用这个shader绘制一遍skybox的reflection信息,然后再根据反射探针的重要程度,依次绘制场景中反射探针的reflection信息。这个shader有两个pass,由Frame Debug可知当前用的是第1个pass,让我们先来看下代码,从vertex shader看起:

    struct unity_v2f_deferred {
        float4 pos : SV_POSITION;
        float4 uv : TEXCOORD0;
        float3 ray : TEXCOORD1;
    };
    
    float _LightAsQuad;
    
    unity_v2f_deferred vert_deferred (float4 vertex : POSITION, float3 normal : NORMAL)
    {
        unity_v2f_deferred o;
        o.pos = UnityObjectToClipPos(vertex);
        o.uv = ComputeScreenPos(o.pos);
        o.ray = UnityObjectToViewPos(vertex) * float3(-1,-1,1);
    
        // normal contains a ray pointing from the camera to one of near plane's
        // corners in camera space when we are drawing a full screen quad.
        // Otherwise, when rendering 3D shapes, use the ray calculated here.
        o.ray = lerp(o.ray, normal, _LightAsQuad);
    
        return o;
    }
    

    由上图可知,在绘制skybox时,_LightAsQuad的值为1,那么上述代码中只需要关注输入的vertex和normal信息。用RenderDoc抓帧得到:

    可以看出,经过透视变换后的SV_POSITION坐标(x,y)分布在(-1, 1)上,而z分量为1,这恰好是clip坐标系中近剪裁面的位置。也就是说,经过vertex shader输出的顶点,就是表示整个近剪裁面。

    再看normal信息,它其实表示的是相机空间中从相机位置出发到达近剪裁面4个角的射线。那么有:

    [ extbf{r} = (pm x,pm y,z) = (pmdfrac{w}{2}, pmdfrac{h}{2}, n) \ tan dfrac{ heta}{2} = dfrac{dfrac{h}{2}}{n} \ aspect = dfrac{w}{h} ]

    其中,n为相机近剪裁面的距离,( heta)为相机的fov:

    截图可以看出,n为0.3,( heta)(dfrac{pi}{3})。aspect的信息可以从GBuffer或者Depth Texture的分辨率得到:

    得到aspect为(dfrac{1150}{531})。代入上面的公式计算出:

    [ extbf{r} = (pm0.37511,pm0.17321,0.3) ]

    与RenderDoc中的信息完全吻合。fragment shader的代码如下:

    half4 frag (unity_v2f_deferred i) : SV_Target
    {
        // Stripped from UnityDeferredCalculateLightParams, refactor into function ?
        i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
        float2 uv = i.uv.xy / i.uv.w;
    
        // read depth and reconstruct world position
        float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
        depth = Linear01Depth (depth);
        float4 viewPos = float4(i.ray * depth,1);
        float3 worldPos = mul (unity_CameraToWorld, viewPos).xyz;
    
        half4 gbuffer0 = tex2D (_CameraGBufferTexture0, uv);
        half4 gbuffer1 = tex2D (_CameraGBufferTexture1, uv);
        half4 gbuffer2 = tex2D (_CameraGBufferTexture2, uv);
        UnityStandardData data = UnityStandardDataFromGbuffer(gbuffer0, gbuffer1, gbuffer2);
    
        float3 eyeVec = normalize(worldPos - _WorldSpaceCameraPos);
        half oneMinusReflectivity = 1 - SpecularStrength(data.specularColor);
    
        half3 worldNormalRefl = reflect(eyeVec, data.normalWorld);
    
        // Unused member don't need to be initialized
        UnityGIInput d;
        d.worldPos = worldPos;
        d.worldViewDir = -eyeVec;
        d.probeHDR[0] = unity_SpecCube0_HDR;
        d.boxMin[0].w = 1; // 1 in .w allow to disable blending in UnityGI_IndirectSpecular call since it doesn't work in Deferred
    
        float blendDistance = unity_SpecCube1_ProbePosition.w; // will be set to blend distance for this probe
        #ifdef UNITY_SPECCUBE_BOX_PROJECTION
        d.probePosition[0]  = unity_SpecCube0_ProbePosition;
        d.boxMin[0].xyz     = unity_SpecCube0_BoxMin - float4(blendDistance,blendDistance,blendDistance,0);
        d.boxMax[0].xyz     = unity_SpecCube0_BoxMax + float4(blendDistance,blendDistance,blendDistance,0);
        #endif
    
        Unity_GlossyEnvironmentData g = UnityGlossyEnvironmentSetup(data.smoothness, d.worldViewDir, data.normalWorld, data.specularColor);
    
        half3 env0 = UnityGI_IndirectSpecular(d, data.occlusion, g);
    
        UnityLight light;
        light.color = half3(0, 0, 0);
        light.dir = half3(0, 1, 0);
    
        UnityIndirect ind;
        ind.diffuse = 0;
        ind.specular = env0;
    
        half3 rgb = UNITY_BRDF_PBS (0, data.specularColor, oneMinusReflectivity, data.smoothness, data.normalWorld, -eyeVec, light, ind).rgb;
    
        // Calculate falloff value, so reflections on the edges of the probe would gradually blend to previous reflection.
        // Also this ensures that pixels not located in the reflection probe AABB won't
        // accidentally pick up reflections from this probe.
        half3 distance = distanceFromAABB(worldPos, unity_SpecCube0_BoxMin.xyz, unity_SpecCube0_BoxMax.xyz);
        half falloff = saturate(1.0 - length(distance)/blendDistance);
    
        return half4(rgb, falloff);
    }
    

    _ProjectionParams是Unity保存投影相关的参数,z分量代表远剪裁面的距离。fragment shader首先取到当前pixel的场景深度,通过Linear01Depth将其转换到线性空间,函数Linear01Depth考虑了reverse-z的情况,对外输出结果保持统一,即0永远是离相机最近,1永远是离相机最远。得到线性深度之后,就可以计算出投影到当前pixel离相机最近的物体,位于相机空间的坐标。通过坐标系转换,进而能得到物体在世界空间中的坐标。我们就是根据该物体的信息(世界坐标,法线,视线向量,反射向量),来从skybox对应的cubemap采样,计算reflection信息。因为对于当前pixel而言,该物体是离相机最近的,意味着位于该物体之后的都会被遮挡,对reflection没有任何贡献。因此只需要计算离相近最近物体的reflection信息即可。

    从Frame Debug可以发现,skybox对应的反射cube范围为无穷大,因此所有物体必定位于cube之中,不必考虑物体在cube之外的情况。函数只需要考虑剔除掉光照的diffuse信息,传递到函数UNITY_BRDF_PBS中,只计算specular信息返回。shader绘制的render target是一个名为Deferred Reflections的texture,这里也设置了模板测试参数,只有通过模板测试的pixel才能成功写入texture。这里的Stencil Ref为128,ReadMask为128,Stencil Comp为Equal,意味着只需要比较第8位的值,只有第8位为1时模板测试才通过。那么什么样的pixel,模板缓存第8位的值为1呢?答案就是前面geometry pass绘制到的pixel。geometry pass会把绘制的pixel的模板值设置为192,192 & 128 = 128 & 128,测试通过。这意味着,只有存在可见物体的pixel,才会绘制reflection信息。这也是合理的,因为如果当前pixel连物体信息都不存在,就更不可能存在reflection信息了。

    Deferred Reflections-反射探针

    接下来的draw call,Unity使用了一个名为StencilWrite的shader进行绘制,该shader代码平平无奇,看上去等于什么也没做:

    Shader "Hidden/Internal-StencilWrite"
    {
        SubShader
        {
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma target 2.0
                #include "UnityCG.cginc"
                struct a2v {
                    float4 pos : POSITION;
                    UNITY_VERTEX_INPUT_INSTANCE_ID
                };
                struct v2f {
                    float4 vertex : SV_POSITION;
                    UNITY_VERTEX_OUTPUT_STEREO
                };
                v2f vert (a2v v)
                {
                    v2f o;
                    UNITY_SETUP_INSTANCE_ID(v);
                    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                    o.vertex = UnityObjectToClipPos(v.pos);
                    return o;
                }
                fixed4 frag () : SV_Target { return fixed4(0,0,0,0); }
                ENDCG
            }
        }
        Fallback Off
    }
    

    但是该draw call设置的rasterizer state就有意思了:

    首先ColorMask设置成了0,意味着fragment shader输出的颜色不会写入到Deferred Reflections这个buffer中。同时,Cull也设置成了Off,意味着物体的正面和背面都会渲染一遍。这里的Stencil Ref设置为128,Stencil Comp设置为Always,意味着模板测试总是通过的,但是这里还设置了Stencil ZFail为Invert,也就是深度测试失败时,需要将模板缓存中的值按位取反,写入到缓存中。注意这里的Stencil WriteMask设置为16,也就是按位取反的结果,只有第5位才会真正写入到缓存中。

    那么,传入该shader的顶点信息又是什么样的呢?用RenderDoc截帧可知,传入shader的其实是一个cube,它的中心位于local坐标系的原点,大小为1:

    但其实,我们更关心的是,这个cube变换到世界坐标系之后,它的坐标是怎样的。由Frame Debug中可看到unity_MatrixVP为:

    [ extbf{VP} = egin{bmatrix} 0.68 & -0.0035 & 0.43 & 0.69 \ 0.24 & -1.7 & -0.4 & 0.25 \ 0.00015 & 0.000081 & -0.00024 & 0.3 \ -0.52 & -0.27 & 0.81 & 10 end{bmatrix} ]

    而实际上经过MVP变换到clip坐标系的坐标我们是知道的,即SV_POSITION里的值,那么矩阵M为:

    [ extbf{VP} cdot extbf{M} cdot v = v' ]

    [ extbf{M} cdot v = extbf{VP}^{-1} cdot v' ]

    问题其实就转换成解线性方程组了,可以解得矩阵M为:

    [ extbf{M} = egin{bmatrix} 9.01 & 0 & 0 & 0 \ 0 & 5.01 & 0 & 2.5 \ 0 & 0 & 9.01 & 0 \ 0 & 0 & 0 & 1 end{bmatrix} ]

    当然,其实有了RenderDoc,这一切计算都可以省掉。我们知道这两个矩阵在shader的vs阶段使用,那么只需定位vs阶段用到的const buffer即可:

    可以发现,const buffer 1框中的部分恰好对应了Frame Debug中VP矩阵的转置形式。类似地,const buffer 0的部分对应了M矩阵的转置形式。有了这个从local坐标系转换到世界坐标系的矩阵,我们便能观察出它所代表的实际意义。对比其中的数值,可以发现该矩阵恰好对应了场景中的一个反射探针:

    这个反射探针位于世界坐标系的(0,2,0)点,它的包围box是一个x=9.01,y=5.01,z=9.01的box,而且box的中心点在y方向上有2个单位的偏移量。翻译成数学语言,就是一个在local坐标系的包围box,经过矩阵M转换到世界坐标系下的坐标应该是:

    [p_w = extbf{M} cdot p_l = (9.01x, 5.01y+2.5,9.01z,1)^T ]

    把local坐标系中box的中心(0,0,0)和顶点(+/-0.5,+/-0.5,+/-0.5)代入上式,得到的结果正是世界坐标系中box中心和顶点的坐标。那么经过这么漫长的过程,我们可以得出结论,这个StencilWrite的shader,输入的顶点信息就是反射探针的box信息。

    再回到这个shader本身的作用上来,它对box的正面和背面进行绘制,如果场景深度小于box正面的深度,那么模板测试会ZFail两次,对当前的模板值连续invert两次,等于无事发生。如果场景深度大于box背面的深度,那么模板测试和深度测试都会通过,模板值保持不变,也等于无事发生。但是,如果场景深度大于box正面的深度,而且小于box背面的深度,那么模板测试只会ZFail一次,当前的模板值就会发生改变,由于~192 & 16 = 16,因此第5位会被写入1,也就是模板值会从192变成208。换言之,只有位于box内部的物体,对应pixel的模板值会被改写。那这个shader的作用就很明显了,它就是为了找到位于反射探针box范围内的物体,通过新的模板值将其标记,只有这些物体才会使用该反射探针的cubemap进行采样,绘制reflections信息。我们也可以使用RenderDoc查看当前的depth buffer的模板值,来验证我们的猜想:

    场景中反射探针的box大小如图所示:

    可以看出,box内部的模板值和外部是不同的。有了这一标记,Unity继续使用DeferredReflections这个shader进行绘制。让我们着重看一下,与前面skybox绘制相比,有哪些不同的地方。

    首先,vertex shader使用的_LightAsQuad变成了0,那么传给fragement shader的ray分量完全取决于顶点的坐标:

        o.ray = UnityObjectToViewPos(vertex) * float3(-1,-1,1);
    

    通过RenderDoc可以发现,这里传入的顶点就是前面stencil pass的反射探针的cube。那么这里的ray分量为:

    [ray = (-x_v, -y_v, z_v) ]

    此时得到的ray分量并非是相机指向cube投影到远剪裁面点的射线。fragment shader中会做进一步处理:

        i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
    

    我们知道,Unity的view坐标系,可见物体的z坐标,一定是负值。那么通过除以ray.z的操作,可以让z坐标的值反转:

    [ray = (dfrac{-x_v}{-|z_v|}, dfrac{-y_v}{-|z_v|}, 1) cdot f ]

    [ray = (dfrac{x_v}{|z_v|}, dfrac{y_v}{|z_v|}, 1) cdot f ]

    这样求出的ray分量,就可以代入到后面计算场景深度,转换到世界坐标系,求出被cube覆盖的区域中离相机最近的物体坐标。这里坐标系转换使用的是unity_CameraToWorld矩阵,这个矩阵接受的view空间的向量,要求z分量为正,而上面的运算刚好满足这一条件。

    此外,与skybox不同的是,反射探针这里还考虑了blendDistance。blendDistance表示在cube之外的物体也有可能接受到该探针的reflections信息,blend的程度由blendDistance和物体离cube的距离共同决定。blendDistance在反射探针inspector中可以设置:

    blendDistance会对box相关的属性产生影响。例如把上面box的blendDistance设置为1,从Frame Debug中观察到:

    unity_SpecCube1_ProbePosition的w分量表示当前box的blendDistance。除此之外,用RenderDoc还能发现,box的几何信息也发生了改变:

    SV_POSITION的坐标发生了变化,仿佛这个box变大了,实际也的确如此:

    可以看出世界坐标系变换的矩阵发生了变化,使得box的尺寸x,y,z方向都增加了2×blendDistance。不过虽然几何上box的尺寸变大了,但是unity_SpecCube0_BoxMinunity_SpecCube0_BoxMax这两个变量依旧保存了box原先的尺寸。只有box的几何尺寸变大,才能覆盖包含blendDistance的投影区域,而只有保存原先尺寸,才能计算出物体到原始cube的距离,进而进行blend。

    从Frame Debug可知,这里blend的模式设置为SrcAlpha OneMinusSrcAlpha,能够成功绘制也需要通过模板测试。这里Stencil Ref设置为144,ReadMask设置为16,模板测试通过的条件为Equal。144 & 16 = 16,那么只有当前模板值第5位为1的pixel才能通过测试。显然,只有位于反射探针box范围内的物体才会被绘制,并且这里的box范围包括原始box外blendDistance的区域。最后,不论模板测试成功或是失败,都会把第5位清零,也就是把模板值复原回stencil pass之前的模样。

    除了这种方式之外,Unity还会使用另外一种策略绘制reflections信息。例如,我们将刚刚这个反射探针的cube尺寸调大(调整box size或者blend distance),这里将blend distance设置为2:

    Frame Debug中发现stencil pass消失了,只剩下DeferredReflections这一绘制pass。不过它设置的rasterizer state发生了变化:

    这里的深度测试从Less Equal变成了Greater,Cull Back也变成了Cull Front。换言之只有物体背面会被渲染,正面会被剔除。这样就能在fragment shader中获得所有在box背面前方的物体。也就是说,它虽然不能像前一种方法那么精确,只获取box内部的物体,但是至少可以剔除掉box背后的物体。

    那这样,会不会不在box内部,在box前方的物体被错误渲染了呢?答案是不会的。别忘记我们还有一个blendDistance,代码会计算物体到原始box范围的距离,如果超出了blendDistance,那么pixel color的alpha分量会设置为0,对最终的结果无贡献。

    至于Unity如何选取绘制的策略,这里并没有找到相关内容,猜测是如果box前方的物体数量较多,走blend alpha为0的开销相对较大,就会跑一遍stencil pass,来通过模板测试省掉不必要的绘制。

    skybox和所有反射探针都绘制完后,Unity会再次使用DeferredReflections这个shader,把刚刚绘制的reflections信息输出到back buffer,只是这次使用的是shader的另一个pass:

    // Adds reflection buffer to the lighting buffer
    Pass
    {
        ZWrite Off
        ZTest Always
        Blend [_SrcBlend] [_DstBlend]
    
        CGPROGRAM
            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile ___ UNITY_HDR_ON
    
            #include "UnityCG.cginc"
    
            sampler2D _CameraReflectionsTexture;
    
            struct v2f {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };
    
            v2f vert (float4 vertex : POSITION)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                o.uv = ComputeScreenPos (o.pos).xy;
                return o;
            }
    
            half4 frag (v2f i) : SV_Target
            {
                half4 c = tex2D (_CameraReflectionsTexture, i.uv);
                #ifdef UNITY_HDR_ON
                return float4(c.rgb, 0.0f);
                #else
                return float4(exp2(-c.rgb), 0.0f);
                #endif
    
            }
        ENDCG
    }
    

    这个pass很简单,这里就不做分析了。

    Deferred Shading Light Pass

    在此之后,就正式进入绘制光源信息的pass。Unity首先跟前向渲染一样绘制shadowmap,如果是平行光源还会有一个collect shadows的pass,真正绘制光源信息是使用DeferredShading这一shader进行的:

    通过查看源码可以发现,关键代码集中在函数CalculateLight中:

    half4 CalculateLight (unity_v2f_deferred i)
    {
        float3 wpos;
        float2 uv;
        float atten, fadeDist;
        UnityLight light;
        UNITY_INITIALIZE_OUTPUT(UnityLight, light);
        UnityDeferredCalculateLightParams (i, wpos, uv, light.dir, atten, fadeDist);
    
        light.color = _LightColor.rgb * atten;
    
        // unpack Gbuffer
        half4 gbuffer0 = tex2D (_CameraGBufferTexture0, uv);
        half4 gbuffer1 = tex2D (_CameraGBufferTexture1, uv);
        half4 gbuffer2 = tex2D (_CameraGBufferTexture2, uv);
        UnityStandardData data = UnityStandardDataFromGbuffer(gbuffer0, gbuffer1, gbuffer2);
    
        float3 eyeVec = normalize(wpos-_WorldSpaceCameraPos);
        half oneMinusReflectivity = 1 - SpecularStrength(data.specularColor.rgb);
    
        UnityIndirect ind;
        UNITY_INITIALIZE_OUTPUT(UnityIndirect, ind);
        ind.diffuse = 0;
        ind.specular = 0;
    
        half4 res = UNITY_BRDF_PBS (data.diffuseColor, data.specularColor, oneMinusReflectivity, data.smoothness, data.normalWorld, -eyeVec, light, ind);
    
        return res;
    }
    

    函数基本上也是一目了然,通过UnityDeferredCalculateLightParams计算出光源信息,综合G-Buffer中的场景几何信息,计算最终的颜色。来看看UnityDeferredCalculateLightParams输出了光源的哪些信息:

    // --------------------------------------------------------
    // Common lighting data calculation (direction, attenuation, ...)
    void UnityDeferredCalculateLightParams (
        unity_v2f_deferred i,
        out float3 outWorldPos,
        out float2 outUV,
        out half3 outLightDir,
        out float outAtten,
        out float outFadeDist)
    {
        i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
        float2 uv = i.uv.xy / i.uv.w;
    
        // read depth and reconstruct world position
        float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
        depth = Linear01Depth (depth);
        float4 vpos = float4(i.ray * depth,1);
        float3 wpos = mul (unity_CameraToWorld, vpos).xyz;
    
        float fadeDist = UnityComputeShadowFadeDistance(wpos, vpos.z);
    
        // spot light case
        #if defined (SPOT)
            float3 tolight = _LightPos.xyz - wpos;
            half3 lightDir = normalize (tolight);
    
            float4 uvCookie = mul (unity_WorldToLight, float4(wpos,1));
            // negative bias because http://aras-p.info/blog/2010/01/07/screenspace-vs-mip-mapping/
            float atten = tex2Dbias (_LightTexture0, float4(uvCookie.xy / uvCookie.w, 0, -8)).w;
            atten *= uvCookie.w < 0;
            float att = dot(tolight, tolight) * _LightPos.w;
            atten *= tex2D (_LightTextureB0, att.rr).r;
    
            atten *= UnityDeferredComputeShadow (wpos, fadeDist, uv);
    
        // directional light case
        #elif defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE)
            half3 lightDir = -_LightDir.xyz;
            float atten = 1.0;
    
            atten *= UnityDeferredComputeShadow (wpos, fadeDist, uv);
    
            #if defined (DIRECTIONAL_COOKIE)
            atten *= tex2Dbias (_LightTexture0, float4(mul(unity_WorldToLight, half4(wpos,1)).xy, 0, -8)).w;
            #endif //DIRECTIONAL_COOKIE
    
        // point light case
        #elif defined (POINT) || defined (POINT_COOKIE)
            float3 tolight = wpos - _LightPos.xyz;
            half3 lightDir = -normalize (tolight);
    
            float att = dot(tolight, tolight) * _LightPos.w;
            float atten = tex2D (_LightTextureB0, att.rr).r;
    
            atten *= UnityDeferredComputeShadow (tolight, fadeDist, uv);
    
            #if defined (POINT_COOKIE)
            atten *= texCUBEbias(_LightTexture0, float4(mul(unity_WorldToLight, half4(wpos,1)).xyz, -8)).w;
            #endif //POINT_COOKIE
        #else
            half3 lightDir = 0;
            float atten = 0;
        #endif
    
        outWorldPos = wpos;
        outUV = uv;
        outLightDir = lightDir;
        outAtten = atten;
        outFadeDist = fadeDist;
    }
    

    函数输出了光源覆盖区域的物体世界坐标,用来采样G-Buffer的uv坐标,光源方向,光照的衰减程度,到阴影衰减中心的距离。函数首先计算场景物体的世界坐标,使用UnityComputeShadowFadeDistance求出物体到阴影衰减中心的距离,该函数定义如下:

    float UnityComputeShadowFadeDistance(float3 wpos, float z)
    {
        float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
        return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
    }
    

    通过Frame Debug发现,三种光源(平行光,点光,聚光)下unity_ShadowFadeCenterAndType均为(0,0,0,0),那么这里的fadeDistance就是vpos.z。接下来,函数根据光源类型的不同,分别计算它们的衰减信息。

    对于聚光灯,和前向光照类似,会对_LightTexture0这张spot cookie纹理和_LightTextureB0这张衰减纹理进行采样,得到光照衰减信息(有关内容可以参考之前的文章《Unity中的多光源》[7])。然后使用UnityDeferredComputeShadow从shadowmap中采样阴影,再拿之前得到的阴影fadeDistance,通过UnityComputeShadowFade计算阴影衰减的程度:

    half UnityComputeShadowFade(float fadeDist)
    {
        return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
    }
    

    在之前的文章《Unity中的shadows(三)receive shadows》[8]我们已经提到过:

    _LightShadowData = new Vector4(
        1 - light.shadowStrength,                                                             // x = 1.0 - shadowStrength
        Mathf.Max(camera.farClipPlane / QualitySettings.shadowDistance, 1.0f),                // y = max(cameraFarClip / shadowDistance, 1.0) // but not used in current built-in shader codebase
        5.0f / Mathf.Min(camera.farClipPlane, QualitySettings.shadowDistance),                // z = shadow bias
        -1.0f * (2.0f + camera.fieldOfView / 180.0f * 2.0f)                                    // w = -1.0f * (2.0f + camera.fieldOfView / 180.0f * 2.0f) // fov is regarded as 0 when orthographic.
    );
    

    对于平行光源,默认的光照衰减为1,如果设置了cookie则还需要采样cookie纹理,然后再计算阴影衰减,得到最终结果。对于点光源,也是类似的,也就不展开说了。

    最后,我们通过Frame Debug看一下这三种光源在CPU层面的绘制信息。首先来看聚光灯,发现Unity采用了类似reflections的绘制方式,先使用一个stencil pass来标记位于聚光灯区域内的物体,然后再去跑真正的light pass。这里,Unity使用了一个4个顶点的pyramid来模拟聚光灯:

    而对于点光源,Unity采用了类似reflections的另一种绘制方式,它只有一个light pass,设置了Cull Front和ZTest Greater,使得点光源区域内和前方的物体都会参与到光照计算。这里Unity使用了一个42个顶点的icosphere来模拟点光源:

    平行光是最简单的,因为它覆盖的区域就是整个场景,所以Unity采用了类似skybox的reflections绘制方式,用一个覆盖整个screen的quad来模拟平行光:

    如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路

    Reference

    [1] Deferred Shading

    [2] 延迟渲染为什么不支持MSAA?

    [3] 对多重采样(MSAA)原理的一些疑问?

    [4] Deferred Rendering(一) : 基础篇

    [5] Deferred Shading rendering path

    [6] 阴影渐变衰减UnityComputeShadowFadeDistance与UnityComputeShadowFade

    [7] Unity中的多光源

    [8] Unity中的shadows(三)receive shadows

  • 相关阅读:
    软件设计师2006年11月下午试题6(C++ 状态模式)
    Delphi中使用RegExpr单元进行匹配与替换操作
    正则表达式中贪婪与懒惰匹配
    C++类属性算法equal和mismatch
    lazarus下使用正则表达式
    正则表达式在每行开头插入行号
    STL向量构造函数
    软件设计师2004年5月下午试题6(C++ 数组下标检测)
    演示STL双端队列的push_back和push_front函数
    用正则表达式改小写为大写
  • 原文地址:https://www.cnblogs.com/back-to-the-past/p/15495584.html
Copyright © 2011-2022 走看看