|
|
(4 intermediate revisions by one user not shown) |
Line 1: |
Line 1: |
− | 本文讲述的是[[KlayGE]]的[[例子程序#Deferred Rendering|Deferred Rendering例子]]中使用的延迟渲染方法。
| + | 本文讲述的是KlayGE的Deferred Rendering例子中使用的延迟渲染方法。 |
| | | |
| == Deferred Lighting的框架 == | | == Deferred Lighting的框架 == |
| | | |
− | [[KlayGE]] 3.11的例子已经从Deferred Shading改成了更节省带宽的Deferred Lighting。这里先对Deferred Lighting作一个简要的介绍,并假设读者已经了解了Deferred Shading。
| + | KlayGE 3.11的例子已经从Deferred Shading改成了更节省带宽的Deferred Lighting。这里先对Deferred Lighting作一个简要的介绍,并假设读者已经了解了Deferred Shading。 |
| | | |
| Deferred Lighting的渲染架构可以分为三个阶段: | | Deferred Lighting的渲染架构可以分为三个阶段: |
Line 26: |
Line 26: |
| Lighting pass在Deferred Lighting框架处于核心地位,在这里我打算先把lighting pass解析清楚。一旦lighting pass表达好了,G-Buffer所需要保存的信息,以及shading pass能得到的信息也都清楚了。 | | Lighting pass在Deferred Lighting框架处于核心地位,在这里我打算先把lighting pass解析清楚。一旦lighting pass表达好了,G-Buffer所需要保存的信息,以及shading pass能得到的信息也都清楚了。 |
| | | |
− | [[基于物理的BRDF]]推出了渲染模型总公式:
| + | 我以前的系列文章游戏中基于物理的渲染推出了渲染模型总公式: |
| | | |
| <center><math>L_{o}(\mathbf{v})=\pi\rho(\mathbf{l_c}, \mathbf{v})\otimes \mathbf{c}_{light} (\mathbf{n} \cdot \mathbf{l_c})=(\mathbf{c}_{diff} + \frac {\alpha + 2} {8}(\mathbf{n} \cdot \mathbf{h})^{\alpha} F(\mathbf{c}_{spec}, \mathbf{l_c},\mathbf{h})) \otimes \mathbf{c}_{light} (\mathbf{n} \cdot \mathbf{l_c})</math></center> | | <center><math>L_{o}(\mathbf{v})=\pi\rho(\mathbf{l_c}, \mathbf{v})\otimes \mathbf{c}_{light} (\mathbf{n} \cdot \mathbf{l_c})=(\mathbf{c}_{diff} + \frac {\alpha + 2} {8}(\mathbf{n} \cdot \mathbf{h})^{\alpha} F(\mathbf{c}_{spec}, \mathbf{l_c},\mathbf{h})) \otimes \mathbf{c}_{light} (\mathbf{n} \cdot \mathbf{l_c})</math></center> |
Line 64: |
Line 64: |
| <math>+ ((\mathbf{n} \cdot \mathbf{h_N})^{\alpha} F(\mathbf{c}_{spec}, \mathbf{l_{cN}},\mathbf{h_N})) \otimes \mathbf{c}_{lightN} (\mathbf{n} \cdot \mathbf{l_{cN}}))</math></center> | | <math>+ ((\mathbf{n} \cdot \mathbf{h_N})^{\alpha} F(\mathbf{c}_{spec}, \mathbf{l_{cN}},\mathbf{h_N})) \otimes \mathbf{c}_{lightN} (\mathbf{n} \cdot \mathbf{l_{cN}}))</math></center> |
| | | |
− | 由于'''c<sub>diff</sub>'''是到最后的shading pass才计算,所以在每一个light pass里面,diffuse和specular必须分开才能保证结果正确:
| + | 由于cdiff是到最后的shading pass才计算,所以在每一个light pass里面,diffuse和specular必须分开才能保证结果正确: |
| | | |
| <center><math>Diffuse: \mathbf{c}_{lightn} (\mathbf{n} \cdot \mathbf{l_{cn}})</math></center> | | <center><math>Diffuse: \mathbf{c}_{lightn} (\mathbf{n} \cdot \mathbf{l_{cn}})</math></center> |
| <center><math>Specular: ((\mathbf{n} \cdot \mathbf{h_n})^{\alpha} F(\mathbf{c}_{spec}, \mathbf{l_{cn}},\mathbf{h_n})) \otimes \mathbf{c}_{lightn} (\mathbf{n} \cdot \mathbf{l_{cn}})</math></center> | | <center><math>Specular: ((\mathbf{n} \cdot \mathbf{h_n})^{\alpha} F(\mathbf{c}_{spec}, \mathbf{l_{cn}},\mathbf{h_n})) \otimes \mathbf{c}_{lightn} (\mathbf{n} \cdot \mathbf{l_{cn}})</math></center> |
| | | |
− | 为了把diffuse和specular放入4个通道的buffer中,就只能牺牲specular的颜色,只剩下亮度,同时'''c<sub>spec</sub>'''也简化成一个标量。所以,lighting pass的计算成了:
| + | 为了把diffuse和specular放入4个通道的buffer中,就只能牺牲specular的颜色,只剩下亮度,同时cspec也简化成一个标量。所以,lighting pass的计算成了: |
| | | |
| <center><math>float4(1, 1, 1, (\mathbf{n} \cdot \mathbf{h_n})^{\alpha} F(c_{spec}, \mathbf{l_{cn}},\mathbf{h_n})) \times \mathbf{c}_{lightn} (\mathbf{n} \cdot \mathbf{l_{cn}})</math></center> | | <center><math>float4(1, 1, 1, (\mathbf{n} \cdot \mathbf{h_n})^{\alpha} F(c_{spec}, \mathbf{l_{cn}},\mathbf{h_n})) \times \mathbf{c}_{lightn} (\mathbf{n} \cdot \mathbf{l_{cn}})</math></center> |
− |
| |
− | == G-Buffer的分配 ==
| |
− |
| |
− | 在Deferred框架中,不管是Deferred Shading还是Deferred Lighting,G-Buffer的分配都是非常关键的。前面得出的lighting pass公式如下:
| |
− |
| |
− | <center><math>float4(1, 1, 1, (\mathbf{n} \cdot \mathbf{h_n})^{\alpha} F(c_{spec}, \mathbf{l_{cn}},\mathbf{h_n})) \times \mathbf{c}_{lightn} (\mathbf{n} \cdot \mathbf{l_{cn}})</math></center>
| |
− |
| |
− | 从公式可以看出,在light pass里需要的量有'''n''','''h''',alpha,c<sub>spec</sub>,'''l<sub>c</sub>'''。因为'''h''' = ('''n''' + '''l<sub>c</sub>''') / 2(见[[基于物理的BRDF]]),而'''l<sub>c</sub>''' = normalize('''l''' - '''p''')('''l'''是光源位置,'''p'''是要计算的点位置),所以最终需要G-Buffer提供的量有:'''n''','''p''',alpha和c<sub>spec</sub>。要完整的保存这些量,一共需要8个通道,normal占3个,position占3个,alpha和c<sub>spec</sub>分别占一个。这样对G-Buffer来说消耗太大了,必须要缩减。
| |
− |
| |
− | 显而易见的是,normal是经过归一化的,只需要保存2个分量。[http://aras-p.info/texts/CompactNormalStorage.html http://aras-p.info/texts/CompactNormalStorage.html]比较了多种保存2分量的方法,其中Spheremap transform速度和效果综合起来最佳,Crytek也在用同样的方法,即:
| |
− |
| |
− | float2 encode(float3 normal)
| |
− | {
| |
− | return normalize(normal.xy) * sqrt(normal.z * 0.5 + 0.5);
| |
− | }
| |
− | float3 decode(float2 n)
| |
− | {
| |
− | float3 normal;
| |
− | normal.z = dot(n, n) * 2 - 1;
| |
− | normal.xy = normalize(n) * sqrt(1 - normal.z * normal.z);
| |
− | return normal;
| |
− | }
| |
− |
| |
− | 下一步是position。实际上像素所在的位置已经提供了x和y,需要保存的仅仅是z。position何以很好地从z和像素位置计算出来。这里保存的是view space的z除以far plane。在lighting pass,pixel shader里拿到像素在view space的位置之后,做这样的计算:
| |
− |
| |
− | p = view_dir * ((z * far_plane) / view_dir.z);
| |
− |
| |
− | 其中,view_dir是在vertex shader中计算之后传到pixel shader。对于把光源的几何体直接作为光源几何的情况(如果你不熟悉这个,请见下篇),那么view_dir就是顶点乘上world * view矩阵之后的结果。对于用全屏的四边形作为光源几何的情况,view_dir就是把view frustum在far plane上的四个点乘上inverse(projection)矩阵之后的结果。z * far_plane就还原出了该点在view space的z,然后根据相似三角形的定理很容易就能推出这个还原公式。现在,position成功地压缩到了1个通道。
| |
− |
| |
− | 剩下的就是alpha和c<sub>spec</sub>。如果不需要fresnel,可以直接忽略c<sub>spec</sub>,留到shading pass再做,这里直接存alpha就可以了。否则,就需要把alpha和c<sub>spec</sub>放入同一个通道。我用的方法是,floor(c<sub>spec</sub> * 100)作为整数部分,clamp(alpha, 0, 255) / 256座位小数部分。这样的限制是,alpha取值范围为[0, 256),一般来说够用了。
| |
− |
| |
− | 由此,所有lighting pass需要的信息都被压进4个通道内,G-Buffer只需要1张texture,省去了MRT。
| |
− |
| |
− | == Shading Pass ==
| |
− |
| |
− | shading pass需要把前面所有lighting pass积累出来的光照信息和物体本身的材质信息组合起来,得出最后的着色。物体材质中的c<sub>spec</sub>已经存在G-Buffer,并在lighting pass中计算了,所以shading pass输入的材质有'''c<sub>diff</sub>''','''c<sub>spec</sub>''','''c<sub>emit</sub>''',alpha。别忘了在前面的公式中,specular号需要乘上归一化系数(alpha + 2) / 8。另一方面,在lighting pass的结果里,rgb存的是积累的diffuse,a存的是积累的specular亮度,如果还有计算AO,那么shading所用的公式就是:
| |
− |
| |
− | <center><math>\mathbf{c}_{emit} + (lighting.rgb * \mathbf{c}_{diff} + \frac{\alpha + 2}{8} * lighting.a) * ao</math></center>
| |
− |
| |
− | 如果在G-Buffer和lighting pass因为不考虑fresnel而至保存了alpha,那么shading pass的公式就变成:
| |
− |
| |
− | <center><math>\mathbf{c}_{emit} + (lighting.rgb * \mathbf{c}_{diff} + \frac{\alpha + 2}{8} * \mathbf{c}_{spec} * lighting.a) * ao</math></center>
| |
− |
| |
− | == Light volume ==
| |
− |
| |
− | 在Deferred Rendering中,表示一个光源最简单的方法就是一个全屏的四边形。它能让G-Buffer的每一个pixel都参与计算,在pixel shader中才过滤掉多余的像素。虽然可以保证结果正确,但毕竟多余计算太多,效率不高。这里常用的一个优化就是用一个凸的几何形状来表示光源。该几何 形状覆盖的pixel才计算该光源对它的贡献。显而易见的是,spot light用圆锥,point light用球或者立方体,directional light和ambient light用全屏四边形。下图画了一个spot light的volume:
| |
− |
| |
− | [[File:spot_volume.jpg|400px|thumb|center|Spot light volume]]
| |
− |
| |
− | 这样的几何体类似于古老的shadow volume技术所用的几何体,所以我把它叫做light volume。但由于light volume保证是凸几何体,在渲染上比shadow volume简单不少。
| |
− |
| |
− | === 优化1:视锥检测 ===
| |
− |
| |
− | 有了light volume,就可以把它和视锥做一个相交检测。light volume完全包住了light能覆盖的范围,所以如果一个light volume在视锥之外,这个光源就可以直接忽略。
| |
− |
| |
− | === 优化2:Conditional Rendering ===
| |
− |
| |
− | D3D10及以上的显卡都支持conditional rendering,基本用法是这样的:
| |
− |
| |
− | BeginQuery()
| |
− | Draw object with simple shader
| |
− | EndQuery()
| |
− | ...
| |
− | BeginConditionalRendering()
| |
− | Draw object with real shader
| |
− | EndConditionalRendering()
| |
− |
| |
− | 如果第一个Draw没有产生可见的像素,那么第二个Draw就会被忽略。与Occlusion query不同的是,在这个过程中不需要把query的结果返回CPU,流水线不会被打断,效率更高。用这种方法,就可以直接忽略掉不照亮任何一个pixel的光源。
| |
− |
| |
− | === 优化3:Stencil Buffer ===
| |
− |
| |
− | 和shadow volume一样,这里可以用stencil buffer来标记出光源能找到的像素。实际上,在shadow volume上用的优化也可以照搬过来。比如说,双面stencil是最常用的一个方法,在一个pass内就能同时加减正反两面的stencil。同 样,light volume也存在视点进入volume的问题,需要改变depth function,cull mode和back stencil pass。
| |
− |
| |
− | === 优化4:Shadowing pass ===
| |
− |
| |
− | [[KlayGE]]用shadow map渲染阴影。其生成shadow map的过程和普通方法一样,这里就不累赘了。在使用shadow map的时候有两个选择,以前的方法是在lighting pass里计算光照的时候就查询shadow map,同时计算阴影。另一个方法来自Screen space shadow map。在每个lighting pass之前加一个shadowing pass,仅仅查询shadow map和计算阴影本身(结果是个灰度图)。这样的好处是,shadowing可以在更低的分辨率上计算,而不用和lighting pass用同样的分辨率,提高效率。另外,shadowing pass的结果可以像screen space shadow map那样做一次blur,在让lighting pass使用。
| |
− |
| |
− | == Anti-Alias ==
| |
− |
| |
− | 从Deferred Shading发明的一天起,anti-alias的问题就一直困扰着所有Deferred的方法。虽然很多无良的游戏厂商直接在Deferred Rendering的游戏里不支持AA,但确实AA对提升画面质量很有帮助。
| |
− |
| |
− | === Edge AA ===
| |
− |
| |
− | 在Deferred的框架里,很自然会想到用Edge AA来处理AA。其过程不外乎:
| |
− |
| |
− | # 边缘检测,得到每个像素“像边缘的程度”
| |
− | # 在shader里根据“像边缘的程度”来控制采样坐标
| |
− |
| |
− | 这本身并不是个复杂的过程,尤其是第二步,非常直截了当了,所以这里集中讨论的是如何进行边缘检测。
| |
− |
| |
− | GPU Gems 2的“Deferred Shading in STALKER”一文提供了一种边缘检测的方法,通过把周围像素的法线差和深度差的和来判断边缘,由e_barrier这个参数来定义阈值和比例,而这个参数和分辨率有关。GPU Gems 3的“Deferred Shading in Tabula Rasa”改进了这个过程,只判断法线差和深度差最大和最小的两组。由于只是局部的相对量而已,这样就做到了和分辨率无关的边缘检测。KlayGE目前用的也是这种方法,得到的边缘如下:
| |
− |
| |
− | [[File:curr_edge.jpg|400px|thumb|center|Edge]]
| |
− |
| |
− | 另一个可能用于边缘检测的方法是,前面提到了如何恢复出每个pixel的view space position,每个pixel取得周围4个pixel的位置之后,就可以直接cross得出一个normal,姑且称为screen space normal。如果一个像素是连续的,那么这个normal就会很接近于G-Buffer中保存的normal,否则它们的方向就会差别很大。下图为G- Buffer中的normal:
| |
− |
| |
− | [[File:normal_g_buffer.jpg|400px|thumb|center|Normal in G-Buffer]]
| |
− |
| |
− | 这是screen space计算出的normal:
| |
− |
| |
− | [[File:normal_ss.jpg|400px|thumb|center|Normal in screen space]]
| |
− |
| |
− | 把这两个normal做一次dot,小于某个阈值的就认为是边缘,得到:
| |
− |
| |
− | [[File:new_edge.jpg|400px|thumb|center|Screen space normal based edge]]
| |
− |
| |
− | === 利用硬件MSAA作边缘检测 ===
| |
− |
| |
− | 前面提到的边缘检测结果虽然不错,但其实都是是参数相关的。能否就用硬件的MSAA来做边缘检测呢?在Shader model 3.0以上的GPU,vertex attribute插值的时候可以选择centroid这个modifier。开启了centroid的attribute,会选择覆盖到的sample 中心来插值,而不是像素中心。所以,同一个属性,如果即有centroid又有不带centroid的版本都传给pixel shader,在pixel shader里面判断两者不一致,就表示这个pixel在边缘上。这样的话,边缘的情况就和硬件MSAA完全一致了。但其实MSAA会过渡判断边缘,所有三角形的边缘都会被认出来,即便只是物体内部的。所以谨慎使用。
| |
− |
| |
− | === 能不能就用MSAA? ===
| |
− |
| |
− | 前面讨论了那么多都是基于Edge的AA。在Deferred Lighting框架下,难道就不能直接用MSAA?可以!这也是Deferred Lighting比Deferred Shading优秀的方面之一。Deferred Shading不能直接MSAA的本质原因是在G-Buffer之后,物体几何信息全部抛弃了。相比Deferred Lighting,在shading pass,物体会被再次渲染一遍,这个时候还是有几何信息的,如果在shading pass打开了MSAA,就可以像Forward shading那样利用硬件MSAA了。唯一不同的是,光照来自于lighting pass的texture,而不是从光源计算。就算硬件MSAA,也只是每个pixel执行一次pixel shader,在按照覆盖情况写入sample的,所以在这里视觉上几乎和Forward shading一样。
| |
− |
| |
− | == 展望未来 ==
| |
− |
| |
− | === shading pass再次渲染物体的改进 ===
| |
− |
| |
− | Deferred Lighting最受争议的一点应属在shading pass需要再次渲染几何体了。如果物体很多,尤其是有tessellation和GS的,多渲一遍有可能抵消了lighting pass带来的性能提升。改进的方法之一就是在建立G-Buffer阶段,用类似Deferred Shading的fat G-Buffer。除了原先的一张纹理,还需要一张纹理用来存放diffuse信息。但是lighting pass和原来一样,不涉及diffuse。shading pass就变成画一个全屏四边形,从G-Buffer的第二章纹理读取diffuse,进行着色。甚至emit也这么处理。这种方法介于Deferred Shading和Deferred Lighting之间。
| |
− |
| |
− | === 彩色的specular ===
| |
− |
| |
− | 在前文提到过,为了把lighting pass中的diffuse和specular都塞到4个通道里,就只能舍弃specular的颜色,只保存亮度。如果要RGB三个通道的specular,近似的方法是通过diffuse积累结果的颜色来计算specular的颜色。这是个很粗糙的近似,虽然不是正确的,不过能骗骗眼睛:
| |
− |
| |
− | <center><math>specular = diffuse(\frac{lum_{spec}}{lum_{diff} + \epsilon})</math></center>
| |
− |
| |
− | 其中lum<sub>spec</sub>是累积出来的specular亮度,lum<sub>diff</sub>是用累积出来的diffuse颜色计算出的亮度。epsilon是为了避免lum<sub>diff</sub>为零。
| |
− | 另一种方法是lighting pass用6个通道。但是如果每个通道都是float 16的,也就是96bpp,带宽开销非常大,就不合适了。我的一个想法是把diffuse和specular都转换到YUV空间。这个空间的一个好处是Y 是float 16的,U和V都只要8 bit就可以了。所以可以这么安排MRT:第一张texture格式为G16R16F,保存diffuse和specular的Y;第二张texture 格式为ABGR8,分别保存两者的U和V。这样只有64bpp,但能保存正确的彩色diffuse和specular。由于YUV格式也是可以相加的,这个地方仍可以用原先的lighting pass积累方法。
| |
− |
| |
− | === inferred lighting ===
| |
− |
| |
− | Lighting pass可以借用inferred lighting的核心思想来加速。也就是说,lighting pass不需要全尺寸,只需要在一个比较小的render target上执行即可(比如3/4大小)。G-Buffer仍是全尺寸的,并在G-Buffer生成后作一次边缘检测。Shading pass也是全尺寸的,在采样lighting pass texture的时候,利用边缘检测的结果进行保边缘的插值(一般称为Discontinuity Sensitive Filtering,DSF),得到全尺寸lighting的近似。
| |
− |
| |
− | [[File:dsf.jpg|400px|thumb|center|DSF]]
| |
− |
| |
− | 上图是使用了800×450的lighting直接拉伸到1280×720做shading的结果,关闭DSF,锯齿严重。下图打开了DSF,基本解决了锯齿问题。
| |
− |
| |
− | === Anti-alias ===
| |
− |
| |
− | 前面文章讲了很多AA的方法,但那些都是在空间上做AA,比较适合近处物体。对于远处物体来说,空间上AA得到的收益有限,必须在时间上进行AA。结合上MLAA的威力,应该能有很小的代价实现很接近16xMSAA的结果。
| |
− |
| |
− | === 各向异性BRDF ===
| |
− |
| |
− | Crytek的“CryENGINE 3: Reaching the speed of light”里提到了在Deferred Lighting框架下加入各向异性BRDF的方法。它用了Spherical Gaussian(SG)来近似出NDF(来自于SIGGRAPH Asia 2009的All-Frequency Rendering of Dynamic, Spatially-Varying Reflectance),但这个SG只是per-object的。在G-Buffer阶段,不保存normal,而保存SG展开成lobe的系数。而 BRDF的其他几个项,Fresnel term、Geometry term,都留到shading pass才计算。这种方法的好处是,对lighting pass来说一切都是透明的,它照样可以按原来的方法累积光照,因为Microfacet BRDF中除了NDF,其他都作为公因数提取出去了(Microfacet BRDF的详细讲解可以参见“[[基于物理的BRDF]]”)。实际上,Fresnel term的系数是'''l'''和'''h''',必须在lighting pass做。这里相信Crytek是用了'''n'''和'''v'''来代替,这样不是物理正确的,只有在高光的中心点,dot('''l''', '''h''')才等于dot('''n''', '''v'''),其他地方dot('''n''', '''v''')会更迅速地衰减,到边缘地方就非常明显了。如果不在乎这个,是可以把NDF都用SG来表示,并用统一的方法进行渲染。
| |
− |
| |
− | 保存lobe的G-Buffer是这个样子的:
| |
− |
| |
− | [[File:g_buffer_lobe.jpg|400px|thumb|center|Lobes in G-Buffer]]
| |
− |
| |
− | 各向异性BRDF渲染出来的结果:
| |
− |
| |
− | [[File:aniso_brdf.jpg|400px|thumb|center|Anisotropic BRDF]]
| |
KlayGE 3.11的例子已经从Deferred Shading改成了更节省带宽的Deferred Lighting。这里先对Deferred Lighting作一个简要的介绍,并假设读者已经了解了Deferred Shading。
与Deferred Shading不同的是,shading(也就是和材质相关)的计算仅仅发生在最后一个阶段。所以,G-Buffer中需要保存的信息得到极大地减小,甚至不再需要MRT。
Lighting pass在Deferred Lighting框架处于核心地位,在这里我打算先把lighting pass解析清楚。一旦lighting pass表达好了,G-Buffer所需要保存的信息,以及shading pass能得到的信息也都清楚了。
为了把diffuse和specular放入4个通道的buffer中,就只能牺牲specular的颜色,只剩下亮度,同时cspec也简化成一个标量。所以,lighting pass的计算成了: