美文网首页
【GTA V】渲染截帧分析 - 翻译

【GTA V】渲染截帧分析 - 翻译

作者: 离原春草 | 来源:发表于2023-09-13 18:37 被阅读0次

    2013年发布的GTA V,第一天卖出一千一百万份,直接创下七项世界纪录。

    地图加载界面是破坏游戏沉浸感的最大杀手。在GTA V里面,则不存在这样的问题,开放大世界的设定可以让玩家在游戏世界里畅游数小时,横跨数百公里。考虑到流式加载的沉重负担以及PS3的硬件配置,不得不让人震惊。

    这篇文章的分析是基于PC版本进行的,虽然跟PS3有所区别,但是我想基本的技术框架应该是大同小异的。

    Dissecting a Frame

    GTA V采用的是延迟渲染管线,每帧绘制过程中会产生多张HDR buffer贴图,这些贴图如果直接打开,其效果会比较诡异,因此我这边采用了Reinhard算法对其进行了简单的处理,将结果转换为普通的8位/通道的贴图格式

    Environment Cubemap

    分析的第一项内容,也是游戏渲染的第一个模块,是环境贴图。环境贴图是每帧实时渲染的,目的是为画面添加真是的反射效果。这个部分的渲染采用的是前向渲染管线进行的。

    cubemap是如何完成渲染的,可以看成是将相机架设在某个位置,朝着上下左右前后六个方向各进行一次场景渲染,就得到了需要的贴图资源。GTA V中的cubemap每个面的分辨率为128*128,格式为HDR的。

    上面是一张从内部看的动图,简书不支持,需要的话,请移步原文(地步参考链接)

    在这个场景中,cubemap上的每个面的渲染DP数大概是30左右,且渲染采用的都是面片数较少的低模。在这个绘制过程中,还进行了一些场景物体的剔除,只对地表,天空以及一些大型的建筑进行了绘制,而人物角色,以及汽车等则从这个过程中去除,从而尽可能的降低这个步骤的消耗。

    Cubemap to Dual-Paraboloid Map

    之后将cubemap转换成dual-paraboloid map(为什么不直接渲染成paraboloid map呢)

    之所以要转换成dual paraboloid map,一方面是通过这种方式可以降低shader中的贴图数目,另一方面是因为dual paraboloid map在这种情况下其实基本上已经够用了。而且由于反射中心大多位于汽车位置,所以实际上大部分的数据应该是分布在top paraboloid上的。

    此外,使用cubemap有可能会在两个面的衔接处产生问题,尤其是各个面的mipmap是分开处理的时候,问题会更加严重。早期的GPU不支持在面片之间进行模糊过滤,但是dual paraboloid则不存在这个问题,在mipmap的过程中不会产生缝隙。

    实际上,在GTA IV的时候,环境贴图也是使用dual paraboloid map来实现的,只是跟V不同的是,IV是通过顶点shader直接生成paraboloid map而不是通过cubemap经过后处理转换而来。那么问题来了,V为什么还使用了一种倒退的手段先生成cubemap再转换成paraboloid map呢,这个过程不但增加了时间复杂度,同时也增加了对应的临时贴图的消耗,在最终精度上也有所下降,是什么原因让GTA V冒着这些问题的风险这么做呢?难道是cubemap除了用作反射之外,还有其他的用处?

    Culling and Level of Detail

    这个过程是通过compute shader(tessellation?)完成,因此此处不再额外叙述其实现细节。

    根据物体距离相机的远近,由compute shader自动对其进行tessellation,生成不同复杂度的面片模型。

    比如,当超出一定范围之后,小花小草就直接剔除不再进行渲染。所以这个过程会进行两项判断:这个物体是否需要渲染?如果需要渲染,那么应该以哪个LOD网格级别进行?

    由于PS3是没有compute shader的,所以在PS3上面,应该不是通过compute shader实现上述过程的,实际上,是通过SPU或者Cell完成上述计算过程(Cell是2001年由索尼,东芝以及IBM(STI联盟)研发的由一个通用的PowerPC核以及流式coprocessor(用于辅助加强主核CPU处理能力的协同处理器)组成的微处理器架构,一个Cell包含一个PPU+8个SPU(Synergistic Processing Unit)核心;PPU就是传统的CPU核心;而SPU则类似于协同处理器;不过这个架构貌似饱受开发者诟病(因为太过怪异,与主流架构格格不入,接入成本巨高))

    G-Buffer Generation

    GTA V使用的是延迟渲染管线,在first pass中会同时输出5个RT(G-Buffer),用于后续光照处理。

    在G-Buffer生成pass中,只会进行不透明物件的渲染,透明物体与延迟渲染是不兼容的,后续会通过前向渲染完成绘制。

    G-Buffer中的RT格式都是LDR的(每个通道采用8bit存储)

    diffuse map通常来说是用来存储物体本来颜色数据的贴图,本来颜色数据只跟物体材质有关,不受光照等外界环境的影响干扰。但是从上面的贴图中我们可以看到,实际上在GTA V中输出的diffuse map却包含了高光数据。

    normal贴图主要用来存储场景中可见像素的法线数据,alpha通道也被用来进行其他数据的保存,不太确定是干什么的,不过看起来似乎是用来指定靠近相机的植被的01标记。

    Specular map:用于记录用于反射的高光数据,包含多个通道:

    • Red: specular intensity//高光强度
    • Green: glossiness (smoothness)//光滑度
    • Blue: fresnel intensity (usually constant for all the pixels belonging to the same material)//菲涅尔强度,对于同一材质,通常是一个常量

    Irradiance map的红色通道用于存储每个像素从阳光处接收到的Irradiance值,这个数值是由像素的法线,位置以及光照方向计算得到的;绿色通道不是很确定是干啥的,不过目测可能是第二光源的irradiance值。蓝色通道是每个像素的自发光属性,alpha通道对于大部分像素而言都是闲置未使用状态,只是对于角色和植被而言,可能会被用来标记相关材质(次表面散射?)

    最后一个RT是用来存储深度stencil buffer的。

    • Depth map: it stores the distance of each pixel from the camera.
      Intuitively you would expect far pixels to be white (depth of 1) and closer pixels to be darker. This is not the case here: GTA V seems to be using a logarithmic Z-buffer, reversing Z. Why do this? Due to the way they are encoded, floating point numbers have much more precision when closer to 0. Here, reversing Z leads to more precision when storing the depth of very distant objects, hence greatly reduces Z-fighting. Given the long draw distance of the game such trick was necessary. It’s nothing new though, Just Cause 2 for example was using a similar technique.

    • Stencil: it is used to identify the different meshes drawn, assigning the same ID to all the pixels of a certain category of meshes. For example, some stencil values are:
      0x89: the character controlled by the player
      0x82: the vehicle driven by the player
      0x01: NPCs
      0x02: vehicles like cars, bikes…
      0x03: vegetation and foliage
      0x07: sky

    这些buffer的DP数大概为1900。需要注意的是,整个场景是按照从前到后的顺序进行的,这个渲染顺序有利于通过early-z test来减少无效像素的渲染消耗。

    我们再来看下diffuse map的alpha通道数据。将之放大之后我们看到,部分像素是缺失的,尤其是树木的渲染,这个表现尤为明显,而且缺失的方式是非常有规律的,基本上是按照棋盘格的形式来进行的,是不是PS3的一个优化呢?

    dp2 r1.y, v0.xyxx, l(0.5, 0.5, 0.0, 0.0)  // Dot product of the pixel's (x,y) with (0.5, 0.5)
    frc r1.y, r1.y                            // Keeps only the fractional part: always 0.0 or 0.5
    lt r1.y, r1.y, l(0.5)  
    

    从代码来看,差不多就是每间隔一个像素,进行一次PS渲染。而至于到底哪个像素会被渲染,则是根据diffuse map的alpha通道来指定。

    为什么要这么做呢,是为了降低fillrate(填充率)吗?应该不是,为什么呢,因为GPU并没有这种粒度,实际上像素是按照2x2的粒度进行渲染的,而不是单个像素单个像素进行的(?真的吗。。。为啥,是因为并行计算的原因吗),所以不是因为性能表现的原因,实际上是因为LOD过渡导致的:通过这种dithering,可以使得不同LOD之间的过渡有一个缓冲过程,不会显得那么生硬。(嗯,这种技术在很多游戏中都有使用)

    这个技术有自己独特的名字: alpha stippling.

    Shadows

    GTA V的阴影渲染采用的是CSM技术。整个CSM大概需要1000个DP完成。普通阴影数据存储在Shadow Map的红色通道中,而由云朵上的阴影则存储在红色与绿色通道中。shadow map的采样并不是连续的,而是遵循一个dither模式,这是为了使得阴影的边缘看起来更平滑,而特意增加的处理。

    而dither导致的不连贯是通过一个模糊操作消除的:阳光产生的阴影以及云层的阴影通过一个depth-aware模糊(很好奇,这个模糊操作怎么做到depth aware的,通过这种方式,感觉都不需要PCF/VSM产生软阴影了)操作合入到一个buffer中,最终数据存储在specular map的alpha通道中。

    关于模糊操作:整个模糊过程的计算消耗比较高,因为需要对多张贴图进行多次采样与计算,而为了减轻消耗,会在进行模糊之前进行一个预计算,将那些不需要进一步模糊处理的像素直接输出结果。预计算过程会输出一张贴图:先将shadow map下采样到1/8分辨率,之后通过像素shader调用四次Gather()操作进行模糊处理。通过这张贴图,可以粗糙估计出哪些像素是完全处于光照之下的(模糊结果输出为1的,那么说明周边像素都是处于光照之中,那么就是完全处于光照中了)。在进行全分辨率shadow map模糊处理的时候,会对这张预计算贴图进行读取,当读取的像素值完全处于光照之中,那么就跳过后续繁重的模糊处理,从而达到减轻消耗的目的。

    Planar Reflection Map

    关于反射的具体实现细节将在第二部分详细介绍,这个地方会主要介绍一下为海洋水面生成反射贴图的实现过程。

    简单来说,在这个过程中,整个场景(实际上是需要被海面倒映的物体,大概是650个DP)会被渲染到一张240x120分辨率的贴图中(目测应该只需要渲染一个paraboloid map就可以了)

    SSAO Noisy

    在进行场景渲染的时候(应该是生成G-Buffer的过程中)会生成一张线性版本的深度贴图(通常透视相机的深度buffer是非线性的,是一个倒数关系,具体可以参考透视投影矩阵的推导)

    SSAO Blurred

    拿到线性深度buffer之后,先添加一些噪声,之后对添加了噪声的贴图分别进行水平与垂直方向上的depth-aware(又是这个!!猜测这个所谓的depth-aware指的是在模糊的时候对深度进行检测,对于深度偏差过大的,认为不处于同一个平面,从而不将其加入模糊数据列表?)模糊处理以获得光滑的结果。

    为了降低消耗,所有的计算都是在原有深度buffer分辨率一半的贴图上完成的(怎么一半,要么就是1/2 * 1/2 = 1/4吧?)

    G-Buffer Combination

    最后进入到最终的多buffer混合输出最终结果的过程。在这个过程中,会通过一个像素shader将上面各个过程的输出buffer拿过来作为输入,经过一定的处理过程,将输出结果以HDR的格式输出。

    对于那些包含了局部光源的夜景渲染,这些局部光源的相关数据也会在这个过程中一个接一个的添加到场景的输出结果中。

    Subsurface Scattering

    SSS Before SSS After

    主角的着色表现在进行SSS之前看起来有点不上档次,面部有一块非常黑的区域,看起来就像是服装店的塑料模型。

    这里使用了次表面散射技术来提升其表现。可以看到使用了SSS之后,主角的耳朵以及嘴唇染上了一层红色,现实世界中,这是光照在皮肤内部发生折射后散射出来的表现。

    如何做到只对皮肤进行SSS处理呢,实际上我们可以通过Stencil贴图判定主角像素,但是这种方法是无法区分皮肤与衣服的。

    实际上,在对G-Buffer数据进行混合处理的过程中,除了输出颜色数据之外,还会输出一些数据到alpha通道。具体来说,之前我们说过,irradiance map以及specular map的alpha通道被用来存储一个两个状态的mask:皮肤以及一些植被(如树叶)被设置为1,而其他像素的这个值被设置为0.通过这个值可以判定是否需要进行SSS处理。(因此通过Stencil与SSS标记,可以判定哪些像素是皮肤像素,需要进行皮肤的SSS处理,哪些是树叶,需要进行树叶的SSS处理)

    对于SSS处理来说,这是一个高消耗的操作,但是能够应用SSS的像素比例其实是非常低的,我们可能会觉得这种操作不太划算,但是实际上在这个游戏中,玩家大部分时间大部分注意力都是落在主角的脸上,在这样的重点区域的轻微提升都可能会给玩家一个显著的观感提升。
    在GTA V中,SSS效果不只是被用在主角上,实际上在NPC上也会有SSS效果。

    Water

    GTA V中的水面既添加了反射效果,也添加了折射效果。

    之前生成的对数深度buffer(什么时候生成的?)被用来生成一个半分辨率的线性深度buffer(干啥用的)

    海面与水池也是在MRT模式下,逐个进行绘制,并将相关的数据一次性输出到多个RT中(这个过程应该是与之前的G-Buffer生成过程是相互独立的,透明物体的渲染,一般是在不透明物体渲染之后进行)。

    • 水面的Diffuse map,用于存储水面的原色
    • 水面不透明map中,红色通道用来存储水面的不透明属性(比如说海水的0.102,水池中水面是0.129)。绿色通道用来存储此像素所对应的水面的深度(深度越大,此像素的不透明程度越高,输出像素的颜色也就越接近于diffuse map的颜色,反射程度越高;而深度越低,此像素的颜色越透明,折射的程度越高)。注意,这里的所有水池都是不经裁剪的完全渲染,不会因为被其他物体遮挡而被裁除,在红色通道中将可以看到所有水面像素的数据,但是绿色通道中则只存储了可见水面像素的数据。

    下面看一下怎么根据上述两张贴图生成水面的折射贴图数据:

    在计算折射贴图的时候,水池会被水面填充(diffuse map数据?),水面深度越大,此像素的模糊程度也越高;此外,由于水面折射而导致的焦散效果也会添加到折射贴图中。

    折射跟反射贴图都计算完成之后,就进入了最终的水面绘制阶段:在这个阶段中,水面的网格数据将被再次绘制,之后会在像素shader中将折射贴图与反射贴图传入,并根据需要添加一些调整水面法线的干扰数据以使得水面表现更真实。

    Atmosphere

    light shaft贴图也被称作体积雾,这个贴图的作用是对未被阳光直接照射的大气或者说雾气进行减暗处理。

    这张贴图只需要普通场景一半分辨率就够用了,通过对每个像素使用ray-marching来与光照的shadow map进行比较判定是否处于阴影之中而生成。通常得到的贴图是一个噪点比较多的版本,之后会需要对此版本进行模糊处理。

    得到light shaft贴图之后,下一步就是为场景添加光照穿透雾气的效果:这个处理可以将低面片建筑的细节缺失的瑕疵掩盖起来。

    这个渲染过程会读取light-shaft贴图以及深度贴图数据以产生雾气信息。

    之后进行天空与云朵的渲染。

    整个天空是通过一个DP完成渲染的:天空的网格数据是一个覆盖住整个场景的巨大穹顶。这个过程中的输入贴图是一些类似于Perlin噪声的贴图。

    云朵的渲染过程跟天空类似,也是一个巨大的网格数据,不过其形状就不再是穹顶了,而是一个圆环,沿着地平线方向进行渲染。在这个过程中会需要用到一张密度贴图以及一张法线贴图:这两张贴图的分辨率为2048x512,且这两张贴图收尾衔接之后的表现是无缝的。

    Transparent Objects

    在不透明物体通过延迟管线渲染完成之后,会使用前向渲染管线对透明或者半透物体进行渲染。在这个场景中,这个过程大概需要11个DP完成,对于大量的特效粒子数据,通常都会使用合批的方案降低DP。

    Dithering Smoothing

    还记得前面提到过的用于物体LOD处理的alpha stippling导致的diffuse map上的棋盘格纹样吧,在这里会对这个效果进行平滑处理。

    这个平滑过程是在后处理中进行的:在后处理中,通过像素shader将原有颜色buffer以及diffuse map读入(alpha通道用作dither标记),对于每个像素,最多采集器周边两个相邻像素的数据进行平滑处理,将结果输出。

    这种方法的实现效率比较高,因为消耗是常量,不随物体面片数而变化(既然需要在最后进行平滑,为什么不在最开始生成color map的时候直接填满呢?这是因为,前面填满的话,由于overdraw的存在,每个像素填充的平均消耗会比较高,而放到最后进行填充,其消耗就是跟屏幕分辨率挂钩的了)

    不过需要注意的是,这种平滑处理方法是有瑕疵的,不管是PC版本还是PS3版本,都能够在某些位置看到漏掉的残留的棋盘格纹样

    Tone Mapping and Bloom

    前面说到,经过G-Buffer混合处理阶段之后,输出的结果就是HDR格式的,每个通道都是用16位的数据存储(之前中间阶段的数据采用8位存储,到了最后才替换成16位有用吗?有用,因为之前没有融入光照影响,没有必要采用HDR格式),此时的贴图数据就包含了较广的光照亮度范围,不过由于显示器的LDR能力,所以还需要对这个输出结果进行一次映射。

    映射的过程就是tone mapping,tone mapping有很多中算法,我这边用的是最广为传播的Reinhard算法,不过游戏中是否用的也是这个算法还需要进行进一步的检验

    // Suppose r0 is the HDR color, r1.xyzw is (A, B, C, D) and r2.yz is (E, F)
    mul r3.xy, r1.wwww, r2.yzyy            // (DE, DF)
    mul r0.w, r1.y, r1.z                   //  BC
    [...]
    div r1.w, r2.y, r2.z                   // E/F
    [...]
    mad r2.xyz, r1.xxxx, r0.xyzx, r0.wwww  // Ax+BC
    mad r2.xyz, r0.xyzx, r2.xyzx, r3.xxxx  // x(Ax+BC)+DE
    mad r3.xzw, r1.xxxx, r0.xxyz, r1.yyyy  // Ax+B
    mad r0.xyz, r0.xyzx, r3.xzwx, r3.yyyy  // x(Ax+B)+ DF
    div r0.xyz, r2.xyzx, r0.xyzx           // (x(Ax+BC)+DE) / (x(Ax+B)+DF)
    add r0.xyz, -r1.wwww, r0.xyzx          // (x(Ax+BC)+DE) / (x(Ax+B)+DF) - (E/F)
    
    

    从代码中看到,GTA V用的不是Reinhard算法,而是John Hable在顽皮狗的《神海2》中提出的filmic tone mapping算法。这个算法是Duiker 2006年在EA工作时发明的

    这个算法的具体实施过程给出如下:
    1.将HDR buffer下采样到1/4分辨率
    2.使用compute shader来计算这个buffer的平均亮度,并输出到一张1x1的贴图中
    3.根据平均亮度,计算整个场景的新的曝光度(用于控制场景的明暗变化)
    4.对整个buffer进行加亮处理:在这个过程中,会用一个像素shader对整个buffer中超出某个设定亮度阈值(这个阈值与buffer的曝光度数值有关)的像素进行抽取,之后这些提取出来的像素将会用来进行bloom等炫光处理。
    5.Bloom处理:将之前提取出来的亮度buffer重复进行多次下采样,直到1/16分辨率,之后上采样到原分辨率的一半,通过这种方式来实现光源亮光的模糊处理。
    6.将bloom后的结果添加到原HDR buffer中,并应用tone mapping转换到LDR空间,在这个时候同步进行一次gamma矫正(encoding gamma),将数据转换到SRGB空间,用于显示器输出。

    最终的输出结果的表现取决于我们之前设定的新的曝光度,下图展示了低中高三种曝光度情况下的表现:

    在实际渲染的时候,曝光度是逐帧渐变的,不会出现曝光度突变的情况。
    之所以会有这种表现,是为了模拟人眼的自适应过程:从一个阴暗环境突然进入到一个户外高光环境中,会在最开始几帧中进入一个高亮状态,之后会逐帧逐帧降低曝光度,呈现出一种从“亮瞎”到“正常”光照的状态。GTA V除了会有从亮到暗的渐变之外,还提供了一种从暗到亮的渐变过程,且从暗到亮的渐变过程持续时间要比从亮到暗的持续时间要短很多,这也符合人眼在暗部环境中适应速度快于明亮环境的适应速度的特性。

    Anti-Aliasing and Lens Distortion

    如果GTA V的抗锯齿用的是FXAA方法,那么应该就是在现在这个时刻开始对锯齿边缘进行检测与平滑处理了。

    之后,为了模拟现实世界中的相机,会使用一个覆盖范围较小的像素shader生成一个lens-distortion效果。这个处理过程不但会对图像进行形变处理,同事也会在边缘位置引入一个较小的色散(不同通道的颜色偏移度不同,红色偏移度高过绿色与蓝色)

    UI

    左下角位置的小地图是通过scissor test来实现裁剪的,而其内容则是通过网格绘制而来,因此可以在任意缩放情况下都能保留相关细节。

    到此为止,所有的内容就绘制完成了,整个过程一共花了4155个DP,用到了1113张贴图以及80个RT。

    Level of Detail

    要说R星的有那一项引擎技术是极具竞争力的话,那么一定是其LOD了。这个游戏里包含了不同细节/面片数的各个不同版本的Los Santos数据,所有的东西都是在运行时流式管理的,在里面找不到一个需要通过加载界面来跨越的区域,给了玩家极高的沉浸感。

    Lights

    R星的一个合伙人曾经说过,玩家在游戏场景里的任何一处亮光都是真实存在的光源,只要走近就能对此进行验证。

    Light Bulbs Before Light Bulbs After

    经过验证,这个结论可以认为是基本真实的:玩家能够看见的每个小光源都是使用32x32分辨率的贴图绘制的quad。

    这些面片都会走合批渲染的流程,不过即使有合批,对于GPU来说,成千上万的面片数依然是一个不小的压力。

    Light Bulbs - Wireframe Light Bulbs - Depth Test Pass/Fail

    而且,这些灯光面片可能不全是静态的,比如车灯可能会随着车辆的移动而移动,其位置数据是每帧更新的。当然,如果车辆距离相机较远的时候,就不需要对车辆模型进行渲染,而只需要绘制车灯即可。不过当相机靠近这些车灯的时候,由于LOD层次变化,车辆的模型就要从不可见状态变成可见状态。

    Low-Poly Meshes

    我们再次进入到我们之前分析的那个场景,可以看到,在场景中,一些体积巨大的物体模型可能只需要通过一个DP就能完成绘制,比如下下图中的山坡网格:

    这座单DP的山坡到底对应的是哪座山呢?

    实际上我们知道,这座vinewood山是一座覆盖数平方公里且其上有着众多建筑的大山。。

    山上的这些建筑实际渲染起来,可能需要不少的DP跟面片才能完成。

    但是由于这座山太远了,所以可以直接使用一个低模来替代:使用一个2500面的DP就能完成。

    在这个DP中,会需要用到一张贴图来表示山体的一些细节。虽然现在有一些工具可以将3D模型自动转换为一个低消耗版本,但是这个过程并不是全自动的,如果真要采用这种方式,不难想象,R星的美术同学可能会花费大量的时间用于对3D模型进行精修,以期望导出符合需求的低模。

    另一个LOD样本是城市中的Little Seoul区,这个区覆盖了多条街巷,不过在低模情况下,同样也只需要一个DP就可以搞定。

    如果没有LOD分级,场景内的所有物体都是高模版本,在渲染环境贴图的时候就会有一个非常高的消耗

    Asset Streaming

    为类似于GTA的游戏创建多个不同LOD的场景版本是一个非常耗时的工作,即使顺利完成这项工作,也并不意味着资源问题就已经全部解决了,实际上这可能只相当于走到了半山腰,后面还需要将这些几十G的模型与贴图数据加载到内存以及GPU中。

    GTA V的流式加载是实时完成的,当玩家从一个区域移动到另一个区域的时候,就会触发流式加载与卸载操作。让人震撼的是,这个实时的流式加载竟然可以保证数小时的稳定运行,非常了不起。

    当然,这种流式加载策略有着一定的局限性:比如说当玩家在游戏中切换操控角色的时候,相机就会从一个区域切换到另外一个区域,在这种情况下,流式加载系统就会出现过载情况,因此会需要一定的时间来处理这个突然的切换过程,这个表现是可以理解的。不过,GTA V通过另一种方式缓解了这个问题,他们是怎么做的呢:在发生这样的突然切换的事件时,就会触发一个缩放/转移动画,通过动画掩盖流式加载的时间消耗,这样既保证了流式加载的顺利运行,也不会对玩家的体验造成打断。

    当玩家在游戏中驾驶车辆移动时,其移动的速度实际上是非常慢的,这样才能保证流式加载系统能够顺利的保证玩家视野里的数据不会出现突然出现这样的不连贯的体验。不过对于飞机来说,实现逻辑就不一样了:飞机飞行的过程要远远快过车辆行驶,因此正常来说流式加载是不可能跟得上节奏的,因此在实现过程中,飞机飞行的速度相对于现实中来说,是严重受限制的,而且在飞行的过程中,视野中的物体都是按照低模来绘制的,这些做法都是为了降低流式加载的压力。即使采取了上述措施,在实际运行中依然还是可以看到物体突然出现的瑕疵。

    Reflections

    之前的场景,水面占据像素较少,这里特地找了个水面像素比较多的场景来继续反射技术的阐述。

    在进行水面渲染之前,先绘制一张环境贴图cubemap(通常来说,每个需要反添加反射效果的物体都应该为之绘制一张环境贴图,实际上,如果多个物体之间距离较近,其实也可以共用一张贴图吧)。

    环境贴图中,没有将角色以及一些细小的物体渲染进去(那为什么上面的水面效果上有角色的反射呢,这个后面会讲)。

    水面的绘制是另一个问题,如果不考虑反射,在水面的绘制中是不需要环境贴图的。

    Reflection Map

    水面绘制的第一步,是为水面生成一张镜面反射贴图(低分辨率,实际上就相当于将水面以上的物体upside down之后进行一遍绘制),这张镜面反射贴图的生成过程跟cubemap类似(就是将相机朝着水面法线方向,将水面之上的物体绘制一遍),在这张贴图里,是需要包含角色以及梯子等细小物件的(这就是为什么环境贴图里没有角色数据,但是水面渲染效果中还是有角色的倒影的原因)

    在后期渲染采样的时候,需要考虑到水面反射的对称特性,否则效果看起来会有问题。

    Refraction Map

    Base Refraction Map

    要实现水面折射效果,需要从折射贴图中抽取出一组贴图(为什么?):以水面作为视窗,创建一张折射贴图,目的是模拟水面的折射效果。

    在这个时候,会给不同的水体添加不同的不透明颜色(前面说到的淡水与海水的颜色不同,水面越深,此颜色的不透明程度越高),同时也会为水面添加焦散特效。最终的输出贴图是原始贴图的1/4尺寸。

    Combination

    在最终的组合阶段,会通过绘制一个刚好覆盖水面形状的多边形,之后在其上进行像素shader处理,在这个shader中,会对像素的法线进行扰动处理以伪造水面在波动的效果,这个扰动的实现需要一张凹凸贴图。

    对于海面的实现会更复杂一点,除了需要对像素的法线进行扰动处理之外,还需要对海面网格顶点数据进行动态更新,从而模拟海面波浪效果。

    在像素shader中拿到各个像素的法线数据后,可以根据菲涅尔公式对反射贴图以及折射贴图进行采样。

    Mirrors

    镜面的渲染跟水面的渲染其实差不多,实际上会更简单,因为镜面只有反射,没有折射。

    跟水面反射不一样的是,镜面是平整的静态的,虽然计算过程更为简洁,但是同时低分辨率的反射贴图则变得更容易穿帮,因此想要得到较好的镜面反射表现,就需要使用一张较高分辨率的反射贴图。

    也正是由于镜面的清晰可见,常用的造假手段如低模LOD以及细节剔除等就不再能取得令人信服的表现,也正是因为这个,所以镜面的渲染消耗通常都会比较高(不作假的将场景重新绘制一遍)。所以,为了节省这个开销,通常的做法就是能不绘制就不绘制:如果镜面不在视线内或者距离相机较远,那么就可以直接跳过镜面渲染。

    Spotlights

    在前面我们说到,在生成环境贴图cubemap的时候,只对大块的物件进行了绘制,而为了提高性能,角色与车辆等细节数据是直接被剔除的。

    如果使用这样的环境贴图的话,那么在雨天的时候,怎么能够做到将车灯的灯光反射在潮湿的路面上呢?

    在之前一篇文章中讲到反射时使用的环境贴图的时候,这个问题还不明显,因为当时是白天。不过实际上,在将G-Buffer数据处理融合并输出之后,就会立即对场景中的光源逐个进行光照叠加处理(对那些受光源影响的物体的像素叠加上对应的光照颜色效果),也就是在这个时候,会计算光源在一些反光物体上的投影(比如说刚才说到的,潮湿路面对于光源的倒影)

    Lights 0% Lights 50% Lights 80% Lights 100%

    在这个过程中,对于每个光源,会生成一个网格模型(这个模型的生成过程有点类似于八面体的tessellation过程,不过顶点shader会根据光源在物体上投影的形状对这个生成的网格模型的形状进行修正),并使用这个模型绘制时的像素shader来生成对应的光源投影。

    Before After

    这个网格模型是不需要使用贴图数据的,其作用只是为了得到这个模型中的各个像素,从而能够在像素shader中根据像素的深度,到光源的距离,像素法线,像素高光/粗糙度属性等动态的计算光照表现。

    从这个过程可以看到延迟渲染相较于前向渲染管线的一个优势:在存在数目较多的光源的场景中,每个光源的光照处理只需要计算那些受到光源影响的像素,避免了无效像素的繁重冗杂的消耗。

    Post Processing Effects

    Lens Flares & Light Streaks

    在现实世界中,由于镜头的散射效果以及光束在镜头内部的反射等原因,实际上拍摄出来的数据会存在一些与人眼看到的现实场景表现不同的异常效果。

    lens flares(镜头光斑)指的是沿着光源与屏幕中心的这条光轴分布的若干个亮斑的几何。light streaks(光束条纹)指的是从光源射出的光束效果。这些效果通常在照片或者电影里非常常见,当将这些效果引入游戏中后就会给人一种大片般的感觉。

    这两种效果的实现方式通常有以下两种:
    1,基于贴图的方法:先将场景buffer中明亮的区域提取出来(光源),之后对这些提取的内容按照一定的分布规律进行复制与形变处理。这种方法能够处理任意数量的光源。
    2.基于面片的:在场景buffer之上添加带贴图的面片的渲染效果,并根据需要人为调整这些面片的输出位置。各个光源的处理过程是相互独立的,意味着大量的重复工作。这种方法实现的优点是美术同学对于这些效果的表现有更强的控制权。

    在GTA V里面,上述的两种方案都有被用到:基于贴图的方法主要用于在屏幕的左下角位置添加一个不明显的蓝色光晕,这个蓝色光晕的结果buffer其实跟上一篇文章中HDR的高光部分提取实现的结果buffer是对称的;上面这个示意图中的镜头特效主要还是通过基于面片的方法得到的:这个方法只用于处理阳光导致的镜头特效(其他光源的镜头特效就直接略过)。其实现步骤大概给出如下:
    1.通过在光源(也就是太阳)周边渲染12个以太阳为中心旋转而来的quad以实现light streaks(光束条纹)
    2.沿着太阳-屏幕中心这条轴线,绘制70个面片用于生成lens flare(镜头光斑)。这些镜头光斑的距离随着相机方向与相机跟太阳连线的角度成反比,也就是说,相机指向的方向越是接近太阳,则光斑之间的距离就越小。

    下面是用于进行镜头特效渲染的一些相关的面片贴图。

    GTA V尤其注重效果的细节,镜头光斑也不例外:镜头光斑的尺寸是跟相机的小孔尺寸成正比的。所以如果玩家将镜头突然对准太阳,那么镜头光斑的尺寸就会立马变得很大,之后会随着曝光度(HDR自适应)的降低的需要而降低相机进光孔尺寸,从而缩小光斑的尺寸。

    另外一个令人赞叹的细节是:如果玩家切换到第一人称视角,将不会看到任何镜头光斑之类的镜头特效,因为这个时候是人眼亲自观察,不会出现镜头什么事了。

    Anamorphic Lenses

    在晚上阴暗区域,GTA V还实现了anamorphic lenses(镜头形变?)效果的模拟:垂直的或者水平的长条光束,通常是蓝色的。由于好莱坞电影在最近的科幻电影里经常运用这个效果,使得anamorphic lenses在游戏行业也越来越流行。

    下面这个效果是通过前面两种方法中的基于面片的方法实现的,这种效果通常出现在亮度较低的光源(如车灯)在直面相机的时候。

    Depth of Field

    如果你在电影里看到下图这样的表现,你会觉得不太真实,因为背后远景画面太过清晰锐利,正常来说使用镜头拍摄的场景,应该只有聚焦处是清晰的,而其他地方应该是处于模糊状态的。

    这种效果通常是通过DOF技术来实现:只对那些焦点之外的区域进行模糊处理。(这其实也是一种depth-aware blur)

    具体如何实现呢?首先,需要为整个场景生成一张COC贴图,这张贴图中的每个像素的值对应的就是这个像素的失焦程度(out-of-focus),失焦程度越高,模糊半径也就越大,模糊程度也就越高。而像素的CoC值只取决于此像素与相机的距离(也就是此像素的深度值)以及相机的视轴参数。

    在GTA V中,CoC的值是带符号的,取值范围为[-1, 1],其中负数表示当前像素处于焦平面的前方(靠近相机),而正数表示当前像素处于焦平面的后方(远离相机)
    之所以需要使用带符号的数值来存储CoC的值,是因为靠近相机与远离相机的模糊处理是有区别的?

    比如说,对于比焦平面更远的那些像素而言(CoC>0),在模糊处理的时候,其模糊的结果不能对焦平面位置的数据造成影响(在进行模糊处理的时候,也不能从相邻的焦平面区域读取数据),而比焦平面更近的那些像素而言(CoC < 0),在模糊处理的时候,其模糊的结果是可以而且也应该对焦平面处的数据造成影响

    上面CoC贴图中的黑色区域指的就是处于焦平面的数据,而绿色则指的是远离焦平面的区域,红色表示比焦平面更近的区域。

    接下来就是对CoC的前景区域进行模糊处理(先水平高斯,后垂直高斯),这个过程需要对焦平面区域也进行相应的采样处理

    为什么要对焦平面也进行相应的处理呢?这是因为如果只对前景区域进行模糊处理,那么前景与焦平面交界处就会表现得很奇怪:从一个非常模糊的表现突变为清晰的画面,这显然是有悖常理的。正确的表现,焦平面与前景交界处的边缘应该是平滑的,前景应该能够会有些数据通过模糊处理叠加的焦平面的交界边缘。

    准备工作做好了之后,现在就要开始DOF处理了。DOF就是一个模糊过程,通常来说,模糊处理都是在半分辨率(这里说的应该是1/4分辨率)模式下采用像素shader完成,且是通过水平垂直两个方向的高斯模糊来加速这个过程。而在GTA V中,模糊过程依然是通过两个方向来完成,不过其处理过程是在原分辨率的贴图上完成(为什么不在半分辨率呢,是不是因为半分辨率上处理后恢复到原分辨率时会导致结果变得模糊?),而为了避免对性能造成压力(为什么从像素shader转到compute shader就能缓解性能压力呢,两者不是都是在GPU上采用并行处理流程进行计算的吗?),这个地方将原来放在像素shader中进行的计算放到了compute shader中,而compute shader本来就擅长处理大尺度的模糊处理。在对每个像素计算其最终的模糊输出结果时,其输出结果是受CoC数值的影响的(CoC的绝对值越大,模糊半径越大,模糊程度越高),且在这个处理过程中,某些处于kernel范围内的相邻像素如果在加入模糊计算会导致结果异常时,还会将这些像素从模糊处理中剔除。

    Conclusion

    除了上述提到的后处理过程之外,GTA V还有很多其他的后处理(heat haze,god rays,motion blur等),不过要是一一列举,内容就太多了。

    参考

    [1]. GTA V - Graphics

    相关文章

      网友评论

          本文标题:【GTA V】渲染截帧分析 - 翻译

          本文链接:https://www.haomeiwen.com/subject/kyepvdtx.html