星际2的一些技术特性

原文链接:http://ati.amd.com/developer/SIGGRAPH08/Chapter05-Filion-StarCraftII.pdf

    我一直认为后处理技术将会被越来越多的游戏所重用,星际2的出炉在一定程度上验证了这点,把光照,阴影都移到后处理中去,这给场景渲染带来最大的灵活性,当然也带来了很多难以克服的问题。翻译这篇文章的主要目的是让自己熟悉这些技术,并留下一点笔记,顺便赏析一下星际2的技术魅力:)。(这片文章的英文叙述方式简直让我呕吐,如果按原文思路来翻译,只会让读者也呕吐,原谅我改变了原文叙述顺序,有偏好原文者请参看英文原版)

    最早我们就决定让GPU来更多的负载画面效果计算,而让CPU解放出来,最主要的原因是,在星际2中玩家可以生成和操纵数量巨大的游戏物件,这样就需要有更多的CPU资源用来计算海量AI和管理庞大的系统资源。GPU主要的负载源于像素着色器,而不是顶点着色器,因为我们的引擎使用尽可能少的绘制次数及顶点数量,而且现代GPU架构对于一次渲染上百万的顶点简直就是小菜一碟;相比之下,因为我的游戏使用了延迟着色,像素着色器负担着几乎所有的美术效果。使用延迟着色的优势就在于能使像素重绘率始终保持在一个很低的水平,像素着色器的负载能基本保持恒定不变,而不受场景复杂度的剧烈影响。

    我们的引擎需要处理两种截然不同的游戏场景,一种是普通的战略模式,而另一种则是剧情模式。在战略模式下,相机通常都高高在上,俯视全局,玩家所关注的主要是整体策略,而不是游戏的细枝末梢,绝大部分的游戏物件在屏幕都只是覆盖了少量的像素,所以游戏物件可以只使用很少的顶点数量,便可达到理想的画面效果。而剧情模式则不同,玩家需要从第一人称模式下来观察游戏世界,更加身临其境,能更多的注意到场景的细节,这样我们就需要更加精细的美术资源来增强玩家的体验。从技术上来说,这两种模式有着截然不同的性质,比如战略模式的渲染,需要绘制很多次才能完成一个画面(看到的游戏物件实在太多),但每个单一游戏物件都不要很精细;而剧情模式下,场景所需要的绘制次数很少,但每个游戏物件都需要仔细渲染,因为玩家能在近距离观察到它们。接下来将阐述如何在技术上实现这些要求。

基于屏幕的特效
    星际2图形引擎设计的一个很重要的目标就是要能在剧情模式下体现出复杂的光照环境,现在我们回想一下魔兽争霸3吧,任意时刻一个物体只能受及其有限的灯光影响,这导致了在切换影响物体灯光的时候,物体的光照效果会有明显的突变,因此在魔兽争霸3很少使用动态光源。而且使用传统的3D渲染方法会极大的提高场景的绘制次数,试想一下场景中有一队星舰,每个星舰上都有很多闪烁的灯光,这些灯光会影响星际自身以及其周围的其他舰艇,在这种情况下,星舰不得不一艘一艘的渲染,因为每艘星舰的灯光环境都不一样,使得GPU效率非常低下。因为我们的分块地形是通过复杂的方法将多层地貌混合而成,传统的绘制方式也会给地形系统的渲染带来严重问题。

    而解决以上问题的方法,最好就是延迟着色(deferred  shading),其原理就是先将所有物体先光栅化成像素,然后将每个像素的深度值、法线和一些与光照无关的颜色缓冲起来,等待后续进行像素光照着色。你可能会说使用这种技术的优势还很难说,毕竟也带来很多的负面问题,比如缓冲这么多数据是要消耗很多显存和带宽的,另外还要增加很多次缓冲采样,是的,你说的都没错,但这项技术给我们带来的最大好处就是让图形计算量与场景复杂性从原来的指数关系变为线性关系,也是说不管我们给画面加什么样的后期特效,其消耗都是固定的,与场景复杂度无关,不管场景中有500个还是5个物件,在特效处理中的负担都差不多,因为所有特效都是针对像素而不是物件的。

    在RTS游戏中保持低的绘制次数相当的重要,因此我们希望能在一次绘制中讲尽可能多的信息缓冲到显存中,大多数硬件支持的MRT数量为4,每个RT通道数量为4,这样我们就有16个通道来存储我们需要的信息,星际2种我们是这样使用这些通道的:
1。不受灯光影响的颜色分量,比如自发光、环境贴图和光照贴图(环境光照颜色)。
2。深度信息
3。像素法线
4。如果使用静态环境遮挡,则保存每个像素的环境遮挡值,如果使用屏幕空间的环境遮挡,那么预生成的环境这样贴图就忽略掉了。
5。漫反射材质
6。镜面反射材质
    需要提醒一下的是,DX9下MRTS的尺寸和位深必须都相同,当然一部分硬件支持MRT的独立写功能,我们也会利用这些特性来尽可能避免那些没有用到的BIT来占用带宽。在星际2中我们使用了HDR效果,这样所有缓冲的4个通道必须是16位浮点格式,当然使用高精度格式的数据能很好的避免精度问题,而且象素着色器也用不着对数据进行解码了,但很不幸的是,每个缓冲都必须使用4*16的格式,这样我们每个象素的输出带宽就增加到24字节了(可能作者没有考虑Specular项),但马上我们就要觉得这样的牺牲对于它带来的灵活性是完全值得的。

    一般渲染的物体都是不透明的,延迟着色的最大问题在于透明的处理,后面我们会讲到如何解决这个问题。我们的地形是多层的,各层的法线,漫反射和高光都是混合到缓冲的,但只有地形的最底层渲染时会写深度值。缓冲起来的各种值被用来实现各种各样的特效,比如深度值用在了光照,体积雾,动态环境光遮挡,智能位移,景深,投影器,边缘检测和厚度测量;法线用在动态环境光遮挡计算中;漫反射材质和高光材质用来计算光照。

延迟着色

    星际2中只有局部灯光才使用延迟着色,如具有一定影响范围的点光源和聚光灯,一般全场景光照使用传统的方式先进行渲染,因为全场景光(比如日光)会照射到所有模型上,这样使用延迟着色的话没有太多好处,实际上,因为需要再次对缓冲进行采样,延迟着色的效率在这种情况下反而更低。
    传统着色和延迟着色效果其实都是一样的,只不过延迟着色将光照作为一个后处理特效来进行,这样对于复杂光照环境,延迟着色拥有更高的效率,因为使用延迟着色的话,光照计算他所影响到的象素上进行,而且在SM3.0中能更加高效的取得象素的3D空间坐标,因为PS3.0中新加入一个寄存器,光栅单元用来向里填充本象素在缓冲中的x,y坐标,这样我们能很快的通过象素的Z值求出这个象素的相机空间坐标:
float2 vViewPos.xy = float2( x, y ) * float2( 2.0f, -2.0f ) / float2( w, h ) + float2( -1.0f, 1.0f );
vViewPos.zw = float2( 1.0f, 1.0f );
vViewPos.xyz = vViewPos.xyz * fSampledDepth;
float3 vWorldPos = mul( p_mInvViewTransform, vViewPos ).xyz;
(我觉得原文可能是错的,如果有朋友看出我的错误,请马上指正,部分项与原文对照如下)
INTERPOLANT_VPOS = float2( x, y )
p_vCameraNearSize = 1.0f
p_vRecipRenderTargetSize = float2( w, h )

模板测试,Early-Z和Early-Stencil
    我们需要一个相当高效的方法来找出屏幕上属于某一灯光影响范围的象素,很自然我们想到了Z TEST 和STENCIL TEST,可以用STENCIL TEST将光照范围后面的象素给剔除掉,用Z TEST将 光照范围前面的象素剔除掉。原文并没有详细的说明如何做到这点,我根据自己对DEFERRED SHADING和SHADOW VOLUME的了解,估计他应该是按如下方法做的:
1。首先看下图2-0,用3个不同颜色的矩形表示3个不同的场景物体,黄圈表示灯光照射范围,绿色物体在光源照射范围的前面,不受灯光影响,红色位于灯光照射范围之内,是受影响的,深蓝色的物体位于光照范围之后,也不受影响。
2。首先让所有场景渲染到相关的缓冲中,建立起Z,NORMAL,COLOR等数据。
3。For 所有 Light
     a:关闭Color Write,Z Write. 设置Stencil测试总是成功,设置D3DRS_STENCILZFAIL为D3DSTENCILOP_INCR,设置Z为 D3DCMP_LESS,绘制光照闭合凸体的backface,这样就能在模板中标出所有位于凸体背面之前的象素,如图2-1
b:正式进行光照着色,设置Stencil测试为D3DCMP_EQUAL,REF值为1,Z测试为D3DCMP_LESS,绘制光照闭合凸体的Frontface,此时early stencil和early z就开始发挥作用了,首先early z会将所有位于灯光照射范围之前的所有象素剔除掉,留下的结果如图2-2中黄色线圈中的部分;early stencil则会将后面深蓝色物体的所有象素剔除掉,如图2-3中黄线围成的范围内象素,只留下a步骤中生成在stencil buffer中的红色部分象素范围中的象素,这最后留下的象素才会正式进入象素着色器,进行光照计算。
 
以上阐述的都是相机在光照范围之外时的算法,当相机在某个灯光照射范围内的话,只需要简单绘制包围体的backface就OK了。暴雪放了一张复杂光源的图如下,可以想象下如果使用传统方法渲染,我们的shader和光照范围判断会有多复杂.

SSAO屏幕空间内的环境光遮挡
    这东西很久以前就有论文,只不过最近让CRYSIS发扬光大,一炮走红,其作用就是让场景明暗更柔和些(其实是错误的),给人一种有全局光照的错觉。大概做法是这样的,按特定方法采样屏幕中指定象素周围的指定象素点的深度值,然后用特定方法对采样结果进行估算,得到一个遮挡值,最关键的地方就是如何采样,如何评估采样值。不过先顺便说下,SSAO有着先天的不足,因为采样的点都是可见SURFACE上的,那么就意味着不能正常评估不可见SURFACE产生的遮挡影响,但因为AO表现出来的就是低频特性,所以这个缺陷对最终效果影响不大。
怎样进行采样最好?
    我相信很多人都自己实现过一些AO,但最终效果都不如人意,比如明暗颗粒感太重,即使进行了BLUR也无法满足我们对美的欲望,现在机会来了,让我看看星际大概是怎么做的吧。首先我们必须提一下3D空间中AO MAP的计算,其原理是对物体表面法线正方向的半球空间进行若干光线跟踪,对各跟踪结果进行取权计算,为了在SSAO中得到一个与其近似的效果,我们采用同样方法,先计算出给定pixel的视空间坐标(前面有讲到),然后以此坐标为基点,在周围选择8~32个点进行采样,然后将采样点的相机空间坐标投影回屏幕坐标,对深度缓冲进行采样,最后得到采样点的深度值,再进行后续计算。最大的问题是如何选择采样点,选择得不得法会导致最后的AO图噪点严重,为了尽量避免缺陷,星际2中采用了随机采样方式,先生成一砣随机向量,并存储在纹理中,注意随机向量的个数并不一定要与象素数量相等,在生成AO图时,在象素着色器中对此纹理采样,会得到一个经过插值的随机法向量,我在通过PS 常量寄存器器传入X(8~32)个随机向量,再使用前面得到的向量对这X个向量进行反射,这样我们就得到了X个伪随机向量了,传入的X个向量的模在0.5~1之间,而不是0~1,是为了防止采样点过于集中在测试点附近,而且这X个向量的模受一个可以由美工调节的变量进行缩放,这样美工就可以控制采样范围了。

    可能现在你很多疑问,为什么要从外界传入X个随机向量,而不在PS中产生?为什么X个向量要用一个预生成的随机向量进行反射?为什么这个用来镜象的向量不是象素法线,而要额外随机生成?有很多困惑,折磨着我们,让我们寝食难安,但很遗憾,原文并没有显式的进行解释,而马上我就要对这些奇怪的做法做出非官方的揣测,如有不满尽可指正。

为什么不直接在PS中产生随机向量?
    AO的随机是有2个硬性要求的,第一点,每个象素的采样点必须都是互相随机,互不相同的。因为每个象素点在3D空间中的位置都互不相同,要做到随机,必须对每一个测试点的采样位置都不一样才能最大的减少最后形成的程序化噪声,让颗粒尽可能分散在整个屏幕上,而不要过分集中;第二点,同一象素在不同帧中必须有相同的采样点,这点就很显然了,否则会导致场景没有变化,画面却还明暗闪烁不停。根据以上两点,在PS中随机生成向量来采样是不可能的,或者是实现起来效率低下特别复杂。

为什么X个向量要用一个预生成的随机向量进行反射?
    这个我觉得意图很明显,就是为了满足以上提到的第一个要求,让每个象素的采样点都互相随机,否则每个象素都使用相同的采样模式,又会产生噪声聚集的情况。星际2使用的方法是用一个随机向量(从随机纹理中取出),对其他传人向量进行反射来达到这个目的,很显然反射计算量很小,而且随机效果很好,当然你也可以把传入的向量与随机向量进行加乘减等任意操作,只要能随机化就行。

为什么这个用来镜象的向量不是象素法线,而要额外随机生成?
    这个我就不太有把握了,但其实在采样中,法线是有很大作用的,就是对穿到物体内部的随机采样向量进行反向,因为采样需要在法线正方向半球内进行,采到物体内部是绝对错误的。可能使用法线来打乱每个象素的采样向量,随机度还不够吧。

如何对采样得到的深度值进行评估?
    好了,现在我们知道大概如何进行采样了,接下来再看最后一个关键点,采样评估。按常理,离测试点近的采样点显然对测试点遮挡得更多,实际上遮挡与距离的平方成反比,但为了让评估更具随意性,我们让美工来控制遮挡系数与距离的关系,但无论如何,这个关系有几个不可违背的特性:
1。如果采样点的深度大于测试点的深度,那么这个采样点给予测试点的遮挡为0,因为测试点在采样点的后方了。
2。距离越近,遮挡越多。
3。当距离大到一定程度时,遮挡要降为0。

遮挡函数的曲线,大概就如下图所示,我们可以把这个函数做成一个1维纹理,在PS中使用。

    我们还可以进一步对AO MAP 进行优化,如利用高斯模糊进一步消除颗粒感,但这个高斯模糊需要做些特殊处理,因为不能简单的把黑的白的到处揉擦,AO MAP的明暗是有空间关系的,比如桌子上的暗绝对不能BLUR到旁边站着的人的身上,因为压根他们空间位置前后距离很远,没什么关系,所以我们BLUR的时候会取采样点和目标点的深度很法线测试下,如果距离太远或者两点法线夹角太大那么采样点的权重就要变0,最后高斯模糊总权重要重新计算,以保证能正确的对结果归一化。

    最后还有点头痛的问题需要解决。因为采样向量是在相机空间中定义的,那就意味着当相机走近物体时,向量在屏幕上的投影会变长,一旦采样点延伸到屏幕之外那就完蛋了,因为屏幕之外的深度我们压根就没有。渲染一个比显示更大的区域很显然不能很好的解决问题,比较简单的解决方案是,如果采样到屏幕之外就返回一个巨大的深度值,这样能保证这个采样点不会对测试点产生任何影响,我们可以通过使用纹理采样的BORD模式达到这个目的。

    为了防止当过度靠近物体,破坏SSAO效果,我们必须在SSAO屏幕空间中限制采样范围,如果相机距离物体太近,那么它的SSAO采样点延伸太远,SSAO区域一致性约束被违反,这样让噪声不至于太明显。另外还有一个解决方法就是根据采样区域大小来决定采样数量,但会严重影响帧率,放弃。
