剖析虚幻渲染体系(14)- 延展篇:现代渲染引擎演变史Part 3(开花期二)

14.5.4 渲染技术

14.5.4.1 Visibility Buffer

4K Rendering Breakthrough: The Filtered and Culled Visibility Buffer介绍了一种新的渲染系统:过滤和剔除的可见性缓冲区,其性能将比具有更高分辨率的传统系统好得多,至少与具有最常见分辨率的传统渲染系统一样好。它将三角形数据存储在可见性缓冲区中,而不是将数据存储在G缓冲区中,G缓冲区随着屏幕分辨率的大幅增加而增加。为了优化该缓冲区中存储的三角形的数量和质量,在预处理步骤中应用了三角形过滤和剔除。此外,三角形和消隐预处理步骤还准备了所有其它读取视图,如阴影图渲染。

前向渲染按三角形提交顺序对所有片段进行着色,浪费对最终图像没有贡献的像素的渲染能力,延迟着色通过两个步骤解决此问题:首先,表面属性存储在屏幕缓冲区->G缓冲区中,其次,仅对可见片段计算着色。但是,延迟着色会增加内存带宽消耗,例如屏幕缓冲区:法线、深度、反照率、材质ID,…G缓冲区大小在高分辨率下变得很有挑战性。以下多图是不同年代的延迟着色和可见性缓冲区的对比图:

以下是可见性缓冲区和GBuffer的显存消耗对比:


 

VisibilityBuffer的填充步骤:

  • 可见性缓冲区生成步骤。
  • 对于屏幕中的每个像素:
    • 将(alpha屏蔽位、drawID、primitiveID)打包到1个32位UINT中。
    • 将其写入屏幕大小的缓冲区。
  • 元组(alpha掩码位、drawID、primitiveID)将允许着色器在着色步骤中访问三角形数据。

VisibilityBuffer的着色步骤:

  • 对于屏幕空间中的每个像素,执行以下操作:
    • 获取drawID/triangleID像素位置。
    • 从VB加载3个顶点的数据。
    • 计算三角形梯度。
    • 使用渐变在像素点处插值顶点属性(可以进行三角形/对象空间照明)。
      • 属性使用w从位置计算透视正确插值。
      • MVP矩阵被应用于位置。
    • 已经准备好所有数据:着色和计算最终颜色。

VisibilityBuffer的优势:更好地从着色中分离可见性,计算导数可以与着色阶段分开进行(将其包括在当前阶段),可以用不同的频率或质量来着色。提高内存效率,提高缓存利用率,内存访问是高度一致的(高速缓存命中率高),G缓冲区需要存储每个屏幕空间像素的数据,与顶点/索引缓冲区相比,其中一些数据是冗余的,可以看到纹理、顶点和索引缓冲区的可见性缓冲区的99%二级缓存命中率。与g缓冲区相比,为复杂照明模型(如PBR)存储的数据更少,可见性缓冲区的PBR数据是由顶点结构中的材质id索引的结构常量内存,该结构将索引保存到各种PBR纹理的纹理数组中,它还包含驱动BRDF所需的材质说明,每像素变化的任何数据都存储在结构引用的纹理中。将缓冲区足印与屏幕分辨率解耦,在高分辨率下提高性能:2K、4K、MSAA…在带宽有限的*台上提高性能。

怎么执行照明?可以选择最喜欢的照明结构:延迟分块、向前分块等。向前分块或Forward++似乎是一种自然的搭配,因为它将受益于通过剔除、过滤和可见性检查减少的顶点数,并为不透明和透明对象提供一致的照明。三角形或物体空间的照明也是可能的。

游戏的多边形复杂性每年都在增加,有效剔除三角形非常重要。2个剔除阶段:1、分簇剔除:在将三角形组发送到GPU之前剔除它们;2、三角形过滤:在发送到GPU后剔除单个三角形。

分簇剔除:将三角形分组为256个具有相似朝向的三角形的小块,块有一个相关的模型矩阵(它们可以移动),每个块在发送到GPU之前必须通过快速可见性测试:圆锥测试。

快速分簇剔除的圆锥测试,如果眼睛在安全区域,我们就看不到任何三角形,因为它们是背向的:

具体的过程和解析如下:

1:找到分簇的中心。

2:从分簇中心开始负向累加法线值。

3:负向累加第一个法线值之后的第二个法线值。

4:累加下一个。

5:在累加最后一个之后,得到了排除体积的起点和方向。

接着,结合下图,分别是用于计算圆锥体张角的最严格的三角形*面、计算出的排除体积:

分簇剔除效率:有效性取决于在簇中的面的朝向,方向越相似,排除/剔除体积越大。根据三角形,无法计算排除体积,用于分簇剔除的分簇无效,只需要略过。

基于计算的三角形过滤:动机是在三角形进入图形管线之前剔除它们,使用异步计算在图形管线执行期间使用未使用的计算单元。基于计算的过滤,每个线程一个三角形,过滤的三角形包含退化三角形剔除、背面剔除、视锥剔除、小图元剔除、深度剔除(需要粗糙的深度缓冲),通过这些测试的三角形索引将附加到索引缓冲区。基于计算的三角形过滤:

  • 退化三角形剔除。允许剔除不可见的零面积三角形。成本:快速测试(如果至少两个三角形索引相等,则放弃),效果:低。

    cull = (indices[0] == indices[1] || indices[1] == indices[2] || indices[0] == indices[2] );
    
  • 背面剔除。允许剔除远离观察者的三角形,如果使用细分,则必须考虑最大面片高度。成本:计算3x3矩阵的行列式,效果:高(可能会剔除50%的几何体)。

  • 视锥剔除。允许剔除投影在剪裁立方体外部的三角形,考虑**面和远*面。成本:检查所有顶点是否位于剪辑空间立方体的负侧,效果:中高(取决于场景大小和眼睛位置)。

  • 小图元剔除。允许剔除太小而看不见的三角形,投影后不接触任何采样点的三角形,不接触任何样本的细长三角形也会被剔除,更高效地利用硬件资源。成本:三角形接触任何亚像素样本,效果:中(取决于三角形的大小和屏幕分辨率)。

  • 深度剔除。允许剔除被场景遮挡的三角形,该测试需要一个粗糙的深度缓冲区。成本:从地图加载深度值并检查三角形/BB交点,效果:中高(取决于场景复杂度和三角形的大小)。

基于计算的三角形过滤的概览如下:

三角形过滤在256个三角形(一批)-> 空绘制的组上执行,绘制批次压缩来援救,可以在计算着色器中并行运行,从多间接绘图缓冲区中删除空绘图。

添加三角形/分簇过滤的帧步骤:

  • [CPU] 使用分簇剔除提前丢弃不可见的几何。
  • [CS] 使用三角形过滤(每个线程一个三角形)生成未被剔除的索引和多绘制间接缓冲区。
  • 像之前一样执行。

添加三角形/分簇过滤的数据管理:对于这个静态场景,一个大的顶点缓冲区和一个由三角形剔除和过滤生成的索引缓冲区。绘制批次,每个批次为一种材质保存一块几何体,只有两种“材质”不透明和alpha遮罩的透明对象和其它材质将进入同一缓冲区。对于动态对象,将为每个对象使用专用的VB/IB对;是可选的。

基于计算的三角形过滤的好处:允许在将三角形发送到图形管线之前剔除它们,避免在图形管线(光栅化器)中占据压倒性的部分,图形管线可以更好地利用可见三角形(光栅化效率、命令处理器等),可以利用异步计算与图形管线重叠。

可以对多个视图/渲染过程重用三角形过滤结果。将算法推广到不同的N视图上进行测试,加载索引/顶点一次,变换每个视图的顶点。

添加三角形过滤数据的重用的步骤:

  • [CPU]使用分簇剔除提前丢弃从任何视图中都看不到的几何体。
  • [CS]对N个视图使用三角形过滤测试生成N个索引和N个多绘制间接缓冲区(每个线程一个三角形)。
  • 对于每个视图i使用(第i个索引缓冲区和第i个MDI缓冲区):
    • [Gfx]清除可视性和深度缓冲区。
    • [VS, PS]可视性缓冲区通道,[PS]输出三角形/实例ID。
    • [PS]从梯度和着色像素插值属性。

结果:


 

总结:

虚拟现实呢?正在处理饥饿问题,它能以非常高的分辨率显示大视场,可见性缓冲区将大大提高性能,可以一次完成所有视图和阴影贴图视图的数据筛选和准备,提供forward+,因此不必处理透明度问题。

总之,建立的渲染系统为不同视图(如主视图、阴影视图、反射视图、GI视图等)分簇剔除和过滤三角形,优化后的三角形用于填充屏幕空间可见性缓冲区或更多视图的更多可见性缓冲区。然后,使用基于可见性的优化几何体渲染灯光、阴影和反弹灯光,可以区分几何体的可见性和着色频率,可以在每个三角形或所谓的物体空间中计算照明。

14.5.4.2 Filmic SMAA

Filmic SMAA: Sharp Morphological and Temporal Antialiasing由任职于暴雪的抗锯齿的鼻祖Jorge Jimenez呈现,讲述了电影级的SMAA的最新成果。

SMAA的目标是锐利、鲁棒性、高性能。形态抗锯齿(SMAA 1x),增加了时间超采样(SMAA T2x)、空间多采样(SMAA S2x)和组合(SMAA 4x)不适用于暴雪的游戏。Filmic SMAA是Filmic过滤(Filmic变体):Filmic SMAA 1x、Filmic SMAA T2x–用于PS4和XB1,时间过滤 ≠ 时间超采样。

先重温以下形态抗锯齿基础。结合下图,先搜索线的左右两端,然后获得线两侧的交叉边,有了距离和交叉边,有足够的信息来计算重建线下的面积。

形态抗锯齿改进包含质量(形态学边缘抑制、轮廓线检测、U形*滑)和性能(延迟队列、使用LDS、双面混合)。文中谈及的质量方面的知识点包含局部对比度边缘抑制、形态边缘抑制、轮廓线检测、U型*滑,性能方面的知识点包含形态多通道法、使用Compute简化为一个通道、像素着色器效率低下、模板剔除效率低下、延迟队列、重复模式搜索、SMAA用CS解决问题的方法、使用LDS、双线性获取和解码等。

局部边缘抑制图例。对于给定的边,如右图中红色标记的边,检查附*的边缘,如果发现另一个边缘在对比度上占主导地位,就会抑制它。

通过(左)和不通过(右)当前边缘的图案的对比。其中右边的方法会检查哪一个得分更高(强度)如果不通过当前边缘的图案获胜,则抑制边缘。

三种基本的U型模式。小U型图案在时间上是不稳定,想要软化或摆脱它们,使用LUT,可以根据感知调整。

SMAA形态多通道方法。

延迟队列图例。上:检测到的边被附加到附加/消耗缓冲区,执行间接分派以使用该缓冲区,允许所有线程执行实际工作。下:避免读取全屏模板缓冲区,在某些情况下,由于边缘分布分散,层次模板并不总是最佳的。文中调整了SMAA以使用类似的方法,显著提高了性能,也适用于时间。

文中也涉及了TAA的基础,诸如抖动、采样模式、重投影、速度等,还涉及了不一致(Disocclusion)和速度加权。不一致是指新帧中的像素在前一帧中不存在,解决方案是如果速度相差太大,不要混合,不适用于没有速度的像素(alpha混合)。

颜色信息也可用于不一致,加上速度加权,无法比较当前帧和上一帧,但由于不同的抖动,即使在静态图像上也会经常被拒绝。解决方案是比较N帧和N-2帧,在没有速度的情况下减轻对象上的重影(alpha混合)。

指数历史/指数移动*均:SMAA T2x和类似使用两个帧的技术:caa=0.5ci+0.5ci−1caa=0.5ci+0.5ci−1。

可以利用指数累积缓冲区:

类似于多帧移动*均[Karis2004],增加有效子样本数,反馈循环。

指数累加缓冲技术通常使用邻域夹紧来消除混淆[Lottes2011],其基本形式:

p=max(min(p,nmax),nmin)p=max⁡(min(p,nmax),nmin)

nminnmin和nmaxnmax是3x3虚线邻域中的最小和最大颜色。

当使用这种夹紧与时间抖动一起使用时,可能会导致闪烁。

在这种情况下,考虑到几何体非常小,它在第二帧中消失了,在帧之间,颜色的邻域范围也发生了变化。使得输出不一样,从而在静态图像上产生闪烁的伪影。

最*的速度:指数历史*滑且抗锯齿,新的帧速度是带锯齿的(在重投影时引入锯齿),解决方案是湖区最前面的邻域速度[Karis2014]。

有关TAA的知识点还有形状邻域的YCoCg裁剪[Karis2014]、软邻域夹紧[Drobot2014]、高阶重采样[Drobot2014]、方差剪裁[Salvi2016]...

之前已有不少相关的研究,如SMAA 1TX [Sousa2013]、Unreal Engine 4 TAA [Karis2014]、HRAA [Drobot2014]。文中给出的关键要点是时间滤波与AA解耦。使用指数历史是一个可以与亚像素抖动的超采样分离的过程,从现在起将被称为临时过滤,它可以在时间上过滤(或模糊)图像。可以认为,作为一种副产品,它可以模糊或*均时间亚像素抖动,即使在不抖动的情况下,运动中的对象也会在每帧中自然地落在不同的子采样位置,即使不使用抖动,运动中的对象也会有AA。Filmic SMAA T2x和SMAA T2x的对比如下:

Filmic SMAA T2xSMAA T2x
子样本:2x(可选对比度感知的Quincunx约4x)
边缘:形态学
时间过滤:
子样本:2x
边缘:形态学
时间过滤:否
减少鬼影
少量的性能预算
锐利
在静态图像中稳定
在没有速度的物体上鬼影
在静态图像中稳定

文中涉及的时间抗锯齿改进:

  • 分离时间超采样和时间滤波。
  • 时间过滤锐利。
  • 时空对比度跟踪。
  • 基于FPS的时域过滤。
  • 用深度测试扩展邻域夹紧。
  • 改进的时间超采样重采样。
  • 改进的时间Quincunx。
  • 超采样导数。
  • 改进的颜色权重。
  • 低轮廓的速度缓冲区。
  • 更快的最*速度。
  • 时间上采样。

Drobot2014的单通道和Filmic的双通道对比:

它们的方法和效果对比:

对于时间锐利度,使用了双三次重采样(Bicubic Resampling),优化的Catmull Rom使用9个双线性样本处理4x4区域。最终的方案是忽略4个角会产生非常相似的结果,从9个样本减少到5个。小数值误差,可能可以应用在其它领域。

左:原始的Bicubic;右:减少到5个样本的*似方法。

两种方法的误差。

// 5样本的采样方法shader代码
float3 SMAAFilterHistory(SMAATexture2D colorTex, float2 texcoord, float4 rtMetrics)
{
    float2 position = rtMetrics.zw * texcoord;
    float2 centerPosition = floor(position - 0.5) + 0.5;
    float2 f = position - centerPosition;
    float2 f2 = f * f;
    float2 f3 = f * f2;
 
    float c = SMAA_FILMIC_REPROJECTION_SHARPNESS / 100.0;
    float2 w0 =        -c  * f3 +  2.0 * c         * f2 - c * f;
    float2 w1 =  (2.0 - c) * f3 - (3.0 - c)        * f2         + 1.0;
    float2 w2 = -(2.0 - c) * f3 + (3.0 -  2.0 * c) * f2 + c * f;
    float2 w3 =         c  * f3 -                c * f2;
 
    float2 w12 = w1 + w2;
    float2 tc12 = rtMetrics.xy * (centerPosition + w2 / w12);
    float3 centerColor = SMAASample(colorTex, float2(tc12.x, tc12.y)).rgb;
 
    float2 tc0 = rtMetrics.xy * (centerPosition - 1.0);
    float2 tc3 = rtMetrics.xy * (centerPosition + 2.0);
    float4 color = float4(SMAASample(colorTex, float2(tc12.x, tc0.y )).rgb, 1.0) * (w12.x * w0.y ) +
                   float4(SMAASample(colorTex, float2(tc0.x,  tc12.y)).rgb, 1.0) * (w0.x  * w12.y) +
                   float4(centerColor,                                      1.0) * (w12.x * w12.y) +
                   float4(SMAASample(colorTex, float2(tc3.x,  tc12.y)).rgb, 1.0) * (w3.x  * w12.y) +
                   float4(SMAASample(colorTex, float2(tc12.x, tc3.y )).rgb, 1.0) * (w12.x * w3.y );

    return color.rgb * rcp(color.a);
}

改进的时间超采样重采样:

尝试了不同的*滑曲线,选择了下图上排右边的曲线,因为它具有最强的S形。接着要做的是用立方**滑来*似它,使用了夹紧和重缩放,也就是下排右边的图:

效果如下:

文中改进了Quincunx采样。结合下图,Quincunx的定义是:0.5×蓝色样本+0.5×橙色样本0.5×蓝色样本+0.5×橙色样本。

其中橙色是单一样本,可以用双线性来获取全部蓝色样本,Quincunx变成纹理坐标移量,与2x相同的性能。对比度感知的Quincunx的想法:根据局部对比度来调整纹理坐标的偏移量,低对比度(纹理细节)用2x,高对比度(边缘)用Quincunx,使用Quincunx子样本确定对比度,如果对比度低,则使用2x偏移重新获取,开销低:约0.02毫秒PS4@1080。

文中还涉及了很多技术改进细节和对比,有兴趣的童鞋可以点击原文阅读。总之,Filmic SMAA T2x的亮点是良好的锐利、高健壮性及高性能。

14.5.4.3 High Dynamic Range Imaging

高动态范围(HDR)图像和视频包含像素,这些像素可以代表比现有标准动态范围图像更大的颜色和亮度范围。这种“更好的像素”极大地提高了视觉内容的整体质量,使其看起来更真实,对观众更有吸引力。HDR是未来成像管线的关键技术之一,它将改变数字视觉内容的表示和操作方式。

High Dynamic Range Imaging对HDR方法和技术进行了广泛的回顾,并介绍了HDR图像感知背后的基本概念,也回顾了HDR成像技术的现状。它涵盖了与用相机捕捉HDR内容以及用计算机图形学方法生成内容相关的主题;HDR图像和视频的编码和压缩;用于在标准动态范围显示器上显示HDR内容的色调映射;反向色调映射,用于放大传统内容,以便在HDR显示器上显示;提供HDR范围的显示技术;最后是适合HDR内容的图像和视频质量指标。

图左:透明实体代表人眼可见的整个色域。在较低的亮度水*下,随着颜色感知的降低,固体逐渐向底部倾斜。为了便于比较,内部的红色固体代表标准sRGB(Rec. 709)色域,由高质量显示器产生。图右:与CRT和LDR监视器上显示的亮度范围相比的真实亮度值。大多数数字内容的存储格式最多能保留典型显示器的动态范围。

从应用角度来看,HDR图像的质量往往要高于LDR,下图是HDR和LDR视觉内容之间的潜在差异,而下表给出的数字只是一个例子,并不意味着是一个精确的参考。

使用标准的高位深度编解码器(如JPEG2000、JPEG XR或选定的H.264配置文件)对HDR图像或视频内容进行编码。HDR像素需要编码到一个亮度通道和两个色度通道中,以确保颜色通道的良好去相关性和编码值的感知一致性。标准压缩可以选择性地扩展,以便为鲜明对比的边缘提供更好的编码。

下图是向后兼容HDR压缩的典型编码方案,深棕色框表示视频编解码器的标准(通常为8位)图像,如H.264或JPEG。

基于仅向前的视觉模型的色调映射的典型处理管线如下图。原始图像使用视觉模型转换为抽象表示,然后直接发送到显示器。

基于正向和逆向视觉模型的色调映射的典型处理管线见下图。使用正向视觉模型将原始图像转换为抽象表示,可以选择编辑,然后通过反向显示模型转换回物理图像域。

色调映射的典型处理管线解决了一个约束映射问题,使用默认参数对图像进行色调映射,然后使用视觉度量将显示的图像与原始HDR图像进行比较。然后,在迭代优化循环中使用度量中的标量误差值,以找到最佳色调映射参数。请注意,在实践中,解决方案往往被简化,并制定为二次规划,甚至有一个封闭形式的解决方案。

驱动HDR显示器中的低分辨率背光调制器和高分辨率前LCD面板所需的图像处理流程:

HDR-VDP-2度量的处理阶段,测试图像和参考图像经过相似的视觉建模阶段,然后在单个空间和方向选择带(BT和BR)水*上进行比较。该差异用于预测可见度(检测概率)或质量(感知的失真程度)。

为色调映射的图片(底部)预测动态范围无关度量(顶部),绿色表示可见对比度的损失,蓝色表示不可见对比度的放大,红色表示对比度反转。

14.5.4.4 Texture Streaming

Efficient Texture Streaming in 'Titanfall 2'分享了Titanfall 2的高效纹理流技术。纹理流动态加载以提高图像质量,概念上是一种压缩形式,常见方法有手动分割、边界几何测试、GPU反馈。工作流程要求尽量减少设计和艺术方面的手工工作,艺术家可以自由映射纹理(无固定密度),可以在不损害其它纹理的情况下添加MIPs,预处理应该是稳定的,一些好的手动暗示,与“资产面包房”合作,包括热插拔。

算法概述:任何低于64k的MIP都是永久性的,可以逐个添加/删除MIP,使用预先计算的信息建立重要/不重要内容的列表,每一帧都对着这个列表工作。

什么是“直方图”?想要根据mip在屏幕上覆盖的像素数(覆盖率)来区分mip的优先级,而不仅仅是“是/否”,‘“直方图”是每种材质每个MIP的覆盖率,16个标量“箱”(通常为浮点数)——每个MIP一个。假设屏幕分辨率为256 x 256的4k x 4k纹理,变换和缩放分辨率和移动模型,使用低密度纹理贴图适当地加权小的、被遮挡的或背面的三角形。

算法-预计算:计算每种材质的直方图,对于静态物体,使用GPU渲染世界的每列,放入文件。对于动态物体,每个模型在加载时:计算每个三角形的纹理梯度,将三角形区域添加到MIP的直方图区域,计划从不同的角度进行项目,但不值得,手动调整比例因子以匹配静态数据。

每帧会发生什么?从磁盘播放播放器的“列”,添加模型覆盖率,将覆盖率除以纹素数量,得到一个“指标”,生成最重要和最不重要的MIP列表,更精细的MIPs级联(更粗糙的始终>=更精细)。加载最重要的MIP,删除最不重要的MIP,捕捉运行数量上限和每帧丢弃的字节数,除非你正在加载更重要的东西,否则不要丢掉东西!

如何选择探针?运行“rstream.exe”,实例化模型,计算边界,将几何图形切为16英尺x16英尺的列,探针位于向上三角形上方的眼睛高度,添加提示探测(在附*的列中也使用Z),使用k-means组合成每列最多8个探针,将探测位置存储在日志文件中以供调试使用。

如何渲染探针?将静态几何信息上传到GPU一次,渲染N个探针的UAV:

float2 dx = ddx( interpolants.vTexCoord ) * STBSP_NOMINAL_TEX_RES; // STBSP_NOMINAL_TEX_RES is 4096.0
float2 dy = ddy( interpolants.vTexCoord ) * STBSP_NOMINAL_TEX_RES;
float d = max( dot( dx, dx ), dot( dy, dy ) );
// miplevel is log2 of sqrt of unclamped_d. (MATERIAL_HISTOGRAM_BIN_COUNT is 16.)
float mipLevel = floor( clamp( 0.5f * log2(d), 0.0f, (float)(MATERIAL_HISTOGRAM_BIN_COUNT - 1) ) );
InterlockedAdd( outHistogram[interpolants.vMaterialId * MATERIAL_HISTOGRAM_BIN_COUNT + (uint)mipLevel], 1 );

每个立方体面做一次(累积结果),不透明通过写入深度,透明仅测试,没有帧缓冲区!

编译探针数据:现在,有每种材质每个MIP在探针上的覆盖率,在每列内以max方式组合探针,记录材质ID、MIP数量、覆盖范围(4字节),每列存储512条最重要的记录,将4x4列分组为约32k的可流化页面,索引到稳定的全局材质ID和位置,每个关卡一个‘.stbsp’文件。

管理纹理资源:每个压缩(和旋转)纹理文件可能有一个“可流化”的片段,在为一个关卡构建快速加载的“rpak”文件时,会聚集到第二个“starpak”文件中。对于发布版本,在所有级别使用共享starpak,磁盘上仅复制了<64k的MIP,Starpak包含对齐的、准备加载的数据。

// Crediting World Textures
Compute column (x,y integer), Ensure active page is resident (cache 4 MRU), or request it.
totalBinBias = Log2(NOMINAL_SCREEN_RES * halfFovX / (NOMINAL_TEX_RES * viewWidthInPixels) )
For each material represented in column,
    For each texture in that material
        For each record (<material,bin,coverage>) in column (up to 16)
            If texture->lastFrame != thisFrame,
                texture->accum[0..15] = 0, and texture->lastFrame = thisFrame
            mipForBinF = totalBinBias + record->bin + Log2(textureWidthInPixels)
            mipForBint = floor( max( 0.0, mipForBucketF ) ), clamped to (16-1).
            texture->accum[mipForBin] += record->coverage * renormFactorForStbspPage;

// Crediting Models
float distInUnits = sqrtf( Max( VectorDistSqr( pos, *pViewOrigin ), 1.0f ) );
if ( distInUnits >= CUTOFF ) continue;
float textureUnitsPerRepeat = STREAMINGTEXTUREMESHINFO_HISTOGRAM_BIN_0_CAP; // 0.5f
float unitsPerScreen = tanOfHalfFov * distInUnits;
float perspectiveScaleFactor = 1.0f / unitsPerScreen;
// This is the rate of pixels per texel that maps to the cap on bin 0 of the mesh info.
// ( Exponentiate by STREAMINGTEXTUREMESHINFO_HISTOGRAM_BIN_CAP_EXPBASE for other slots )
float pixelsPerTextureRepeatBin0 = viewWidthPixels * textureUnitsPerRepeat * perspectiveScaleFactor;
Float perspectiveScaleAreaFactor = perspectiveScaleFactor * perspectiveScaleFactor;
pixelsPerTextureRepeatBinTerm0 = (int32)floorf(-Log2( pixelsPerTextureRepeatBin0 ); // Mip level for bin 0 if texture were 1x1.
                                               
For each texture t:
    if first use this frame, clear accum.
    if high priority, t->accum[clampedMipLevel] += HIGH_PRIORITY_CONSTANT (100000000.0f)
    For dim 0 and 1 (texture u,v):
        const int mipLevelForBinBase = (i32)FloorLog2( (u32)textureAsset->textureSize[dim] ) + pixelsPerTextureRepeatBinTerm0 ;
        For each bin
            // Log2 decreases by one per bin due to divide by two. (Each slot we double pixelsPerTextureRepeatBin0, which is in the denominator.)
            const int32 clampedMipLevel = clamp(mipLevelForBinBase - (i32)binIter, 0..15 )
t->accum[clampedMipLevel] += modelMeshHistogram[binIter][dim] * perspectiveScaleAreaFactor;
    If accum exceeded a small ‘significance threshold’, update t’s last-used frame.
              
                                               
// Prioritization
For each texture mip,
    metric = accumulator * 65536.0f / (texelCount >> (2 * mipIndex));
    If used this frame:
        non-resident mips are added to ‘add list’, with metric.
        resident mips are added to ‘drop list’ with same metric.
    If not used this frame:
        all mips added to ‘drop list’ with metric of ( -metric + -frames_unused.)
            (also, clamped to finer mips’ metric + 0.01f, so coarser is always better)
Then partial_sort the add and drop lists by metric to get best & worst 16.
     
                                               
// Add/Drop
shift s_usedMemory queue
for ( ; ( (shouldDropAllUnused && tDrop->metric < 0.0f) || s_usedMemory[0] > s_memoryTarget) && droppedSoFar <16MiB && tDrop != taskList.dropEnd; ++tDrop ) { drop tDrop, increase droppedSoFar; }
for ( TextureStreamMgr_Task_t* t = taskList.loadBegin; t != tLoadEnd; ++t ) { // t points into to add list
    if ( we have 8 textures queued || t->metric <= bestMetricDropped ) break;
    if ( s_usedMemory[STREAMING_TEXTURES_MEMORY_LATENCY_FRAME_COUNT - 1] + memoryNeeded <= s_memoryTarget ) {
        for ( u32 memIter = 0; memIter != STREAMING_TEXTURES_MEMORY_LATENCY_FRAME_COUNT; ++memIter ) {
            s_usedMemory[memIter] += memoryNeeded; }
        if ( !begin loading t ) { s_usedMemory[0] -= memoryNeeded; } // failure eventually gets the memory back
} else for ( ;; ) { // Look for ‘drop items’ to get rid of until we'll have enough room.
    if ( planToLoadLater + memoryNeeded + s_usedMemory[0] <= s_memoryTarget ) {
        planToLoadLater += memoryNeeded; break; }
    if ( droppedSoFar >= 16MiB || tDrop >= taskList.dropEnd || t->metric <= tDrop->metric ) { break; }
    bestMetricDropped = Max( bestMetricDropped, tDrop->metric );
    drop tDrop, increase droppedSoFar;
    ++tDrop; } }

如何调整纹理大小?在Windows/DirectX下,最初的CPU可写纹理、贴图、读取新MIPs,创建GPU纹理,GPU复制新的和旧的MIPs,现在只需加载到堆中并传递到CreateTexture。控制台下,直接读取新的MIP进来,放入排队3帧,以刷新管线。

异步I/O:异步线程,运行中的2个请求,多优先级队列,纹理优先级低,音频优先级很高,为了提高可中断性,读取以64kb的数据块进行。

14.5.4.5 Frame Graph

FrameGraph: Extensible Rendering Architecture in Frostbite阐述了2017年的Frostbite的演变历史,最终采用了帧图的方式。

Frostbite引擎在07年(左)和17年(右)的渲染系统对比。

渲染体系的简化图如下:

其中WorldRenderer协调所有渲染,采用代码驱动的架构,是主要的世界几何(通过着色系统),照明、后处理(通过渲染上下文),掌管所有视图和渲染通道,在系统之间管理设置和资源,分配资源(渲染目标、缓冲区)。

WorldRenderer面临诸多挑战,例如显式立即模式渲染,显性资源管理,定制、手工制作的ESRAM管理,不同游戏团队的多种实现,渲染系统之间的紧密耦合。有限的可扩展性,游戏团队必须fork / diverge才能定制,从4k增长到15k SLOC,具有超过2k SLOC的单个功能,维护、扩展和合并/集成成本高昂。

WorldRenderer模块化的目标是高层次知识框架,改进的可扩展性,解耦和可组合的代码模块,自动资源管理,更好的可视化和诊断。新的架构组件如下:

h3.png)

其中帧图(Frame Graph)是渲染通道和资源的高级表示,完全掌控整帧;临时资源系统(Transient Resource System)负责资源分配、内存重叠。它的目标是建立整帧的高层次信息,简化资源管理,简化渲染管线配置,简化异步计算和资源屏障,允许独立且高效的渲染模块,可视化和调试复杂的渲染管线。

引擎资源的生命周期视图,引用十分复杂。

帧图的设计是远离立即模式渲染,将代码拆分为通道的渲染,多阶段保留模式渲染API:设置阶段、编译阶段、执行阶段,每一帧都是从零开始建造的,代码驱动的架构。

设置阶段定义渲染/计算通道,定义每个通道的输入和输出资源,代码流类似于立即模式渲染。

// 资源示例
RenderPass::RenderPass(FrameGraphBuilder& builder)
{
    // Declare new transient resource
    FrameGraphTextureDesc desc;
    desc.width = 1280;
    desc.height = 720;
    desc.format = RenderFormat_D32_FLOAT;
    desc.initialSate = FrameGraphTextureDesc::Clear;
    m_renderTarget = builder.createTexture(desc);
}

// 设置示例
RenderPass::RenderPass(FrameGraphBuilder& builder, FrameGraphResource input, FrameGraphMutableResource renderTarget)
{
    // Declare resource dependencies
    m_input = builder.read(input, readFlags);
    m_renderTarget = builder.write(renderTarget, writeFlags);
}

高级的帧图操作:延迟创建资源,尽早声明资源,在第一次实际使用时分配,基于使用情况的自动资源绑定标志。派生资源参数,根据输入大小/格式创建渲染通道输出,根据使用情况派生绑定标志。移动子资源,将一种资源转发给另一种资源,自动创建子资源视图/重叠,允许“时间旅行”。

移动子资源示例。

编译阶段剔除未引用的资源和通道,在声明阶段可能会有点粗糙,旨在降低配置的复杂性,简化条件传递、调试渲染等。计算资源生命周期。根据使用情况分配具体的GPU资源,简单贪婪分配算法,首次使用前获得,最后一次使用后释放,延长异步计算的生命周期,根据使用情况派生资源绑定标志。

上图处于调试的特殊渲染模式,因此会剔除掉红框内的通道和资源。

执行阶段为每个渲染通道执行回调函数,立即模式的渲染代码,使用熟悉的RenderContext API,设置状态、资源、着色器、绘制调用、派发,从设置阶段生成的句柄中获取真正的GPU资源。

异步计算:可以自动从依赖关系图派生,需要手动控制,节省性能的潜力很大,但是内存增加,如果使用不当,可能会影响性能。每次渲染通道选择性加入,在主时间线上开始,第一次使用另一个队列上的输出资源时的同步点,资源生命周期自动延长到同步点。

Async compute的同步点示意图。

// 异步设置示例
AmbientOcclusionPass::AmbientOcclusionPass(FrameGraphBuilder& builder)
{
    // The only change required to make this pass
    // and all its child passes run on async queue
    builder.asyncComputeEnable(true);

    // Rest of the setup code is unaffected
    // …        
}

渲染模块:

  • 有两种类型的渲染模块:

    • 独立无状态函数。输入和输出是帧图资源句柄,可以创建嵌套的渲染通道,Frostbite中最常见的模块类型。

    • 持久化渲染模块。可能有一些持久性资源(LUT、历史缓冲区等)。

  • WorldRenderer仍在协调高级渲染。不分配任何GPU资源,只需在高级启动渲染模块,更容易扩展,代码大小从15K减少到5K SLOC。

模块之间的通讯:模块可以通过黑板进行通信,组件哈希表,通过组件类型ID访问,允许受控耦合。

void BlurModule::renderBlurPyramid(FrameGraph& frameGraph, FrameGraphBlackboard& blackboard)
{
    // Produce blur pyramid in the blur module
    auto& blurData = blackboard.add<BlurPyramidData>();
    addBlurPyramidPass(frameGraph, blurData);
}

#include ”BlurModule.h”
void TonemapModule::createBlurPyramid(FrameGraph& frameGraph, const FrameGraphBlackboard& blackboard)
{
    // Consume blur pyramid in a different module
    const auto& blurData = blackboard.get<BlurPyramidData>();
    addTonemapPass(frameGraph, blurData);
}

UE的RDG没有blackboard的概念,所有信息都放到FRDGBuilder(类似于FrameGraph)中。

临时资源系统(Transient resource system):Transient是活动时间不超过一帧的资源,如缓冲区、深度和颜色目标、UAV等。在1帧内尽量减少资源使用时间,在使用资源的地方分配资源,直接在叶子节点渲染系统中,尽快释放分配,使编写独立功能变得更容易。是帧图的关键组件。

临时资源系统的实现取决于*台功能,物理内存中的重叠(XB1)、虚拟内存中的重叠(DX12、PS4)、对象池(DX11)。用于缓冲区的原子线性分配器没有重叠,只用于快速传输内存,主要用于向GPU发送数据。纹理的内存池。

以下是不同*台的临时资源分配机制图:


 

内存重叠注意事项:一定要非常小心,确保有效的资源元数据状态(FMASK、CMASK、DCC等),执行快速清除或放弃/重写资源或禁用元数据,确保资源生命周期是正确的,比听起来更难,考虑计算和图形流水线,考虑异步计算,确保在重新使用之前将物理页写入内存。

资源的丢弃和清除:必须是新分配资源上的第一个操作,要求资源处于渲染目标或深度写入状态,初始化资源元数据(HTILE、CMASK、FMASK、DCC等),类似于执行快速清除,资源内容未定义(未实际清除),如果可能的话,宁愿放弃资源也不要清除。

重叠屏障(Aliasing barriers):在GPU上的工作之间添加同步,添加必要的缓存刷新,使用精确的屏障将性能成本降至最低,可以在困难的情况下使用通配符屏障(但预期IHV分裂),在DirectX 12中批量处理所有其它资源屏障!

重叠屏障示例。上:管线化CS和PS工作导致的潜在重叠危险,CS和PS使用不同的D3D资源,所以过渡屏障是不够的,必须在PS之前刷新CS或延长CS资源生命周期。下:串行计算工作确保了内存重叠时的正确性,在某些情况下可能会影响性能,当重叠对性能至关重要时,使用显式异步计算。

720p下使用重叠内存前(上)后(下)的对比。其中下图是DX12的内存重叠布局,可以节省*50%的内存占用,4k分辨率下可以节省超过50%。

总之,整帧的信息有很多好处,通过资源重叠节省大量内存,半自动异步计算,简化渲染管线配置,很好的可视化和诊断工具,图形是渲染管线的一种有吸引力的表示形式,直观而熟悉的概念,类似于CPU作业图或着色器图,现代C++功能减轻了保留模式API的痛苦。更多可参阅:

14.5.4.6 Display Latency

Controller to Display Latency in 'Call of Duty'详细且深入地讨论了控制器的显示延迟:玩家按下按钮和在屏幕上看到按下结果之间的最短持续时间,还介绍Call of Duty游戏中添加的动态调节功能,以控制影响输入延迟的权衡,最终目标是减少控制器到显示器的延迟。最先讨论如何测量延迟,然后将深入研究引擎的特定方面,这些方面必须考虑到节流阀(throttle)。玩家按下控制键按钮到看到画面的流程实际上包含了以下方面的步骤或阶段:

上面的Controller/OS sample、Game engine query、Game logic and rendering、Video scan-out是在游戏引擎涉及的阶段。

首先要明白,延迟不等于性能,延迟只是对性能变化的适应性,延迟在整个渲染管线中的流向图和简化图如下:

COD将采用的减少延迟的策略是在输入样本之前引入一个节流阀(throttle)。通过在此处添加延迟,输入样本将被压缩到更接*帧末尾的位置。

为了更容易地考虑这个限制,可以将延迟持续时间分为两类。首先是工作,工作是为这个特定的帧积极处理某些东西所花费的时间:例如游戏逻辑或渲染,就是下图的彩色框中表示的时间。

除了工作的其它一切,可称之为“slop”:

Slop不一定是空闲时间:它通常是一条时间线在前一帧上工作的片段,或者一条时间线在等待另一条时间线释放共享资源的片段。如果你看一个通用的生产者-消费者系统,生产者执行一个工作单元,将结果传递给消费者,然后开始生产下一个工作单元。如果消费者始终比生产者慢,系统将变为“消费者受限”。消费者的时间线保持完整,试图跟上生产者,但生产者可以尽可能领先。这就是slop存在的原因:生产者领先于消费者,所以生产者何时完成与消费者何时开始之间存在差距。生产者的领先程度取决于生产者和消费者之间允许的缓冲量,以及它们获取和释放对缓冲数据的访问的确切时间。

另一方面,当我们被生产者限制时,slop通常会消失。消费者的时间线被释放了,所以一旦生产者完成了一个画面,消费者就可以立即开始,我们不会得到一个空隙。

延迟输入采样时,slop会从第一段开始挤出,直到消失,然后移动到第二段,依此类推。另一方面,工作会在时间上向前移动,而整个帧的总工作持续时间理想情况下保持不变。

延迟 = 工作+Slop,更高的slop ⟷ 更高的延迟,更低的slop ⟷ 更低的延迟。

让我们先看看延迟持续时间结束时会发生什么。游戏将最终的图像渲染到一个名为帧缓冲区的内存中。然后,视频扫描硬件从上到下逐行读取帧缓冲区,并通过电缆将像素数据传输到显示器。扫描输出以与显示器刷新率相匹配的速率连续传输。如今,传统显示器的刷新率为60Hz,因此帧缓冲区通常也会以60Hz的频率扫描,或者每16.6ms扫描一次。16.6ms的大部分时间用于主动传输可见像素数据,但在传输的数据与可见像素不对应的情况下,会有短暂的暂停。首先,在每一行的末尾有一个暂停,称为水*空白(HBLANK),在最后一行之后有一个暂停,称为垂直空白(VBLANK)这些暂停在旧的CRT监视器上是物理上必要的,但由于遗留原因和传输元数据,它们今天仍然存在。

如果我们把它放在一个时间轴上,我们得到扫描接着是vblank,扫描接着是vblank等等,所有这些都以固定的60Hz频率发生。

现在我们知道延迟时间的结束是固定的:每16.6毫秒发生一次,意味着我们可以提前准确地预测这一帧的扫描何时开始。我们现在的目标是找出与固定扫描相关的其它时间线的位置。向上移动,GPU将图像渲染到帧缓冲区。

扫描输出是不断从帧缓冲区读取数据并将数据发送到显示器,意味着我们同时在同一个内存中读写:一种竞争条件。传统的解决方案是双缓冲:分配两个帧缓冲区,并在每一帧渲染到备用缓冲区,在渲染到帧缓冲区A时,帧缓冲区B会被扫描出来,然后我们渲染到帧缓冲区B,然后从帧缓冲区A扫描出来。

在GPU使用帧后立即翻转是不够的,比方说渲染完成,接着立即翻转,A开始向外扫描,而B开始渲染。B在A的扫描结束之前完成渲染。如果翻转现在发生,B完成渲染之后会发生什么?然后,扫描输出将在从A读取到从B读取的中间切换,而无需重置扫描线位置。屏幕上生成的图像将从缓冲区A扫描上半部分,而屏幕的下半部分将从缓冲区B扫描。这种伪影被称为撕裂,当A和B之间有较大的水*移动时,最为明显,比如游戏相机在旋转。解决这个问题需要另一条规则:只在扫描完成后翻转,换言之,仅在VBLANK期间翻转,此规则称为垂直空白同步(VSyync)

简而言之,中扫描渲(Mid-scan)染用双缓冲修正,撕裂用等待vblank翻转修正。使用60Hz和vsync的双缓冲,在vblank期间每次扫描出后,帧缓冲区A和B之间会发生翻转。

结合下图,假设GPU正在渲染到帧缓冲区A中,GPU不允许触摸A的时间段是蓝色大矩形中的区域。在第一个阶段,GPU必须空闲等待帧缓冲区A可用,对A的扫描完成后,可以开始渲染到A。一旦GPU完成渲染,不能立即翻转,因为会导致撕裂。相反,GPU“将翻转排队”:它表示A已准备好在下一次vblank中翻转。这是slop(斜坡)的第一个来源:介于翻转队列和实际翻转之间。

如果计算slop持续时间,它是约16.6ms减去GPU的总工作时间。但请记住,我们希望尽可能多地使用可用硬件,GPU周期是一种特别有价值的资源,如果GPU的工作负载一直很低,就相当于浪费。如果我们工作做得好,可以以更高的分辨率绘制更多的图形。如果在更重的GPU负载下,slop会发生什么?

当GPU工作负载满时,接*16.6ms,slop消失。但请记住,节流阀应该将slop挤出延迟持续时间。如果这里没有斜坡,那么首先查看GPU进行扫描有什么意义?

事实上,事情要复杂一点,实际上有一个巨大的slop源,即使GPU负载很重。原因是,帧的大多数绘制都是在其它地方渲染的,而不是在帧缓冲区中。几乎所有的GPU时间都花在了渲染屏幕外缓冲区上,大部分3D绘图都渲染到了第三个屏幕外缓冲区,称为“场景缓冲区”。场景缓冲区的分辨率可以低于最终显示分辨率,并且可以使用不同的颜色编码。场景渲染完成后,场景缓冲区将上采样到帧缓冲区中,通常在上采样期间应用时间抗锯齿。在上采样之后,UI元素以显示分辨率呈现到帧缓冲区中,然后是排队等待翻转的帧缓冲区。这里最重要的一点是,大部分GPU帧时间都花在渲染3D场景上,帧缓冲区直到帧后期的上采样才被触及。这并不完全是三重缓冲,因为场景缓冲永远不会扫描到显示器上。但是计时的结果类似于三重缓冲,所以我们称之为“伪三重缓冲”。

在最初的双缓冲设置中,GPU需要在帧的最开始处等待帧缓冲,将GPU时间线与扫描输出时间线同步。现在有了伪三重缓冲,GPU只需要在帧的后期,即上采样之前等待帧缓冲。

对于严格的双缓冲,只允许在蓝色矩形之间的时间段内渲染帧缓冲区A。

现在,GPU工作负载分为两部分:场景渲染(不使用帧缓冲区)和帧缓冲区渲染(使用帧缓冲区)。帧缓冲区渲染部分仍然必须位于两个蓝色矩形之间,以避免中间扫描渲染,但场景渲染部分可以随时启动。

一切都被允许在时间上向后移动,GPU在完成最后一帧后立即开始场景渲染,帧缓冲区渲染仍然必须与扫描输出同步,但它会被移回场景渲染留下的空间。

在严格的双缓冲中,slop仅为~16.6ms减去GPU的总工作时间,随着GPU工作负载的增加,slop消失了。

现在使用伪三重缓冲,slop被分成两部分。第一个slop持续时间是帧缓冲区等待:介于场景渲染结束和帧缓冲区渲染开始之间,等于16.6ms减去GPU的总工作量,第二个slop持续时间介于翻转队列和实际翻转之间,大约16.6ms减去帧缓冲区渲染工作负载。

一个完整的GPU工作负载通常意味着更多的场景渲染:更多的3D对象和更高的场景分辨率,帧缓冲区渲染通常很短。因此,当GPU工作负载满时,第一个slop仍然会消失,而第二个slop可以保持很长时间。

即使充分利用,也可能有很多slop。

但伪三重缓冲还有另一个后果。到目前为止,我们一直假设GPU的工作负载总是快于16.6ms。但是,如果你想让GPU尽可能地忙碌,很容易不小心会有一点过度,使得帧变慢。假设在严格的双缓冲情况下,帧B的渲染速度低于16.6ms。在即将到来的vblank间隔之前,帧尚未准备好扫描,因此它“错过了vblank”。没有帧缓冲区排队等待翻转,因此在下一次vblank期间不会发生翻转。取而代之的是,scan out保持指向帧缓冲区A,A再次被扫描出来,B的扫描必须等到下一个vblank开始。现在,由于A正在被第二次扫描,GPU必须一直等到下一次翻转开始渲染A。如果渲染帧A也很慢,它会错过另一个vblank,以此类推。

如果GPU渲染时间始终低于刷新速度,即使是微秒,也会错过每一个vblank帧速率将固定在30Hz而不是60Hz。这是一个非常灾难性的后果,尤其是如果我们试图让GPU的工作负载尽可能满。

现在在伪三重缓冲的情况下,假设第一个vblank仍然丢失:帧缓冲区A仍然需要扫描两次。但现在允许立即开始A的场景渲染,即使A仍在被扫描,因为GPU可以渲染到屏幕外缓冲区的时间没有限制。这与双缓冲区的情况有很大不同,在双缓冲区的情况下,GPU必须一直闲置到下一个vblank。即使GPU的工作负载仍然大于16.6ms,A也不会错过下一个vblank。

缩小并查看多个帧上的行为(假设固定的、低于16.6ms的GPU工作负载)。第一帧未命中vblank,A被扫描两次,但是下一帧,下一帧,下一帧都会生成它们的Vblank。最终,一个vblank确实会再次被忽略,但在六个缓慢的帧通过之前,不会错过它们的vblank。

使用伪三重缓冲时,*均帧率不会达到30Hz。取而代之的是,*均帧速率从60Hz慢慢下降到50Hz

看看底部的大括号:这些是测量翻转队列和每个帧的实际翻转之间的slop。请注意,在第一次错过vblank之后,slop是高的。当慢速帧通过时,斜率逐渐减小。每一个慢帧都会消耗可用的slop,直到slop最终降到零以下,vblank就会丢失。这说明了slop的一个重要方面:slop充当缓冲空间,让慢速帧在不丢失vblank的情况下通过

能够保持在50Hz比降到30Hz要好,但它仍然会产生一种令人不快的效果,即每隔几帧就会错过一帧。我们更愿意保持在一个稳定的60,这就是动态分辨率的来源——当游戏注意到GPU时间低于阈值时,引擎就会启动。

但现在有了动态分辨率,让我们再次看看伪三重缓冲的时间线。几帧缓慢的画面经过,一帧也没有漏掉。Slop变得非常低,但最终分辨率下降。现在,GPU的帧时间快于16.6ms。请注意,随着快速帧的流逝,slop会累积回安全水*。其想法是,慢帧会吞食slop,而快帧会将其重建。使用伪三重缓冲带来的额外slop,慢帧阈值可以提高到接*16.6ms,并且有可能在该阈值以上的峰值中存活。这种特性对于动态分辨率成为一种可行的技术至关重要。

低slop意味着低延迟,但在慢帧上更容易错过Vblank。高slop意味着更高的延迟,有缓冲空间,以容忍几个慢帧。

1、在一般情况下要最大化slop。在缓冲方面权衡额外的内存,给定缓冲范围,考虑最大化slop。

下面是一个非常简单的例子,从最*的一个任务召唤游戏中,可以在实践中最大化slop。添加HDR支持后,场景缓冲区-帧缓冲区分割发生了变化。第一个场景渲染通常对场景缓冲区执行,场景缓冲区被上采样到显示分辨率,但这次以显示分辨率进入另一个屏幕外缓冲区,UI以屏幕外缓冲区的显示分辨率呈现,最后,在帧的最后,该缓冲区被转码到其最终颜色空间中的实际帧缓冲区。请注意,第一次接触真正的帧缓冲区是在转码之前。帧缓冲区仅用于帧时间的一小部分,即1080p时约170微秒。

在一个*台上,帧缓冲区最初是在帧的最开始处获取的,完全抵消了伪三重缓冲的好处,使其行为与严格的双缓冲相同。任何慢帧都会导致vblank丢失,从而降低动态分辨率的效率。

在另一个*台上情况有所好转:等待帧缓冲区是在场景渲染后插入的,但当添加HDR支持时,此等待未被移动。此设置的性能优于前一种情况,但仍会导致帧缓冲区部分的长度超出必要的长度。即使允许帧比严格的双缓冲更早开始,slop也没有最大化,导致丢失Vblank的几率高于最佳值。

获取帧缓冲区的正确位置就在转码之前,这将最大限度地提高slop,并最小化丢掉VBlank的机会。

建议检查代码,确保等待帧缓冲区的时间尽可能晚。它通常是一个单一的函数,但当帧的结构发生变化时,很容易忘记移动它,因为当vsync关闭时,它对性能或计时没有影响。对于DX12,在帧缓冲区资源上执行从当前状态到渲染目标(或其它写入)状态的转换屏障时,会发生等待。

D3D12_RESOURCE_STATE_PRESENT → D3D12_RESOURCE_STATE_RENDER_TARGET

其它*台上也有类似的功能。还建议对这些等待进行计时,可以在这些调用周围卡住GPU时间戳,并将测量结果纳入GPU计时系统。启用vsync时,必须从GPU总时间中减去这些值,才能获得动态分辨率的精确帧定时。

CPU的很大一部分工作负载被用来告诉GPU该做什么,意味着记录状态更改、绘制和分派到命令缓冲区中供GPU使用,以及生成相关的渲染数据,如动态顶点和索引缓冲区。CPU写入的缓冲区和GPU读取的缓冲区通常是双缓冲区或从环中分配的,在CPU建立了命令缓冲区之后,它会告诉GPU通过“kick”开始处理它:这个kick事件类似于GPU和扫描输出之间的帧缓冲区翻转。就像GPU进行扫描一样,slop可以在CPU的启动和GPU实际启动这一帧之间累积。

不过,与GPU扫描系统不同的是,CPU记录命令的顺序与GPU使用命令的顺序大致相同。例如,CPU上的一个帧可能由录制prepass命令、阴影命令、不透明命令等组成。在kick之后,GPU也会按这个顺序绘制。

我们利用这个事实在CPU和GPU之间以比一帧更细的粒度分割缓冲区。CPU不需要预先生成整个帧的所有数据,并一次性启动整个帧,而是可以在每个帧上进行多个较小的启动。例如,CPU可以在记录prepass前半部分的数据后立即启动。GPU开始处理prepass,而CPU同时记录prepass后半部分的数据,依此类推。这允许CPU和GPU帧之间有明显的重叠。为了正确解释这种重叠情况下延迟的工作方式,需要修改slop和work的定义。

在不重叠的情况下,slop是从CPU帧的末尾到GPU帧的开头的段。回顾slop和work的定义:当节流阀时,工作是延迟持续时间的一部分,它会随着时间向前移动,但不会收缩;Slop是延迟持续时间缩短的部分。

现在有了重叠的情况,GPU可以在CPU第一次启动后立即开始工作,Slop需要定义为CPU的第一次启动和GPU启动帧之间的范围。工作就是其它一切:CPU帧开始到第一次启动之间的持续时间,加上整个GPU帧时间。请注意,仅通过允许重叠,测量的工作就显著减少了。

这并不是说CPU和GPU被迫重叠。CPU帧的结束和GPU帧的开始之间可能仍有延迟。因为GPU可以更早地启动,所以slop的测量值会高得多。通过允许重叠,所做的就是允许throttle比在非重叠情况下挤压得更远。

让我们来谈谈性能。GPU周期通常是最宝贵的硬件资源:在《使命召唤》中,更有可能在GPU上工作繁重,而不是CPU上。在理想情况下,CPU记录命令的速度始终比GPU消耗命令的速度快,这样GPU就永远不会空闲和闲置。

但如果其中一个CPU段被kick得太晚,GPU可能会闲置:这被称为GPU气泡。当GPU和CPU明显重叠,并且GPU只稍微落后于CPU时,气泡的风险会更高,尤其是在帧的早期。气泡是低效的,意味着空闲的GPU周期,但它也会使用于动态分辨率的GPU帧时间测量发生偏差,导致不必要的分辨率下降。

但是,如果GPU和CPU之间有一些slop,那么这个slop可以作为缓冲空间,以最小化CPU峰值,产生一个较小的气泡或一起避免气泡。

低slop——重叠越多,延迟越低,易受泡沫影响。高slop——重叠越少,延迟越高,有足够的空间吸收尖峰,避免起泡。避免气泡:并行命令缓冲区生成,调整绘制列表拆分,仔细安排工作,避免争用。减少绘制调用次数,启用CPU剔除、实例化、多重绘制。减少绘制调用开销,如材质排序、bindless。

现在我们有了超快速的命令缓冲区生成,它可以在多个内核上广泛运行。一个线程提取完成的命令缓冲区,然后将它们kick到GPU,在理想情况下,GPU的运行速度比CPU慢,因此它保持稳定,并为整个帧提供数据。

不幸的是,理想的情况并不总是发生。也许其中一个绘图作业开始晚了,或者它被系统线程踢出了一个内核,或者它只是有太多的工作要做。提交线程必须等待作业完成后才能启动命令缓冲区,如果等待时间太长,GPU就会闲置并产生气泡。

但是在一个代码库中有一件很酷的事情可以避免出现气泡。提交线程对绘制作业的等待有一个超时,如果作业没有及时完成,就会发出中止信号,通知作业停止。作业会定期检查信号,如果检测到中止,它会停止迭代并关闭命令缓冲区。然后提交线程接管作业并执行作业本应完成的所有工作。但这一次它不止一次,而是在工作的最后一次。通过更频繁地kick,GPU比提交线程等待作业完成的时间要早一点获得填鸭式的工作。这是以更频繁的kick带来的额外CPU和CP开销为代价的,但它可能会减少气泡的影响或完全避免气泡。

单帧CPU工作涉及许多系统的交互,下面是有一个非常简单的总结:首先,从服务器获取权威的游戏状态,以更新客户端游戏状态;然后,对输入进行采样,并将输入因素纳入客户端游戏模拟;最后,完成了渲染游戏模拟结果所需的所有工作。让我们关注渲染部分。

渲染分为许多小任务,包括遍历场景图、剔除、为模型拾取LOD等等,所有这些作业的最终输出都是一组可见表面列表。然后将作业分配给迭代每个单独的列表,这些作业生成命令缓冲区和相关的渲染数据。最后,一个作业收集所有已完成的命令段,并将它们kick到GPU。此作业还记录表面列表中未包含的命令:解压缩表面、后处理和二维图形。

从概念上讲,所有这些工作可以分为三组:场景准备、绘图和GPU提交。

一帧上的所有工作都是多线程的,并尽可能广泛地运行。然而,*均而言,场景准备和绘图是从广泛运行中受益最多的部分。与可用的内核相比,帧的开头和结尾实现了更低的利用率,留下了更多空闲的CPU周期。

为了使CPU内核更加一致地饱和,绘图和提交工作被分开。然后在这一帧的场景准备之后,下一帧就可以立即开始了。这样一来,下一帧的开头与该帧的绘图和提交重叠,可以产生相当大的加速。

从概念上讲,整个场景准备过程中的一切都可以称为“客户端帧”,有一个专门的客户端线程来协调所有相关的作业。第二个渲染线程执行所有命令缓冲区提交、PostFX、2D绘图等,并包含第二个并行“渲染帧”。在这两条时间线之间是生成命令缓冲区的所有绘图作业,客户端启动这些作业,渲染线程等待它们并启动它们的结果。

与其它生产者-消费者对一样,slop可以在客户端和渲染帧之间累积。

但让我们关注一下这两条时间线之间的同步,以及它如何影响slop。首先,客户端必须与GPU同步,因为它会写入与场景渲染共享的缓冲区。这些资源是双缓冲的,因此在客户端可以启动其帧之前,它必须等待两帧前的GPU场景完成。客户端和渲染线程也共享此数据。最初,渲染线程在下一个客户端帧开始之前不允许启动帧。

缩小之后,底部是GPU时间线。客户端帧的开始与两帧前GPU场景渲染的结束同步。然后,渲染帧的开始与下一个客户端帧的开始同步,会在客户端帧的结束和渲染帧的开始之间创建一些slop。如果管线没有绑定到渲染帧上,为什么会存在这种slop?

渲染帧不能从这里开始,没有正确的理由:就在当前客户端帧的末尾。进行此更改对总延迟没有影响:整个帧的总slop保持不变,只是从渲染帧的左侧移动到右侧。

2、稍后在帧中移动slop。

记住,slop可以防止尖峰,但并不是所有的slop都能*等地防止所有可能的尖峰。

例如,如果客户端帧出现尖峰,帧中稍后的所有slop源都可用于吸收尖峰并避免丢失帧。

但是如果GPU出现尖峰,只有GPU帧后的slop可以吸收尖峰。

需要尽快开始所有工作。

同步已更改为允许渲染帧在客户端帧完成后立即开始。现在请记住为什么渲染帧和客户端帧不允许同时运行:因为客户端写入渲染线程读取的缓冲区。

事实上,渲染帧可以更早开始,从而允许它在客户端帧结束之前开始工作。这种重叠在一个代码分支中实现,它不会像CPU-GPU重叠那样显著减少延迟,但它仍然可以收回几毫秒。这种重叠是通过在客户端和渲染器之间分割共享数据并最小化分割之间的依赖关系来实现的,重叠的程度取决于数据拆分可以在客户端帧依赖关系中执行的距离。

需要修改客户端和渲染帧之间的slop定义,以考虑这种重叠:现在是在客户端中较早唤醒渲染线程和渲染帧实际启动之间的延迟。

关于同步还有最后一件事。请记住,只有GPU在两帧前完成场景,客户端才能启动,可以保护对CPU和GPU之间共享的双缓冲数据的访问。但是,这些缓冲区仅在渲染相关操作(场景准备、绘制和提交)期间由CPU写入。在帧的前半部分,所有的客户端模拟工作都不会触及GPU可见缓冲区(下图上)。这意味着等待可以在稍后的帧中移动,就在写入任何共享缓冲区之前。(下图下)

通过此更改,客户端帧被分为两部分:一部分在与GPU同步之前,另一部分在与GPU同步之后。GPU现在等待在输入样本之后,因此在延迟路径中引入了一个新的slop。

现在我们已经查看了引擎中的所有主要时间线,并确定了延迟路径中的所有工作和slop部分。

现在,让我们将所有这些整合到throttle实现中。我们在输入样本之前引入throttle,这样样本和后续工作就会延迟并向右移动。

注意slop是如何从左边开始被挤出的,但总的工作保持不变。

现在,客户端、渲染器和GPU之间不再有任何slop,throttle已经足够长了。唯一剩下的slop在GPU和扫描输出之间。

如果我们再延迟一点,GPU工作的结束就会被推过vblank并错过一帧。

所以问题是:throttle应该等多久?throttle位于客户端帧的中间,就在输入示例之前。延迟持续时间的结束是在未来的vblank期间,此时该帧将被翻转。从现在到预期翻转之间的时间是我们正在解决的throttle加上帧剩余部分的工作和slop。求解throttle,我们得到从现在到vblank的持续时间减去总工作和slop。但请记住,throttle接*帧的开始,在它运行之前,我们不知道会有多少工作和slop。

取而代之的是,我们必须根据之前的帧数据来估计此帧将有多少工作,而且必须事先决定要达到此帧的目标slop值。问题就变成了:给定一个目标量的slop,throttle应该休眠多长时间?

让我们先看看如何找到预期翻转的时间,需要计算当打算翻转时的vblank的绝对时间。因为每16.6ms出现一次vblank,所以未来帧的vblank可以根据之前的帧计时进行推断。

翻转时间:专用的高优先级线程,等待GPU中断事件,自己使用API提供的时间戳或时间戳。Xbox One的翻转时间过程:1、在当前状态下传递引擎的帧索引:

DXGIX_PRESENTARRAY_PARAMETERS params
params.Cookie = [internal frame index]
...
DXGIXPresentArray( ..., &params )

2、查询帧统计信息以获取时间:

DXGIX_FRAME_STATISTICS stats[4]
DXGIXGetFrameStatistics( 4, stats )

然后获取首个有效的stats[i].Cookiestats[i].CPUTimeFlip

为了预测这一帧的工作量,需要从之前帧中收集工作段的时间戳。在throttle打开之前,需要收集并汇总最新的测量结果,以从前一帧中获得总的工作估计。因为throttle在客户机时间线中,所以客户机工作测量不需要双缓冲,而是可以直接从最后一帧读取,因为它们保证是完整的。但是,由于客户端帧与渲染和GPU帧同时运行,所以在收集时间戳时,工作持续时间可能会处于运行状态,从而导致开始时间戳位于结束时间戳之后。相反,非客户端时间线上的时间戳可以进行双缓冲。每对时间戳中至少应有一个完整:通过比较两对的开始和结束时间,可以确定最*的有效时间戳对。

newEstimate = estimate * smooth + work * (1 - smooth)

目标slop:减少延迟与漏帧风险?值判断;工作有多紧张?从工作值的历史中估算方差;可调整的slop,根据方差进行调整,如果slop低于阈值,则设置为无限(unthrottled))。

减少方差:持续的问题,需要定期重新审视,引擎内测量,通常由计划不周的工作引起:重新安排依赖关系,限制关键作业可以在哪些核心上运行,拆分/合并作业。

我们现在已经讨论了计算throttle所需的每一项:下一帧应该翻转的时间可以从之前的翻转推断出来,通过*滑前一帧的测量值,可以预测该帧的总工作,在这个帧中允许的slop可以通过基于观察到的方差的启发式方法来决定。使用这些项,我们可以计算客户端帧在采样输入之前需要睡眠多长时间。

退一步,让我们回顾一下为什么我们必须通过识别引擎中每一个工作源和slop的过程。

有了工作和slop的正确定义,throttle对这些值的影响变得非常可预测。使得在给定目标slop的情况下,计算合适的throttle变得容易。然后,测量的slop在一帧内迅速收敛到目标附*。

当该文献作者第一次研究延迟时,将引擎视为一个大黑匣子,把大黑匣子里的一切都称为工作,唯一的问题是从队列翻转到翻转的持续时间,忽略了发动机内部所有额外的slop源,但它使功测量变得容易得多。

问题是throttle不再是可预测的,一些最初的throttle挤压是有效的,但不会影响slop。

不再是线性的,需要搜索多个帧。

结果:经审核的数据流和同步提高CPU密集型场景的帧率,修复错误,增加了throttle的测量。*台1下,带throttle:∼*均帧可节省5毫秒延迟,提前移动帧缓冲区等待:避免重场景中的vblank未命中。*台2下,带throttle:约*均每帧减少22毫秒延迟,帧缓冲区等待已在帧中延迟。未来的工作:关注内容和解决方案的工作预测,可变刷新率支持,延迟输入样本重投影。建议:默认情况下打开Vsync的配置文件,使用引擎内定时器可视化延迟,映射从输入样本到扫描输出的数据路径,确保同步尽可能紧密,寻找重叠的机会,测量并减少方差。

14.5.4.7 Mesh Shading

Mesh Shading: Towards Greater Efficiency of Geometry Processing分享了Mesh着色器的技术,包含简史、背景和动力、网格着色编程模型、新兴应用和未来方向。图形管线vs计算着色器是GPU的人格分裂:

如果将计算管线化进光栅呢?

基本网格着色模型:

Meshlet是屏幕空间的标准化接口:

网格着色器编程模型:应用程序定义的线程角色,比如计算、协同生成输出网格,结合顶点和几何体着色,假设面与顶点的比率是固定的,有限动态扩展。

输入表示是应用程序定义的,自定义压缩、非B-rep方案…可使用网格着色器id直接寻址。管线顶部的固定功能消失了…无索引重复数据消除,无顶点属性提取。避免序列化点—可扩展性,负责利用顶点重用的应用程序,可以预计算优化的图元分簇,运行时没有重复工作-节省功率。

动态扩展:几何合成需要支持放大,推广了细分扩展模型,删除固定功能拓扑生成。

带有任务和网格着色器的几何体管线:

任务和网格包含旧着色器阶段:

网格着色器剔除:任务着色器剔除图元分簇,*截头体、背面、亚像素,逐图元的FF剔除。利用预计算,局部化图元分簇,预计算法线分布等。更紧凑,内存比索引缓冲区少25-50%!

动态加载*衡:英伟达的“小行星”演示,每帧5000万以上三角形,使用任务着色器的动态LOD,从一组预计算的LOD中进行选择,生成要渲染的网格着色器,没有CPU干预!

自适应曲面细分:动态三角形细分格式,使用二进制密钥进行高效编码,多帧上的增量细化,网格着色器支持单通道管线,任务着色器更新隐式细分,网格着色器从二进制键解码。

还可以模拟dx11细分。总之,网格着色–几何体的新编程模型,结合了计算的灵活性和流水线调度的效率,通过消除串行瓶颈优化管线,在几何图形处理中实现更高的效率和控制,支持网格着色的新应用程序的机会,如LOD管理、数据结构遍历、几何合成、程序化。

14.5.4.8 Nanite

A Deep Dive into Nanite Virtualized Geometry由Epic Games的Brian Karis等人在siggraph2021峰会上呈现,讲述了UE5 Nanite的实现细节。

行业的研发人员一直有个梦想,就是将几何体虚拟化,就像使用纹理一样,但无需更多的预算,直接使用电影质量源艺术,无需手动优化,没有质量损失。但实现起来比虚拟纹理更难,不仅仅是内存管理,还有几何细节直接的影响。Nanite团队对比了体素、曲面细分、位移映射、点云、三角形等表达方式,最终发现比三角形更高质量或更快的解决方案,使用三角形是Nanite的核心(但其它表达方式也可用于其它方面的实现)。

GPU驱动的流水线:渲染器仍处于保留模式,GPU场景表示在多帧之间保持不变,在事情发生变化的地方很少更新,单个大型资源中的所有顶点/索引数据。逐视图进行GPU实例剔除、三角光栅化,如果仅绘制深度,则整个场景可以使用1个DrawIndirect绘制。三角形分簇剔除的方式先将三角形分成簇,为每个簇构建边界数据,基于边界剔除分簇,如视锥剔除、遮挡剔除等。

其中遮挡剔除是基于层次Z缓冲区(HZB)的遮挡剔除,从边界计算屏幕矩形,在屏幕矩形小于等于4x4像素的情况下,测试最低mip。怎么建立HZB?本帧还没有渲染任何内容,将Z缓冲区从上一帧重新投影到当前帧?需要填洞才能有用,不保守。最终使用2通道的裁剪剔除,最后一帧的可见对象可能在此帧中仍然可见,至少是遮挡体的好选择。2通道解决方案:绘制上一帧中可见的内容,构造HZB,绘制现在可见但不在最后一帧中的内容,几乎完美的遮挡剔除!保守的,只有在可见性发生极端变化时才会出现视觉瑕疵。

将可见性与材质分离,以消除在光栅化过程中切换着色器、材质计算过绘制、深度prepass避免过绘制、密集网格导致的像素quad效率低下,可选的方案有REYES、纹理空间着色、延迟材质。

可见性缓冲区:将几何数据写入屏幕(深度、实例ID、三角形ID),每像素材质着色器:加载缓冲区,加载实例转换,加载3个顶点索引,加载3个位置,将位置转换为屏幕,导出像素的重心坐标,加载和插值属性。听起来很疯狂?不像看上去那么慢,因为有大量缓存命中,没有过绘制或像素quad的效率低下。材质通道写入GBuffer,与其它延迟着色渲染器集成。现在可以用1个绘制调用绘制出所有不透明的几何体,完全由GPU驱动,不仅仅是深度prepass,每个视图栅格化一次三角形。

次线性缩放:可见性缓冲区比以前快得多,但仍然与实例数和三角形数成线性比例。实例中的线性缩放是可以的,至少在通常希望加载的关卡的缩放范围内。这里可以轻松处理一百万个实例,三角形中的线性缩放不合适,如果线性扩展,就无法实现“不管你怎么努力都能成功”的目标。光线追踪是LogN,很好但还不够。即使渲染速度足够快,也无法将这些场景的所有数据存储在内存中。虚拟几何部分与内存有关。但是光线追踪对于Nanite的目标来说速度不够快,即使它适合内存,需要比logN更好的。

换一种说法,屏幕上只有这么多像素。为什么要画更多的三角形而不是像素?对于簇,希望在每一帧中绘制相同数量的簇,而不管有多少对象或它们的密度。一般来说,渲染几何体的成本应该随着屏幕分辨率而定,而不是场景复杂度。就场景复杂度而言,意味着恒定的时间,而恒定的时间意味着LOD。

LOD的解决方案是分簇层次结构(Cluster hierarchy),以簇为基础确定LOD,建立层次结构的LOD,最简单的是簇树,父节点是子节点的简化版(下图左)。LOD运行时找到所需LOD树的切割,基于知觉差异的视点依赖(下图中)。使用流,整棵树不需要同时存储在内存中,可以把树上的任何一块都标记为叶子,然后把剩下的扔出去,渲染期间按需请求数据,类似虚拟纹理(下图右)。

如果每个簇独立于相邻簇决定LOD,则会出现裂缝!粗略的解决方案:在简化过程中锁定共享边界边,独立的簇总是在边界上匹配。锁定的边界:收集密集的杂乱部分(dense cruft),尤其是在深层的子树之间。

可以在构建过程中检测到这些情况,分组簇:强迫它们做出同样的LOD决策,现在可以自由解锁共享边并折叠它们。

不同LOD的切换过程示意图如下:

LOD裂纹的选项:

  • 直接索引相邻顶点。
    • *行视图相关的详细程度控制。
    • *行视图相关的核心外渐进网格。
    • 无依赖并行渐进网格。
    • 需要能够索引任何状态下的边界顶点,由于精度原因,不可能出现裂缝,在计算和内存方面复杂且昂贵,三角形的粒度太细了。
  • 裙子(skirt)。
    • 与邻居的软关系。
    • 体素没有裂缝,为什么没有?实体体积数据,而非边界表示,将网格视为实体体积,分簇必须闭合,至少在移动范围内。
    • 分块LOD。
    • 只有当边界是笔直的空间分割时。
  • 隐式依赖。空间意味着节点之间的依赖关系。
  • 显式依赖。节点之间的依赖关系在构建和存储期间确定。

合批多重三角剖分是一个很好的理论框架,但Brian Karis发现这篇论文非常难以理解,因为它过于抽象和理论化。直到几年后,在部分实现了QuickVDR之后,Brian Karis才再次尝试重新阅读它,发现它作为一个由多个方案组成的超级集合是多么有洞察力。构建步骤的分解与接下来将介绍的基本步骤相同,并进行一些调整。之前的工作会对三角形本身进行分组,从而使每组的三角形数量可变。但需要128个三角形的倍数,这样它们就可以被分成正好128个的簇,将簇分组(而不是三角形分组)可以实现这一点。

构建操作:分簇原始三角形,当NumClusters>1时:将簇分组以清理其共享边界,将组中的三角形合并到共享列表中,将三角形数量简化为50%,将简化的三角形列表拆分为簇(128个三角形)。

合并和拆分使其成为DAG而不是树:

DAG:哪些簇需要分组?将那些具有最多共享边界边的对象分组,更少的边界边更少的锁定边,这个问题称为图分区。最小化边切割代价图的划分优化,图节点=簇,图边=用直接连接的三角形连接簇,图边权重=共享三角形边的数量,用于在空间上闭合簇的附加图边,为孤岛情况添加空间信息、最小图边切割最小锁定边,使用METIS库来解决。

图划分:挑两个,希望剩下的都能解决,簇边界边的数量,每簇的三角形数量<=最大值。与簇分组问题完全相同,图是网格的对偶。需要严格的分区大小上限,图分区算法不能保证这一点,设法用小缺口(small slack)和fallback来强制它。

网格简化:边缘折叠,首先选择最小误差边,使用二次误差度量(QEM)计算的误差,优化新顶点的位置,使误差最小,高度细化,返回引入的错误估计,稍后投影到屏幕上的像素数出现错误,也是最难的部分。

误差度量:基本二次曲面是面积上距离^2误差的积分,具有属性的二次曲面将所有错误与权重混合在一起,完全启发式hack。能做得更好吗?Hausdorff网格距离?渲染结果并使用基于图像的感知?没有比率失真优化的概念。导入和构建时间也很重要,Nanite builder的所有代码都经过了高度优化,希望折叠以优化与返回的像素错误相同的度量。缩放独立性,需要知道屏幕上的尺寸才能知道权重,鸡和蛋的问题,假设大多数集群以恒定的屏幕大小绘制,表面积归一化,边长度限制,大量的调整,非常注意浮点精度,二次曲面中的许多地方都具有固有的灾难性相消。

下面阐述运行时视图相关的LOD。首先是LOD的选择。具有相同边界但LOD不同的两个子图,根据屏幕空间误差在它们之间进行选择,由投影到屏幕的simplifier计算的误差,修正了球体边界中最坏情况点的距离和角度失真,组中的所有簇必须做出相同的LOD决策,相同的输入=>相同的输出。

LOD并行选择:LOD选择对应于剪切DAG,如何并行计算?不想在运行时遍历DAG。定义切割的是父子节点之间的差异。在以下情况下绘制簇:父节点误差太高且当前节点的误差很小,可以并行评估(下图左)!只有当有一个独特的切割,强制误差是单调的(monotonic),父视图误差>=子视图误差,仔细执行以确保运行时更正也是单调的(下图右)。

无缝LOD:二元选择父或子,这不会产生明显的跳变吗?需要*稳过渡吗?涉及几何过渡(Geomorphing)和跨簇过渡。如果误差小于1像素,则它们会有细微差别,TAA将任何差异视为锯齿。

基于表面角度的LOD:简化产生的簇误差是对象空间的标量,未知方向,位置误差可能是方向性的,属性错误的混合使得这很困难。投影到屏幕不考虑表面角度,类似于如果mipmap仅仅是距离的函数,也适用于细分因子计算。表示在细分上扫掠角度曲面,求解需要各向异性LOD,不可能通过簇选择,簇选择必须是各向同性的,就像mip选择一样。其它方案也会产生扫视角成本,如基于点的过绘制、SDF和SVO中的表面读取。

层次LOD选择:可见的簇可能是*的(全部来自单个实例)或远的(来自不同实例的所有根簇)。需要分层,但是DAG遍历是复杂的!记住:LOD决策完全是局部的,可以使用任何想要加速的数据结构!

层次剔除:什么时候可以LOD剔除一个簇?

ParentError<=阈值,基于ParentError的树,而不是ClusterError!BVH8:子节点的最大ParentError,内部节点:8个子节点,叶节点:组中的簇列表。

持久线程:理想的情况是父节点一结束就开始子节点,直接从compute生成子线程。而持久线程模型相反,无法生成新线程,重新使用它们!管理自己的作业队列,单次调度,有足够的工作线程来填充GPU,使用简单的多生产者多消费者(MPMC)作业队列在线程之间进行通信。层次剔除:当工作队列不是空的,将节点提取出队列,测试,让通过测试的子节点入队。单次dispatch,没有递归深度或展开(fanout)限制,无需反复排空(drain)GPU,节省10-60%(通常约25%),具体取决于场景复杂度。依赖于调度行为,要求一旦一个组开始执行,它就不会无限期地挨饿,D3D或HLSL未定义调度行为,在控制台和测试过的所有相关GPU上工作,仅是优化要求,而非Nanite的要求。分簇剔除:叶子是有着共同父亲的簇,作为节点进行类似的剔除检查,输出可见簇。在同一个持久着色器中进行簇剔除,一次可能没有足够的活动BVH节点来填充GPU,执行时间最终可能由最深遍历的深度决定,尽早开始簇剔除工作,并使用它来填补孔洞。两个队列,等待节点出现在节点队列中时,从簇队列处理,合并成64的批。

2通道的遮挡剔除:显式跟踪以前可见的状态变得复杂,LOD选择可能不同,上一帧中可见的簇可能已经不在内存中了!测试当前选定的簇在最后一帧是否可见,使用以前的变换测试以前的HZB。

剔除总览:

接下来聊光栅化。

像素级细节:能用大于1个像素的三角形达到像素级细节吗?取决于如何*滑,一般来说没有。需要绘制像素大小的三角形。对于小三角形,如果用典型的光栅化器来说太可怕了。典型光栅化器:大型tile用binning,微型tile用4x4,输出2x2像素quad,像素高度并行而非三角形。现代GPU设置最大4个三角形/clock,输出SV_PrimitiveID会让情况变得更糟,能用软件光栅打败硬件光栅吗?实际上,软件光栅是硬件光栅的3倍速度!!!

对于小三角形,将三角形binning和只写最后的像素一样困难,即使是单个向量戳也会对小三角形进行浪费的测试,基本边界框更快。在tile级别进行序列化,以处理深度和ROP,输出2x2像素四边形,通用的VS+PS调度、输出格式、排序、混合、clip...针对覆盖多个像素的较大三角形进行了优化,在像素上广泛运行,想要很多像素的三角形,在三角形三运行。

微型软件光栅化器:128三角形簇=>线程组大小128,每个顶点1个线程,变换位置,存储在groupshared中,如果超过128个顶点循环(最多2个)。每个三角形1个线程,获取索引、变换的位置,计算边缘方程和深度梯度,计算屏幕边界矩形,对于rect中的所有像素,如果在所有边内,则写入像素。

for( uint y = MinPixel.y; y < MaxPixel.y; y++ )
{
    float CX0 = CY0;
    float CX1 = CY1;
    float CX2 = CY2;
    float ZX = ZY;
    
    for( uint x = MinPixel.x; x < MaxPixel.x; x++ )
    {
        if( min3( CX0, CX1, CX2 ) >= 0 )
        {
            WritePixel( PixelValue, uint2(x,y), ZX );
        }
        CX0 -= Edge01.y;
        CX1 -= Edge12.y;
        CX2 -= Edge20.y;
        ZX += GradZ.x;
    }
    
    CY0 += Edge01.x;
    CY1 += Edge12.x;
    CY2 += Edge20.x;
    ZY += GradZ.y;
}

硬件光栅化:大三角形使用硬件光栅化器,逐簇选择软件或硬件光栅化,还用于64b原子写入UAV。

扫描线软件光栅化器:多大才算太大?比预期的要大得多,边缘小于32像素的簇被软件光栅化,在rect上迭代测试大量像素,最好的情况包括一半,最糟糕的情况是没有一个(下图左)。扫描线可以更快吗?传统的梯形比较复杂,很多设置和边缘遍历(下图右)。

for( uint y = MinPixel.y; y < MaxPixel.y; y++ )
{
    float CX0 = CY0;
    float CX1 = CY1;
    float CX2 = CY2;
    float ZX = ZY;
    
    for( uint x = MinPixel.x; x < MaxPixel.x; x++ )
    {
        // 难道不能知道经过的x区间是多少吗?
        if( min3( CX0, CX1, CX2 ) >= 0 )
        {
            WritePixel( PixelValue, uint2(x,y), ZX );
        }
        CX0 -= Edge01.y;
        CX1 -= Edge12.y;
        CX2 -= Edge20.y;
        ZX += GradZ.x;
    }
    
    CY0 += Edge01.x;
    CY1 += Edge12.x;
    CY2 += Edge20.x;
    ZY += GradZ.y;
}

// 改进版本
float3 Edge012 = { Edge01.y, Edge12.y, Edge20.y };
bool3 bOpenEdge = Edge012 < 0;
float3 InvEdge012 = Edge012 == 0 ? 1e8 : rcp( Edge012 );

for( uint y = MinPixel.y; y < MaxPixel.y; y++ )
{
    // 这不再是固定点,也就是说它不完全相同。
    float3 CrossX = float3( CY0, CY1, CY2 ) * InvEdge012;
    float3 MinX = bOpenEdge ? CrossX : 0;
    float3 MaxX = bOpenEdge ? MaxPixel.x - MinPixel.x : CrossX;
    // 求解经过的x间隔
    float x0 = ceil( max3( MinX.x, MinX.y, MinX.z ) );
    float x1 = min3( MaxX.x, MaxX.y, MaxX.z );
    float ZX = ZY + GradZ.x * x0;
    
    x0 += MinPixel.x;
    x1 += MinPixel.x;
    // 现在只迭代填充像素
    for( float x = x0; x <= x1; x++ )
    {
        WritePixel( PixelValue, uint2(x,y), ZX );
        ZX += GradZ.x;
    }
    ...
}

光栅化过绘制:没有逐三角形的剔除,没有硬件HiZ剔除像素,软件HZB来自上一帧,剔除簇而不是像素,基于簇屏幕大小的分辨率。过绘制来自大型簇、重叠簇、聚合体、快速运动,过绘制成本:小三角——顶点变换和三角形设置边界,中等三角形——像素覆盖测试边界,大三角形——原子约束。

小实例:当整个网格只覆盖几个像素时会发生什么?DAG以1个根簇结束,128个三角形,停止分辨率缩放。太小的时候剔除?如果结构化成块则不能。显然需要在某个时候合并,即使渲染按次线性扩展,内存也不会。实例内存积累得很快,10M * float4X3 = 457MB,未来所需的分层实例化,实例的实例的实例。Nanite没有合并的特殊解决方案,合并的唯一代理必须在极端距离处替换实例,关键的改进是将这段距离推远。

可见性缓冲区替代物(imposter):atlas中12 x 12的视图方向,XY图集位置八面体映射到视图方向,抖动方向量化。每个方向12 x 12个像素,正交投影,适用于网格AABB的最小范围,8:8的深度和三角形ID,每个网格40.5K始终驻留。光线行进以调整方向之间的视差,由于视差小,只需几步,直接从实例剔除通道中绘制,绕过可见实例列表,想换个更好的方法。

接下来聊延迟材质评估。

材质ID:这个像素是什么材质的?VisBuffer解码:

  • VisibleCluster => InstanceID、ClusterID。
  • ClusterID+TriangleID => MaterialSlotID。
  • InstanceID+MaterialSlotID=>MaterialID。

材质着色:每种独特材质的全屏四边形,跳过与此材质ID不匹配的像素,CPU不知道某些材质是否没有可见像素,无论如何都会发出材质绘制调用,GPU驱动的不幸副作用。如何有效地做?不要测试每个像素是否匹配每个材质通道的材质ID。

材质剔除:模板测试?不想为每种材质重置。可以利用深度测试硬件,材质ID->深度值。构建材质深度缓冲区,CS还为两个深度缓冲区输出标准深度和HTILE,对于所有材质:绘制全屏四边形,quad的Z=材质深度,深度测试设置为等于。

UV导数:仍然是一个连贯的像素着色器,所以有有限差分导数。像素quad跨度(三角形),好!也跨越深度间断、UV接缝、不同的物体(不好!)。解析导数:计算解析导数,三角形上的属性梯度,利用链式规则在材质节点图中传播,如果导数不能用解析的方法计算,回到有限差分,用于使用SampleGrad对纹理进行采样。额外成本微乎其微,<2%的材质通道成本,仅影响纹理采样的计算,虚拟纹理代码已经完成了SampleGrad。

管线数字:

性能:上采样到4k*均约2496x1404,当时使用TAAU,现在使用TSR。约2.5ms以绘制整个VisBuffer,查看+GPU场景=>完成VisBuffer,几乎零CPU时间。约2ms的延迟材质通道,VisBuffer=>GBuffer,CPU成本低,每种材质1次绘制。

接下来聊Nanite的阴影。Nanite的阴影用光线追踪?DXR不够灵活,复杂的LOD逻辑,自定义三角形编码,没有部分BVH更新。想要光栅解决方案,利用所有的其它工作,大多数灯都不移动,应该尽可能多地缓存。

虚拟阴影图:Nanite使新技术成为可能,16k x 16k阴影贴图无处不在,聚光灯用1x投影,点光源用6倍立方体,*行光用Nx clipmap。选择mip级别,其中1纹素=1像素,仅渲染可见的阴影图像素,按LOD需求的Nanite剔除和LOD。

页面大小=128 x 128,页表=128 x 128,带mip。标记需要的页面,屏幕像素投影到阴影空间,选择mip级别,其中1 texel=1像素,标记那一页。为所有需要的页面分配物理页面,如果缓存页面已经存在,请使用该页面,如果没有缓存则无效,从需要的页面掩码中删除。

多视图渲染:具有显著同步开销的深层管线,NumShadowViews = NumLights x NumShadowMaps x NumMips,可以同时剔除和光栅化不同的视图,以分摊成本,使用视图id标记元素。

剔除和寻址页面:如果不重叠所需页面,则进行剔除,与HZB试验类似。软件光栅逐重叠页面发出簇,硬件光栅在原子写入前逐像素的页表间接寻址。

Nanite阴影LOD:使用1 texel=1像素渲染页面,与屏幕像素成比例的NumPages,LOD匹配<1个像素的误差,与阴影像素成比例的numtriangle。阴影成本与分辨率成正比!而不是场景的复杂性,和每像素的光源数量成正比。

接下来聊流(streaming)。虚拟几何,固定内存预算下的无限几何体。概念上类似于虚拟纹理,GPU请求所需的数据,CPU完成了填充, 独特的挑战。在运行时将DAG剪切为仅加载的几何图形,必须始终是完整DAG的有效切割,类似于LOD切割,没有裂缝!

流单元:簇可以与任何父簇重叠,Rendering cluster=>所有父级都不应渲染,父节点没有被渲染=>所有兄弟姐妹(或其后代)都需要渲染以填充孔洞,需要完整的组才能渲染组中的簇。应该在组粒度上进行流,几何体的大小是可变的,使用固定大小的页面以避免内存碎片 => 每页可变的几何体数量。

分页:用组填充固定大小的页面,基于空间局部性以最小化运行时所需的页面。根页面的第一页包含DAG的顶层,总是常驻,所以总是有东西要渲染。页面内容:索引数据、顶点数据、元数据(边界、LOD信息、材质表等),驻留页面存储在一个大的GPU页面缓冲区中。

对于分组部件,松散的:簇很小(约2KB),组可以很大(8-32个集群),如果分配整个组,则会出现明显的松弛。拆分组:组可以跨越多个连续页面,根据内存使用情况在簇粒度上进行拆分,在装载所有部件之前,未激活组,页面现在以簇粒度(约2KB)填充,约*均每页1KB的空闲时间!(128KB页面约有1%的空闲时间)

决定流的内容:对于虚拟纹理而言容易,直接由UV和渐变/LOD级别给出。对于Nanite,需要层次遍历,找到本应绘制的簇,需要超越流式切割。完全剔除层次结构始终驻留,关于簇组的元数据(微小),遍历可以超出加载的级别,立即要求达到目标质量所需的所有级别。

流请求:持久着色器在剔除遍历期间输出页面请求,根据LOD误差请求具有优先级的页面范围,更新已加载页面的优先级。请求的异步CPU回读:添加任何缺少的DAG依赖项,对总优先级最高的页面发出IO页面请求,逐出低优先级页面。处理已完成的IO请求:在GPU上安装页面,修复GPU侧指针,修复指向新加载/卸载页面中的组的指针,修复指向已完成或不再完成的拆分组的指针,将簇标记/取消标记为叶子。

接下来聊压缩。有两种表示:内存表示和磁盘表示。内存表示:直接用于渲染,*即时解码时间,需要支持从可见性缓冲区进行随机访问,量化和位压缩,目标是节省内存和/或带宽。磁盘表示:当数据流入时,转码到内存表示,能够承受更高的解码成本,不需要随机访问,假设数据将由(硬件)基于字节的LZ压缩,目标是减少压缩磁盘大小。

顶点量化与编码:全局量化,艺术家控制和启发的结合,簇以局部坐标存储值,相对于最小值/最大值范围。每簇自定义顶点格式,使用每个组件的最小位数:ceil(log2(range)),顶点比特流需要解码顶点声明,只是一串位,甚至没有字节对齐。使用GPU位流读取器解码:不同格式=>每次读取时重新填充?读取指定读取大小的编译时界限,仅当累计编译时读取大小溢出时重新填充。

顶点位置:不一致的量化会导致裂缝!对于单个对象容易避免,如何避免物体之间的裂缝?用模块构建标高几何图形的常用方法。量化到对象空间网格:每物体2N次方的绝对指数(例如1/16cm),以对象原点为中心,不要标准化到边界。在以下情况下,顶点落在同一网格上:量化级别是相同的,对象之间的*移也是步长的倍数。只有叶子级别是完全对齐的,简化决策在对象之间不一致,运行时LOD决策不同步。

隐式切线空间:0位!切线/副切线是视图空间法线*面中的U/V方向,推导出它们!类似于屏幕导出的切线空间(Mikkelsen),直接在三角形坐标上计算,而不是使用屏幕ddx/ddy,重用已为材质通道中的重心和纹理LOD计算而计算的局部三角形uv/位置三角形。传统的显式切线坐标是通过邻域*均来计算的,对于高多边形网格不太重要。

材质表:每个簇存储材质表,指定材质指定的三角形范围,两个编码别名相同的32位,快速路径编码3个范围,慢路径指向内存,最多64种材质。

磁盘表示:硬件LZ解压,在游戏机里,在使用DirectStorage的PC上,速度惊人,用途广泛,字符串重复数据消除和熵编码。为了更好地压缩:特定于域的转换,假设数据将被LZ压缩,关注LZ尚未捕获的冗余,转换成更可压缩的格式。

GPU转码:在GPU上进行代码转换,并行转换的高吞吐量,只要是并行的,每个字节都有大量(异步)计算,目前运行速度约为50GB/s,PS5上的代码相当未优化,最终将数据直接传输到GPU内存。结合硬件LZ功能强大,LZ处理串行熵编码和字符串匹配工作,可能会成为这一代人的共同模式。Nanite:GPU有上下文,页面可以引用父页面中的数据,而无需CPU拷贝。

Lumen in the Land of Nanite的结果:4.33亿个输入三角形,8.82亿个Nanite三角形,原始数据:25.90GB,全浮点、字节索引、隐式切线,内存格式:7.67GB,压缩:6.77GB,压缩磁盘格式:4.61GB, 自EA版本以来改善了约20%,每个Nanite三角形5.6字节,每个输入三角形11.4字节,1百万个三角形=磁盘上约10.9MB。

关于Nanite的源码剖析,可参见:剖析虚幻渲染体系(06)- UE5特辑Part 1(特性和Nanite)

14.5.4.9 Radiance Caching

Radiance Caching for real-time Global Illumination由Epic Games的Daniel Wright呈现,讲述了UE5 Lumen的实时全局光照实现技术——辐射率缓存(Radiance Caching)。Radiance Caching是Lumen中使用的最终收集技术,针对下一代游戏机上的游戏,在高端PC上提升为质量第一的企业。光线追踪很慢,存在二级BVH、非相干树遍历、实例重叠等问题:

每像素只能提供1/2光线,但高质量的GI需要数百个!

以往的实时研究有辐照度场(Irradiance Fields)、屏幕空间降噪器(Screen Space Denoiser)等方式。而Lumen使用了屏幕空间降噪器(Screen Space Radiance Caching)。

下采样入射辐射,入射光是相干的,而几何法线不是,以全分辨率积分BRDF上的输入照明:

在辐射缓存空间中过滤,而不是屏幕空间(下图左)。首先要进行更好的采样——重要的是对入射光进行采样(下图中)。稳定的远距离照明和世界空间辐射缓存(下图右)。

最终收集管线:

其中屏幕空间的辐照率缓存可以细分成以下阶段:

屏幕探针结构体:带边框的八面体图集,通常每个探针8x8个,均匀分布的世界空间方向,邻域有相同的方向,二维图集中的辐射率和交点距离。

屏幕探针放置:分层细化的自适应布局[Křivánek等人2007],迭代插值失败的地方,最终级别的地板填充(Flood fill)。

自适应采样:实时性需要上限,不希望在处理自适应探头时遇到额外障碍,将自适应探头放在图集底部。

屏幕探针抖动:时间抖动放置网格和方向,直接放置在像素上,没有泄露,屏幕单元格内的遮挡差异必须通过时间过滤来隐藏。

插值:*面距离加权,防止前台未命中泄漏到后台,插值中的抖动偏移,只要还在同一个*面上,在空间上分布探针之间的差异,通过扩展TAA 3x3的邻域达到时间稳定最终照明。

重要性采样:对于入射辐射率Li(l)Li(l),重投射最后一帧的屏幕探针的辐射率!不需要做昂贵的搜索,光线已按位置和方向索引,回退到世界空间探针上。对于BRDF,从将使用此屏幕探针的像素累积,更好的是,希望采样与入射辐射率Li(l)Li(l)和BRDF的乘积成比例。

结构重要性采样(Structured Importance Sampling):将少量样本分配给概率密度函数(PDF)的层次结构区域[Agarwal等人2003],实现良好的全局分层,样本放置需要离线算法。

完美地映射到八面体mip四叉树!

集成到管线中:向追踪线程添加间接路径,存储RayCoord、MipLevel,追踪后,将TraceRadiance组合进均匀的探头布局,以进行最终集成。

光线生成算法:计算每个八面体纹理的BRDF的PDF x 光照的PDF,从均匀分布的探针射线方向开始,需要固定的输出光线计数-保持追踪线程饱和。按PDF对光线进行排序,对于PDF低于剔除阈值的每3条光线,超级采样以匹配高PDF光线。

改进:不允许光照PDF来剔除光线,光照PDF为*似值,BRDF为精确值,借助空间过滤可以更积极地进行剔除,具有较高BRDF阈值的剔除,在空间过滤过程中减少剔除光线的权重,修复角落变暗的问题。

重要性采样回顾:使用最后一帧的光照和远距离光照引导此帧的光线,将射线捆绑到探针中可以提供更智能的采样。

接下来聊空间过滤的技术。

