Tabula Rasa中的延迟着色技术

Deferred Shading,看过《Gems2》的应该都了解了。无论是Unreal3、Crysis还是星际2,都已经支持或者准备支持这个技术。

不过因为国内这种环境,真正在项目中能用到的可能并不多,不知道这次星际2出来后,情况会不会有所变化。^_^

本文是对Gems2这篇文章的一个补充,小生在做此次外包的时候,由于需要,翻译了这篇文章,不敢独享,遂贴于此,望能抛砖引玉,愿众位前辈不吝赐教。 ^_^

Tabula Rasa中的延迟着色技术

作者:Rusty Koonce NCSoft

翻译:noslopforever(天堂里的死神)

本翻译仅用于学术目的。

这篇文章是对GEMS2里《Deferred Shading in S.T.A.L.K.E.R.》(中文译名《S.T.A.L.K.E.R.中的延期着色》,原作者Oles Shishkovtsov)的一个补充。它是在我们耗时两年时间、为游戏Tabula RasaRichard Garriott担纲的MMORPG)完成的渲染引擎的基础上形成的。GEMS2的这篇文章覆盖了实现一个Deferred Shading引擎的基本原理,而我们将重点放在了基于Deferred Shading引擎的工作中时可能遇到的更高层面的问题、技术和解决方案上。

1 Introduction

在计算机图形学的词典里,Shading表示“对受光物体的渲染”,这个渲染过程包括下面几步:

1, 计算几何多边形(也就是Mesh)。

2, 决定表面材质特性,例如法线、双向反射分布函数(bidirectional reflectance distribution functionBRDF)等等。

3, 计算入射光照。

4, 计算光照对表面的影响,并最终显示。

一般渲染引擎,渲染场景中的物体的时候,是将这四步一次执行完的。延迟着色则将前两步和后两步分开到渲染管道相互独立的两个部分来执行。

我们希望读者在阅读本文前,能先了解一下延迟着色的基本原理。以下的文章都不错,可以读读:Shishkovtsov 2005Policarpo and Fonseca 2005Hargreaves and Harris 2004

在本文中:Forward Shading(前向渲染)是指4个步骤一齐处理的传统着色方法。Effect就是Direct3DD3DX Effect,而TechniqueAnnotationPass,与它们在D3DX中的概念一样。

材质着色(Material Shader)是指用来渲染几何图元的Effect(也就是前两步),光着色则是指用来渲染可见光源的Effect。几何体(Body)用来指代那些需要渲染的物体。

在这里我们忽略了显卡相关的优化或实现,所有的解决方案都是普遍适应于SM2SM3硬件的。我们希望能强调这个技术,而非实现。

2 Some Background

Tabula Rasa中,我们一开始的渲染引擎是基于最初的DX9而完成的传统前向渲染技术的,使用了HLSLD3DX Effect。我们的Effect使用了Pass里的Annotation来描述这个Pass所支持的光照。而在CPU这边,引擎可以算出来每个几何体被那些光源所影响——这个信息连同那些在PassAnnotation里的信息一起,用于设置光源的参数、以及确定每个Pass该调用多少次。

这种前向着色有多种问题:

1, 计算每个几何体受那些光影响耗费了CPU的时间,更坏的是,这是个O(n*m)的操作。

2, Shader经常需要超过一次以上的Pass来渲染光照,渲染n个灯光,对于复杂的Shader,可能需要O(n)次运算。

3, 增加新的光照模型和新的光源类型,可能需要改变所有Effect的源文件。

4, Shader很快就将达到或者超出SM2的指令限制。

MMO里,我们对游戏环境很少会有过于苛求的要求。我们无法控制同屏可见的玩家数量、无法控制同屏会有多少特效和光源。由于传统前向渲染缺乏对环境的控制,且对于光源的复杂度难于估量,因此我们选择了延期着色。这可以让我们的画面更接近于当今顶尖的游戏引擎,并且让光照所耗费的资源独立于场景的几何复杂度。

延期着色提供了下面的好处:

1, 光照所耗费的资源独立于场景复杂度,这样就不用再费尽心机去想着处理那些光源影响几何体了。

