什么是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个角的射线。那么有:

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

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

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

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

\[\textbf{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为:

\[\textbf{VP} =
\begin{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为:

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

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

\[\textbf{M} =
\begin{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 = \textbf{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

Unity的Deferred Shading的更多相关文章

  1. Deferred Shading 延迟着色(翻译)

    原文地址:https://en.wikipedia.org/wiki/Deferred_shading 在3D计算机图形学领域,deferred shading 是一种屏幕空间着色技术.它被称为Def ...

  2. Deferred shading rendering path翻译

    Overview 概述 When using deferred shading, there is no limit on the number of lights that can affect a ...

  3. D3D Deferred Shading

    在3D图形计算中,deferred shading是一个基于屏幕空间的着色技术.之所以被称为deferred shading,是因为我们将场景的光照计算与渲染"deferred"到 ...

  4. 引擎设计跟踪(九.14.3.2) Deferred shading的后续实现和优化

    最近完成了deferred shading和spot light的支持, 并作了一部分优化. 之前forward shading也只支持方向光, 现在也支持了点光源和探照光. 对于forward sh ...

  5. 引擎设计跟踪(九.14.3.1) deferred shading: Depthstencil as GBuffer depth

    问题汇总 1.Light support for Editor编辑器加入了灯光工具, 可以添加和修改灯光. 问题1. light object的用户互交.point light可以把对应的volume ...

  6. 引擎设计跟踪(九.14.3) deferred shading 准备

    目前做的一些准备工作 1.depth prepass for forward shading. 做depth prepass的原因是为了完善渲染流程, 虽然架构上支持多个pass, 但实际上从来没有测 ...

  7. opengl deferred shading

    原文地址:http://www.verydemo.com/demo_c284_i6147.html 一.Deferred shading技术简介 Deferred shading是这样一种技术:将光照 ...

  8. Deferred Shading,延迟渲染(提高渲染效率,减少多余光照计算)【转】

    Deferred Shading,看过<Gems2> 的应该都了解了.最近很火的星际2就是使用了Deferred Shading. 原帖位置:   http://blog.csdn.net ...

  9. Deferred Shading延迟渲染

    Deferred Shading 传统的渲染过程通常为:1)绘制Mesh:2)指定材质:3)处理光照效果:4)输出.传统的过程Mesh越多,光照处理越费时,多光源时就更慢了. 延迟渲染的步骤:1)Pa ...

随机推荐

  1. SQL:1999基本语法

    SQL:1999基本语法 SELECT [DISTINCT] * | 列名称 [AS]别名,........ FROM 表名称1 [别名1][CROSS JOIN表名称2 别名2]| [NATURAL ...

  2. Java比较两个浮点数

    浮点数的基本数据类型不能用==比较,包装数据类型不能用 equals 比较 浮点数的表示 在计算机系统中,浮点数采用 符号+阶码+尾数 进行表示.在Java中,单精度浮点数float类型占32位,它的 ...

  3. ESP8266- AP模式的使用

    打算通过该模式,利用手机APP完成配网 • AP,也就是无线接入点,是一个无线网络的创建者,是网络的中心节点.一般家庭或办公室使用的无线路由器就是一个AP. • STA站点,每一个连接到无线网络中的终 ...

  4. PHP中操作任意精度大小的GMP扩展学习

    对于各类开发语言来说,整数都有一个最大的位数,如果超过位数就无法显示或者操作了.其实,这也是一种精度越界之后产生的精度丢失问题.在我们的 PHP 代码中,最大的整数非常大,我们可以通过 PHP_INT ...

  5. PHP的zlib压缩工具扩展包学习

    总算到了我们压缩相关扩展的最后一篇文章了,最后我们要学习的也是 Linux 下非常常用的一种压缩格式:.gz 的压缩扩展.作为 PHP 的自带扩展,就像 zip 一样,zlib 扩展是随着 PHP 的 ...

  6. PHP统计当前网站的访问人数,访问信息,被多少次访问。

    <?php  header('Content-type:text/html;charset=utf-8'); //统计流量(人数,访问次数,用户IP) //假设用户访问,得到IP地址 $remo ...

  7. 一文彻底掌握Apache Hudi异步Clustering部署

    1. 摘要 在之前的一篇博客中,我们介绍了Clustering(聚簇)的表服务来重新组织数据来提供更好的查询性能,而不用降低摄取速度,并且我们已经知道如何部署同步Clustering,本篇博客中,我们 ...

  8. 鸿蒙内核源码分析(管道文件篇) | 如何降低数据流动成本 | 百篇博客分析OpenHarmony源码 | v70.01

    百篇博客系列篇.本篇为: v70.xx 鸿蒙内核源码分析(管道文件篇) | 如何降低数据流动成本 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...

  9. Go变量与基础数据类型

    一.基础介绍 Go 是静态(编译型)语言,是区别于解释型语言的弱类型语言(静态:类型固定,强类型:不同类型不允许直接运算) 例如 python 就是动态强类型语言 1.Go 的特性: 跨平台的编译型语 ...

  10. vue+element UI 使用select元素动态的从后台获取到

    VUE select元素动态的从后台获取到 <el-form-item label="选择店铺"> <el-select v-model="value& ...