辐射缓存空间中的过滤:廉价的大空间滤波,探针空间为3232,屏幕空间为482482,可以忽略空间邻域之间的发现差异,仅深度加权。从邻域收集辐射率:从相邻探针中匹配的八面体单元收集,误差权重:重投影的相邻射线击中的角度误差,过滤远处的灯光,保留局部阴影。

对于*坦表面的效果是良好的,但对于几何接触的地方,存在漏光的问题:

保持接触阴影:角度误差偏向远光=泄漏,远距离光没有视差,永远不会被拒绝。解决方案:在重投影之前,将邻域的命中距离夹紧到自己的距离。

接下来聊世界空间的辐射缓存。

远距离光存在问题,微亮特征的噪点随着距离的增加而增加,长而不连贯的追踪是缓慢的,远处的灯光正在缓慢变化——缓存的机会,附*屏幕探针的冗余操作。解决方案:对远距离辐射进行单独采样。用于远距离照明的世界空间辐射缓存(The Tomorrow Children [McLaren 2015]的技术),自世界空间以来的稳定误差-易于隐藏,就像体积光照图一样。

管线集成:在屏幕探针周围放置,然后追踪计算辐射,插值以解决屏幕探测光线的远距离照明。

避免自光照:世界探针射线必须跳过插值足迹。

连结光线:屏幕探针光线必须覆盖插值足迹+跳过距离。

还存在漏光的问题。世界探针的辐射应该被遮挡,但不是因为视差不正确。

解决方案:简单的球面视差。重投影屏幕探针光线与世界探针球相交。

稀疏覆盖:以摄像头为中心的3d clipmap网格将探针索引存储到图集中,Clipmap分布保持有限的屏幕大小。

图集:八面体探针图谱存储辐射、追踪距离,通常每个探针为32x32的辐射率。

放置和缓存:标记将在后面的clipmap间接中插入的任何位置,对于每个标记的世界探针:重用上一帧的追踪,或分配新的探针索引,重新追踪缓存命中的子集以传播光照更改。

问题:高度可变的成本,快速的摄像机移动和不连续需要追踪许多未经缓存的探针。解决方案:全分辨率探针的固定预算,缓存未命中的其它探针追踪的分辨率较低,跳过照明更新的其它探针追踪。

重要性采样。BRDF的重要采样:从屏幕探针累积BRDF,切块(Dice )探针追踪分块,根据BRDF生成追踪分块分辨率。超采样*的相机,高达64x64的有效分辨率,4096条追踪!非常稳定的远距离照明。

探针之间的空间过滤:再次拒绝邻域交点,问题是不能假设相互可见性。理想情况下,通过探测深度重新追踪相邻射线路径,单次遮挡试验效果良好,几乎免费-重复使用探针深度。

世界空间辐射缓存还用于引导屏幕探针重要性采样、头发、半透明、多反弹。

回到积分,现在已经在屏幕空间的辐射缓存中以较低的分辨率计算了入射辐射,需要以全分辨率进行积分,以获得所有的几何细节。

重要性采样BRDF会导致不一致的获取,8spp*4相邻探针方向查找,可以使用mips(过滤重要性采样),但会导致自光照[Colbert等人2007],尤其是在直接照明区域周围。将探针辐射转换为三阶球谐函数:SH是按屏幕探针计算的,全分辨率像素一致地加载SH,SH低成本高质量积分[Ramamoorthi 2001]。

粗糙镜面:高粗糙度下的光线追踪反射,在漫反射上聚集。重用屏幕探针:从GGX生成方向,采样探针辐射,自动利用已完成的探针采样和过滤!下采样追踪会丢失接触阴影。全分辨率弯曲法线:使用快速屏幕追踪进行计算,与屏幕探针之间的距离耦合的追踪距离:约16像素。与屏幕空间辐射缓存积分:将屏幕探针GI视为远场辐照度,全分辨率弯曲法线表示*场的数量,基于水*的间接照明[Mayaux 2018],多重反弹*似给出*场辐照度。

时间过滤:抖动探针位置需要可靠的时间过滤,使用深度剔除,结果稳定,但对光线变化的反应也很慢。追踪过程中追踪命中速度和命中深度,属于快速移动对象的投影面积。当追踪击中快速移动的对象时,切换到快速更新模式,降低时间过滤,提高空间过滤。

最终收集性能:


 

未来的工作是去遮挡质量、高动态场景中的时间稳定性、将屏幕空间辐射缓存应用于Lumen的表面缓存以实现多反弹GI。Radiance Cache只是Lumen的一小部分技术,Lumen还涉及表面缓存、软件射线追踪、硬件光线追踪、反射、透明GI等内容。关于Lumen的源码剖析可参见:剖析虚幻渲染体系(06)- UE5特辑Part 2(Lumen和其它)

14.5.4.10 Surfels GI

Global Illumination Based on Surfels讲述了EA内部引擎使用面元来实现实时全局光照的技术。“基于曲面(GIB)的全局照明”是一种实时计算间接漫反射照明的解决方案。该解决方案将硬件光线跟踪与场景几何体的离散化相结合,以跨时间和空间缓存和分摊照明计算。它不需要预先计算,不需要特殊的网格,也不需要特殊的UV集。GIBS支持高保真照明,同时可容纳任意比例的内容。“

基于面元(surfel)全局光照的面元可视化。

该文演讲的内容包含Surfel离散化、非线性加速度结构、辐照度积分技术、颜色溢出缓解、多光源采样、透明度等。首先要弄清楚面元的概念,Surfel=表面元素(Surface Element),一个surfel由位置、半径和法线定义,并*似了给定位置附*表面的一个小邻域。

场景的面元化:从GBuffer中生成面元,当几何图形进入视图时填充屏幕,在世界空间中持久存在,累积和缓存辐照度。迭代屏幕空间填充,将屏幕拆分为16x16块,找到覆盖率最低的tile,应用面元覆盖率和追踪权重,如果tile超过随机阈值,则生成surfel。

除了支持刚体,还支持蒙皮骨骼的面元化。由于所有东西都假设是动态的,所以蒙皮几何体和移动几何体都与解决方案的其余部分交互,就像静态几何体一样。

面元根据屏幕空间投影进行缩放,生成算法确保覆盖范围在任何距离,由非线性加速度结构支撑。

面元管理:所有东西都有固定大小的缓冲区,可预测的预算,固定数量的面元,固定的加速度结构,回收未使用的面元。

回收启发式:让相关的面元保持活跃,最后一次见到时追踪,如果在间隙检测期间看到,则重置,位置更新期间增加。启发式基于激活的面元总数、自从见过的时间、距离、覆盖率。下图是距离启发式:

光照应用:对每个像素:查找表面网格单元,从单元格里取N个面元,累积表面辐照度,按距离和法线加权,如果辐照度权重<1,则添加加权的*均单元格的辐照度。存在光照溢出的问题,使用径向高斯深度来解决:

修复前后对比:

积分辐照度图示:

积分器:修正指数移动*均估值器[BarréBrisebois2019],跟踪短期均值和方差估计值,使用短期估计器调整混合因子,使我们能够快速响应变化,同时收敛到低噪点。基于短期方差的偏差光线计数,使用射线计数通知相对置信度的多尺度均值估计器,反馈回路对变化和变化做出快速反应,在稳定的情况下保持光线计数小。

BRDF的重要性采样:累积漫反射辐照度,假设是兰伯特BRDF,通过对余弦叶进行重要采样来生成光线。

射线引导:

每个surfel在其半球上生成一个移动*均6x6亮度图,存储在单个4K纹理中(可支持所有surfels的7x7),每个纹理8位+每个纹理单个16位缩放,规范化每帧函数。

有了重要性采样变量,函数的每个离散部分都将根据其值按比例选取,还有它的概率密度函数,这是函数在那个位置的值。

辐照度共享:利用附*的面元数据,允许surfels查找相邻surfels的辐射,结构加速,使用与surfel VPL相同的权重,Mahalanobis 距离、深度函数。

辐照度共享前后对比:

还可以使用BF5方法对光线进行排序,按位置和方向排列的箱射线,12位表示空间,4位表示方向,空间散列的单元定位,射线方向定向,计算箱子总计数和偏移量,根据光线索引和以前计算的面元偏移对光线重新排序。

多光源采样使用了重要性采样(随机光源分割、储备采样)。随机光源切割是小样本快速收敛,需要预先构建的数据结构,采样可能开销很大。

蓄水池采样(Reservoir Sampling)示意图:

光线追踪探针示意图:

RT探针体积结构:透明对象需要大屏幕支持,例如不透明对象,Clipmap是满足需求的最佳选择:保持*距离的细节,支持大规模场景,具有低内存成本的稀疏探针放置,LOD的变速率更新。

Clipmap更新算法:计算更新方向和距离,复制移位后有效的探针数据,用更高级别的探针初始化新创建的探针。

4级clipmap的放置示意图:

Clipmap采样过程如下:

进一步的采样优化:蓝色噪声梯度抖动采样。

一帧概览:

  • 持续的。位置更新,回收利用,网格分配,射线排序,光线追踪,Clipmap更新,探针追踪。
  • 创建。几何法线重建,空隙填充,射线排序,光线追踪,写入持久存储,写入探针体积。
  • 过滤。空间降噪,时间降噪。
  • 应用。注入新的创建,应用照明(以四分之一区域分辨率运行),照明上采样,Clipmap采样。

14.5.5 结果期总结

CPU和GPU的总计算能力和并行度都有了显著的提升,各类省电、高性能的架构、部件和管线诞生,例如多核、TBDR、HSR、FPK、RT Core等等。

游戏引擎向着更加逼真、可信度、影视级效果发展,各类实时的全局光照计算层出不穷,最为突出的是基于硬件的光线追踪和UE5的Nanite和Lumen。现代图形API的诞生和发展,给游戏引擎带来了新的机会,使得游戏引擎有了更大的发挥空间,从而也催生了基于渲染图的中间层渲染计算,也使得渲染画质迈向新的高度。

各类渲染技术(如AA、地形、物理、景观模拟、特殊材质等)的出现,为游戏引擎和游戏开发团队提升了扎实的动力。

移动端、XR、云渲染、Web端等分支也得到了充分的发展,成为行业丰富生态的重要组成部分。

14.6 本篇总结

14.6.1 游戏引擎的未来

未来将实现和普及一个场景一次绘制调用。GPU管线命令序列几乎每帧不变,命令不会根据场景结构进行更改,绘制调用的数量可预测且合理较低,可管理的最大索引缓冲区大小,最糟糕的内存开销约为6%,在嵌套命令列表中记录所有可能损害的GPU管线命令。

设备生成的命令。有条件地编写GPU管线命令的可能性,GPU上的PSO选择更具灵活性,基于运行时条件,通过编译时优化切换PSO的可能性,避免空牵引链的可能性,减少了保留内存大小。

此外,硬件和引擎的架构更加完善、全面,工具链更加人性化、自动化、智能化,光线追踪逐渐普及,视觉效果更加逼真、绚丽。PBR更加彻底,并引入纳米级的光照模型,如干涉、色散、衍射、叠加、焦散等等。AI的融合更广泛、普遍,并行化、多GPU、多设备逐渐普及。GPU-Driven、离线技术实时化、其它分支充分发展,渲染调试工具更加完善、智能、可视化。

免责声明:以上纯凭经验预测,不具备权威性。

14.6.2 结语

本篇是笔者撰写的所有文章中,参考文献最多的一篇,达到540多篇。在整理参考文献之前,参阅了1000多篇各类文献,即便如此,笔者尚以为这只是图形渲染技术和游戏引擎技术的冰山一角。

很多技术深深刻着时代的烙印,但也很多技术突破时代的枷锁,即便过去数十年,依然历久弥新,流传于当今主流的引擎之中。如同历史一样,技术演进的轮回一直在上演着,从未间断。

本篇从规划到完成,耗费*半年,总字数达到43万多,参考文献达到540多,配图达到3400多,蕴含了*几十年来实时渲染领域的主要研究和应用成果,信息密度高,总量也大。所以,童鞋们学fei了吗O_O?发量可还健在?(反正博主先掉为敬,童鞋们随意~)

特别说明

  • 感谢所有参考文献的作者,部分图片来自参考文献和网络,侵删。
  • 本系列文章为笔者原创,只发表在博客园上,欢迎分享本文链接,但未经同意,不允许转载
  • 系列文章,未完待续,完整目录请戳内容纲目
  • 系列文章,未完待续,完整目录请戳内容纲目
  • 系列文章,未完待续,完整目录请戳内容纲目

参考文献

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ANSI/ISA-95.00.03-2013企业控制系统集成-第3部分:活动是一个标准,用于指导企业在其生产和运营过程中如何进行控制系统集成的活动。 企业控制系统集成是指将不同的控制系统和业务系统连接起来,以实现生产和运营过程的协调和优化。这个标准的第3部分主要关注的是活动方面,即在控制系统集成过程中需要进行的各种活动和任务。 这些活动包括但不限于需求分析、系统设计、软件开发、系统测试和验证、部署和维护等。标准提供了详细的指导,包括定义每个活动的目标、输入和输出,以及所需的资源和技能。 通过遵循这些指导,企业可以更好地组织和管理其控制系统集成活动,以确保实施的成功和可持续性。此外,标准还可以帮助企业减少集成过程中的错误和风险,提高生产效率和质量。 在实施ANSI/ISA-95.00.03-2013标准时,企业应该首先进行需求分析,了解集成的目标和要求。然后,根据需求来制定系统设计,并进行软件开发和测试。在完成开发和测试后,系统需要进行验证,以确保其符合预期的功能和性能要求。最后,系统需要部署和维护,以确保其正常运行和持续改进。 总而言之,ANSI/ISA-95.00.03-2013企业控制系统集成-第3部分:活动是一个详细的指导标准,用于帮助企业进行控制系统集成的各种活动和任务。通过遵循这个标准,企业可以更好地组织和管理其集成活动,提高生产效率和质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值