(SSAO area consistency constraint is violated确实是我没能想明白到底是怎么回事,我就照字翻了,我的理解是这样的,SSAO  area 应该为影响指定象素AO的空间区域,这个区域在你走近物体时,在3D空间中的体积是基本保持不变的,但如果你走得特别近了,这个区域的投影已经超出屏幕范围,现在就必须violate这个SSAO area的consistency,比如对采样向量进行缩小,让采样点回到屏幕空间中。MGD,还是懂的人来解释下吧)
SSAO性能分析
    SSAO最大的性能瓶颈在于采样,随机的采样会严重破坏了GPU纹理缓冲系统的连续性,而且采样的纹理区域大小也直接影响纹理缓冲的性能,因此我们可以只使用1/4尺寸的深度缓冲来提高性能。如果采样区域变大,那么犄角旮旯里的黑暗部分会变得更加柔和,现在我们就遇到了两难的问题,美术既需要在场景中的旮旯明暗对比强烈,又需要平坦部分拥有大范围采样的柔和效果。为了达到这个要求,我们将SSAO采样点分成2组,一组在小范围内采样,使用变化剧烈的深度-遮挡函数,另一组使用大范围采样,使用比较平坦的深度-遮挡函数,2组采样各自计算遮挡因子,最终我们使用遮挡比较大的那个。

景深效果

    在延迟着色中,每帧都需要产生深度值缓冲和法线缓冲,既然产生了我们就要有效的加以利用,比如在我们的剧情模式下用得特别频繁的景深特效。接下来我们将讨论在实现景深效果过程中所碰到的问题以及如何解决它们.


弥散圆(circle of confusion)
    物体上任何一点反射的光线通过透镜成象,如果接受平面完美处于象平面,理论上来说,这个点的象也应该是一个点,但如果不是,就会发散成一个圆,一个物体上的每个点的象都成了一个圆,当然看起来就模糊了,所谓的景深也就是容许接受平面在象平面前后偏移的最大距离(更精确的解释请参考摄影相关的文章)。我们希望能让美工更好的控制DOF效果,而不是完全基于物理的,所以我们定义了如下参数供美工调节,相距:FocalDepth,景深范围:NoBlurRange,最大模糊范围:MaxBlurRange,现在我们定义个模糊因子,A0 = DofAmount * max( 0, abs(Depth - FocalDepth) - NoBlurRange ), A1 = MaxBlurRange - NoBlurRange, F = A0 /A1, F = 0不模糊 F = 1完全模糊,我们可以看到模糊因子是根据深度线形变化的。一般来说,我们根据F的值来增加高斯模糊核的采样范围来得到更想要的效果,但实际并不理想,完美的做法应该是增加采样点数量,但这个效率和硬件支持度确实不允许,//未完待续


其实在Shawn Hargreaves的PPT中提到过最终的解决方案应该是depth peeling来实现顺序无关的alpha混合,不过据说效率很“惊人”,目前来说也就算了,但不排除N年后GPU更快了再拿出来用哦,原理类似的还有woo shadow mapping,相关知识请到GOOGLE搜论文看看。

一些疑问:
虽然后处理引擎是发展趋势,但也还存在几个比较麻烦的问题需要解决
1。ALPHA的完美解决方案
2。对于特殊光照材质的支持,比如一个人的头发,服装,皮肤肯定是采用不同的光照方法渲染的,头发是各相异性的,服装是漫反射的,皮肤是带SSS的,如何知道每个象素采用哪种着色方式是非常重要的,从画面上开SC2好象没能很好的解决这个问题,比如虫族应该是表面看上去湿辘辘的有黏液,人族应该是粗糙镀层的金属表面居多,神族物件看上应该是闪闪发亮反光率很高的材质,现在所有的物体看上去象橡皮泥捏的一样,特别是战略模式下尤为明显。

3。还有一些杂碎,比如反射,阴影等。

(机器问题,图片贴不上,随后再补)

通过技术分析,得到一个结论,如果效果全开,星际2一定是相当的费机器,想买电脑的朋友,可以等等看

原文地址:https://www.cnblogs.com/effulgent/p/1350332.html