2, 不必要再为几何体的受光提供附加的Pass了,这样就节省了Draw Call和状态切换的数量。

3, 在增加新的光源类型和光照模型时,材质的Shader不需要做出任何改变。

4, 材质Shader不产生光照,这样就节省了计算额外的几何体的指令数。

延期着色需要显卡提供MRT的支持,且利用了不断增加的存储器的带宽——这也就意味着我们可能得对玩家所使用的硬件提出更高的要求。因此我们既实现了前向着色,也实现了延期着色。我们优化了前向着色管道,并在此基础上完成了延期着色管道。

有了一个完全基于前向着色的系统作为后盾,我们就可以以更高的硬件标准来完成延期渲染系统了。我们使用了SM2的显卡作为前向着色系统的最低配置,而延期着色系统,则需要支持SM3的显卡。这样就更易于开发一个延期渲染管道,因为我们不必要再顾虑指令数的限制,且能使用动态分支语句。

3前向着色支持

即便是工作在延期着色引擎下,对于半透明物体的渲染依旧需要前向渲染管道的支撑(详见本文第8节)。我们的引擎里保留了对整个前向着色管道的支持,这个管道用来处理半透明物体,以及用于在低端显卡上替代延期着色引擎。

本节讲述了我们是通过什么方法来同时支持前向和延期渲染的。

3.1 受限的特性

我们限制了前向渲染管道的特性,只让它实现延期渲染管道所有特性的一个很小的子集。有些特性因为技术上的原因无法支持,有些是因为工期太紧,但更多的,是为了开发起来方便而被我们丢弃掉了。

我们的前向渲染管道支持球状光源(hemispheric),方向光源和点光源,其中点光源是可选的,其他的所有类型光源都不支持(包括Spot LightBox Light,它们只由延期着色管道来支持)。在延期渲染管道里构建的阴影和其他特性,在前向渲染管道中都不支持。

最后,前向渲染中的Shader是可以做逐顶点光照和逐像素光照的。在延期渲染管道中,所有的光都是逐像素的。

3.2 一个Effect,多个Technique

我们使用了在Effect中使用了不同的Technique来完成前向着色、延迟着色和Shadow Map,以及更多的东西。我们对每个Technique指定了Annotation来标明这个Technique使用了什么样的渲染方式。这就允许我们将所有的Shader代码放到一个统一的Effect文件里,来实现渲染引擎所需的所有Shader(见表19-1)。这包括前向着色中的静态和骨骼模型,延期着色使用到的“材质着色”(Material Shading)的静态和骨骼模型,以及Shadow Map

Effect所能用道德所有的Shader放到一个地方,我们就可以尽可能多地共享一些可以跨越不同渲染技术的代码。当然,我们不会去做一个超长的文件来储存这些代码,而是将这些Shader建立了一个由多个文件组成的Shader库,包含了各个Effect都能用到的共享的顶点和像素代码,以及常用的函数。这减少了Shader代码的复制,使得维护变得容易,减少了Bug,以及增强了各个Shader之间的稳定性(consistency)。

19-1:材质示例

Code View:

// These are defined in a common header, or definitions
// can be passed in to the effect compiler.
#define RM_FORWARD 1
#define RM_DEFERRED 2
#define TM_STATIC 1
#define TM_SKINNED 2
// Various techniques are defined, each using annotations to describe
// the render mode and the transform mode supported by the technique.
technique ExampleForwardStatic
<
 int render_mode = RM_FORWARD;
 int transform_mode = TM_STATIC;
>
{ . . . }
technique ExampleForwardSkinned
<
 int render_mode = RM_FORWARD;
 int transform_mode = TM_SKINNED;
>
{ . . . }
technique ExampleDeferredStatic
<
 int render_mode = RM_DEFERRED;
 int transform_mode = TM_STATIC;
>
{ . . . }
technique ExampleDeferredSkinned
<
 int render_mode = RM_DEFERRED;
 int transform_mode = TM_SKINNED;
>
{ . . . }
  

3.3 光照优先级

我们的前向渲染在对一个集合体使用多个光源时,很容易就需要增加额外的Pass了。增加Pass不仅会产生更多的Draw Call,也会造成更多的状态切换和更多重绘(原文是Overdraw,我感觉这里可能想表达重绘的意思)。我们发现在有很多光源的情况下,我们的前向渲染只绘制一小部分光源,就会比延期渲染慢。因此,为了更好的性能,我们严格限制了前向渲染管道里,对一个集合体受光的最大数量。

延期渲染管道每帧可以处理30个、40个、50个、甚至更多的动态光源,它们消耗的资源与几何体的数量,大小,以及受光程度毫无关系。然而,在前向渲染管道中,当有两个光源影响了一大坨几何体时,瞬间就卡了。由于两个渲染管道存在如此显著的性能差别,使用相同数量的光源几乎是不可能的。

我们为美术和策划提供了对光源优先级的编辑操作,提供了光源用于前向渲染、还是用于延期渲染、还是都用的开关。光源的优先级在两个管道中都有作用——当心能不足的时候,我们可以知道该关哪些光源;在延期渲染中,为了性能、质量设置,光源可能需要依据优先级关掉因它产生的阴影。

地图通常是按照延期渲染管道进行打光的。我们提供了一个很快的Pass来确认光源在前向渲染管道中是否是可接受的。一般地,在前向渲染管道下唯一的一个额外工作是增加Ambient Light的数量,来补偿相对于延期渲染管道少得多的灯光。

4 高级光照特性

下面的这些技术在前向和延期渲染引擎中都有可能实现。在我们的延期渲染管道中,我们支持了所有这些技术。即便我们不用延期渲染,这些技术仍然可以使用(Even though deferred shading is not required, it made implementation much cleaner.)。在延期渲染中,我们将这些特性的实现与材质Shader分离开,这样我们就可以增加新的光照模型和光源类型,而不必要修改材质属性。这就正如我们可以添加新的、独立于光照模型和光源类型材质。

4.1 Bidirectional Lighting各向异性光照

传统的球面光照(hemispheric lighting),正如DX文档里所说的那样,太普通了。这种光照模型使用了两个颜色,一般标记为TopBottom,然后基于表面法线对这两个颜色进行线性插值。标准的球面光照,根据表面法线方向朝正上方和正下方(这也就是为什么叫TopBottom),来对颜色进行插值。在Tabula Rasa中,我们支持了这种传统的球面光照,但我们也为方向光源提供了背部颜色(Back Color)。

在延期渲染中,美术可以很简单地增加多盏方向光源。我们发现他们经常使用一盏与另一盏光源恰巧相反的光源来模拟辐射度。他们很喜欢这种方法的结果,因此一个自然而然的优化就是:将这两个光源统一成一个特殊的方向光源——一个正面颜色和一个背部颜色。这给了他们相同的控制,但少了一半工作量。

对于之后的优化,背部颜色只是一个N·L的运算,或者一个简单的朗伯(Lambertian)光照模型。我们不必要为背部实现Specular,阴影,遮挡,以及更多高级光照技术。这些背部颜色只是对整个场景环境光和辐射度一个简单的近似。我们将正面颜色的N·L存了下来,将它的方向取反以用到背部颜色的计算上。

4.2 Globe Mapping

Globe Map是用来对光照添加颜色的,就像我们生活中的玻璃球(溜溜弹)那样。光线从光源发射出来,穿过玻璃球,然后被玻璃球赋予颜色和遮挡。对于点光源,我们使用一个Cube Map来完成这个功能,而对于聚光灯,我们使用2D纹理。这可以用于高效地模拟彩色玻璃的效果,或者通过一个模板来对光线进行遮挡。我们也为美术提供了旋转和让这些Globe Map动起来的效果。

可能的话,美术可以使用Globe Map来高效模拟Shadow Map,模拟彩色玻璃,迪斯科球(就是一般舞厅里那个旋转的,闪着暧昧和刺眼光芒的球球),以及更多。我们引擎里所有的光源都支持这些。请参考图19-1 19-3

19-1:基本的聚光灯

Tabula Rasa中的延迟着色技术

19-2:简单的Globe Map

Tabula Rasa中的延迟着色技术

19-3:融合了Globe Map的聚光灯。

Tabula Rasa中的延迟着色技术

4.3 Box Lights

Tabula Rasa中,方向光是影响整个场景的全局光,且用于模拟太阳光和月亮光。我们发现,美术有时候想用方向光影响一小块区域,而不是整个个场景。

我们的解决方案是Box Light。这些光也是方向光,但他们只在一个长方体中起作用。在这个长方体中,我们可以支持类如聚光灯那样的衰减,这样,他们的强度就会随着距离边界越近而衰减得越厉害。Box Light也支持Shadow MapGlobe Map,背面颜色,以及所有其他被我们引擎支持的光源特性。

4.4 Shadow Maps

Tabula Rasa中,没有预计算的光照。我们只用到了Shadow Map,而没有使用Stencil ShadowLight Map。美术们可以让任何光源产生阴影(除了球面光照外)。对于Point Light,我们使用了Cube Map来产生Shadow Map,其它的情况下,我们都使用了2D纹理。

Tabula Rasa中的所有Shadow Map都使用了浮点纹理,且使用了抖动采样(Jitter Sampling)来进行柔化。美术可以控制抖动的幅度,以控制软阴影“软”的程度。这个方法允许我们用一个固定的方法,在所有的硬件上实现相同的效果,当然,对于Shadow Map,我们肯定是要使用硬件相关的纹理格式的。硬件相关的纹理格式可以提供诸如更好的精度,更好的硬件过滤。

Global Shadow Maps

很多论文讨论了全局阴影图,或者由一盏方向光的平截台体所产生的单独的阴影图。我们花了两个星期的时间来研究透视阴影图(Perspective shadow Maps[Stamminger and Drettakis 2002]和梯形阴影图(trapezoidal shadow maps[Martin and Tan 2004]。这两个方法最大的问题是最后的结果取决于光源方向和眼睛方向。只要摄像机一变化,阴影的质量就会发生改变,最坏的情况下,变成了标准的正交投影。

Tabula Rasa里面是有白天和夜晚的循环的,太阳和月亮持续的在天上划过。在黄昏和拂晓的时候,光源方向与水平面几乎平行,这就增加了摄像机方向与光源方向平行的几率。这是前述两种方法面对的最糟糕的情况。

由于摄像机和光源方向不断移动,阴影质量变得很难把控,我们最终(end up,这里不知道作者是想说最终,还是说不再)使用一张大的2048 X 2048Shadow Map进行正交投影。这使得最后的结果很统一,而且与光源和摄像机的夹角无关。当然,肯定会有比我们这种方法好得多的方法,例如Cascaded Shadow Map

我们使用了抖动采样来柔滑阴影边缘,我们对光源的位移进行了离散化,因此他总是指向Shadow Map中固定的位置,我们也对光源的方向离散化了,这样Shadow Map计算时的值不需要每帧都发生变化。最终的结果是,我们获得了一个稳定的阴影,无论摄像机如何移动。

请看表19-2

19-2 离散化光源位置以计算Shadow Map投影矩阵。

Code View:

// Assumes a square shadow map and square shadow view volume.
// Compute how "wide" a pixel in the shadow map is in world space.
const float pixelSize = viewSize / shadowMapWidth;
// How much has our light position changed since last frame?
vector3 delta(lightPos - lastLightPos);
// Project the delta onto the basis vectors of the light matrix.
float xProj = dot(delta, lightRight);
float yProj = dot(delta, lightUp);
float zProj = dot(delta, lightDir);
// Quantize the projection to the nearest integral value.
// (How many "pixels" across and up has the light moved?)
const int numStepsX = static_cast<int>(xProj / pixelSize);
const int numStepsY = static_cast<int>(yProj / pixelSize);
// Go ahead and quantize "z" or the light direction.
// This value affects the depth components going into the shadowmap.
// This will stabilize the shadow depth values and minimize
// shadow bias aliasing.
const float zQuantization = 0.5f;
const int numStepsZ = static_cast<int>(zProj / zQuantization);
// Compute the new light position that retains the same subpixel
// location as the last light position.
lightPos = lastLightPos + (pixelSize * numStepsX) * lgtRight +
 (pixelSize * numStepsY) * lgtUp +
 (zQuantization * numStepsZ) * lgtDir;
  

Local Shadow Maps

在我们的引擎中,所有光源都可能产生阴影,而整个地图有上百盏灯。引擎必须提供管理和使用Shadow Map的方法。所有的Shadow Map知道他们需要时才会被创建出来,并且,大多数Shadow Map是静态的,不需要每帧都重新创建。我们为美术提供了控制每个产影灯是使用静态Shadow Map还是动态的Shadow Map。静态的Shadow Map只生成一次,之后就一直使用,而动态的则每帧都会被刷新。

我们同样标定了几何体是静态的还是动态的,也就是运行时是否可动。我们可以根据这个标志来在计算中裁减掉部分几何体。当创建静态Shadow Map时,我们排除了动态几何体部分。这可以防止类如Avatar这样的动态物体产生的动态影“getting ‘baked‘ into a static shadow map”(这句话不是特别明白,可能想表达的意思就是,静态Shadow Map产生时仅考虑静态物体,而不考虑场景中当前的动态物体吧?这不废话么?!拍静态物体的Shadow Map当然不应该考虑动态物体了,要不干嘛弄这一套静态Shadow Map?!)。动态物体如同其它静态物体那样,使用静态Shadow Map来对自己打影。例如,沿着楼梯走的Avatar,将会被楼梯投到他身上的影子所影响。(个人感觉这里作者可能想表述的就是他们把静态物体和动态物体的产影分开了,互相独立,不过业界的应该都是这么做的吧?需要这么特别说明一下么?搞不明白!当然,也可能使我理解错了,欢迎大家批评指正!

这里有很多种自动化和优化的方法。我们并不一开始就生成所有的静态Shadow Map,而是在需要用到的时候再去创建。这就意味着我们并不需要发布这些Shadow Map文件,且减少了Loading时和运行时从磁盘读取数据的数量。为了节省显存和节省纹理创建的开销,我们使用了Shadow Map池。关于这些本文后面会有更多地描述。

动态产影光源是最耗费的,他们需要常时重新生成他们的Shadow Map。如果动态产影光源不移动,或者移动得不那么剧烈,则就有一些方法可以提升一些性能了。最简单的是,除非有动态几何体在光源的影响范围内,否则就不要重新生成这些Shadow Map。另一个选择是将静态模型渲到各自独立的静态Shadow Map上,这样这些Shadow Map就只用创建一次了。每帧都需要将动态物体渲染到独立的动态Shadow Map上,在最后,只需要判断两个Shadow Map中最小的,或者最近的值就可以了。最后的结果就类似于整个场景的所有物体都产生了Shadow Map——其实我们生成的只有动态物体。

4.5 将来的扩展

由于基于延期着色的引擎已经将光照和几何渲染完全分开, 因此我们就可以很方便的修改或增加光照的特性了。事实上,前面说的Box Light,从会议上的提议到最后编辑器里的完整功能,我们只花了三天时间。

HDRBloom,以及其它特效,添加到延期渲染引擎里、与添加到传统渲染引擎的难度相当。延期渲染引擎的架构,使得他更易于扩展。一般的,在延期渲染引擎里增加一个特性,比在前向渲染引擎里增加一个特性显得简单,或者起码不会难太多。限制延期渲染引擎特性的最大问题是能添加到每个象素中的材质属性,可用的显存,以及显存带宽。

5 可读的DepthNormal Buffer的优势

延期着色的一个前提是,需要创建储存深度和法线信息的纹理。这些信息将被用到光照计算中。然而,他们也可以超越光照的范畴,用于计算雾,深度Blur应该是指DOF),体积粒子,以及消除半透明物体穿入不透明物体时的硬边。

5.1 高级水和折射

Tabula Rasa的延期渲染中

你可能感兴趣的:(游戏,生活,配置管理,项目管理,UP)