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

 

 

14.4 开花期(2010~2015)

本章将阐述2010年度前期的引擎架构、渲染和相关模块的技术。

14.4.1 图形API

14.4.1.1 DirectX

在2010时代的前期,DirectX发布了11的两个小版本(11.1、11.2)和DirectX 12。

DirectX 11.1于2012年8月发布,新增的特性有:

DirectX 11.2于2013年10月发布,新增的特性有:

DirectX 12.0于2015年7月发布,新增的特性有Resource Binding、Tiled Resources (Texture2D)、Typed UAV Loads (additional formats)等。

DirectX 11的曲面细分阶段有Hull Sahder(壳着色器)、Tessellator(细分器)、Domain Shader(域着色器)。它们的作用如下:

  • Hull Sahder。有两个阶段:控制点阶段(每个控制点运行一次)和打包阶段(每个输入图元运行一次,返回细分因子)。
  • Tessellator。生成新的顶点,更高的细分因子意味着生成更多的三角形。
  • Domain Shader。每个顶点运行一次,用于每个顶点的表面计算,作为参数坐标传入的顶点数据。

DirectX 11曲面细分管线。

DirectX 11曲面细分的应用案例1。

DirectX 11曲面细分还可以应用于曲面参数化、贴花细分等:

Domain Shader开销很大,特别是小三角形效率更低,硬件希望三角形至少为8像素,优化方式是减少细分顶点的数量,以降低Domain Shader执行,Hull Shader可以调整细分因子。

利用DirectX 11曲面细分,还可以实现距离自适应细分(Distance Adaptive Tessellation)、位移自适应(Displacement Adaptive)、贴花细分的位移自适应以及基于Hull Shader的背面裁剪等技术。

此外,DirectX 11曲面细分的优化技术有:视锥体剔除、组合细分的绘图调用、方向自适应细分(使用dot(V, N) 查找轮廓块)
、减少Hull Shader的输入和输出数据、尝试将Domain Shader的工作转移到像素着色器(减少Domain Shader输出的数据)、使用Stream Out避免重复细分对象。

Direct3D 11 Performance Tips & Tricks谈及了DirectX 11的SM 5.0、资源和资源视图、多线程等方面的技术。其中SM 5.0的特点:

  • 使用Gather/GatherCmp()进行快速多通道纹理提取。

    • 使用较少数量的RT,同时仍然有效地获取,将深度存储到SSAO的FP16 alpha,使用Gather*() 获取alpha/深度的区域。
    • 仅在三个操作中获取4个RGB值,常用于图像后处理。

  • 使用“保守深度”为快速深度精灵保持提前深度拒绝有效。

    • 从PS输出SV_DepthGreater/LessEqual而不是SV_Depth。即使使用经过着色器修改的Z,也能保持提前深度剔除有效。
    • 硬件/驱动程序将强制执行合法行为。如果写入无效的深度值,它将被截取为光栅化值。

  • 使用EvaluateAttribute*()进行快速着色器AA,无需超级采样。

    • 在子像素位置调用EvaluateAttribute*(),用于程序化材质的更简单的着色器AA。
    • 输入SV_COVERAGE以计算每个覆盖子样本的颜色并写入*均颜色,图像质量比纯MSAA稍好。
    • MSAA alpha测试的输出SV_Coverage。此功能自DX 10.1以来一直存在,EvaluateAttribute*()使实现更简单,但要检查覆盖率的alpha是否已经满足需求,因为它应该更快。
  • UAV和原子:明智地使用PS散射、UAV和Interlocked*()操作。

  • 减少流输出通道。可寻址的流输出,单个通道输出最多4个流,所有流都可以有多个元素。

  • 使用几何着色器实例化编写更简单的代码。使用SV_SInstanceID代替循环索引。

  • 使用[earlydepthstencil]对PS强制进行早期深度模板测试。

    • 如果写入UAV或AppendBuffers,则可以显著提高速度。
    • 将[earlydepthstencil]放在像素着色器函数声明上方以启用它。

启用[earlydepthstencil]后,像素着色器将剔除UV之外的所有像素。

  • 使用众多新的内在函数来实现更快的着色器。

    • 快速的位操作:countbits()、reversebits()(FFT中需要)等。
    • 转换指令:f16to32() 、f32to16(),更快的打包/拆包。
    • 快速粗糙导数 (ddx/y_coarse)。
    • ...
  • 明智地使用子程序(subroutine)的动态着色器链接。

    • 子程序不是免费的,没有跨函数边界优化。
    • 仅对大型的子程序使用动态链接,避免使用大量小的子程序。

资源和资源视图的特点:

  • 减少内存大小和带宽以获得更高的性能。
    • BC6和BC7提供新功能,非常高的质量和HDR支持,所有静态纹理都应该是可压缩的。

  • 使用只读深度缓冲区以避免复制深度缓冲区。
    • Direct3D 11允许对仍绑定用于深度测试的深度缓冲区进行采样。
      • 如果深度是GBuffer的一部分,则对延迟光照很有用。
      • 适用于软粒子。
    • AMD:使用深度缓冲区作为SRV可能会触发解压缩步骤,尽可能晚地进行。

DX11的其它特性:

  • 免费线程资源创建。

    • 使用快速的异步资源创建,通常应该更快、更高并行。
    • 不要在使用资源的帧中销毁资源。销毁资源很可能会导致同步事件。
    • 避免创建、渲染、销毁的串行序列。
  • 显示列表(从延迟上下文创建的命令列表)。

    • 确保应用程序是多线程的。
    • 仅当命令构造是一个足够大的瓶颈时才使用显示列表。
    • 考虑显示列表来表达GPU命令构造中的并行性,避免细粒度的命令列表。
    • 驱动程序已经是多线程的。
  • 延迟上下文。

    • 在延迟上下文中,Map()和UpdateSubResource()将使用额外的内存。请记住,所有初始Map都需要使用DISCARD语义。
    • 请注意,在单核系统上,延迟上下文将比仅使用立即上下文慢。对于双核,最好只使用立即上下文。
    • 除非有显著的并行性,否则不要使用延迟上下文。
  • 其它。

    • 使用DrawIndirect进一步降低CPU开销。使用来自GPU写入缓冲区的参数启动实例化绘制调用/调度,可以使用GPU进行有限的场景遍历和剔除。
    • 使用Append/Consume缓冲区用于快速的流输出(stream out)。比GS更快,因为没有输入顺序约束,具有“无限”数据扩展的流输出。

14.4.1.2 OpenGL

在2010到2015年期间,OpenGL发布的版本和特性如下表:

版本 时间 特性
OpenGL 3.3 2010年3月 Mesa支持软件驱动程序SWR、软件管线和带有NV50的旧Nvidia卡。
OpenGL 4.0 2010年3月 对标Direct3D11。硬件支持:GeForce 400系列及更新版本、Radeon HD 5000系列及更新版本、英特尔Ivy Bridge处理器和高清显卡。
OpenGL 4.1 2010年7月 硬件支持:GeForce 400系列和更新版本、Radeon HD 5000系列和更新版本、英特尔Ivy Bridge处理器和高清显卡。GPU实现此规范的最小“最大纹理大小”为 16k×16k。
OpenGL 4.2 2011年8月 带原子计数器的着色器和加载-存储-原子读取-修改-写入操作到纹理;绘制从GPU顶点处理(包括曲面细分)捕获的多个数据实例,以使复杂对象能够有效地重新定位和复制;支持修改压缩纹理的任意子集,而无需将整个纹理重新下载到GPU以显著提高性能;部分硬件支持。
OpenGL 4.3 2012年8月 计算着色器、着色器存储缓冲区对象、图片格式参数查询、ETC2/EAC纹理压缩作为标准功能、完全兼容OpenGL ES 3.0 API、接收调试消息、纹理视图以不同方式解释纹理、提高安全性和稳健性。
OpenGL 4.4 2013年7月 强制缓冲区对象使用控制、对缓冲区对象的异步查询、更多界面变量布局控件在shader中的表达、同时高效绑定多个对象。
OpenGL 4.5 2014年8月 直接状态访问 (DSA) 、刷新控制、稳健性、OpenGL ES 3.1 API和着色器兼容性。

同期,OpenGL ES发布的版本和特性如下表:

版本 时间 特性
OpenGL ES 3.0 2012年8月 渲染管道增强:遮挡查询、变换反馈、实例化、MRT、ETC2/EAC作为标准;新版的GLSL ES着色语言,完全支持整数和32位浮点运算;增强纹理功能,包括保证支持浮点纹理、3D 纹理、深度纹理、顶点纹理、NPOT纹理、R/RG纹理、不可变纹理、2D阵列纹理、swizzles、LOD和mip级别钳制、加强的纹理和渲染缓冲区格式。
OpenGL ES 3.1 2014年3月 计算着色器、独立的顶点和片段着色器、间接绘图命令。
OpenGL ES 3.2 2015年8月 几何和曲面细分着色器、浮点渲染目标、ASTC、增强的混合、高级纹理目标:纹理缓冲区、多重采样2D数组和立方体贴图数组、调试和健壮性功能。

14.4.1.3 其它图形API

  • Mantle

Mantle由AMD最初于2013年开始与DICE合作开发,被设计为Direct3D和OpenGL的替代方案,主要用于个人计算机。它是针对3D视频游戏的低开销渲染API。

不久之后,AMD将Mantle API捐赠给了Khronos组织,后者将其开发为Vulkan API。简而言之,Vulkan的前身就是Mantle。2015年,Mantle的公共开发暂停,并在2019年完全停止,因为DirectX 12和源自Mantle的Vulkan越来越受欢迎。

  • Vulkan

2015年初,LunarG(由Valve资助)展示了一款Linux驱动程序,在HD 4000系列集成显卡上实现了Vulkan兼容性。

2015年8月,Google宣布未来版本的Android将支持Vulkan。2015年12月,Khronos集团宣布Vulkan规范的1.0版本已接*完成,将在符合标准的驱动程序可用时发布。

  • Metal

Metal是由Apple创建的低级、低开销的硬件加速3D图形和计算着色器API,在iOS 8中首次亮相,结合了类似于OpenGL和OpenCL的功能,旨在通过为iOS、iPadOS、macOS和tvOS上的应用程序提供对GPU硬件的低级访问来提高性能,可以与Vulkan和DirectX 12等其它*台上的低级API进行比较。

Metal自2014年6月起在搭载Apple A7或更高版本的iOS设备上可用,自2015年6月起在运行OS X El Capitan的Mac(2012 型号或更高版本)上可用。

14.4.2 硬件架构

PowerVR Graphics - Latest Developments and Future Plans阐述了2015年Imagination的PowerVR Rogue硬件架构和特性。PowerVR Rogue支持基于分块的延迟渲染器,建立在前五代技术的基础上,在2012年消费电子展上正式宣布,USC(Universal Shading Cluster,通用着色簇)是新的标量SIMD着色器内核,通用计算机是核心的最大特色,同时也非常适合传统的图形渲染。

该架构还支持延迟光栅化(Deferred rasterisation),不直接让GPU做任何像素着色,硬件支持完全延迟的光栅化和像素着色,光栅化是像素精确的。该技术被称作隐藏表面删除(Hidden Surface Removal,HSR)。

TBDR可以在渲染的所有阶段节省带宽,仅获取tile所需的几何图形,仅处理tile中的可见像素。高效处理,最大限度地利用可用的计算资源,尽可能利用硬件带宽。最大化核心效率,少激活USC以节省消耗。最小化带宽,减少纹理以省电,几何体提取和装箱通常超过每帧带宽的10%,为渲染的其它部分节省带宽。

Rogue USC是架构的搭建积木,全称统一着色簇,Rogue架构的基本构建块,成对布局,共享TPU(纹理处理单元),1、0.5和0.25的USC设计是特殊的,设计中的不同*衡,倾向于用在非游戏应用程序。

16宽的硬件,32宽的分支粒度,每个时钟运行半个任务/warp,标量SIMD,优化的ALU管线,混合使用F32、F16、整数、浮点特殊值、逻辑运算。可在IP核心中配置,F16路径有时是可选的,F16路径的性能在第一代之后显著提高。着色器中的性能:F32路径为双FMAD,F16路径下每个周期可以执行不同的操作,具体取决于着色器,不过,ISA可以使用反汇编编译器进行查询。

向量架构难以良好地编程,标量ALU好处多,虽然不是免费的午餐,但可以让性能更加可预测。


PowerVR Series6XT Rogue硬件架构。


PowerVR Series7XT Rogue硬件架构。

Series6XT到Series7XT:改变了架构的扩展方式,改进了USC,流线型ISA,新特性是硬件曲面细分、DX11兼容USC(主要是精度)、FP64。

PowerVR Graphics Wizard硬件架构,新增了光线追踪相关的单元和处理。

Wizard的3个独特功能:固定功能射线盒和射线三角形测试器,一致性驱动的任务形成与调度,流式场景层次生成器。相干引擎(Coherency Engine)可以让我们同时处理下图所示的所有光线:

PowerVR还提供了PVRTrace、PVRTune、PVRShaderEditor等开发和分析工具。

对于Rogue图形驱动程序,DDK(驱动程序开发工具包)发布流程:向PowerVR IP许可证持有人发布的参考驱动程序源代码,大约每6个月进行一次小修改,顶级客户尽早参与,DDK正式发布后不久产品中的驱动程序。

14.4.3 引擎演变

14.4.3.1 综合演变

2010前后的通用游戏引擎都具备场景管理器,包含所有对象、材质、灯光以及所有场景设置(例如分辨率、视角、抗锯齿级别等)的列表。为了避免加载已经加载的对象和纹理,可能还提供了纹理和对象缓存。其中材质可以从文件(自定义着色器)加载或动态生成(下图),材质属性可以从颜色和表面纹理等基本材质设置到具有多个纹理的视差映射等更高级的功能。

材质生成器是主要组成部分,可以根据用户定义的材质表面属性动态生成着色器,这些属性可以是用于精确定义表面属性的颜色或纹理。除了可以通过相加、相乘或混合来组合的多层颜色纹理外,还可以定义高级表面,包括凹凸、视差、环境和立方体贴图(静态或动态)。基于所有这些属性,材质生成器生成着色器,能够执行生成所需材质表面的命令。

着色器链接和编译后,默认顶点属性就可以绑定到材质,设置材质配置(通过uniform结构),着色器即可使用,其它公共变量(如变换矩阵)在统一块(uniform block)中定义并由所有着色器共享。

同时,HDR渲染管线也逐渐在游戏引擎中普及开来,以适应HDR照明的发展趋势,由于当时的显示设备大多依然是LDR,需要色调映射将HDR映射回LDR。(下图)

下图是基于物理照明模型和感知色调映射的虚拟环境HDR渲染管道的一种实现流程:

The Game Engine Space for Virtual Cultural Training: Requirements, Devices, Engines, Porting Strategies and a Future Outlook谈到了2010年的游戏*台功能的趋势及它们与文化建模的相关性和模拟,还有行业的未来发展,阐述了当时流行的游戏引擎的特点,并针对不同的设备和*台给予多维度的技术选型方案。

文中提到幻引擎 3 (UE3) 是最常用的商业引擎之一,部分原因是它的存在时间比大多数引擎都长,还因为Epic Games定期添加新功能和改进。在视觉保真度方面,虚幻引擎3与其他顶级3A游戏引擎不相上下,非常擅长在为文化培训而设计的3D虚拟环境中创建所需的真实感。

CryENGINE 3的优势是高保真图形、角色和开发工具方面的首要游戏引擎,纹理、材料、照明和动画的质量非常出色,强大的动画系统允许使用面部和全身动作捕捉,以及游戏内动画混合、同步、分层和重定向;用于环境创建、动画、材料编辑和脚本编写等的开发工具对用户友好且功能强大;具有高质量的人工智能和寻路系统,支持可视化脚本系统进行修改;对室外和室内环境都有很好的支持。

Gamebryo Lightspeed的优点是提供快速的应用程序开发框架,还支持处理声音、图形、物理和多人游戏的各种技术。Gamebryo的视觉保真度可与业内最好的引擎相媲美,曾被用于制作具有广阔景观和复杂面部细节的游戏,例如Fallout 3和Oblivion。

Unity3D的优点是一流的跨*台开发支持,强大的社区和最低的入门成本,简化了大多数工具,所有3D资源都可以以原始格式导入,从而消除了导入为引擎特定格式的任务,保留资产的原始格式允许非破坏性工作流程,从而显著提高管线效率。

以上引擎在跨*台与满足需求的排名如下图:

该文改给出了一种加速游戏开发工作流的管线:

Future graphics in games阐述了CryTek在2010年使用的渲染技术,并预测未来的图形趋势。文中对比了延迟光照、延迟着色及前向渲染在带宽和材质种类之间的对比:

文中还提到,渲染架构的突破并非易事,已由硬件供应商多次证明,尤其是最*多次尝试使用软件渲染器,围绕着庞大的基础设施的拖尾,需要多年开发经验的成果。图形架构将更加多样化,回到旧的好技术,如体素、微多边形等。

未来将给某些游戏打上烙印的替代品:基于点的渲染、光线追踪、像往常一样光栅化、微多边形,数据表示有稀疏体素八叉树(数据结构)、稀疏面元八叉树。

不得不说,当年(2010年)的这个预测真是准啊,截至当前(2022年),已经流行或逐渐流行的技术包含了基于点的渲染、光线追踪、微多边形及稀疏体素八叉树(数据结构)、稀疏面元八叉树。

稀疏体素八叉树(数据结构)的优点:数据结构是未来替代渲染的证明,非常适合独特的几何、纹理几何,纹理预算变得不那么相关,艺术自由成为现实,自然地适合自动LOD方案。缺点是基础设施和硬件都没有,稍微占用内存,非常适合光线追踪,但仍然太慢。

CryTek已经在生产中使用稀疏体素八叉树:在关卡导出期间用于烘焙几何体和纹理,存储在三角形分区的稀疏八叉树中,非常易于管理和流式传输几何和纹理,无需GPU计算(尽管有虚拟纹理),自动校正LOD构建,自适应几何和纹理细节(取决于游戏玩法)。每个级别的磁盘空间都很大!使用积极的纹理压缩,明智地烘焙,而不是整个世界。

感知驱动的图形有:基于PCF的软阴影、随机OIT、基于图像的反射、SSAO、大多数后处理、LPV、很多随机算法等,实时图形中的大多数都是假设,因为人类的感知有限。实时图形是感知驱动的,因为人的眼睛有一些特点:

  • 约350M(3.5亿)像素的空间分辨率,在这方面很难欺骗它。
  • 约24Hz的时间分辨率,非常低,给了技术操作的空间,当大于40Hz时,人眼就不会注意到闪烁。
  • 我们不会为另一台机器创建图像,我们的目标客户是人类。

欠采样/超采样的技术有:

  • 空间
    • 欠采样
      • 推断着色(Inferred shading)
    • 景深
      • 解耦采样
  • 时间
    • 时间抗锯齿
    • 运动模糊
  • 混合
    • 时空(spatio-temporal,后来的不少文献称为spatial-temporal)抗锯齿

存在混合渲染。没有灵丹妙药的渲染管线,即使是REYES也没有以原始形式用于电影。通常结合所有合适和有帮助的内容:光线追踪的反射和阴影(可能是三角形/点集/体素结构/等),体素以获得更好的场景表示(部分),屏幕空间接触效果(例如反射)等等。

*期的趋势是立体(stereoscopic)渲染,技术存在很长时间,因技术而流行,在游戏中也是如此,没有新概念,但类似于摄影艺术,一条黄金法则:不要让观众感到疲倦。Crysis 2已经具备一流的3D立体支持,使用深度直方图确定轴间距离。

CryENGINE 3中支持的立体渲染模式:强力立体渲染,带重投影的中央眼框,一只眼睛的实验性随机渲染。立体输出模式:浮雕(分色)、隔行扫描、水*联合图像、垂直联合图像、两台显示器。

为了找出当时的硬件架构的问题,使用小型综合测试(模拟 GPU 行为)模拟高度并行调度,拥有512个内核(也可以解释为共享缓存的插槽),32k个相同的小任务要执行,每个项目在一个核心上需要1个时钟(所以合成),在256到2048个线程的范围内,在总时间中考虑调度开销(任务投送、上下文切换、开销权重并不重要)。输出的几个重要参数的曲线如下图:

上图可以知道,总时间曲线可分为饱和阶段和并发并行阶段,拐点在调度开销从0爬升处。在饱和阶段总时间和执行时间不断下降,但到了并发并行阶段,调度开销随着线程数量增加而增加,总时间也随调度开销成类似的曲线增加。

另一个测试使用真正的GPU!渲染屏幕空间效果 (SSAO),带宽密集型像素着色器,每个项目在一个核心上需要1个时钟(所以合成),在5到40个线程的范围内,缓存污染在饱和状态之后立即导致峰值,时间渐*地达到更多线程的饱和性能:

调度开销是个问题,并行可扩展性,对于同质任务,饱和时达到最大值,异构工作负载如何?最小值的存在取决于调度对性能的影响,我们需要减少它,需要可配置的硬件调度程序,使用它可以实现类似GRAMPS的架构,光线追踪变得更快,SoL出现带宽瓶颈。

实际上,需要不同的原子,主要使用它来进行收集/分散操作,而必须可以处理浮点数!在大多数情况下,不需要结果:改进无回读的原子(即发即忘的概念),操作应该在内存控制器/智能内存端进行。需要一个数量级的图形原子性能。

未来的技术挑战:切换到可扩展的代码库,考虑并行和异步作业,多线程调度,更大的代码库、多个*台和API。未来的生产挑战:资产成本每年增加约50%,内容除了质量提高,还变得越来越“可互动”,考虑改进工具、管道和瓶颈以产生反作用,自动化源后端 ->资源编译器,工具越好,产出就越便宜和/或越好。

在效率上,可以降低数据精度,不需要像静止图片那样的高分辨率和清晰度,图形硬件应该挑战缓存不一致的工作负载。

总之,实时渲染管线改造指日可待,需要硬件改进,当前生产实时渲染技术的演变,准备新的表示和渲染管道,更好的并行开发基础设施,工具和创作管道需要现代化,考虑服务器端渲染:可能会彻底改变方向。感知驱动的实时图形是技术驱动力,避免图形技术中的恐怖谷。

DirectCompute Optimizations and Best Practices分享了DirectCompute的概念、特性、机制、使用及在NVIDIA GPU内的优化技巧。

DirectCompute(直接计算)适用于Windows Vista和Windows 7的微软标准GPU计算*台,在DX10和DX11硬件受支持,CUDA架构的另一个实现,和OpenCL、CUDA C是同级API。

DirectCompute允许通过计算着色器在CUDA GPU上进行通用计算,与Direct3D资源相互操作,包括所有纹理特征(立方体贴图、mip 贴图),类似于HLSL,在Windows上跨所有GPU供应商的统一API,保证跨不同硬件有相同的结果。

DirectCompute程序将并行工作分解为线程组,并调度多个线程组来解决一个问题。如下图,调度(Dispatch)是线程组的3D网格,数十万个线程;线程组(Thread Group)是线程的3D网格,数十或数百个线程;线程(Thread)是着色器的一次调用。

并行执行模型如下图,同一组中的线程并发运行,不同组中的线程可以同时运行。

内存合并(Memory Coalescing)是half-warp的协调读取(16个线程),是全局内存的连续区域:64字节 - 每个线程读取一个字:int、float、...,128 字节 - 每个线程读取一个双字:int2、float2、...,256 字节 – 每个线程读取一个四字:int4, float4, ...

内存合并的附加限制有区域的起始地址必须是区域大小的倍数,half-warp中的第k个线程必须访问块中的第k个元素,例外情况是并非所有线程都必须参与,比如预测访问、half-warp内的分歧。

读取浮点数的合并访问的示例如下图,上排是所有线程都参与,下排则不是:

读取浮点数的合并访问的示例如下图,上排是按线程排列访问,下排则不是,因为是未对齐的起始地址(不是64的倍数):

对于合并(Compute 1.2+的GPU),在10系列架构中大大改进了合并功能,硬件将half-warp内的地址组合成一个或多个对齐的段(32、64 或128字节),一个段内地址的所有线程都由一个内存事务处理,无论段内的顺序或对齐方式如何。下图显示了对于未对齐的起始地址(不是 64 的倍数),递归减少事务尺寸以最小化尺寸:

共享内存库寻址在无冲突和有冲突的图例如下:

2路冲突和9路冲突的图例如下:

什么是占用率(Occupancy)?GPU 通常同时运行1000到10000个线程,更高的占用率 = 更有效地利用硬件,在硬件中通过在任何给定时刻并发运行的warp(32个线程)执行的并行代码,线程指令按顺序执行,通过执行其它warp,可以在硬件中隐藏指令和内存的延迟。尽可能最大化占用率,占用率为1.0为最佳方案。

\[\text{占用率} = \cfrac{\text{驻留warp数}}{\text{最大可能的驻留warp数}} \]

一个或多个线程组驻留在单个着色器单元上,占用率受资源使用限制:线程组大小声明、线程组共享内存使用、线程组使用的寄存器数。示例:一个硬件着色器单元最多8个线程组、48KB总共享内存、最多1536个线程,以256个线程的线程组大小启动着色器并使用32KB的共享内存,导致每个硬件着色器单元只能运行1个线程组。此时受到共享内存的限制。

调度/线程组大小启发式:

  • 让线程组数 > 多处理器(multiprocessor)数。所有的多处理器至少有一个线程组来执行。
  • 线程组数/多处理器数 > 2。多个线程组可以在多处理器中同时运行,不在屏障处等待的线程组使硬件保持忙碌,视资源可用性而定 – 寄存器、共享内存。
  • 线程组 >100以扩展到未来的设备。以流水线方式执行的线程组,每次调度1000个组将跨多代扩展。
  • 线程/线程组数是warp尺寸的倍数。warp中的所有线程都在工作。

DirectCompute优化的用例之一是并行规约(Parallel Reduction),常见且重要的数据并行原语(例如,求一个数组的总和),易于在计算着色器中实现(更难做到正确)。文中介绍了7个不同的版本,展示了几个重要的优化策略。

每个线程块中使用的基于树的方法,需要能够使用多个线程块,处理非常大的数组,让GPU上的所有多处理器保持忙碌,每个线程块减少数组的一部分,但是如何在线程块之间传递部分结果呢?

着色器分解,通过将计算分解为多个调度来避免全局同步。

交叉寻址,会引入新问题——共享内存库冲突。

顺序寻址。

不同并行规约的算法性能对比。

文中还提到了多GPU并行,多个GPU可在单个系统中用于任务或数据并行GPU处理,主机显式管理每个GPU的I/O和工作负载,选择最佳分割以最小化GPU间通信(必须通过主机内存发生)。

多GPU和CPU的通讯模型,可以实现任务并行和数据并行。

内存合并(矩阵乘法),每次迭代,线程访问A中的相同元素,仅适用于CS 1.2+。

DirectCompute Performance on DX11 Hardware阐述了DirectCompute的特点、收益、优化及性能监测等方面的内容。

文中说到使用DirectCompute的原因有允许对GPU进行任意编程(通用编程、后处理操作等),更好地针对具有严重TEX或ALU瓶颈的PS,使用CS线程来划分工作并*衡着色器。虽然并不总是能战胜PS,但即便*衡良好的PS也不太可能被CS击败。

GPU是面向吞吐量的处理器,延迟由工作覆盖,需要提供足够的工作以提高效率,寻找细粒度的并行性,简单的映射效果最好(屏幕上的像素、模拟中的粒子)。如果有助于避免往返主机的数据交换,则在GPU上运行小型计算仍然是有利的,包含延迟的收益,例如为后续的内核启动或绘图调用准备参数,与DispatchIndirect()结合,无需CPU干预即可完成更多工作。

NVIDIA的GPU是标量,不需要显式矢量化,在大多数情况下不会有影响(但也有例外),将线程映射到标量数据元素。AMD的GPU是向量,向量化对性能至关重要,避免依赖标量指令,使用IHV工具检查ALU使用情况。

相比CS4.0,CS5.0的优势很多,如线程、线程组共享内存、原子灵活性等,利用CS5.0的功能通常运行更快。声明合适数量的线程组对性能至关重要:

numthreads(NUM_THREADS_X, NUM_THREADS_Y, 1)
void MyCSShader(...)
{
    (...)
}

总线程组大小应高于硬件的wavefront大小(大小因GPU而异,ATI 的最大为64,NV的为32),避免尺寸低于wavefront尺寸,避免numthreads(1,1,1) 这样的线程组,较大的值通常适用于各种GPU,使用低端GPU更好地扩展。

使用线程组时,尝试在组中的所有线程之间*均分配工作,动态流控制将为线程创建不同的工作流,意味着工作较少的线程将处于空闲状态,而其它线程仍处于忙碌状态:

[numthreads(groupthreads, 1, 1)]
void CSMain(uint3 Gid : SV_GroupID, uint3 Gtid: SV_GroupThreadID)
{
    (...)
    
    if (Gtid.x == 0)
    {
        // 这里的代码只针对一个线程执行!
    }
}

可以混合Compute和光栅化,减少Compute和Draw调用之间的转换(transition)次数,而转换通常会有很高的开销!举个具体的例子,假设有以下顺序的调用质量:

Compute A
Compute B
Compute C
Draw X
Draw Y
Draw Z

可以改成交叉地调用Compute和Draw:

Compute A
Draw X
Compute B
Draw Y
Compute C
Draw Z

对于无序访问视图(Unordered Access View,UAV),它并非严格意义上的DirectCompute资源,也可以与PS一起使用。无序访问支持分散的读写,分散访问 = 缓存丢弃,优先分组读/写(强烈建议),例如从/向float4(而不是float)读取/写入,但NVIDIA标量架构不会从中受益。对UAV的连续写入,如果不需要,请勿创建带有UAV标志的缓冲区或纹理,渲染操作后可能需要同步,仅在需要时使用D3D11_BIND_UNORDERED_ACCESS。避免将UAV用作便签本,使用TGSM(线程组共享内存)更佳。

带计数的UAV缓冲是Shader Model 5.0支持的特性,不支持纹理,在CreateUnorderedAccessView()中使用D3D11_BUFFER_UAV_FLAG_COUNTER标志。访问方式有uint IncrementCounter()uint DecrementCounter()
,比使用UINT32大小的R/W UAV实现手动计数器更快,因为避免了对UAV进行原子操作。但在NVIDIA硬件上,更倾向于使用Append buffer。

追加/使用缓冲区(Append/Consume buffer)用于将数据并行内核的输出序列化为数组,也可用于图形,例如延迟片元处理。需要小心使用,可能代价高昂,在API中引入序列化点,大的记录尺寸可以隐藏append操作的成本。

原子操作是不能被其它线程中断直到完成的操作,通常与UAV一起使用,原子操作由于同步需求会影响性能。仅在需要时使用它们,许多问题可以重铸为更有效的并行规约或扫描,带有反馈(feedback)的原子操作成本更高,例如:

Buffer->InterlockedAdd(uAddress, 1, Previous);

线程组共享内存(Thread Group Shared Memory,TGSM)是组内线程间共享的快速内存,不会跨线程组共享!例如:

groupshared float2 MyArray[16][32];

在Dispatch()调用之间不持久(亦即TGSM只能用于单次Dispatch),用于减少计算量,通过将相邻计算存储在TGSM中来使用它们(如后处理纹理指令)。

影响TGSM性能的因素主要有:

  • 访问模式。

I/O的bank(存储库?)数量有限,ATI和NVIDIA硬件上的32个bank,bank冲突会降低效率。32bank示例,每个地址为32位,bank按地址线性排列:

相距32个DWORD的TGSM地址使用相同的bank,从多个线程访问这些地址将产生bank冲突,将TGSM二维数组声明为MyArray[Y][X],并先增加X,再增加Y(如果X是32的倍数,则必不可少!),填充数组/结构以避免bank冲突会有所帮助,例如MyArray[16][33] 代替MyArray[16][32]

  • 尽可能减少访问。例如将数据打包成uint而不是float4(但请注意增加的ALU)。

  • 基本上每个TGSM地址都尝试读/写一次。复制到临时数组可以帮助避免重复访问。

  • 展开循环访问共享内存,帮助编译器隐藏延迟。

屏障(Barrier)为组内的所有线程添加同步点,如GroupMemoryBarrier()GroupMemoryBarrierWithGroupSync(),屏障过多会影响性能,如果工作未在线程之间*均分配,则尤其如此,当心使用许多屏障的算法。

最大化硬件占用。线程组不能跨多个着色器单元拆分,无论是进还是出,与像素工作不同,像素工作可以任意分割。影响占用的因素有线程组大小声明、声明的TGSM大小、使用的GPR数量,这些数字会影响可以实现的并行度,例如,硬件着色器单元的参数是最多8个线程组、32KB总共享内存、最多1024个线程,线程组大小为128个线程,需要24KB共享内存,每个着色器单元(128 个线程)只能运行1个线程组(错误)。

寄存器压力也会影响占用,但我们对此几乎没有控制权,依赖驱动做正确的事。需要调整和实验才能找到理想的*衡,但这种*衡因硬件而异!存储不同的预设以获得跨各种GPU的最佳性能。

DiRT2 DirectX 11 Technology介绍了游戏Colin McRae Dirt 2使用的DirectX 11的技术,包含移植到DX11、曲面细分、基于直接计算的HDAO及线程资源加载。文中提到了基于曲面细分的网格细节化、布料和水体模拟。

基于DX11曲面细分的布料。左边是原始的网格,右边是PN曲面细分+位移的网格。

HDAO(High Definition Ambient Occlusion)的工作原理和HBAO(Horizon Based Ambient Occlusion)基本一致,通过考虑环境光和环境而不仅仅是像素来解决SSAO像素深度测量带来的颗粒和噪声问题。主要缺点是需要更多的CPU和GPU处理能力。HBAO+提供了一种性能任务较少的光影采样算法,将AO的细节级别提高了一倍,运行速度提高了三倍。

HBAO的与以前的SSAO变体不同,使用了基于物理的算法,该算法与深度缓冲区采样*似积分,使HBAO能够生成更高质量的SSAO,增加每个像素的样本数量以及AO的定义、质量和可见性。出于性能原因,HBAO通常以半分辨率渲染,将AO像素数量减少四分之三,但是,以降低的分辨率渲染HBAO会导致在所有情况下都难以隐藏的闪烁。

如果使用PS方式的后处理,可能存在大量过采样,样本覆盖的区域是内核大小,然后对中心像素周围的一大堆纹素进行采样。文中尝试使用Computer Shader来加速。

首先是重叠地分块,使用LDS大幅降低纹理采样成本,将屏幕划分为tile以供线程组处理。内核大小决定重叠程度,下图显示了涉及LDS写的纹素采样区域、涉及LDS读/写的ALU PP计算区域和内核大小:

// 实现代码
// CS result texture
RWTexture2D<float> g_ResultTexture : register( u0 );
// LDS
groupshared float g_LDS[TEXELS_Y][TEXELS_X];

[numthreads( THREADS_X, THREADS_Y, 1 )]
void CS_PPEffect( uint3 Gid : SV_GroupID, uint3 GTid : SV_GroupThreadID )
{
    // Sample texel area based on group thread ID – store in LDS
    g_LDS[GTid.y][GTid.x] = fSample;
    // Enforce barrier to ensure all threads have written their
    // samples to the LDS
    GroupMemoryBarrierWithGroupSync();
    // Perform PP ALU on LDS data and write data out
    g_ResultTexture[u2ScreenPos.xy] = ComputePPEffect();
} 

基于CS的HDAO相比PS的性能,深度提升1.3倍,深度+法线提升3.6倍(测试环境Windows 7 64-bit, AMD Phenom II 3.0 GHz, 2 GB RAM, ATi HD5870, Catalyst 10.2):

另外,还可以使用DX11的GatherCmp() 来快速采样PCF阴影,实现更简单、统一。

DiRT2使用后台加载线程,放置在队列中的资源,在DX9模式下,资源在主线程上创建。在DX11模式下,资源在加载线程上创建,更简单、更快速的实现,加载时间明显加快,约快50%。

R-Trees -- Adapting out-of-core techniques to modern memory architectures介绍了R-Tree的特点、原理,展示了如何使它们适应内存使用,在缓存行为以及SIMD处理等方面获得重大优势。

R-Tree本质上就是一个AABB树,但有一些特定的属性和提前准备的大量工作。其节点是由大的固定大小的子AABB和指针组成的块,AABB用于存储在其父节点中的节点,给定访问模式是有意义的,有些松散(通常高达50%),减少拓扑更改的频率。

以2-3的R-Tree的构建为例(2-3是指子节点数量控制在2-3个,实际会使用更大的节点数量,如16-32的R-Tree):

R-Tree的优点是:

  • 缓存友好的数据布局。

  • 没有刚体细分的模式。

  • 更高的分支系数。

    • 更短的深度,更少的读取,每个节点内更多的工作。
  • 预读取(宽度优先遍历)

    • 堆栈(深度优先):当前节点可以更改下一个是哪个节点。
    • 队列:知道下一个是哪个节点,所以预取之。
  • 每个节点有很多子节点。

    • 展开测试以隐藏VMX延迟。
    • 隐藏预读取的延迟。
  • 可用于动态物体。

    • 即使物体移动,拓扑结构依然有效。需要传播任意的AABB更改给父节点。
    • 可能最终表现不佳,但是仍然是正确的。
    • 延迟重新插入,直到物体移动了较大的距离。调整AABB比重新插入要快得多。

总之,R-Tree是快速的基于块的AABB树,具有层次树的所有常规优点,对缓存和SIMD非常友好,不需要桶加载(bulk-loading),但需要大量的前期开发工作。

Streaming Massive Environments from 0 to 200 MPH介绍了赛车游戏Forza Motorsport 3中用于制作和渲染大型赛道环境的流程,从艺术到游戏的流程,以及渲染大量的高细节模型的一些关键技术。

Forza Motorsport 3的流式目标是以60fps渲染,包含赛道、8辆汽车和用户界面等模块,支持后期处理、反射、阴影、颗粒、滑道、人群,支持分屏、回放等。游戏拥有海量环境:100多条轨道,有些长达13英里,超过47000个模型和超过60000个纹理。典型的海量模型可视化层次:

现代内存模型如上图,速度依次提升但容量依次下降:磁盘/局部存储、压缩缓存、解压堆、GPU/CPU缓存、GPU/CPU。下面对这些存储类型加以说明。

在硬盘上,以zip包的形式存储,以zip格式存储一些额外的数据,但遵循基本格式,因此标准浏览工具仍然有效(资源管理器、WinZip 等),在存档中以LZX格式存储,每个轨道(track)有90-300MB。

从磁盘到压缩的缓存时,利用高速缓存块大小的快速IO,Block是zip中的一组文件,文件的总大小,直到达到块大小,通过单次读取检索该文件组。压缩缓存减少了查找,峰值达到15MB/s,*均10MB/秒,但查找需要100ms。

压缩缓存以LZX格式在内存中存储,按需流入和流出LRU的缓存块,约56MB,逐轨道调整块大小,但通常为1MB。

从压缩缓存到解压堆时,使用快速的*台特定解压,*均20MB/秒,解压堆实现时针对分配和释放操作的速度进行了优化,并且使用地址排序优先的良好分段特性。

解压堆准备好供GPU或CPU使用,每个分配连续且对齐,约194MB。

多级的纹理存储,每个纹理的三个视图:Top Mip是Mip 0,全分辨率纹理,Mip–Chain是Mip 1,下采样到1x1,小纹理则从32x32到1x1。此处针对*台的支持不需要重新定位纹理,因为Top Mip是流式传输的。

多级的几何存储,将不同的LOD视为不同的对象,以允许流式传输在更高的LOD没有贡献时转储它们,模型使用每个实例的变换和着色器数据进行实例化。

从内存到GPU/CPU缓存时,针对缓存友好渲染的CPU特定优化,高频操作具有扁*、高速缓存行大小的结构,CPU的L1/L2高速缓存,大量使用命令缓冲区以避免接触不必要的渲染数据。

GPU/CPU缓存根据着色器需求调整格式大小,GPU的顶点/纹理提取缓存(如顶点格式、流计数、纹理格式、大小、mip使用情况),使用*台特定的渲染控件来减少mip访问等。

对于预先计算的可见性,标准解决方案是给定场景在给定位置实际可见的内容,许多实现使用保守遮挡。而本文使用的变量包括遮挡(深度缓冲区拒绝)、LOD 选择、贡献拒绝(如果小于n像素,则不绘制模型)。

剔除的方式有:遮挡剔除——在视图中被其它物体阻挡的物体(下图红色方形)和贡献被剔除——对视图的贡献不足的物体(下图黄色圆圈)。

可以在运行时做到,LOD和贡献很容易,可以实现遮挡。最重要的是必须在运行时进行优化,或者根本不这样做,但意味着流式传输和渲染过多。可见性信息通常是大量数据,意味着需要接触大量数据,对缓存性能不利。Forza Motorsport 3的解决方案是不要将CPU/GPU花费在可以离线处理的工作负载上。

Forza Motorsport 3的轨道处理流程分为5个采样主要步骤:采样、拆分、构建、优化和运行。所有步骤都是全自动的,源自场景中的艺术检查,管线生成优化的游戏就绪轨道。

在优化步骤,为包裹创建缓存有效的序号,缩短查找距离并提高缓存命中率,使用“首次看到”的指标,走过区域并跟踪哪个区域首先使用模型或纹理,将所有模型组合在一起并按第一个区域排序,与纹理相同。

在运行步骤,创建区域增量,确定摄像机在可见空间中的位置,将摄像机位置映射到要加载的区域,当前加载的区域和要加载的区域的差异。根据区域增量创建资源增量,基本上是引用计数,整合工作以确保免费首次订购(这是为了帮助解决碎片化问题)。在尾部区域流出(免费)数据,在前导区域流入(分配、IO 和解压缩)数据。

运行时注意事项,重点领域包含工作顺序、堆效率、解压效率、磁盘效率,对于许多问题,任何解决方案都比什么都不做要好,确保层次结构的所有级别都得到解决。

管线的错误主要有:

  • 跳变。
    • 仅限于两个层级。
      • 延迟加载(通过限制每个区域保持在系统吞吐量内所需的数量进行调整)。
      • 可见性错误(通过进一步聚类对象或使抽样结果有偏差进行调整)。
    • 虽然这些调整有冲突。
    • 提供手动操作。
      • 几何偏差(影响采样结果)。
      • 纹理偏差(在优化期间影响纹理工作集中的位置)。
  • 再多的自动化也无法与不切实际的期望相抗衡。
    • 例如,所有模型都在单个区域中可见,意味着不会有任何用于纹理的空间。

A Dynamic Component Architecture for High Performance Gameplay详细介绍了系列游戏Resistance中使用的动态组件架构,以表示实体和系统行为的各个方面。该组件系统解决了传统游戏对象模型在高性能游戏中的几个弱点,特别是在多线程或多处理器环境中。动态组件从高效的内存池中按需分配和释放,系统提供了一个方便的框架,用于在不同的处理器(如SPU)上并行运行更新。该系统可以分层在传统游戏对象模型之上,因此代码库可以逐渐迁移到这种新架构。本文将讨论系统的动机、目标和实现细节。

以往的游戏对象组织层次比较单一、高深度和不*衡,内存上在编译期绑定数据,性能上缓存一致性差,架构上通过继承来获得能力,另外是使用习惯。

解决方案是通过在运行时组合组件来构建游戏对象,小块(Small chunk),表示数据变换。可以并行实现,无需重构现有代码,与组件共存。但是,动态组件不解决反射、序列化、数据构建、实例版本控制等问题。

动态组件系统的特点有:

  • 组件

组件是最初的特点,基础组件类有8字节的管理数据,从池中分配,每种具体类型一个池,“名册”索引实例,“分区”分隔已分配/空闲的实例。

  • 高性能

微量的恒定的时间操作,包含分配/免费、解析句柄、获取类型、类型实现(派生自),无实例复制。按类型更新(按池),缓存友好,促进异步更新(例如在SPU上),名册是分配实例的连续列表,分区名册是DMA列表。解析句柄具有微量的恒定的时间操作:索引到池中、比较生成、返回组件。

  • 动态

游戏对象的运行时组合,无包袱动态地改变行为,已分配的组件 == 正在使用,池大小 == 最大并发分配。

高频地alloc()free()alloc()包含可用性测试、从索引和生成构造句柄、增加名册分区、Component::Init()free()包含Component::Deinit()、交换名册索引与分区相邻索引、减少分区、增量生成。动态组件的释放接口如下:

// free the component from host's component chain                                                   
void DynamicComponent::Free( Type type, HostHandle host_handle, Chain& chain, ComponentHandle& component_handle );

结合下图,有一个给定组件类型的实例池,还有有一个名册,它是实例池中的索引数组。注意,池中的所有实例都属于同一类型,因此它们的大小相同。因此,名册索引处的值本身就是池中的索引。可以看到有一个分区值,它将代表已分配实例和空闲实例的名册索引分开。

现在将释放由第3个roster元素表示的组件,它当前在池中的索引为3:

要做的是交换第3个和第4个名册元素:

现在正在释放第4个roster元素,它代表池中的第3个实例。由于将要释放的元素的roster条目交换到分区相邻的roster的条目中,因此释放该实例所需要做的就是将分区的索引上移1个单位(减1),就是如此简单:

  • 系统

不是全有或全无!例如对话、脚本事件、投篮:无游戏对象。下面是动态组件系统的相关接口定义:

namespace DynamicComponent                                                                           
{                                                                                                    
    // Hosts' API                                                                                      
    Component*        Allocate                  ( Type type, HostHandle host_handle, Chain* chain, void* prius = NULL );                  
    Component*        ResolveHandle             ( Type type, ComponentHandle component_handle );       
    Component*        Get                       ( Type type, HostHandle host_handle, Chain chain );    
    Component*        GetComponentThatImplements( Type type, HostHandle host_handle, Chain chain );    
    Component**       GetComponents             ( Type type, HostHandle host_handle, Chain chain, u32& count );                           
    Component**       GetComponentsThatImplement( Type type, HostHandle host_handle, Chain chain, u32& count );                           
    void              Free                      ( Type type, HostHandle host_handle, Chain& chain, ComponentHandle& component_handle );                 
    void              FreeChain                 ( HostHandle host_handle, Chain& chain );              

#define COMPONENT_CAST(component, type) \                                                            
  ((type##Component*)ValidCast(component, DynamicComponent::type))                                   
  inline Component* ValidCast                 ( Component* component, Type type );                   

    // Systems' API                                                                                    
    Type*             GetTypesThatImplement     ( Type type, u32& count );                             
    bool              TypeImplements            ( Type type, Type interface );                         
    u32               GetNumAllocated           ( Type type );                                         
    Component**       GetComponents             ( Type type, u32& count );                             
    Component*        GetComponentsIndexed      ( Type type, u16*& indices, u32& count );              
    void              UpdateComponents          ( UpdateStage::Enum stage );                           
    void              Free                      ( Type type, ComponentHandle& component_handle );      
    UpdateStage::Enum GetCurrentUpdateStage     ( );                                                   
    u8                GetTypeUpdateStages       ( Type type );                                         
}

实现的过程涉及脚本事件、分配和初始化、异步更新等细节。

无独有偶,Entity Component Systems也谈及了Unity引擎ECS和作业系统的特点。

实体组件系统 (ECS) 是一种主要用于游戏和模拟的数据组织方式。实体(或游戏对象)是游戏中可以看到或与之交互的任何对象,例如玩家、敌人、障碍物、通电。组件是分配给实体的属性,例如附加到玩家实体,可以拥有健康、碰撞、变换和运动组件。系统是向组件添加功能的地方,即使用运动和变换组件,可以制作基本的运动系统。下图分别展示了实体、组件、系统管理器:

ECS模型包含纯粹(Pure)与混合(Hybrid)方式,两者对比如下:

Pure Hybrid
实体是新的游戏对象 包含Pure ECS的所有功能
没有更多的mono行为 包括将游戏对象转换为实体并将mono行为转换为组件的特殊辅助类
数据存储在组件中,逻辑存储在系统中
利用提供性能优势的新C#作业系统

Burst是Unity开发的一种新的数学感知编译器,可以生成高度优化的机器代码,充分利用正在编译的*台,完全自动化。需要做的就是将 Burst编译器包添加到项目中,然后确保C#作业标有Burst Compile属性。随后Unity将获取作业系统代码并将其编译为高度优化的机器代码,意味着用C++甚至C等语言编写逻辑可以直接在处理器上执行。Burst的优势是非常容易实现,不需要了解复杂的低级代码。与ECS相结合,大大提高了性能。

Unity作业系统让开发人员可以轻松安全地编写多线程代码,它通过创建作业而不是线程来做到这一点。作业表示系统可以作为一系列线程处理的工作单元,安排作业时,系统会将其放入一个特殊的队列中, 工作线程会将作业从队列中拉出并执行。工作线程是由作业系统管理的单个线程,它们在后台完成作业,因此不会中断主线程。

使用Unity作业系统的原因包括:它确保多线程代码是确定性的的。例如,作业系统将通过为每个逻辑CPU内核创建一个工作线程来尽量避免上下文切换,使得开发人员可以(在合理范围内)创建任意数量的作业,而不必担心它会如何影响CPU的性能。作业系统还有一个内置机制,用于以作业依赖的形式防止竞争条件。例如,如果作业A需要为作业B准备一些数据,则可以将其分配为作业B的依赖项,这样,作业A将始终首先运行,作业B将始终拥有正确的数据。

Unity作业系统允许应用层开发人员以安全、简单且完全由Unity管理的方式使用多线程。当它与Unity ECS和Burst Compiler结合使用时,会立即获得性能极佳的代码并进行优化。

Reflection for Tools Development探讨程序员在开发内容创作工具时遇到的常见模式。无论是用于动画、音频、图形还是游戏玩法,开发提供出色工作流程的工具并非易事,工具必须稳定,同时适应生产过程中的变化,必须与用C++编写的游戏集成,必须是特定于游戏的,但也必须是可重复使用的。Jeremy Walker分享了育碧温哥华使用的一个内部开发的框架,可以快速开发具有出色工作流程的工具。

在引擎开发过程中,经常遇到硬编码和数据驱动的系统的权限、选择和设计:


各个模块或子系统中有着错综复杂的联系、交互或依赖:

这些子模块部分可以直接选择硬编码:

然后分别将每个子系统的编辑、构建、序列化、渲染部分单独合并成一个模块组,形成整体引擎方法:

问题是如何*衡低开发成本和出色的工作流、适应变化和稳定工具、可重用的系统和游戏和特定类型的需求配对之间的关系呢?

解决方案是对于所有类型的内容:最大限度地降低开发出色工作流程的成本,在满足特定游戏需求的同时设计可重复使用的系统,开发能够适应生产过程中不断变化的稳定工具。软件包发布流程图如下:

单体软件包发布的问题:

可以采样解耦包:

锁步发布的问题:

可以采用反射系统来降低开销和复杂度。下面是硬编码、单体引擎、使用反射的解耦系统对比图,其中使用反射的解耦系统可以达到低开销、简单、高度可重用、出色的工作流等目标:

现在再转向阐述计算机语言的反射(Reflection)机制。下图是C++的编译流程和反射机制,其中反射在函数声明时收集信息,然后将收集到的信息绑定到函数定义:

对于混合语言的反射,流程相似但略有不同:

游戏脚本语言的反射:

游戏序列化的反射:

通用语言的反射规范:

C++反射的流行方法:宏(Macros)、代码解析器(Code Parser)、类型定义语言(Type Definition Language)。它们的示例代码如下:

// ----- 宏 -----

class SimpleVehicle : public Entity
{
public:
    DECLARE_TYPE();
     float    m_MaxSpeedKPH;
     void     Reset(bool useDefaults);
    float     GetMaxSpeedMPH() const;
    void    SetMaxSpeedMPH(float maxSpeedMPH);
};

//In a separate .CPP file:
DEFINE_TYPE(SimpleVehicle)
    BASE_CLASS(Entity)
    FIELD(“MaxSpeedKPH”, m_MaxSpeedKPH)
    METHOD(Reset)
    PROPERTY(GetMaxSpeedMPH, SetMaxSpeedMPH)
DEFINE_TYPE_END()
    
    
// ----- 代码解析器 -----
    
/// [Class]
class SimpleVehicle : public Entity
{
public:
    /// [Field(“MaxSpeedKPH”)]
    float    m_MaxSpeedKPH;
    /// [Method]
    void Reset(bool useDefaults);
    /// [Property]
    float     GetMaxSpeedMPH() const;
    /// [Property]
    void    SetMaxSpeedMPH(float maxSpeedMPH);
};

// ----- 类型定义语言 -----

class SimpleVehicle : Entity
{    
    float    MaxSpeedKPH;
    void     Reset(bool useDefaults);
    float     MaxSpeedMPH { get; set; }
};

以上三种C++反射方法的优缺点见下表:

C++反射方法 优点 缺点
没有外部工具 实现起来很尴尬,难以调试,运行时发现
代码解析器 更容易实现,编译期发现 缓慢的预构建步骤
类型定义语言 最容易实现,没有缓慢的预构建 不能反映现有的类

顺便提一下,UE的反射是以代码解析器为主结合宏为辅的混合方式。

导出游戏的类型定义的流程如下:

上面的示例代码中的类型SimpleVehicle经过代码解析器导出的Types.xml数据如下:

<type name=“SimpleVehicle”>
  <field name=“MaxSpeedKPH” type=“float”/>
  <method name=“Reset” returntype=“void”>
    <parameter name=“useDefaults” type=“bool”/>
  </method>
  <property name=“MaxSpeedMPH” type=“float” hasget=“true” hasset=“true”/>
</type>

导出工具的类型定义和游戏的稍有不同:

其中生成的C#代理类型如下:

[ProxyType(“SimpleVehicle”, 0x81c37132)]
public partial class SimpleVehicle : Entity
{
   public float MaxSpeedKPH
   { 
      get { return this.Instance.GetField(“MaxSpeedKPH”).Get<float>(); }
      set { this.Instance.GetField(“MaxSpeedKPH”).Set<float>(value); } }
   }
   public float MaxSpeedMPH { ... }
   public void Reset(bool useDefaults) { ... }
}

反射在工具中的主要用途有序列化、客户端-服务端远程处理、生成GUI:

客户端-服务器远程处理流程和步骤:

存在的问题和解决方法:

  • 类型定义不同步。检测类型校验和不匹配,及早发现问题,自动同步类型信息,数据自动迁移。
  • 与游戏紧密耦合的工具。避免过度使用生成的代理类,尽可能使用生成的UI,使用多态代理类。

  • 内存使用过多。基于使用情况的剥除类型的无用信息,自动检测未使用的反射类型。

反射的其它用途有多处理器架构的编组事件,在线客户端-服务器远程处理,保存游戏数据的序列化。

下图是育碧的内容框架:

获得良好的结果有快速工具开发,适用于所有类型内容的出色工作流程,具有改进的可重用性和对变化的弹性的解耦系统。

The Asset pipeline for Just Cause 2: Lessons learned概述了游戏Just Cause 2中使用的资产调节管线,包含分析管线需求和设计一个可以消除关键瓶颈、提供稳健环境并提高吞吐量的系统的过程,讨论了识别可以简化管线的关键底层系统的过程。在这些系统中,有一个独立于*台和语言的数据管理层、一个资产依赖解析器和使用Python的编译器脚本框架,还讨论了一种以受控方式处理新编译器部署的系统,基于这个新基础重建现有编译器管线的过程、好处、副作用、可维护性、稳健性,以及改进的反馈水*和监控管道统计数据的能力。Just Cause 1的工作流如下图:

这个工作流程有一些重大缺陷。首先,JustEdit的手动导出步骤太多,更糟糕的是,它导出了游戏就绪格式,一个实体的变化会影响很多位置或任务,因此需要再次重新导出所有资产。

到了Just Cause 2,内容创建者订购了许多新工具,编写了详细的设计文档,每个工具都分配了一个“客户”,程序员与客户保持密切沟通,客户负责测试工具并批准它,作为JustEdit插件的工具,WTL用于编辑器/插件中的GUI。

项目开始时有10个新工具!!代码没有在应该完成的90%-100%完成。工作流程中的缺陷,并非所有工具都按预期使用,易于编辑的内容在性能和内存方面不太适合游戏。设计文档按“原样”接受,它们本质上是内容创作者的愿望清单。

顾客不是开发团队的一员,沟通受到影响,经常计划全职从事其它任务,责任不明。插件导致了不稳定的C++接口,应该与网络通信一起去,过于复杂。图形用户界面:WTL和C++,WTL太简单了,而C++减慢了迭代速度。

C++中的编译器,使用C++编写脚本并不总是很方便,构建时间长,调整起来很尴尬,一些编译器包含所有游戏代码:非常特定于*台,如除了其它的事情,带了DirectX的Win32繁重,无法在没有桌面的自动构建器上运行。

程序员实现的东西略有不同,导致整体行为异常,打破依赖检查,编译器中的硬编码参数。文件格式不够稳定,如果出现问题,编译器可能会崩溃,或者更糟糕的是,损坏的数据进入了游戏。

不愿使用编译器,管线被认为有点魔法,缺乏文档,整体流程不容易概览,调试经常在游戏代码中完成的损坏数据,程序员已经设置了所有代码和数据,这有时会导致完成运行时修复,而不是修复编译器中的错误。

没有中心代码,集成时不使用中心代码意味着问题,修复丢失了,必须重新找到它们,内置编译器在Perforce中进行版本控制,需要时不会自动重建和使用,仍然可以使用旧的错误代码。

数据格式方面,有许多不同的文件类型(约30),许多格式都是基于xml的,将“属性名称”映射到“值”,格式对编辑器比对游戏更优化,未检测到读/写错误,没有版本号,无法记录正确的错误消息,很多东西要改进。

内容构建系统是有点粗略的解决方案,很慢的依赖检查,不完整的依赖树,时间戳不够好。由于行为中的错误和异常,用户不能100%信任它,没有信任意味着完全清理和完全重建。

为解决JC2的问题,提出了全新的目标:重建对管道的信任,增量构建,没有神奇的工作流程,易于配置,使用依赖检查,100% 准确率,简化开发,将通用代码移至中央存储库,减少创建工具时的周转时间。

重大决定,需要一个中央代码的构建/部署系统,内容构建系统也需要大力推动,需要一种简单易用但功能强大的通用数据格式,开发必须与 JC2 一起完成,需要致力于任务才能真正快速到达任何地方。

需要依赖解析器和部署系统,内部使用python编写,非常轻量级,处理包体:构建库 (.h + .lib/.so)、构建的可执行文件、捆绑的 python 脚本(或任何你想要的)、第三方库、可执行文件。将包部署到中央存储库,获取包到本地系统。

编码过程的巨大变化,代码库向更小的库发展,真正快速的编译时间,帮助跟踪版本冲突,允许非常快速的开发和测试。

用于代码和数据的轻量级构建系统,开源 (BSD),源代码约为80kb,依赖检查,多核支持,易于维护和扩展,替换内部的项目编译器系统,用Python编码。

雪崩数据格式,序列化框架:C/C++, Python 支持、Xml <-> 二进制支持,二进制文件的基于哈希的版本控制,错误处理,从Python和C/C++访问数据的能力实现了无缝的跨语言交换,跨*台支持,例如将数据从工具传递到控制台/从控制台传递数据。

ADF和Python使读取和写入数据变得非常容易,读取一个类型库,然后从xml格式加载源文件。接下来,更改第一个对象的名称,最后,以二进制格式(big endian)写回所有内容。下图是文件如何相互关联的示意图:

C++用于共享库,从Python加载,移除了对旧游戏代码的依赖,用标准代码替换内部代码,fopen() 而不是CFile(),降低复杂性,增加可移植性。脚本和逻辑用python,数据处理用Python或C++,例如使用C++读取/写入/处理Havok文件。

Python用于极端的周转时间,大量内置模块,经常使用optparse、ctypes、md5、numpy、cStringIO…许多模块都有用C++实现的后端,大多数编译器被完全重写,减少很多依赖,代码大小降至1/10。

依赖检查方面,使用Waf处理机制,扫描程序找到依赖项,Waf缓存结果,MD5校验和命令行参数,源/依赖文件内容,新的编译器路径将触发编译,使用Needy更新软件包时非常方便。

增量自动生成器,从版本控制同步,只构建必要的东西,全自动生成器,将增量构建与完整构建进行比较,发现依赖问题,查找数据格式中的错误。

2011年,Multi-Core Memory Management Technology in Mortal Kombat分享了在游戏真人快打中使用的多核内存管理技术。

《MK vs DC》主要使用了两个内存管理器:虚幻内存管理器 (FMalloc),引擎端资源,基于C/C++的内存管理。“游戏”内存管理器,游戏端资源,面向控制台。

Unreal内存管理器的限制有LibC++功能集,不支持多堆,不是原生线程安全/多核,非线程安全内存分配器受“全局锁”保护,“MK vs DC”在内部使用DLMalloc,某些操作会导致较长的卡顿。

游戏内存管理器的限制有不是线程安全的,不是“虚拟内存感知”,仅支持静态固定后备库,非常慢的O(N) 次操作,容易碎片化(。

全局锁定并非良策,未经过多核优化,所有操作都可能导致其它线程上的轻微停顿或上下文切换,某些操作可能会导致大系统范围的停顿,例如大型应用程序分配请求、堆后备存储分配、重新分配操作。

下图是全局锁定重新分配,线程1的重新分配会锁定内存块,导致线程2一直等到线程1完成内存分配之后才能进行锁定并执行内存操作:

良好粒度的锁定重新分配可以减少等待:

非阻塞的重新分配:

虚拟内存解决内存碎片:

多核下的内存操作默认情况下是线程安全的,尽可能无锁(且直接),需要时首选非阻塞锁:非排他锁(例如读写器)、细粒度锁定、条纹锁(Striped Locking)。也可以考虑单线程的高性能,无竞争的访问不会出现明显的性能损害。

新的内存管理器优化线程安全和多核,为游戏和虚幻引擎统一单独的内存管理器,支持具有额外功能的多个堆,提高性能(CPU 周期和内存使用效率),通用跟踪和调试实用程序。

并发堆具有最小的线程“串扰”,可以在单个堆上同时分配/释放多个线程(如果堆类型支持,大多数堆类型都可以!),后台存储和内部堆查询操作通常同时运行(使用无锁、条带化或读写器锁),Realloc()在复制发生时从不阻塞。简化的内存管理架构如下:

在实现堆时,Heap API使用虚函数,Backstore和OS Allocs的通用支持API,Global Free() “知道”返回的堆内存。易于制作不同的堆实现,直接操作系统堆,最佳拟合堆(使用红黑树),小块堆(无锁分配/无条纹),固定块堆(无锁 - 用于MK游戏对象)。

可以使用混合主堆,即主堆使用混合方法来处理分配:大型分配直接通过操作系统以最大程度地减少碎片(但在内部进行跟踪),中等分配进入最佳适合堆,小块分配由它们自己的堆处理。C++ new/delete和C malloc/free调用路由到主(混合)堆。

UE的内存管理就是使用这种混合主堆的方法,详见1.4.3 内存分配

下图显示了内存分配的占比中,小堆占约23%,中堆占约52%,大堆占约25%;而在分配次数上,小堆占约96.7%,中堆占约3.2%,大堆占约0.26%,可见在次数上,小堆占了绝大部分,管理好小堆的分配等操作至关重要!!

下图显示了不同大小的堆分配的次数,基本上越小的堆次数越多:

SBMM = 小块内存管理器,它的特点是非常低的线程争用,支持许多同时操作,分箱分配器(尺寸化的箱、Lock Striping = Lock Per Bin),(大部分是)无锁的Alloc(),后备缓存使用无锁分配的victim(Lockfree Lookaside cache for a Block’s Items,块项目的无锁后备缓存)块,快速的条纹锁定(Stripe-Locked)释放。SBMM的装箱过程如下图:

SBMM的内存布局如下图:

SBMM主要是无锁分配,LockFree freelist缓存“受害者”块的项目,为空时,获取Bin条带锁,并从下一个带有空闲项目的Block建立新的空闲列表,是一个非常快的操作,直到所有块都用尽为止,在这种罕见的情况下,必须从超级块中取出一个新块,并为这些项目初始化一个空闲列表, 如果所有的SuperBlock都用完,则从OS请求一个新的SuperBlock。

SBMM的释放本来是无锁的,但需要延迟GC,条纹锁定 == 轻松修剪(无延迟GC),查找块和箱的大小,快速锁定箱,推送内存项目并检查计数,如果需要修剪,拉取块,释放锁定,修剪,否则释放锁定,无竞争的案例与无锁的速度非常相似,条纹一如既往无竞争。

无锁的NR-Pool是用于MK内存系统中的简单控制结构:

Mega Meshes - Modeling, Rendering and Lighting a World Made of 100 Billion Polygons分享了用于研发游戏Milo and Kate(下图)的引擎在模型和管线、构建、压缩和流、虚拟纹理、实时GI等方面的内容。

与传统的环境模型管线不一样的是,本文的引擎使用了大型网格工具,管理具有数十亿个多边形的模型,支持多个用户,联合了DCC工具、VC和雕刻等工具,窗口可在可变的细节级别编辑,负责协调构建过程。

在物理内存中存储如此大的数据集是不现实的,几何数据分层存储,连续的细分关卡以差异数据存储,按需加载的关卡。过程见下面系列图:





分级存储的好处可以在低细分级别编辑世界的区域,并保留高频率的细节,可以改变地形的宏观地形而不丢失所有细节,只需修改较低的细分层,可以做出大比例的色调调整。为了解决多人编辑的关卡的缝隙,需要额外的步骤生成运行时网格:

稀疏虚拟纹理(Sparse Virtual Texture)的出现是为了解决高内存占用很多多的全局通道,需要良好的流/渲染系统,可以使用虚拟纹理,在纹理空间虚拟化。下面几幅图分别是Milo and Kate早期的虚拟纹理图集、大型纹理和mipmap分拆到tile、虚拟纹理的物理内存加载过程:



虚拟纹理的细节:数组中的多个虚拟纹理,16 * 32k * 32k的地址空间,128 x 128的tile,8:8位的UV tile地址,一些角色/道具的虚拟纹理,其余的则被划分到世界的不同区域,可以在关卡边界解除未使用的绑定,允许连续的流式世界,2048 x 4096 (28mb)的物理页面。

下图展示了编译巨型纹理的流程图:

渲染时,渲染线程、纹理缓存线程、GPU线程的交互图如下:

此外,该引擎在实现基于SH的GI的流程图如下:

Game Worlds from Polygon Soup: Visibility, Spatial Connectivity and Rendering描述了Halo系列游戏的虚拟世界内的Polygon Soup(无组织的多边形组)在可见性、空间链接及渲染方面的技术内容。

在虚拟环境方面,Halo应用了单元格和门户(portal)、防水壳几何形状、艺术家手动放置传送门、从壳几何构建BSP树、Floodfill BSP进入单元格、建立单元格连接等技术。这样的技术选型优点是统一的可见性/空间连接、精确的空间分解、内部/外部测试、非常适合带有自然门户的室内空间。缺点是手动门户化并非易事!、水密性对内容创作来说是痛苦的、强制进行早期设计决策、仅针对室内场景进行了优化。

下面依次是门户化、多边形组的图例:


Polygon Soup是只是一些多边形块聚在一起,非防水结构,没有手动门户,增量构建/快速迭代,允许后期设计更改。可用于细分场景、体素化细分体积、将体素分割成区域、构建区域之间的连接图、从体素区域构建简化的体积等。下面图是2D的寻路应用案例:

从左到右、上到下依次是:输入场景、体素化、可行走体素、距离场、分水岭变换、轮廓。

最终生成的导航网格。

上述图例是2D空间,实际在3D空间存在诸多问题,包含3D更难/更慢、过度分割(小区域)、对场景变化敏感、简化表示并非易事、能见度如何表达等。

解决方法是与Umbra协作,自动生成门户,增量/本地更新,基于CPU的解决方案,低延迟,相同的可见性和空间连通性解决方案,处理门和电梯,精确围绕用户放置的门户,快速运行时间/低内存占用。Umbra解决方案的流程如下:

总体分为将场景离散为体素、确定与输入几何相关的体素连通性、传播连接以查找连接的组件、确定本地连接组件之间的门户等步骤。

瓦片体素化的过程。

将体素转换成单元格和入口。

构建单元格和门户。

Halo Reach游戏循环的特点如下:

  • 粗粒度并行。
  • 线程上的系统。
  • 通过状态镜像显式同步。
  • 主要是手动*衡负载。

Halo Reach细粒度并行。

这个并行系统在帧结束时需要镜像(复制)整个游戏状态(Halo Reach约20MB),以便模拟线程可以自由地处理下一帧,渲染线程可以将当前帧提交给 GPU。镜像游戏状态的原因之一是在复制游戏状态之后计算可见性。复制20MB游戏状态的串行任务很耗时(约3ms),需要对整个游戏状态进行双重缓冲。 注意,此处的游戏状态仅指确定性游戏状态数据。

Halo Reach的并行系统在帧尾需要复制整个游戏状态。

下图展示了各个线程的普遍利用率:

有没办法改进这一痛点?

观察#1:Halo Reach不需要整个游戏状态来渲染。在Reach中,游戏状态提取发生在进行可见性计算之前,这就是为什么必须复制整个游戏状态,开销大(以毫秒和内存占用为单位)。可见性占据渲染线程上的大量CPU时间,然而,CPU时间未得到充分利用,未充分利用的硬件线程。但是实际上可以反转那个操作,仅将可见对象的数据复制到游戏状态之外,仅提取将要渲染的对象的数据。

更好的做法是根据可见性结果推动游戏提取和处理,仅提取可见对象的数据(静态和动态),无需双缓冲整个游戏状态,仅为可见对象的每帧瞬态缓冲游戏数据,更小的内存占用。

更好的负载*衡做法:首先将可见性计算拆分为每个视图的作业,包括玩家、阴影、反射视图的可见性计算,可见性作业可以具有视口到视口的依赖关系,可以重用一个可见性作业计算的结果作为另一个的输入。

减少输入延迟的做法:在游戏对象更新的同时错开可见性计算,在帧中尽早使用预测相机开始静态可见性,在对象更新之前执行。

改善CPU延迟:仅为可见对象运行昂贵的CPU渲染操作,只要确保在可见性之后运行它,仅渲染操作(蒙皮、布料模拟、多边形排序)——不会影响游戏玩法。改进游戏循环和并行方式后的运行情况如下:

收益是将游戏状态遍历与绘图分离,通过交错的可见性计算提高CPU利用率,渲染线程成为流线型内核处理器。下图是一个简单的小作业树:

Culling the Battlefield: Data Oriented Design in Practice分享了Battlefield 3中采用的面向数据的设计和实践。

以往的裁剪剔除方法已经有层次球体树、静态剔除树、动态剔除树等。

但Battlefield 3依然打算重构之,原因有动态剔除树缩放、子关卡、管线依赖、难以扩展、每个视锥体一个作业等。

新系统的要求是更好的缩放、可破坏、实时编辑、更简单的代码、子系统的统一等。

不能在这些系统上良好地工作的技术有:非局部数据、分支、寄存器类型之间的切换(LHS)、基于树的结构通常是分支繁重、要解决最重要的数据。可以在这些系统上良好地工作的技术有:局部数据、(SIMD) 计算能力、并行度。

新的裁剪剔除为了应对游戏场景中海量的物体(最多有约15000个),第一次尝试是仅使用并行蛮力,比旧剔除快3倍,1/5的代码大小,更容易进一步优化。线性数组规模大,可预测的数据,很少分支,充分利用计算能力,可以达到5倍的速度提升:

新的裁剪剔除通过简单的网格提高性能,就是一个AABB,分配给一个带有球体的“单元格”,单独的网格用于静态渲染、动态渲染、静态物理和动态物理。其数据数据布局如下:

增加物体时,可以从中获取数据的预分配的数组:

删除物体时,使用“交换技巧”,数据无需排序,只需与最后一个条目交换并减少计数。

渲染裁剪时,先看看渲染的数据:

struct EntityRenderCullInfo
{
    Handle entity;    // handle to the entity
    u16 visibleViews; // bits of which frustums that was visible
    u16 classId;      // type of mesh
    float screenArea; // at which screen area entity should be culled
};

裁剪节点代码如下:

while (1)
{
    uint blockIter = interlockedIncrement(currentBlockIndex) - 1;
    if (blockIter >= blockCount) break;
    u32 masks[EntityGridCell::Block::MaxCount] = {}, frustumMask = 1;
 
    block = gridCell->blocks[blockIter];
    foreach (frustum in frustums, frustumMask <<= 1)
    {
         for (i = 0; i < gridCell->blockCounts[blockIter]; ++i)
         {
             u32 inside = intersect(frustum, block->postition[i]);
             masks[i] |= frustumMask & inside;
        }
    }
    
    for (i = 0; i < gridCell->blockCounts[blockIter]; ++i)
    {
        // filter list here (if masks[i] is zero it should be skipped)
         // ...
    }
}

相交检测代码如下:

bool intersect(const Plane* frustumPlanes, Vec4 pos)
{
    float radius = pos.w;
    
    if (distance(frustumPlanes[Frustum::Far], pos) > radius)
    return false;
    if (distance(frustumPlanes[Frustum::Near], pos) > radius)
    return false;
    if (distance(frustumPlanes[Frustum::Right], pos) > radius)
    return false;
    if (distance(frustumPlanes[Frustum::Left], pos) > radius)
    return false;
    if (distance(frustumPlanes[Frustum::Upper], pos) > radius)
    return false;
    if (distance(frustumPlanes[Frustum::Lower], pos) > radius)
     return false;
    
    return true;
}

以上代码出现很多问题,如非局部数据和浮点分支:

该怎样改进?点积对SIMD不太友好,通常需要随机打乱数据才能得到结果,(x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1),将数据从AoS(Array of Struct,结构体数组)重新排列到SoA(Struct of Array,数组结构体):

现在只需要3个指令就可以完成4个dot!

新的相交性测试代码下每个循环两个*截头体与球体相交、4*3 个点积、9条指令,遍历所有视锥体并合并结果。

Vec posA_xxxx = vecShuffle<VecMask::_xxxx>(posA);
Vec posA_yyyy = vecShuffle<VecMask::_yyyy>(posA);
Vec posA_zzzz = vecShuffle<VecMask::_zzzz>(posA);
Vec posA_rrrr = vecShuffle<VecMask::_wwww>(posA);
// 4 dot products
dotA_0123 = vecMulAdd(posA_zzzz, pl_z0z1z2z3, pl_w0w1w2w3);
dotA_0123 = vecMulAdd(posA_yyyy, pl_y0y1y2y3, dotA_0123);
dotA_0123 = vecMulAdd(posA_xxxx, pl_x0x1x2x3, dotA_0123);

Vec posAB_xxxx = vecInsert<VecMask::_0011>(posA_xxxx, posB_xxxx);
Vec posAB_yyyy = vecInsert<VecMask::_0011>(posA_yyyy, posB_yyyy);
Vec posAB_zzzz = vecInsert<VecMask::_0011>(posA_zzzz, posB_zzzz);
Vec posAB_rrrr = vecInsert<VecMask::_0011>(posA_rrrr, posB_rrrr);
// 4 dot products
dotA45B45 = vecMulAdd(posAB_zzzz, pl_z4z5z4z5, pl_w4w5w4w5);
dotA45B45 = vecMulAdd(posAB_yyyy, pl_y4y5y4y5, dotA45B45);
dotA45B45 = vecMulAdd(posAB_xxxx, pl_x4x5x4x5, dotA45B45);

// Compare against radius
dotA_0123 = vecCmpGTMask(dotA_0123, posA_rrrr);
dotB_0123 = vecCmpGTMask(dotB_0123, posB_rrrr);
dotA45B45 = vecCmpGTMask(dotA45B45, posAB_rrrr);
Vec dotA45 = vecInsert<VecMask::_0011>(dotA45B45, zero);
Vec dotB45 = vecInsert<VecMask::_0011>(zero, dotA45B45);
// collect the results
Vec resA = vecOrx(dotA_0123);
Vec resB = vecOrx(dotB_0123);
resA = vecOr(resA, vecOrx(dotA45));
resB = vecOr(resB, vecOrx(dotB45));
// resA = inside or outside of frustum for point A, resB for point B
Vec rA = vecNotMask(resA);
Vec rB = vecNotMask(resB);
masksCurrent[0] |= frustumMask & rA;
masksCurrent[1] |= frustumMask & rB;

额外的剔除包含视锥体和AABB、将AABB投影到屏幕空间、软件遮挡。将AABB投影到屏幕空间时,计算屏幕空间中AABB的面积,如果面积小于设置就跳过它,由于FOV取距离不起作用。

软件遮挡已在Frostbite中使用了3年,跨*台,艺术家制作的遮挡物,主要用于地形。使用软件遮挡的原因有:想要去除CPU时间而不仅仅是GPU,尽早剔除,GPU查询由于落后于CPU很棘手,必须支持破坏系统,易于艺术家控制。具体做法是使用软件渲染将PS1风格的几何图形渲染到Z缓冲区,其中Z缓冲区是256x114的浮点。具体步骤包含遮挡三角形设置、地形三角设置、光栅化三角形、剔除。

上:场景遮挡体;下:光栅化遮挡体的结果。

并行裁剪作业图例如下:

以下是并行设置遮挡体三角形的图例:


Z缓冲区测试过程是计算对象的屏幕空间AABB,获取单个距离值,根据Z缓冲区测试正方形:

总之,准确和高性能的剔除至关重要,减少低级系统/渲染的压力,一切都与数据有关,简单的数据通常意味着简单的代码,充分了解目标硬件的特性、架构和参数。

Firaxis LORE And other uses of D3D11是系列游戏文明5(Civilization V)的引擎利用D3D11实现多线程绘制的架构改造和优化技巧。

文明5游戏截图。

早期目标是希望图像引擎能够“经得起时间的考验”,使用D3D11 Alpha建立引擎原生的D3D11架构,并向后兼容到DX9。

第一步:降低开销。着色器开始于Firaxis着色语言(FSL)的HLSL超集,编译到CPP和头文件,所有的着色器常量是映射到结构体,分组到包中所有的包具有相同的绑定,模型代码被模板化——FSL生成的头文件被绑定到模板代码中,结果是少量的代码,填写所需的着色,在性能分析几乎没有影响。

第二步:抽象渲染。仍然需要支持DX9,未来可能需要支持主机,可能需要写一个“驱动程序”,解决方案是让DX9看起来像DX11。

渲染封装层的特点:无状态渲染,比D3D简单得多;一个命令集可以包含一个要渲染的表面列表,每个表面都有一个shader常量负载;一个表面是一个由IB、VB、纹理、着色器定义等组成的不可变包(bundle),命令引用这些状态包之一;整帧被队列化,最小化每帧分配。只有5种命令:

  • COMMAND_RENDER_BATCHE
  • COMMAND_GENERATE_MIPS
  • COMMAND_RESOLVE_RENDERTEXTURE
  • COMMAND_COPY_RENDERTEXTURE
  • COMMAND_COPY_RESOURCE

渲染封装层架构图。

基于作业的多线程并行系统。

为什么要对整个帧执行排队?看起来像是额外的开销,但性能分析显示是净收益,内部命令设置超级便宜,只是一些内存拷贝,引擎缓存一致性要好得多,D3D驱动程序缓存一致性在一个巨大的转储中要好得多,提交时间占总CPU时间的百分比非常低,允许过滤冗余的D3D调用,即使在DX9中也很快。

实现优势:一旦掌握了“无状态”的概念,代码维护变得容易。几乎没有状态泄漏(闪烁的alpha、纹理等),因为渲染是分组的,每个任务之间很少或根本不需要通信,没有线程错误。

线程化的D3D11命令提交的问题:批量提交的驱动开销通常很高,但是,D3D11有多线程提交命令流不一定1:1映射到CommandList,文明5可以通过设置配置文件来改变它的提交方式。

线程化的D3D11命令提交。

总之,高吞吐量渲染是可能的,前提是:小心降低应用程序开销,基于作业、基于载重的渲染,过滤冗余状态和调用,使用D3D11命令列表,引擎可以在97%的情况下充分利用12个线程(无驱动)。

DirectX 11 Rendering in Battlefield 3分享了2011年的Frostbite 2引擎利用DirectX 11的特性,在引擎中实现诸多渲染特性,包含延迟渲染、分块渲染、立体3D渲染、各类抗锯齿和性能分析等等。

当时Frostbite 2面临的渲染选择有:切换到延迟着色,BF3中丰富的户外 + 室内 + 城市环境组合,想要更多的光源。为什么不用前向渲染?灯光剔除/着色器排列效率不高,昂贵且更难的贴花/破坏遮蔽。为什么不使用Light Pre-pass?CPU和GPU上的2倍几何通道太高开销,能够将BRDF推广到足够的几个变体,看到了基于分块的延迟着色的巨大潜力。

传统延迟照明/阴影的缺点是拥有大量大光源时的大量透支和ROP成本,在光照着色器中拥有多个逐像素材质的成本很高,MSAA照明可能很慢(不连贯,额外的BW)。

Frostbite 2的解决方案是使用基于分块的延迟渲染(Tile-based Deferred Shading)

1、将屏幕分成瓦片(tile),确定哪些灯影响哪些瓦片。

2、仅对像素应用可见光源。具有多个光源的自定义着色器,降低带宽和设置成本。

使用计算着色器的基于图块的延迟着色,主要用于解析光源(点光源、聚光灯、线光源),没有阴影,需要计算着色器5.0。可使用混合图形/计算着色管道:图形管线光栅化不透明表面的gbuffer,计算管道使用gbuffers、剔除灯光、计算光照并与着色相结合,图形管线在上面渲染透明表面。

计算着色器的第一步是设置输入输出数据,相关代码如下:

// 输入:gbuffers、深度缓冲区和灯光列表
Texture2D<float4> gbufferTexture0 : register(t0);
Texture2D<float4> gbufferTexture1 : register(t1);
Texture2D<float4> gbufferTexture2 : register(t2);
Texture2D<float4> depthTexture : register(t3);

// 输出:完全合成和点亮的 HDR 纹理
RWTexture2D<float4> outputTexture : register(u0);

// 每像素1个线程,16x16线程组
#define BLOCK_SIZE 16
[numthreads(BLOCK_SIZE,BLOCK_SIZE,1)]
void csMain(
   uint3 groupId          : SV_GroupID,
   uint3 groupThreadId    : SV_GroupThreadID,
   uint groupIndex        : SV_GroupIndex,
   uint3 dispatchThreadId : SV_DispatchThreadID)
{
    (...)
}

第二步是加载 gbuffers & depth,计算threadgroup/tile中的min & max z,在组共享变量上使用 InterlockedMin/Max,原子仅适用于整数,可以将float转换为int(z始终为+):

groupshared uint minDepthInt;
groupshared uint maxDepthInt;
// --- globals above, function below -------
float depth =
 depthTexture.Load(uint3(texCoord, 0)).r;
uint depthInt = asuint(depth);
minDepthInt = 0xFFFFFFFF;
maxDepthInt = 0;
GroupMemoryBarrierWithGroupSync();
InterlockedMin(minDepthInt, depthInt);
InterlockedMax(maxDepthInt, depthInt);
GroupMemoryBarrierWithGroupSync();
float minGroupDepth = asfloat(minDepthInt);
float maxGroupDepth = asfloat(maxDepthInt);

第三步是裁剪,确定每个tile的可见光源,针对*截头体剔除所有光源,输入全局的 灯光列表、截锥体和SW遮挡剔除,输出可见光源和可见光源索引列表。

struct Light 
{
    float3 pos; float sqrRadius;
    float3 color; float invSqrRadius;
};

int lightCount;
StructuredBuffer<Light> lights;
groupshared uint visibleLightCount = 0;
groupshared uint visibleLightIndices[1024];

// --- globals above, cont. function below ---
uint threadCount = BLOCK_SIZE*BLOCK_SIZE;
uint passCount = (lightCount+threadCount-1) / threadCount;
for (uint passIt = 0; passIt < passCount; ++passIt)
{
    uint lightIndex = passIt*threadCount + groupIndex;
    // prevent overrun by clamping to a last ”null” light
    lightIndex = min(lightIndex, lightCount);
    if (intersects(lights[lightIndex], tile))
    {
        uint offset;
        InterlockedAdd(visibleLightCount, 1, offset);
        visibleLightIndices[offset] = lightIndex;
    }
}

GroupMemoryBarrierWithGroupSync();

最后的步骤是对于每个像素,累积来自可见光的光照,从群组共享内存中的瓦片可见光索引列表中读取。结合光照和阴影反照率,输出为 MSAA HDR纹理,在顶部渲染透明表面。

float3 color = 0;
for (uint lightIt = 0; lightIt < visibleLightCount; ++lightIt)
{
    uint lightIndex = visibleLightIndices[lightIt];
    Light light = lights[lightIndex];
    color += diffuseAlbedo * evaluateLightDiffuse(light, gbuffer);
    color += specularAlbedo * evaluateLightSpecular(light, gbuffer);
}

对于带MSAA的计算着色器光照,只有边缘像素需要完整的每个样本照明,但是边缘的屏幕空间一致性很差,效率低。Compute Shader可以构建高效的连贯像素列表,评估每个像素的照明(样本0),确定像素是否需要按样本照明,如果是,添加到共享内存中的原子列表,当所有像素都完成后,同步,遍历并点亮样本1-3以获取列表中的像素。性能可以大幅提升!

在Frostbite 2的地形渲染中,使用实例化(instancing)进行优化。DX9风格的流实例化很好,但有限制,例如额外的顶点属性、GPU开销,不能(有效地)与蒙皮结合,主要用于微小的网格(粒子、树叶)。DX10/DX11带来了对着色器缓冲区对象的支持,顶点着色器可以访问SV_InstanceID,可以完全任意加载,不限于固定元素,可以支持每实例数组和其他数据结构。

实例化数据包含多种对象类型(刚体、蒙皮、复合网格)、多种物体照明类型(小型/动态:光照探针、大/静态:光照贴图),拥有的不同类型的实例数据:变换float4x3、蒙皮变换float4x3数组、SH光照探针float4x4、光照贴图UV缩放/偏移float4。可以将所有实例化数据打包到一个大缓冲区中!

// 实例化示例:变换矩阵+SH

Buffer<float4> instanceVectorBuffer : register(t0);

cbuffer a
{
    float g_startVector;
    float g_vectorsPerInstance;
}

VsOutput main(
    // ....
    uint instanceId : SV_InstanceId)
{
    uint worldMatrixVectorOffset = g_startVector + input.instanceId * g_vectorsPerInstance + 0;
    uint probeVectorOffset = g_startVector + input.instanceId * g_vectorsPerInstance + 3;
    float4 r0 = instanceVectorBuffer.Load(worldMatrixVectorOffset + 0);
    float4 r1 = instanceVectorBuffer.Load(worldMatrixVectorOffset + 1);
    float4 r2 = instanceVectorBuffer.Load(worldMatrixVectorOffset + 2);
    float4 lightProbeShR = instanceVectorBuffer.Load(probeVectorOffset + 0);
    float4 lightProbeShG = instanceVectorBuffer.Load(probeVectorOffset + 1);
    float4 lightProbeShB = instanceVectorBuffer.Load(probeVectorOffset + 2);
    float4 lightProbeShO = instanceVectorBuffer.Load(probeVectorOffset + 3);

    // ....
}

// 实例化示例:蒙皮

half4 weights = input.boneWeights;int4 indices = (int4)input.boneIndices;

float4 skinnedPos = mul(float4(pos,1), getSkinningMatrix(indices[0])).xyz * weights[0];
skinnedPos += mul(float4(pos,1), getSkinningMatrix(indices[1])).xyz * weights[1];
skinnedPos += mul(float4(pos,1), getSkinningMatrix(indices[2])).xyz * weights[2];
skinnedPos += mul(float4(pos,1), getSkinningMatrix(indices[3])).xyz * weights[3];

// ...
float4x3 getSkinningMatrix(uint boneIndex)
{
    uint vectorOffset = g_startVector + instanceId * g_vectorsPerInstance;
    vectorOffset += boneIndex*3;
    float4 r0 = instanceVectorBuffer.Load(vectorOffset + 0);
    float4 r1 = instanceVectorBuffer.Load(vectorOffset + 1);
    float4 r2 = instanceVectorBuffer.Load(vectorOffset + 2);
    return createMat4x3(r0, r1, r2);
}

实例化的好处在于每个对象类型而不是每个实例的单个绘制调用,对CPU的最小冲击以获得较大的CPU增益,蒙皮时实例化不会中断,更具确定性和更好的整体性能。最终结果通常是1500-2000次绘制调用,无论艺术家放置了多少对象实例!

DX11的关键特性包含通过将D3D调度扩展到更多内核来提高性能,减少帧延迟。为了利用此特性,每个硬件线程的DX11延迟上下文,渲染器为帧的每个渲染“层”构建我们想要执行的所有绘制调用的列表,将每一层的绘制调用拆分为约256个块,与延迟上下文并行调度块以生成,使用命令列表,呈现到即时上下文并执行命令列表。

当时仍然没有高性能的驱动程序,原因是大型驱动程序代码库需要时间来重构,IHV(Independent hardware vendor,独立硬件供应商)与微软的困境,重驱动线程与游戏线程冲突。运行原理是驱动程序不创建自己的任何处理线程,游戏将工作负载并行提交到多个延迟上下文,驱动程序确保几乎所有需要的处理都发生在延迟上下文的绘图调用上,游戏在即时上下文中调度命令列表,驱动程序使用它做的工作绝对最少。

对于资源流,即使使用具有大量内存的现代GPU,通常也需要资源流,不能要求1+GB显卡,BF3关卡拥有超过1 GB的纹理和网格,减少加载时间。但是在帧内创建和销毁DX资源从来都不是一件好事,可能导致非确定性和大型驱动程序/操作系统停止,在DX中一直是个问题。

已与Microsoft、Nvidia和AMD合作,以确保可以在DX11中对GPU资源进行无停顿的异步资源流处理,不希望CPU和GPU性能受到影响,关键基础是DX11的并发创建。

资源创建流程:流系统确定要加载的资源(纹理mipmap或网格LOD),将DX资源创建添加到单独的低优先级线程上的队列中,线程使用初始数据创建资源,发信号给流系统,资源已创建,游戏开始使用它。在驱动程序中启用异步无停顿DMA!资源销毁流程:流系统删除D3D资源,驱动程序使其在内部保持活动状态,直到使用它的GPU帧完成,没有停顿!

Secrets of CryENGINE 3 Graphics Technology分享了2011年的CryEngine 3的渲染技术,包含渲染管线、位置重建、覆盖缓冲区、延迟照明、阴影、屏幕空间技术、延迟技术、批量HDR后处理、立体渲染等。

文中提到Z缓冲区的注意事项,Z值以双曲线分布,在着色器中使用之前需要转换为线性空间:

// Constants
g_ProjRatio.xy = float2( zfar / (zfar-znear), znear / (znear-zfar) );
// HLSL function
float GetLinearDepth(float fDevDepth)
{ 
    return g_ProjRatio.y/(fDevDepth-g_ProjRatio.x);
}

问题是第一人称视图(FPV)对象,深度缓冲区可以用于防止FPV对象与场景的其余部分重叠,不同的 FOV 和*/远*面(艺术特定选择),不同的深度范围,以防止实际重叠,导致延迟技术不能100%用于此类对象。解决方案是修改深度重建功能,将硬件深度转换为线性深度,第一人称视图对象的不同深度比例,根据深度选择:

float GetLinearDepth(float fDevDepth) 
{
       float bNearDepth = step(fDevDepth, g_PS_DepthRangeThreshold);
     float2 ProjRatio.xy = lerp(g_PS_ProjRatio.xy, g_PS_NearestScaled.xy, bNearDepth);
     return  ProjRatio.y/(fDevDepth-ProjRatio.x);
}

从深度重建位置的思路:将VPOS从屏幕空间S直接线性变换到目标齐次空间W(阴影空间或世界空间),从屏幕剪辑空间到齐次矩阵的直接转换,VPOS是渲染延迟光量的最简单方法,s3D单独调整。

float4 HPos = (vStoWBasisZ + (vStoWBasisX*VPos.x)+(vStoWBasisY*VPos.y) ) * fSceneDepth;
HPos += vCamPos.xyzw;

覆盖缓冲区(Coverage Buffer)作为主要的遮挡剔除系统,本质上是低分辨率深度缓冲区,用于可见性Z测试的对象AABB/OBB的粗略CPU光栅化,在CPU上准备完全详细的C-Buffer太慢,巨大的计算成本,必须在软件中复制完整的渲染管线以获得C-Buffer的所有细节。

在CPU上回读前一帧的GPU深度缓冲区,G-Buffer通道后缩小GPU上的ZBuffer(最大过滤器),通过在单独的CPU线程中光栅化BBox来完成剔除。

左:正常场景;右:覆盖缓冲区。

覆盖缓冲区用于X360/PS3和DX11硬件,PC上的回读延迟较高但仍可接受(最多 4 帧),C-Buffer 大小在控制台上的孤岛危机 2 中限制为256x128。问题:前一帧/当前帧相机之间的不匹配,导致错误的可见性测试。

解决方案是对覆盖缓冲区重投影(Coverage Buffer Reprojection)。使用上一帧相机中的C-Buffer CPU重投影,重投影片元的点溅射。相机信息被编码到C-Buffer数据中,CPU回读和重新投影在单独的线程中,在SPU上约2毫秒,在带有矢量化代码的Xbox 360上约3-4毫秒。重投影后在C-Buffer内缝合孔,3x3的扩大通道,剩余的C-Buffer洞:假设对象是可见的。重投影大大提高了剔除效率,解决各种遮挡测试伪影,检测到无效区域,以更高的帧率更高效地工作。

覆盖缓冲区重投影,红色是重投影之后仍然存在的洞。

CryEngine 3的延迟光照包含环境光、环境探针、GI、SSDO、RLR、光源等。

CryEngine 3的延迟阴影对太阳使用了阴影遮蔽(Shadow mask),特殊渲染目标累积阴影遮挡,阴影遮罩在使用实际阴影之前将多种阴影技术相互叠加。点光源阴影直接渲染到光照缓冲区。

级联阴影从孤岛危机1开始使用,级联拆分方案:*似对数纹理像素密度分布,阴影截头体调整为保守地覆盖相机视图截头体,阴影截头体的方向在世界空间中是固定的。更多级联允许是由于更好地*似对数分布,提高了纹素密度,减少了粉刺并改进了更宽阴影范围的自阴影。对于每个级联,将阴影截头体捕捉到SM的纹理网格。

级联阴影的通道:以延迟方式渲染的级联/点光源的阴影通道,通过渲染截锥体在模板缓冲区中标记的潜在阴影接收区域,允许将更复杂的拆分为级联,在重叠区域中选择具有最高分辨率的级联,避免浪费阴影贴图空间。

级联阴影的缓存:并非所有级联都在单帧中更新,更新成本分布在多个框架中,性能原因(尤其是 PS3),允许更多的级联——更好的阴影贴图密度分布,缓存阴影贴图使用缓存阴影矩阵,远距离级联更新频率较低,最后级联使用 VSM 并与阴影蒙版相加混合,允许从巨大的遥远物体获得大半影。

点光源阴影:总是将泛光灯分成六个独立的投影器,每个投影器的阴影贴图单独缩放,基于阴影投影覆盖率,最终比例是对数阴影图密度分布函数的结果,使用覆盖率作为参数。大纹理图集,可在缩放后每帧打包所有阴影贴图,永久分配纹理图集以避免内存碎片,模板标记的接收区域。

实时局部反射(RLR):光栅化的反射很昂贵,通常是*面反射或立方体贴图,需要重新渲染场景,标准反射受限,无论是*面,立方体贴图的小区域,通常没有曲面,光线追踪直接反射,屏幕空间中的光线追踪以*似局部反射。

实时局部反射基本算法:计算每个像素的反射向量,使用延迟的法线和深度目标,沿反射向量Raymarch,采样深度并检查光线深度是否在场景深度的阈值内,如果命中,重新投影到前一帧的帧缓冲区和样本颜色,结果相对便宜,随处可见的局部反射(即使在复杂表面上),由于屏幕空间中的数据有限而导致大量问题案例。

实时局部反射实现技巧:非常有限的屏幕空间数据,与其破碎的倒影不如倒影,如果反射矢量面向查看器,则*滑淡出,因为在这种情况下没有可用数据,在屏幕边缘*滑淡出反射样本,将抖动添加到步长以隐藏明显的步长伪影,在孤岛危机 2 中采样的HDR颜色目标,基于表面光泽度的抖动或模糊。

接触阴影:首先生成遮挡信息,在SSAO过程中计算和存储弯曲法线 N',弯曲法线是*均未遮挡方向,需要干净的SSAO,没有任何自遮挡和相对较宽的半径。然后对于每一盏灯,像往常一样计算 N dot L和 N' dot L,通过遮挡量乘以两个点积之间的钳位差来衰减照明。

屏幕空间自阴影:无法承受每个角色的阴影贴图(内存),解决内存不足的问题是通过简单的技巧/*似:射线沿屏幕空间光矢量行进,所有角色的宏观自阴影细节。

角色的屏幕空间自阴影。

Bokeh DOF效果采用了另一种内核和权重:

Rendering in Cars 2是迪斯尼继Toy Story 3之后的又一次分享,介绍了游戏汽车总动员2的渲染技术,包含光照探针、HDR色彩精度、提前模板阴影剔除及PS3后期处理。

光照探针的目标是同时支持4个玩家,作用于所有世界几何体的光照贴图,匹配实时照明。

光照探针的处理过程是从空间中的一点捕捉光照,反弹光照,环境映射。反弹光照数据存储为球谐函数,3阶SH = 每个探针108字节,可以免费在直接照明中打包(pack)。

光照探针捕捉时,在GPU上渲染立方体贴图,另存为16F用于HDR,用于速度的图集,反弹照明(立方体贴图到SH投影)。

辐照度体积是带有一堆光照探针的体积,允许在世界范围内使用各种反射光,非常流行与光照贴图一起使用。

体积的选择,用于赛车游戏,每个世界2-5英里的轨道,大部分在外面,许多薄而弯曲的区域,覆盖不是必需的。

均匀的网格体积是盒子体积,可以旋转和缩放以适应任何地方,具有可变切片数量的框拆分(密度 x/y/z),沿着体积切片放置的光照探针。结构简单,易于实现,整个数据保存到一个连续的数组中,带有长方体相交测试的样本,可以通过偏移量访问每个探针,O(1) 的网格内采样,成本只花在体积,但浪费空间。

网格还需要处理过渡区域、无效的点、体积查找等。对于体积查找,有CPU和GPU两种方式,基于CPU:每个网格分配/混合最*的SH,将SH数据传递到GPU;基于GPU:逐像素或逐顶点,GPU上的采样探针。

着色器常量是逐实例的,在着色器中计算颜色,分解大物体,可以通过顶点颜色混合将网格分开,世界照明有同样问题。

对于体积的重叠,需要进行混合,但*滑混合重叠体积很复杂。由于要与全局探测器混合,不能只收集最*的点并*均照明,重叠的区域会产生难以隐藏的疯狂过渡。

可以采用时间*均的方法,将最后一帧的SH的%混合到当前的SH中(逐世界可调),三线性过滤替代品,避免混合第一帧。

无体积的探针非常适合道路反射,将环境映射探针分配给体积。分配环境贴图时,如果在体积内,则使用体积的环境贴图,否则,使用全局探针,基于衰落区域的切换,重叠体积通过共享立方体贴图避免跳变。

渲染直接光照时,将直接照明打包进探头中,可以评估SH中的照明并添加到反弹,没有额外的性能成本,取决于网格密度。

还提供了光照覆盖,如果在体积内,则添加定向和环境光,允许艺术家控制照明,包含二维体积、一维体积、区域光灯类型。

均匀网格简单快捷,使用很少的内存并且可以很好地适应4个玩家,与艺术家有很大的灵活性。

为了同时支持4个玩家操作,需要对GPU的管线进行优化,例如降低HDR渲染的成本,降低阴影成本,为4人分屏缩放阴影贴图,使用多分辨率、延迟渲染、阴影遮蔽等。

首先考虑的是渲染纹理的格式,下表是不同格式的具体说明:

下图是不同格式产生的颜色误差:

可见LogLuv的误差最低且稳定但消耗ALU,7e3较高但较稳定,RGBM高且不稳定!

利用以上特殊格式还可能造成图像的色阶问题:

造成这个问题的原因是HDR处理管线中,从场景渲染到Render Target时损失了精度:

可以将色调映射组件分为两部分,曝光校正和色调映射运算符将其缩放到 [0,1] 范围。它们是可分离的,可以在不同的时间完成。注意,曝光校正是用于Cars 2的,但色调映射运算不是。Cars 2使用Hable提到的基于ALU的电影色调映射算子。

将色调映射分拆之后的HDR管线如下:

获得的结果对比:

对动态物体,还需要从光照贴图接收阴影,进出阴影时只需要粗略的过渡。所以使用了低分辨率的阴影图:使用256x256阴影贴图,超级便宜(约0.1ms),使用简单的代理几何。绘制阴影图时,仅在两个级联中绘制动态对象,减少阴影距离,重投影伪影可接受,因为轨道位于2d*面上。

对于延迟的阴遮蔽,减少处理的像素数,以1/4尺寸渲染RT,采用双边滤波上采样到正常分辨率。

不可避免的伪影是边缘伪影,较低的分辨率,太明显而无法忽略。

提前模板裁剪(early stencil culling)在到达像素着色器之前剔除片元,支持PS3、360和现代PC显卡,PC是自动的,PS3和360手动控制,编写和测试之间的延迟。不过需要注意的是,当时的Early阶段的像素是4x4的像素块。

结合了提前模板裁剪的延迟阴影的过程如下:

1、以1/16分辨率渲染阴影。

使用有限过滤以1/16分辨率渲染阴影遮罩,需要扩大阴影边缘,因为高分辨率下的边缘与低分辨率下的边缘不同,扩大的宽度可以根据它覆盖边缘的程度来配置。

2、用1/16阴影遮蔽填充全分辨率的early stencil。

点采样1/16的RT,打开提前模板写入,如果它在扩张区域内,则texkill。

3、使用提前模板测试以全分辨率重新渲染阴影边缘。

打开提前模板测试,提前模板剔除先前通道中填充的像素,仅渲染约30%的像素。两个通道的双边模糊,保持开启提前模板测试,仅模糊约30%的像素。

双边模糊效果对比,右边是模糊后的效果。

对于边缘阴影下次,也可以使用提前模板测试解决。提前模板只是一个遮罩,扩大不覆盖模糊区域,仅发生在具有大范围扩张的极端特写镜头中。

阴影值为0或1,级联选择,大多数像素处于交叉双边滤波器中。渲染到精度有限的目标时,预曝光颜色非常有效,动态对象的低分辨率阴影贴图很便宜,延迟阴影遮罩渲染时间有效地减少了一半。

该文还详细描述了基于SPU的后处理管线和优化以及立体3D渲染的双摄像头渲染优化(如遮挡、视锥体合并)等。

立体3D渲染的遮挡体瑕疵优化。

DX11 Performance Gems详细阐述了DX11的高性能优化技术和建议,给出了案例不透明度映射实现和优化(曲面细分加速光照、GatherRed加速上采样、SV_SampleIndex改善AA、软粒子的只读深度等)。

DX11的延迟上下文是用于构建命令列表的类似设备的接口,DX11使用相同的ID3D11DeviceContext接口进行“即时”API调用,即时上下文是最终向GPU提交工作的唯一方法,通过ID3D11Device::GetImmediateContext()访问,ID3D11Device没有提交API。

DirectX11的多线程命令生成和提交机制。图中有两个带有延迟上下文的线程,它们各种记录和生成命令,生成的命令列表被线程间同步、整理/排序/缓冲,然后被渲染主线程提交给即时上下文,再由即时上下文提交给GPU。

DX11内部结构比较灵活的,DX11运行时有内置实现,但是驱动程序可以负责并使用自己的实现,例如命令列表可以构建在较低级别,将更多的CPU工作转移到提交线程上。延迟上下文的优化建议:

  • 尝试通过上下文/线程*衡工作负载。但提交工作量很少情况是可预测的,粒度有帮助(如果提交线程能够动态处理工作),如果可能,先做较重的提交工作量,每个内核约12 个CL(命令列表),每个CL约1ms是一个很好的目标。
  • 保证合理的命令列表大小。想一想命令列表中的绘制调用数量很像绘制调用中的三角形数量,即每个列表都有开销,约相当于几十个API调用。
  • 留一些空闲的CPU时间。让所有线程忙碌会导致CPU饱和并阻止来自线程渲染的服务线程(游戏引擎不要使用超过N-1个 CPU内核),“忙”包括忙等待(即轮询),始终为图形驱动程序留一个。
  • 留意内存!每个Map()调用都将内存与CL相关联,释放CL是释放内存的唯一方法,可以在2GB的虚拟地址空间中变得紧张!

文中使用DX11的案例是不透明度图。使用DX11曲面细分在DS中以中间“最佳点”速率计算光照,高频分量可以根据需要保持在每像素或每采样率,如不透明度、可见度。

PS、VS、DS光照的fps和效果对比如下:

自适应曲面细分提供两全其美,类VS计算频率,与屏幕像素频率的类PS关系(1:15 在这种情况下效果很好),适用于任何缓慢变化的着色结果(GI、其他体积算法)。主要瓶颈是曲面细分操作之后的填充率,所以将粒子渲染到低分辨率离屏缓冲区,显著优势,即使使用 曲面细分(GTX 560 Ti / HD 6950 为 1.2 倍至 1.5 倍)。但是,从低分辨率进行简单的双线性上采样会导致边缘出现伪影。

相反,使用最*深度上采样(nearest-depth up-sampling),概念上类似于交叉双边过滤,将高分辨率深度与相邻的低分辨率深度进行比较,在深度不连续处来自最*匹配邻居的样本(否则为双线性)。

最*深度上采样计算过程。

效果对比。

使用SM5的GatherRed()一次有效地获取2x2低分辨率深度邻域:

float4 zg = g_DepthTex.GatherRed(g_Sampler, UV);
float z00 = zg.w;    // w: floor(uv)
float z10 = zg.z;    // z: ceil(u), floor(v)
float z01 = zg.x;    // x: floor(u), ceil(v)
float z11 = zg.y;    // y: ceil(uv)

在每个样本运行时,最*深度的上采样与AA配合得很好,而且性能惊人!(FPS影响 < 5%)

float4 UpsamplePS( VS_OUTPUT In,
                   uint uSID : SV_SampleIndex // 样本序号
                 ) : SV_Target

软粒子(基于深度的Alpha渐变)需要从场景深度读取,对于DX11之前,意味着要么牺牲深度测试(以及任何相关的加速度)要么保持两个深度表面(以及所需的任何复制)。DX11的解决方案是使用D3D11_DSV_READ_ONLY_DEPTH声明的深度模板缓冲:

软粒子的步骤如下:

1、将不透明对象渲染到深度纹理。

2、使用深度测试渲染软粒子。

最终效果对比如下:

整体性能提升5到10倍,DX11曲面细分给了大部分贡献,但是以降低的分辨率进行渲染会降低填充率并让曲面细分发光,GatherRed()和RO DSV也节省了周期。

High Performance Post-Processing谈及了后处理的特点、瓶颈及优化技术,并提供了几个实战案例。

文中提到DX11的新资源类型:缓冲区/结构化缓冲区、无序访问视图 (UAV):RWTexture/RWBuffer,允许从PS和CS进行任意读写,“分散”的能力提供了新的机会,必须意识到危险和访问模式。

新增的DirectCompute在任意线程上运行的新着色器模式,将处理从图形管道的限制中解放出来,完全访问传统Direct3D资源。DispatchIndirect从设备缓冲区而不是从CPU中获取调度参数,让计算工作驱动计算!仍受CPU约束以发出DispatchIndirect调用,与Append缓冲区结合使用以生成动态工作负载时非常好。以下是DX11的内存类型和属性表:

内存容量 速度 可见性
全局内存(缓冲区、纹理、常量) 最长的延迟 全部线程
共享内存(groupshared) 单个线程组
局部内存(寄存器) 非常快 单个线程

对于线程间通信,组中的线程可以通过共享内存进行通信,线程执行不能依赖其它组!并非所有组同时执行,组可以在Dispatch中以任何顺序执行,组间依赖可能导致死锁,如果一个组依赖于另一个组的结果,建议将着色器拆分为多个调度。

对于数据危险和停顿,重新绑定用作UAV的资源可能会停止硬件以避免数据危害,必须确保所有写入完成,以便下次调度可以看到它们,驱动程序可能会重新排序不相关的Dispatch调用以隐藏此延迟。

上下文切换存在开销,必须注意上下文切换成本:图形和计算之间的惩罚切换,通常很少,除非反复刺激,连续(Back-to-Back)调度避免了这种情况,所以分组调用。

常见的陷阱有内存限制和计算限制。内存限制包含低效的访问模式、低效的格式、数据太多。计算限制包括分支(Divergent)线程、错误的指令组合、硬件利用率低。

DX11的内存架构有点特殊,引入了复杂的内存系统。缓存行为取决于访问模式,对于线性访问,缓冲区会更好地命中缓存,纹理更适用于组内更不可预测/更多的2D访问。

使用特殊的分层采样(Stratified Sampling)。下图是两个稀疏采样模式,分布在一个2x2的线程块中。左边的图案是一个简单的抖动,它从像素给定半径内的随机位置收集四个样本。每个样本仅使用随机径向偏移,导致相同访问的相邻像素之间的位置可能有很大差异。因此,虽然它们可能存在从相似邻域获取的某些局部性,但随机偏移量意味着并发访问不太可能命中纹理缓存中的同一块,从而增加了带宽需求。在右侧,在最大半径内看到类似的4-tap稀疏采样模式。但是,这种方法不是使用任意偏移,而是使用分层采样,使得所有线程中的访问对应于圆的同一个扇区,因此同时访问更有可能命中同一个缓存区域,并且可以合并。

分层采样示意图。左侧随机采样,可能导致命中率低下;右侧使用分层采样,使得所有线程中的访问对应于圆的同一个扇区,提示命中率,并且可以合并读写操作。

与纹理不同,缓冲区是线性内存,确保尽可能读取映射到缓冲区的二维数组的间距!

Buffer<float> srvInput;
[numthreads(128,1,1)]
void ReadCS(
  uint3 gID : SV_GroupID
  uint3 tID : SV_DispatchThreadID)
{
  float val;

  // 好: 沿间距读取。
  val = srvRead[128*gID.x + tID.x];
  // ... Use data ...

  // 不好: 不沿间距读取。
  val = srvRead[128*tID.x + gID.x];
  // ... Use data ...
}

理论上线程独立执行,实际上它们以并行的wavefront执行,在wavefront执行时,线程被“屏蔽”以用于未执行分支中的指令。不同的wavefront可以免费分支,wavefront大小取决于硬件(NV:32、AMD:64)。

分支的图例。该计算着色器被定义为一组由两个wavefront组成。第一种条件情况将每个wavefront分成两个交错的集合,使它们发散。因此,每个线程都执行两个分支,wavefront基本上是50%空闲。第二个条件导致线程组内的分支; 但是,在这种情况下,一个wavefront中的所有线程都采用一个分支,而第二个wavefront中的所有线程都采用另一个。 因此,每个wavefront只执行一个分支,没有空闲线程。

在PS中,线程被分组为2D样本簇,如果它们在图像中是连贯的,则分支是可以的,特别是如果它可以节省工作!

在PS中,分支可能会降低着色效率,但是否有分支并不重要,重要的是它们在一个小区域内分叉多少。事实上,如果分支允许减少昂贵的操作(例如使用更少的纹理样本),则可以大大提高着色器的性能。

为了提升利用率,创建足够的工作以使硬件饱和,几十个线程组差不多是甜蜜点;最大化每组的线程数,需要足够的时间来隐藏硬件中的延迟,256-512是一个很好的目标;尝试共享内存使用,更多共享内存 = 更少的组/处理器,当共享内存/线程增加时尝试使组更小。

计算线程可以通过“groupshared”内存进行通信和共享数据,预加载组中每个线程使用的数据(解包值、动态规划),节省带宽和计算,共享常见任务的工作量,计算集合的总和/最大值/等,比共享原子更有效。

预加载数据到组内共享内存的示例。可分离卷积:将内核的整个足迹读入共享内存,从共享缓冲区中获取值并乘以每个像素的内核,尽量少读取,效率更高!



几种在着色器中求和的算法和图例。性能从上到下依次提升。

文中还举了具体的例子加以说明后处理的优化技巧,包含SAT DOF、Scattered Bokeh DOF等。


在研究和工业中开发的大量渲染和图形应用程序都是基于场景图,传统的场景图封装了完整3D场景的层次结构,并结合了语义和渲染方面。Separating Semantics from Rendering:A Scene Graph based Architecture for Graphics Applications提出了场景图的语义和渲染部分的清晰分离,可获得一种普遍适用的图形应用程序架构,该架构松散地基于众所周知的模型视图控制器 (MVC) 设计模式,用于分离应用程序的用户界面和计算部分。还探索了这种新设计对各种渲染和建模任务的好处,例如渲染动态场景、大型场景的核外渲染、树木和植被的几何生成以及多视图渲染。最后,展示了在大型框架中使用该软件架构的过程中已经解决的一些实现细节,用于快速开发可视化和渲染应用程序。在传统设计中,应⽤程序可以维护对部分场景图的引⽤,以便动态修改场景图。

状态也可以直接存储在场景图中,使遍历变得复杂。

以上两种设计都会导致⼤型或复杂应⽤的缺陷。在第⼀种情况下,状态与场景图分离,场景图的层次结构不⼀定反映在应⽤程序中状态存储的⽅式上。为了克服这个问题,渲染场景图的结构可以在应⽤程序中部分实现并行结构,导致重复⼯作,或者不同的结构状态可能难以维护。如果场景图的不同部分之间存在依赖关系,则与第⼆种情况⼀样,在场景图中存储状态可能会导致复杂性显著增加。

通过将语义与渲染场景图完全分离并引⼊拆分场景图架构来寻求更完整的解决⽅案来解决这些问题:

  • 语义场景图(semantic scene graph):体现了⽤⼾建模的场景。在纯渲染应⽤程序中,此图在其初始创建后永远不会被修改。
  • 渲染场景图(rendering scene graph):传统意义上的场景图,⽣成显⽰场景所需的渲染操作序列,它的结构受所使⽤的渲染后端的影响。

⼀个典型的图形应⽤程序就像⼀个编译器,它将⼀个真实的或隐含的语义场景图作为输⼊,并为3D输出⽣成渲染场景图。在这个翻译操作期间,语义场景图的单个节点通常被翻译成渲染场景图的多个连接节点(下图)。

典型的图形应⽤程序通过转换真实或隐含语义场景图的节点来构建渲染场景图。

通过使用从语义场景图生成渲染场景图的相同技术,渲染场景图将成为小场景图片段的森林,这些片段在遍历语义场景图时根据需要动态生成(下图)。在某种程度上,对语义和渲染场景图的分离让人想起作为总线系统的场景图,然而,渲染场景图块的森林可以完全从语义场景图重建,并代表一个扩展的场景图或翻译版本。

在动态翻译期间,语义场景图被翻译成渲染场景图片段的森林。

为了使渲染场景图真正动态化,在转换步骤中引入状态,并允许遍历场景图来修改现有渲染场景图片段以反映新状态(下图)。已通过创建翻译规则字典来实现,该字典包含规则对象的创建者函数,这些规则对象包含翻译的当前状态。这样一个规则对象的每个构造函数都会构建一个渲染场景图片段,该片段对应于翻译后的语义场景图节点,并存储对它的引用。

每个语义场景图节点的动态翻译会创建一个包含当前翻译状态的规则对象,每个规则对象都包含对其渲染场景图片段的引用,这种结构可以看作是模型-视图-控制器(MVC)设计模式的一种变体。

文中还详细阐述了实现细节及应用案例,有兴趣的童鞋自行点击原文阅读。

Scaling the Pipeline分享了Frostbite 2引擎的资产管理及管线的伸缩。

Frostbite 2引擎的资产管线的目标是支持多站点协作(上海、欧洲、北美)、大型团队(在某些情况下有400多个工作人员)、多个VCS分支、许多目标*台(个人电脑、PS3、Xbox 360)、内容丰富的游戏。

Battlefield 3的规模达到500GB的原始DCC资产,80GB的原生Frostbite资产,10万个文件,约18GB目标数据 (PC),10万个单独的构建步骤 (PC),当时正在开发的游戏更大。

Frostbite引擎的资产管线如下图,采用结构化存储,以构建为中心,单一资产加载路径,始终在目标上预览,支持资产热插拔,直接调整路径,游戏中的一些显式实时编辑代码。

资产打包模型见下图:

  • 捆绑包(Bundle)。资产的线性流(通常),关卡、子关卡(流式传输),线性只读(推送)。
  • 数据块(Chunk)。免费的流数据块,纹理mips、电影、网格,随机访问(拉取)。
  • 超级捆绑包(Superbundle)是容器文件,存储捆绑包和数据块。一旦安装了超级捆绑包,里面的数据就可见了。

在开发过程中,布局存储在Avalanche Storage Service中,存储捆绑包和超级捆绑包的完整描述,存储为带有块引用的包,游戏/工具请求时(通过 HTTP)即时组装捆绑包,游戏不知道网络和磁盘构建的区别(单路径)。每次构建过程都会执行完整的打包逻辑,包括迭代构建!所以必须非常快。

资产管线目标:等待构建所花费的时间 = 浪费,优化引导时间(初始构建),构建吞吐量,优化反馈时间(迭代构建),大型游戏需要高度可扩展的解决方案,具有挑战性的!还有一点吃力不讨好的任务……如果人们注意到你的工作,可能是因为你弄坏了东西,或者太慢了!

以下是存储架构及其延迟、吞吐率:

存储类型 延迟 吞吐率
寄存器 < 1 ns -
缓存 < 10 ns > 100 G/s
内存 < 500 ns > 1 G/s
网络缓存 < 50 μs -
SSD < 200 μs > 200 M/s
HDD < 20 ms > 50 M/s

这是一个缓存层次结构,更大的缓存有助于提高性能,可用系统RAM用作缓存。不要忘记将大量内存放入工作站,它将减少I/O的影响,工作集适合免费RAM -> 好!如果工作集不适合系统缓存,性能就会下降,就像不在L1/L2/L3缓存中时CPU工作一样。

构建缓存实现:

  • 从构建输入生成的密钥。输入文件内容 (SHA1),其它状态(构建设置等),构建函数版本(“手动”哈希)。

  • 可缓存的构建函数分为两个阶段。第一阶段记录所有输入,第二阶段执行工作。

  • 构建调度程序。执行第一阶段,查询缓存,如果可用,使用结果——否则运行第二阶段。

构建模型是应用函数将源数据映射到目标:

\[\text{资产}_\text{目标}= f(\text{资产}_\text{来源}, \ \ ...) \]

目标:纯功能,无副作用!简单的并行性

资产数据库:在Avalanche存储服务中管理的数据,建立商店的类似实现,即日志结构化,由将数据“导入”到数据库的映射过程产生,非常像常规的数据构建过程!数据可以从原生格式文件导入... 或其它数据源(SQL、Excel 等),保存涉及将数据库资产“导出”回文件,即反向映射。

这种数据库的好处是在构建之前无需保存到磁盘(或签出);构建的快照隔离;用于创建多个会话的廉价分支,即并排预览相同的级别/对象/着色器,不同的设置;与构建系统紧密集成;快速同步,几秒钟即可启动并运行,延迟获取等等。

Cutting the Pipe: Achieving Sub-Second Iteration Times提出了一种迭代流程,以便快速响应需求迭代,提示生产力和质量,优化流水线延迟。文中提出的迭代流程如下图:

需要更快迭代时间的原因是提升生产力,降低等待构建的时间;提升质量,增加更多调整,在控制台上在游戏中测试的资产。

在编辑场景时,不用缓存,而是实时编辑,但实时编辑的有:游戏并不总是最好的编辑器,如果游戏数据是正在使用的二进制图像,版本控制很棘手,协同工作和合并变更也很棘手,适合编辑的数据格式没有最佳的运行时性能。

两全其美的快速迭代是快速游戏和快速工作流。快速游戏需具备二进制资源、就地装载、没有寻道时间,快速工作流程需要编译时间短,热重载,即时反馈。进攻战略是尽可能快地编译并用重新加载替换重启:

重新编译和重新加载所有数据 (>1 GB) 的速度永远不够快,必须分小块工作:将游戏数据视为个体资源的集合,每个资源都可以单独编译,然后在游戏运行时重新加载,按类型+名称标识,两者都是唯一的字符串标识符(经过哈希处理),名称来自路径,但可将其视为 ID(仅通过相等比较)。

编译资源时,每个资源都编译为特定于*台的运行时优化二进制块,由名称哈希标识。

加载资源时,资源被分组到用于加载的包中,包由后台线程流入,在开发过程中,资源存储在以哈希命名的单个文件中,对于最终版本,包中的文件捆绑在一起以进行线性加载。

重新加载资源:运行游戏侦听TCP/IP端口,消息是JSON结构,来自内部工具的典型命令,启用性能HUD,显示调试行,Lua REPL(读取-评估-打印-循环),重新加载资源,也用于所有的工具可视化。

重新加载资源的细节:加载新资源,根据类型通知游戏系统,指向新旧资源的指针,游戏系统决定做什么,如删除实例(声音)、停止和启动实例(粒子)、保留实例,更新它(纹理),销毁/卸载旧资源。

// 重新加载资源示例
if (type == unit_type) 
{
    for (unsigned j=0; j<app().worlds().size(); ++j)
    {
         app().worlds()[j].reload_units(old_resource, new_resource);
    }
}

void World::reload_units(UnitResource *old_ur, UnitResource *new_ur)
{
    for (unsigned i=0; i<_units.size(); ++i) 
    {
          if (_units[i]->resource() == old_ur)
             _units[i]->reload(new_ur);
    }
}

void Unit::reload(const UnitResource *ur)
{
    Matrix4x4 m = _scene_graph.world(0);
    destroy_objects();
    _resource = ur;
    create_objects(m);
}

存在的问题:将数据部署到控制台,处理大量资源,编译缓慢的资源,重新加载代码。其中部分问题解决如下:

  • 大资源。永远无法快速编译和加载非常大的资源 (>100 MB),找到合适的资源粒度,不要将所有关卡的几何图形放在一个文件中,在单独的文件中存放具有实体的几何图形,让关卡对象引用它使用的实体。

  • 缓慢的资源。冗长的编译使快速迭代变得不可能(光照贴图、导航网格等),将烘焙与编译分开,烘焙始终是一个明确的步骤:“立即制作光照贴图”(编辑器按钮),烘焙数据保存在源数据中并检入存储库,然后像往常一样编译(从原始纹理到*台压缩)。

  • 重新加载代码。重新加载是最棘手的资源,有四种代码(着色器(Cg、HLSL)、二进制流(可视化脚本)、Lua、C++),流和着色器被视为普遍资源,只是二进制数据。

实时重新加载LUA如下图:

重新加载C++代码:工具支持“重启Exe”,exe已重新加载,但仍然在同一位置看到相同的对象,只是使用新的引擎代码,状态由工具持有。没有达到<1s 的目标,但仍然非常有用,小尺寸的exe有所帮助。

快速编译图例:

还支持增量编译:查找自上次编译以来修改的所有源数据,确定依赖于这些文件的运行时数据,重新编译必要的部分,重要的是过程坚如磐石,信任来之不易,也容易失去,“完全重新编译是最安全的”。

依赖性是一个挑战。base.shader_source包括common.shader_source,如果common.shader_source改变需要重新编译,如果不读取每个文件,我们怎么能知道这一点?解决方案:编译数据库,存储以前运行的信息,在启动时打开,在关闭时保存更新。编译文件时,将其依赖项存储在数据库中,通过跟踪open_file()自动确定它们。

二进制版本也是个挑战。如果纹理资源的二进制格式发生变化,每个纹理都需要重新编译。解决方案是重用数据库:将每个编译资源的二进制版本存储在数据库中,检查数据编译器中的当前版本,如果不匹配,重新编译,为数据编译器和运行时使用相同的代码库(甚至相同的 exe),因此二进制版本始终保持同步。

启动和关闭编译器,仅在启动和关闭编译器进程上花费了几秒钟。解决方案:重用该过程!作为服务器运行,通过TCP/IP接收编译请求。

扫描源文件,可以是以下代码:

foreach (file in source)
    dest = destination_file(file)
    if mtime(file) > mtime(dest)
          compile(file)

这种方式很慢,检查每个项目文件的mtime,而且碎片化(视日期而定)。可尝试显式编译列表,工具发送一个它想要重新编译的文件列表,工具跟踪已更改的文件,纹理编辑器知道用户更改的所有纹理,快速但碎片化:在工具svn/git/hg update之外不起作用,在Photoshop中编辑的纹理,在文本编辑器中编辑的Lua文件。

解决方案:目录观察者。在服务器启动时执行完整扫描,初始扫描后,使用目录监视来检测更改,ReadDirectoryChangesW(...),无需进一步扫描,使用数据库避免脆弱性。将上次成功编译的mtime存储在数据库中,如果扫描期间mtime或文件大小不同 - 重新编译,如果目录观察程序通知更改 - 重新编译。但会引发条件竞争,可由下图的方式解决竞争:

对于依赖项,由于不破坏进程,可以将依赖数据库保存在内存中,只需要在服务器启动时从磁盘读取,可以将数据库作为后台进程保存到磁盘,当要求重新编译时,不必等待数据库被保存,稍后编译器空闲时保存。

最后处理:处理请求时唯一的磁盘访问是:编译修改后的文件,创建目录观察者“fence”文件,否则一切都发生在内存中。结果如下:


通用规则:

  • 考虑资源粒度。大小合理,适合单独编译/重新加载。
  • TCP/IP是你的朋友。优先通过网络做事而不是访问磁盘,将进程作为服务器运行以避免启动时间。
  • 使用数据库+目录观察器来跟踪文件系统状态。数据库还可以在编译器运行之间缓存其它信息,保留在内存中,在后台反映到磁盘。

Putting the Plane Together Midair谈到了游戏中AI行为树的常见几种设计模式。脚本、层级有限状态机、行为树的优缺点和特点:

类型 优点 缺点
脚本 完整和直接的游戏控制
可广泛应用于诸多模块和系统
难以调试和优化
需要设计师的大量工程专业知识
层级有限状态机 直观的设计师
不错的低级控制
难以扩展和重用
难以达到目标导向
行为树 利用脚本的强大功能和灵活性,使其成为设计师的简单视觉语言。
采用分层 FSM 的直观和反应能力,使其可重用和目标导向

行为树图例。

行为树的节点以下有几种类型:

  • 序列(Sequence):与(&&)。
  • 选择器(Selector):或(||)。
  • 装饰器(Decorator):FOR循环。
  • 条件(Condition):游戏状态检查。
  • 动作(Action):玩法互动。

实现行为树的设计模型有:

  • 组合(Composite)。用户定义的节点和行为树。

  • 轻量级(Flyweight)。使用相同行为树定义的多个AI角色。

  • 访问者(Visitor)。需要每个AI角色能够走同一棵树并保持自己的状态。

还存在事件驱动树(Event-Driven Tree),其目标是让设计师学习并成为单一语言的专家,快速执行基于更新的AI脚本和基于事件的关卡脚本。事件驱动树就像更新驱动树(Update-Driven Tree),只是它们只tick一次。

2012年,Accelerating Rendering Pipelines Using Bidirectional Iterative Reprojection描述了利用迭代重投影和双向重投影来加速渲染管线的技术。

当前的图形架构需要对每一帧进行强力渲染,因此它们不能很好地扩展到高帧速率。然而,由于时间相干性,附*的帧通常非常相似,通过重用相邻帧的渲染结果,可以在不执行光栅化和着色的情况下合成一个合理的帧。

帧间插值示意图。

涉及实时重投影的策略有:

  • 从目标视点栅格化场景并从源视点采样着色。(Nehab2007)
  • 使用每像素图元将现有帧扭曲到目标视点。(Mark1997)
  • 使用某种*似。(Andreev2010,Didyk2010)
  • 使用迭代搜索扭曲帧。(Yang2011, Bowles2012)

假设有一个使用渲染器生成的渲染帧,如何在给定渲染帧的情况下合成新帧?MV(运动向量)是渲染管线常见的衍生数据,提供从源帧到目标帧的映射。

迭代投影示意图。

基于图像的迭代重投影过程如下:

  • 通过方程知道每个像素的映射:

    \[p_{tgt} = p_{src} + V(p_{src}) \]

  • 在目标帧上运行GPU着色器:\(p_\text{tgt}\)已知,如何解析\(p_\text{src}\)

  • 可以迭代地解析:

  • 定点(Fixed Point)迭代。算法如下:

    • 选择一个起点:(例如\(𝑝_{𝑡𝑔𝑡}\))。
    • 应用递归关系直到收敛:

第1次到第4次迭代依次是:左上、右上、左下、右下。

几种方法的性能对比。

迭代初始化,简略地说,FPI贪婪地收敛到最*的解。如下图的Source图像所示,两个正在移动的球体的示例,并最终在目标图像中重叠配置。三种解决方案——源图像中的三个表面点位于目标图像中,运动边界在MV中产生不连续性,以红色显示。在每个区域中开始迭代返回最接*的解决方案,想从这里开始迭代。

细分为四边形并在扭曲位置栅格化:

有一种特殊的情况,在静止的汽车像素和快速移动的道路像素之间存在很大的运动不连续性,导致汽车后面的区域在源视图中被遮挡。(下图)

之前已经有一些方法解决不连续的问题:

  • 重新着色 (Nehab2007)。需要再次遍历场景。
  • 修复 (Andreev2010, Bowles2012)。基于图像,取决于该区域的孔大小和视觉显着性。
  • 双向重投影(Yang2011)。

而文中提出了自己的解决方案:从两个源图像重投影。

场景:帧插值:渲染I帧(帧内或关键帧),插入插值B帧(双向插值帧),这就是双向重投影(Bireproj)

  • 为每对\(I\)帧生成运动流场。
  • 对于\(B\)帧中的每个像素$ t + α$:
    • 在正向流场\(𝑉_𝑡^𝑓\)中搜索以重新投影到\(I\)\(t\)
    • 在反向流场\(𝑉_{𝑡+1}^𝑏\)中搜索以重新投影到$ I \(帧\) t +1$。
    • 从帧$ t $和 $t +1 $加载和混合颜色。

  • 运动流场映射 I 帧 $t $和 \(t +1\) 之间的像素,独立于𝛼。

  • 假设\(t\)\(t +1\)之间的运动是线性的:将向量缩放 𝛼 (或 1−𝛼 )。

  • 使用迭代重投影解决\(𝑝_{𝑡+𝛼}\)

后续还需要生成运动向量场、选择正确的像素、额外搜索初始化、分区渲染等步骤。

选择正确的像素

额外搜索初始化

分区渲染

其存在的限制:

  • 动态着色插值。
    • 不好:仅在一个来源中可见时不起作用。
    • 好:每个B帧分离和渲染有问题的组件。
  • 快速移动的薄物体可见性。
    • 不好:重投影可能未正确初始化。
    • 好:使用健壮的初始化(使用DX 10+级别的硬件)。
  • Bireproj引入了一个小的延迟。
    • 不好:位置延迟小于一个(I 帧)时间步长。
    • 好:响应延迟最小化 (约等于0)。

总之,重用着色结果以减少冗余计算;基于图像的迭代重投影纯基于图像(无需遍历场景),速度快,在PS3 (1280x720) 上为0.85毫秒,给定适当的初始化时非常准确的重投影;双向重投影几乎消除了遮挡伪影,将帧速率提高* n(插值帧数)倍,插入动态着色变化。

[Deus Ex is in the Details](http://twvideo01.ubm-us.net/o1/vault/gdc2012/slides/Programming Track/DeSmedt_Matthijs_Deus Ex Is.pdf)讲述了DirectX 11的特点及应用。

DirectX 11 GPU优化包含只读深度缓冲区、计算着色器本地存储、收集指令。早期的CPU是瓶颈,场景中有许多独特的对象,非常灵活的材质系统,状态变化过多,最小化drawcall之间的状态变化(实例化、状态对象、常量缓冲区、池化静态顶点和索引缓冲区)。

状态对象绑定到持久对象,如材质中的BlendState,哈希表中的JIT创建和缓存,哈希创建参数比Create…State更高效,创建仍然需要时间,可在启动期间预热状态对象。常量缓冲区绑定到对象,如灯光状态(用于前向渲染)、材质参数、实例参数,按更新频率划分的其它常数(可绘制、场景)。

用DirectX 11实现的效果有抗锯齿、SSAO、景深、曲面细分、软阴影等。

DX11实现的几种抗锯齿。

高斯模糊在PS和CS的性能对比,卷积核越大,CS加速越明显。

SSAO在控制台 SSAO模糊深度,PC则在半球中采样,更少失真, 开销更大,类似于星际争霸2。SSAO双边模糊在DX9上采用9x9内核的像素着色器,DX11则使用19x19内核的可分离计算着色器,更流畅、降低噪点、性能影响小。SSAO自遮挡的问题:深度缓冲区不是法线映射的,夸张的法线贴图导致半球与*面几何体相交,没有可用的顶点法线。解决方案:深度缓冲区包含几何,想要视图空间顶点法线,SSAO内计算视图空间位置,ddx() 和 ddy() 返回任何变量的斜率,视图空间顶点法线重建:normalize(ddx(viewpos)×ddy(viewpos))

曲面细分裂缝(下图)由多个子网格组成的角色、多个顶点位置相同、不连续法线导致。曲面细分裂缝解决方案:生成“Tessellation Normal”通道,均衡该位置所有顶点的法线,按三角形大小加权的法线,修复网格边界上的裂缝,低开销。

镶嵌凸起问题:硬边改为*滑法线,部分机型出现此问题,通过*均法线来修复裂缝!Phong曲面细分创建圆形几何图形。解决方法:艺术家在边缘添加额外的多边形。

细分优化:对约10m的距离启用曲面细分,禁用前淡出曲面细分,保持Hull着色器简单快速,曲面细分仅基于距离,最大曲面细分系数3.0,剔除因子为0.0的背面三角形。

软阴影需要SM5着色器,9x9滤波器内核来完成软PCF,使用GatherCmpRed获取4个样本。软阴影的问题:使用前向照明渲染的所有阴影投射灯,9x9内核只需几秒钟即可编译,导致着色器构建时间爆炸,延迟所有光源的风险太大。解决方案:渲染屏幕空间中延迟的软阴影,游戏只需要一个着色器,前向光照期间从软阴影缓冲区中采样,不用于半透明。

多显示器渲染使用供应商特定的API扩展,应该支持所有配置,如何处理挡板上的十字准线?使用偏心投影矩阵保持主显示器的原始视野,FOV高时在裁剪*面附*拉回,增加贴花的深度偏差。

立体渲染使用供应商特定的API扩展,为每只眼睛渲染帧,只进行一次剔除,立体的投影矩阵。

Mastering DirectX 11 with Unity由NVIDIA和Unity的研发人员共同分享的主题,关于DirectX 11在Unity的渲染器、新功能和效果。

当时Unity新增的功能包含Unity DirectX 11 渲染器、Unity“基于物理”的着色器、修改后的照明管线、Catmull-Clark曲面细分、混合形状、PointCache (PC2) 支持、反射(立方体贴图、四边形)探针、后期处理、增量光照贴图烘焙。

Unity“基于物理”的着色器:受 Mental Ray “架构” (MIA) 着色器的启发,足以用于高质量离线渲染大多数硬表面材料(金属、木材、玻璃、粘土),熟悉CG/非游戏美术师,基于物理减少了参数的数量,能量守恒(有点)不可能打破物理定律设置材质,可预测的结果避免直观的Hack影响,几乎所有东西都只有1个着色器。

漫反射用Oren-Nayar,1个参数:粗糙度,兰伯特(粗糙度 == 0),不同的*似值。

镜面反射:Cook-Torrance,2个参数:粗糙度(光泽度)、反射率,能量守恒,基于物理的微观层面理论。

菲涅耳曲线 - 反射率如何取决于视角,2个参数:面对相机(0度),90度到相机,使用 Schlick *似插值:\(lerp (Refl_{at0},\ Refl_{at90},\ (N\cdot V)^5)\),在MIA中称为BRDF(双向反射分布函数)。

能量守恒:漫反射 + 反射(+ 折射)<= 1,热力学第一定律,反射率从漫反射和透明度中获取能量,增加反射率将减少漫射能量,100% 反射永远不会是漫反射或透明的!透明度从漫反射中吸收能量,标准Alpha混合。Cook-Torrance和Oren-Nayar都是能量守恒的,不像原来的Blinn-Phong,强烈的亮点很窄,宽的亮点不太强烈。

模糊的反射,通过采样不同的miplevel (LODbias) 实现低质量模糊,DX9/GL需要跨立方体贴图边缘进行修复,不适用于*面反射,只有方框过滤。通过多次采样提高模糊质量 (1..8),弯曲表面法线,模拟粗糙表面的微面,根据“弯曲”法线反射视图方向,可以模拟各种法线分布。

组合2个法线贴图:艺术家想要详细的法线贴图,混合2个法线贴图只会“压*”两者,想要像混合2个高度图一样获得结果。扭曲(Warp)第二张法线贴图,使用第一个法线贴图的法线:

float3x3 nBasis = float3x3(
    float3 (n1.z, n1.x,-n1.y),
    float3 (n1.x, n1.z,-n1.y),
    float3 (n1.x, n1.y, n1.z ));
n = normalize (n2.x*nBasis[0] + n2.y*nBasis[1] + n2.z*nBasis[2]);

组合2个法线贴图示例。从左到右:没有法线、仅细节法线、组合法线。

优化“基于物理的”着色器:

  • 小的代码内核:易于优化,皮肤和汽车着色的代码也相同。
  • 消耗:*均90条ALU指令,最多270条ALU指令,最多24个纹理提取。
  • 排列:根据参数值选择。如果漫反射粗糙度 = 0,则使用Lambert而不是OrenNayar,不同的OrenNayar*似,光泽度值影响反射纹理样本的数量,ETC。

全局照明:Unity支持使用Beast进行光照贴图烘焙,只有间接光照存储在光照贴图中。光照探头用于动态物体的照明,反射探头包含立方体贴图(无限距离反射)和四面体(局部反射)。

光照探针的四面体(tetrahedras)。

环境光遮蔽:基于地*线的环境光遮蔽 (HBAO),由英伟达开发,环境光遮挡不应影响直接照明,根据定义!否则看起来很脏。尽早计算 SSAO,将结果输入像素着色器:min (bakedAO, objectAO, SSAO),仅适用于间接光照贴图、光照探针和反射。

文中提到的DX11的特效包含APEX破坏、头发管线、体积爆炸、运动模糊。

NVIDIA APEX的可扩展动态框架。

APEX破坏的工作流。

头发管线:头发和皮毛是游戏角色的下一个大挑战,随着动画和皮肤着色变得更好,低质量的头发更加明显,Unity头发系统被设计为尽可能类似于离线系统,发型可以在Softimage XSI / Maya / MAX中建模,使用PC2点缓存格式导出到Unity,使用DirectX 11细分渲染,渲染头发几何体的生成完全在硬件中完成,几何放大,GeForce GTX 580曲面细分硬件非常快。

头发的渲染使用了曲面细分,期间会增加噪点、结块以提升真实感。头发着色方面,系统支持细几何头发或带有多根头发图像的宽纹理条带。8x MSAA效果出奇的好,使用带有随机抖动的Alpha-To-Coverage,无需混合即可提供OIT,使用着色器覆盖输出的透明度超级采样也将可能,但开销太大。着色器包括在根部和尖端的淡入淡出,使用Kajiya-Kay各向异性着色模型,支持自阴影、假边缘照明效果。

运动模糊:提高快速移动场景中的“可读性”,带来电影般的外观。Unity具有现有的“运动模糊”图像效果,但不是真正的运动模糊,只是通过在旧框架上混合新框架来在物体后面留下痕迹。

速度缓冲运动模糊:生成速度缓冲区,着色器计算屏幕空间中先前位置和当前位置之间的差异,Unity已提供,之前的模型矩阵 (UNITY_MATRIX_PREV_M)、蒙皮对象的先前世界空间位置 (POSITION1),从深度缓冲区和先前的模型视图投影矩阵计算的相机运动模糊,必须小心相机剪辑第一帧的运动模糊!使用这些速度在运动方向上模糊当前场景的图像。

重建滤镜运动模糊:“用于合理运动模糊的重建过滤器”,使用当前帧图像和深度,加上屏幕空间速度缓冲区,模拟分散作为聚集,使用深度来解释遮挡,正确模糊外部物体轮廓,还是有一些失真,特别是在tile边界。

总之,DirectX 11为Unity带来了最先进的图形,来自离线CG世界的技术正在成为可能,旨在使Unity尽可能对艺术家友好。

Effects Techniques Used in Uncharted 3: Drake's Deception分享了神秘海域3的特效系统、工具、运行时等技术内容。

神秘海域的粒子系统基于布局(Scheme)的宏语言,全能着色器,PPU和SPU之间的处理拆分:

新系统大幅缩短迭代时间,更快的迭代==更多的效果和润色,更多数据驱动,更好的动态,具有现代功能的更灵活的着色器,100%异步SPU代码。

构建效果时,从节点中提取的信息用于导出(粒子定义、字段、曲线和斜率表、表达式),将表达式编译成VM字节码,以DC格式写出所有数据。粒子到游戏阶段,粒子器使用插件生成交互式DC会话,DC坚持加快未来构建以实现快速迭代。

下图是粒子涉及的相关接口:

对于粒子的运行时设计,系统设计考虑了硬件和引擎,GPU的粒子周期预算非常有限,SPU提供复杂计算的能力,更小的粒度,但处理的数量增加。


粒子的一帧涉及的阶段:

graph LR A(预计算) --> B(更新) --> C(裁剪) --> D(构建几何体)--> E(排序)--> F(构建渲染列表)

每个阶段的详情如下:

  • 预计算:碰撞。

逐粒子与环境碰撞开销太大,只能用*似。每个粒子都存储一个碰撞*面,让粒子与之碰撞。每一帧,选择一个碰撞粒子子集 (10) 并为它们选择光线投射与环境的对比,下一帧返回结果,并存储在粒子数据中。粒子*面以循环方式更新,每帧成本低,结果可以接受,一些粒子会穿过几何体,但实际上并不明显。

  • 更新。

    • 应用场。场包含重力、湍流、阻力、体积轴、径向、涡旋等,烘焙动画参数,测试与音量,对速度施加力。
    • 字节码执行。具有16个向量寄存器的VM,非分支,创建和更新表达式。
    (particle-expr runtime
        (1const 0 2) 
        (1const 12)
        (lconst 2 2)
        (store 10 0)
        (store 11 1) 
        (store 122)
        (ramp 0 1 0)
        (store 9 0)
        (ramp 0 0 0)
        (store 13 0)
        (load 0 13)
        (store 14 0)
    )
    
  • 裁剪和构建几何体。

剔除与视锥体,构建顶点,创建索引和渲染数据结构,写入内存。

  • 排序。

对粒子中的每个发射器集进行排序,距离、生成顺序、反向生成顺序,艺术家可以在particler(粒子器)中设置发射器绘制顺序。

  • 构建命令列表。

RSX的输出命令列表,设置视口、着色器、渲染状态、顶点格式等,缓存着色器和设置。

后面需要设置作业链,想要在所有SPU上运行,不想让PPU在启动之后才参与,每个阶段必须在下一阶段运行之前完成,不知道每个阶段需要多少工作。使用设置作业收集结果并设置新作业,作业链如下:

文中还给出了沙子足迹的特殊用例,过程大致是向地面投影粒子,构建粒子几何体,粒子投射,变换深度到世界空间再到粒子空间。

沙子足迹效果。

2012年的主流游戏引擎都已经支持延迟渲染,但Forward Rendering Pipeline for Modern GPUs反其道而行之,阐述了在GPU上实现前向渲染,充分发现它的潜力和优势,实现惊人的效果。

前向渲染支持复杂材质、多种灯光类型、硬件抗锯齿,高效的内存使用,良好的缓存使用,更好地将渲染数据与引擎分离。但是,由于硬件寄存器和API限制,以前无法支持大量光源,分支太慢。

动机是想要更接*离线着色器的着色样式,如V-ray、Maxwell、Renderman等,艺术家和技术艺术家友好, 从想法到视觉结果的快速迭代,从DirectX 11及更高版本开始。 利用现代GPU标量架构,至少在理论上,视觉质量没有限制,一个地方完整的“渲染方程”。在跳转到基于计算的完整路径跟踪之前的良好光栅化设计。

Forward+渲染是前向渲染器的变种,为光源剔除添加计算着色器, 修改主光照循环。照明和阴影在同一个地方完成,所有信息都被保留, 有助于将复杂的着色器抽象为漂亮的“构建块”,以便以不同的方式组合。对灯光和材质的参数没有限制,如全方位、点光源、电影(任意衰减,barndoor)、每个材质实例的唯一 BRDF。设计简单,渲染数据与运行时c++引擎解耦,可以在Maya中动态添加新的复杂材质类型。

Forward+步骤:为每个“间接光”生成 RSM(可选),生成VPL和/或创建动态灯光并更新现有灯光(可选),Z预通道,光源剔除,着色。

Forward+总览。

其中光源裁剪的着色器代码如下:

//    1. prepare
float4 frustum[4];
float minZ, maxZ;
{
    ConstructFrustum( frustum );
    minZ = thread_REDUCE(MIN, depth );
    maxZ = thread_REDUCE(MAX, depth );
    ldsMinZ = SIMD_REDUCE(MIN, minZ );
    ldsMaxZ = SIMD_REDUCE(MAX, maxZ );
    minZ = ldsMinZ;
    maxZ = ldsMaxZ;
}

//    2. overlap check, accumulate in LDS
__local u32 ldsNLights = 0;
__local u32 ldsLightBuffer[MAX];

for(int i=threadIdx; i<nLights; i+=WG_SIZE)
{
    Light light = fetchAndTransform( lightBuffer[ i ] );
    if( overlaps( light, frustum ) && overlaps ( light, minZ, maxZ ) )
    {
        AtomicAppend( ldsLightBuffer, i );
    }
}

//    3. export to global
__local u32 ldsOffset;
if( threadIdx == 0 )
{
    ldsOffset  = AtomAdd( ldsNLights );
    globalLightStart[tileIdx] = ldsOffset;
    globalLightEnd[tileIdx] = ldsOffset + ldsNLights;
}

for(int i=threadIdx; i< ldsNLights; i+=WG_SIZE)
{
    int dstIdx = ldsOffset + i;
    globalLightIndexBuffer[dstIdx] = ldsLightBuffer[i];
}

着色阶段:

  • 渲染场景材质。基础光累积功能,使用屏幕xy位置确定tileID,从tileID获取灯光开始和结束索引和从开始索引到结束索引循环,条目是光源数组的索引,累积光源击中的像素,返回击中像素的总直接和间接光。
  • 材质着色器。决定如何处理总入射光,例如传递到材质的BRDF,使用着色构建块,环境光、 基础光累积、BRDF等放在一起形成最终的像素颜色,可以在不影响底层着色器的情况下添加新的光源类型和BRDF 还可以根据*台实现更低开销的版本。
// 光源累积伪代码

StructuredBuffer<float4>  LightParams          : register(u0);
StructuredBuffer<uint>    LowerBoundLights  : register(u1);
StructuredBuffer<uint>    UpperBoundLights  : register(u2);
StructuredBuffer<int2>    LightIndexBuffer     : register(u3);

uint GetTileIndex(float2 screenPos)
{
   float tileRes  = (float)m_tileRes;
   uint numCellsX = (m_width + m_tileRes - 1)/m_tileRes;
   uint tileIdx   = floor(screenPos.x/tileRes)+floor(screenPos.y/tileRes)*numCellsX;

   return tileIdx;
}

StartHLSL BaseLightLoopBegin    // THIS IS A MACRO, INCLUDED IN MATERIAL SHADERS

 uint tileIdx    = GetTileIndex( pixelScreenPos );
 uint startIdx  = LowerBoundLights[tileIdx]; 
 uint endIdx   = UppweBoundLights[tileIdx];

 [loop]
 for ( uint lightListIdx = startIdx; lightListIdx < endIdx; lightListIdx++ )
 {
    int lightIdx = LightIndexBuffer[lightListIdx];
    
    // Set common light parameters
    float ndotl = max(0, dot(normal, lightVec));
        
    float3 directLight   = 0;
    float3 indirectLight = 0;

    if( lightIdx >= numDirectLightsThisFrame )    {
       CalculateIndirectLight(lightIdx , indirectLight);
    }    else      {
        if( IsConeLight( lightIdx ) )        {            //    <<==  Can add more light types here
            CalculateDirectSpotlight(lightIdx , directLight);
        }     else     {
         CalculateDirectSpherelight(lightIdx , directLight);
     }
 }    

 float3 incomingLight = (directLight + indirectLight)*ndotl;
 float shadowTerm = CalcShadow();

EndHLSL

StartHLSL BaseLightLoopEnd   
      }
EndHLSL

    
// 材质着色器模板
    
#include "BaseLighting.inc"

float4 PS ( PSInput i ) : SV_TARGET    
{
    float3 totalDiffuse = 0;
    float3 totalSpec    = GetEnvLighting();;

$include BaseLightLoopBegin
    totalDiffuse += GetDiffuse(incomingLight);
    totalSpec    += CalcPhong(incomingLight);
$include  BaseLightLoopEnd

    float3 finalColor = totalDiffuse + totalSpec;
    return float4( finalColor, 1 );
}

基于计算的延迟渲染与Forward+的性能对比。

深度预通道关键点:像素过绘制削弱了Forward+,因此需要深度预通道,深度预通道是使用MRT生成后期特效和其它渲染特效所需的其它全屏数据的好机会。XBOX 360具有良好的带宽,因此考虑到前向渲染的限制,延迟渲染很有意义。然而,ALU计算的增长速度比带宽快,只做计算比读写大量数据更可行。动态分支对性能的损害不像以前那么糟糕,作为一种优化,计算着色器可以按光照类型进行排序,例如以最小化光照分支损失。可以放弃所有用于决定哪些光源照射到每个对象以设置常量寄存器的“光源管理”CPU端代码!

总之,修改后的前向渲染器,可处理具有1000多个光源的场景,自动的硬件抗锯齿 (MSAA) ,带宽友好,充分利用GPU的ALU能力(增长速度快于带宽),着色器可以类似于离线着色器以获得高视觉质量。

Advanced Procedural Rendering with DirectX 11阐述了基于DX11的程序化效果,如网格生成、GPU流压缩、几何体生成、网格改进、动态流体、照明等内容。

距离函数是返回从给定点到表面的最*距离,有符号距离函数是返回点到表面的最*距离,如果点在形状之外,则返回正数,如果在形状内,则返回负数。有符号距离场用于程序几何创建的有用工具,易于在代码中定义,“随机公式”的合理结果,可以从网格、粒子、流体、体素创建,CSG、失真、重复、变换等一切都变得轻松,不关心几何拓扑,只需在空间中定义字段,稍后进行多边形化。利用SDF添加网格细节的过程和图例如下:

以上图例对应的伪代码如下:

// 1: 一个长方体
Box(pos, size)
{
    a = abs(pos-size) - size;
    return max(a.x,a.y,a.z);
}

// 2: 用布尔值切割
d = Box(pos)
c = fmod(pos * A, B)
subD = max(c.y,min(c.y,c.z))
d = max(d, -subD)

// 3: 更多布尔值
d = Box(pos)
c = fmod(pos * A, B)
subD = max(c.y,min(c.y,c.z))
subD = min(subD,cylinder(c))
subD = max(subD, Windows())
d = max(d, -subD)

// 4: 重复的布尔值
d = Box(pos)
e = fmod(pos + N, M)
floorD = Box(e)
d = max(d, -floorD)

// 5: 切割孔洞
d = Box(pos)
e = fmod(pos + N, M)
floorD = Box(e)
floorD = min(floorD,holes())
d = max(d, -floorD)

// 6: 组合结果
d = Box(pos)
c = fmod(pos * A, B)
subD = max(c.y,min(c.y,c.z))
subD = min(subD,cylinder(c))
subD = max(subD, Windows())
e = fmod(pos + N, M)
floorD = Box(e)
floorD = min(floorD,holes())
d = max(d, -subD)
d = max(d, -floorD)

// 7: 重复空间
pos.y = frac(pos.y)
d = Box(pos)
c = fmod(pos * A, B)
subD = max(c.y,min(c.y,c.z))
subD = min(subD,cylinder(c))
subD = max(subD, Windows())
e = fmod(pos + N, M)
floorD = Box(e)
floorD = min(floorD,holes())
d = max(d, -subD)
d = max(d, -floorD)

// 8: 重复空间
pos.xy = frac(pos.xy)
d = Box(pos)
c = fmod(pos * A, B)
subD = max(c.y,min(c.y,c.z))
subD = min(subD,cylinder(c))
subD = max(subD, Windows())
e = fmod(pos + N, M)
floorD = Box(e)
floorD = min(floorD,holes())
d = max(d, -subD)
d = max(d, -floorD)

// 9: 增加细节
AddDetails()

// 10: 增加光照和色调映射
DoLighting()
ToneMap()

// 11: 增加贴花和耶稣光
AddDeferredTexture()
AddGodRays()

// 12: 艺术调整,最终成像
MoveCamera()
MakeLookGood()

实践中的程序化SDF:

  • 生成的场景可能不会取代3D艺术家。
  • 生成的SDF是真实网格的良好代理。
  • 结合一些比艺术数据更便宜的图元的代码。
  • 结合艺术家构建的网格转换为SDF。
  • 布尔、修改、剪切、程序变形。

来自三角形网格的SDF:

  • 将三角形网格转换为3D纹理中的SDF。
    • \(32^3\)\(256^3\) 体积纹理典型。
    • SDF插值很好,一般用双三次插值。
    • 低分辨率3D纹理仍然可以正常工作。
    • 与多边形数量无关(处理时间除外)。
  • 经常可以离线完成。

将网格转换为64x64x64的SDF并进行多边形化。

  • 初级的方法是计算每个单元格到每个三角形的距离,非常缓慢但准确。

  • 将网格体素化为网格,然后扫掠(sweep)并不是好方法,扫描计算体素到细胞的有符号距离,*表面的体素化太不准确,但*地表距离很重要 - 插值。

  • 结合精确的三角距离和扫掠。

几何阶段:绑定3D纹理目标,VS转换到SDF空间,几何着色器将三角形复制到受影响的切片,将三角形展*为二维,将位置输出为纹理坐标,每个顶点的所有3个位置。

像素着色器阶段:计算从3D像素到三角形的距离,计算三角形上最*的位置,使用重心评估顶点法线,使用加权法线评估距离符号,写有符号距离到输出颜色,距离到深度,深度测试保持最*距离。

后处理步骤:网格表面周围的单元格现在包含准确的符号距离,网格的其余部分为空,在后期处理CS中填写网格的其余部分,快速扫描算法。

快速扫掠:需要能够读取和写入相同的缓冲区,每行一个线程,线程的读写不重叠,无需联锁,在同一轴上先前后后扫描,依次扫描各轴。

d = maxPossibleDistance
for i = 0 to row length
    d += cellSize
    if(abs(cell[i]) > abs(d))
        cell[i] = d
    else
        d = cell[i]

SDF还可以来自粒子系统,也可以对SDF可视化,此处忽略。

现在聊聊GPU上的流压缩。流压缩的过程是取一个稀疏的数组,将所有填充的元素推到一起,记住计数和偏移映射,现在只需要处理数组的填充部分。

计数通道 - 并行减少,迭代地将数组大小减半(如mip链),写出父单元数的总和,直到达到最后一步:1个单元格、总计数。偏移通道- 迭代回走,单元格偏移 = 父位置+兄弟位置,组织金字塔(Histopyramid):3D流压缩。

组织金字塔(Histopyramid):以块为单位累加mip链,从基数向上计数以计算偏移量。

使用组织金字塔:使用活动蒙版填充网格体积纹理(0为空,1有效),在mip链中向下生成计数,对单元格位置使用第二体积纹理,遍历mip链。

压缩实战:使用组织锥体压缩活性细胞,也知道活动单元格数,GPU只为活动单元格数量的调度drawcall,使用DrawInstancesIndirect,GS根据单元格索引确定网格位置,为此使用组织金字塔,为GS中的单元格生成行进立方体。对蛮力的巨大改进,从11ms下降到约5ms,大大提高并行度,减少绘图调用大小,几何图形仍然在GS中生成,为每个渲染通道再次运行,无索引/顶点重用。

DX11还可用于在渲染管线利用生成几何、顶点等数据,以及*滑网格(下图)。

此外,DX11还可用于*滑粒子流体动力学、AO光线追踪、基于SDF的AO等渲染技术。

Survey of Physics Engines for Games调查了2012年的主流物理引擎的基础知识、特点、特性、使用比例等内容。

游戏应用程序上下文中的物理引擎。

当时的主流物理引擎包含PhysX、Havok、ODE、Bullet等,前三者的使用情况如下:

它们的特性如下:

The Technology Behind the “Unreal Engine 4 Elemental demo”详细分享了2012年的Unreal Engine在图形功能、间接照明、阴影、后期处理、粒子等方面的渲染、实践和优化技术。

当时的UE4考虑一个通用的实时GI解决方案,受Interactive Indirect Illumination and Ambient Occlusion Using Voxel Cone Tracing [Crassin11]的启发,最终选用了体素化的方案。

体积射线投射:从一些开始偏差开始,内容自适应步长,查找辐射和遮挡,通过遮挡累积光,如果被遮挡或足够远就停止。锥形追踪,局部锥体宽度的Mip级别,逐步增加步长。

对GI使用体素锥追踪:

  • 类似“光线追踪到一个简化的场景”。
  • 漫反射GI:多个方向取决于法线,锥数的张角。
  • 镜面反射:来自镜像眼睛矢量的方向,高光power的张角。
  • 不如光线追踪精确,但分数几何相交、无噪点、LOD。

椎体追踪示意图。其中红色代表镜面反射,绿色代表漫反射GI。

  • 可以进一步优化/*似。较低的体素分辨率,在体素照明通道中聚集而不是分散,自适应采样,样本复用。
  • 额外的好处。着色IBL,发光材质的着色区域灯。

体素锥追踪挑战:穿过薄薄的墙壁,宽锥体显示伪影,但窄锥体速度较慢,Mip贴图需要依赖于方向,从三角形网格创建体素数据,运行时内存管理,GPU硬件上的高效实现,稀疏数据结构。

稀疏体素八叉树:映射函数允许本地更高分辨率,世界3D位置 <=> 索引和本地3D位置。在GPU上完全维护。访问渲染阶段特定数据的索引,每个节点/叶子数据,2x2x2 体素数据(放置在八叉树节点角落),6x 3x3x3 体素数据(如带有附加边框的 2x2x2)。

体素照明管线:

上图的Voxelization:所有静态区域的关卡加载后,动态对象移动时;Lighting:聚光明暗;Filtering:创建方向相关的体素和mip;Finalize:创建冗余数据并复制到体积纹理。

  • 体素化。

    • 在区域中创建体素几何数据,输入:八叉树、三角形网格、实例数据、材质、区域,输出:八叉树、2x2x2材质属性、法线。
    • 区域重体素化。几何变化、重大变化、分辨率变化。
    • 针对少数动态对象进行了优化。按需重体素化,区域保持静态体素数据分开。
    • 渲染方法:
      • 方法1:使用硬件光栅化器的像素着色器通道,每个轴(X、Y、Z)一次光栅化,以避免孔洞,着色器评估艺术家定义的材质,输出:跟随CS处理的分片队列。
      • 方法2:计算着色器通道,更新八叉树数据结构(并行),在叶子中存储体素数据。
      • 方法2有更好的占用率(2x2四边形),着色器编译时间(重用CS)。
  • 体素光照。

    • 计算着色并存储 Radiance。输入:2x2x2材质属性、法线,输出:2x2x2HDR颜色和不透明度。
    • 累积辐照度和阴影。使用阴影贴图添加直射光,添加环境色,结合反照率颜色,添加发光颜色。

  • 过滤体素并完成。

    • 生成mip-maps,创建冗余边框,压缩。输入:2x2x2 HDR颜色、遮挡和法线,输出:HDR乘法器,6 x 3x3x3LDR颜色和遮挡。
    • 生成方向相关的体素。在体素法线的叶子级别,仅在同一方向的节点级别。

镜面采样:

  • 每像素局部反射,Specular Power的锥角,单锥通常已足够,可能的复杂BRDF。
  • 自适应以获得更好的性能,高光亮度,深度差,法线差异。


  • 使用Dispatch()进行上采样。
  • 分散通道使用DispatchIndirect()。

漫反射采样:

  • 类似于Final Gathering [Jensen02]。
  • 问题:很少的样本以获得良好的性能,足够的质量样本(锥角),在半球上分布均匀以减少误差,不想要噪音,不想模糊正常的细节。
  • 漫反射多为低频。
  • 一致性对效率很重要。

体素光照对比图。

接下来聊聊UE的着色。

UE使用了新的镜面power编码:IBL更高的镜面反射功率,共享值的更多定义,调整以在宽度为1000像素的远距离球体上提供像素清晰的反射。

OldEncode(x): sqrt(x / 500)
OldDecode(x): x * x * 500

NewEncode(x): (log2(Value) + 1) / 19
NewDecode(x): exp2(Value * 19 - 1)

高斯镜面反射用于减少锯齿[McKesson12],以经验*似:

Dot = saturate(dot(N, H))
Threshold = 0.04
CosAngle = pow(Threshold, 1 / BlinnPhongSpecularPower)
NormAngle = (Dot - 1) / (CosAngle – 1)
LightSpecular = exp(- NormAngle * NormAngle) * Lambert

区域光镜面反射,软球区域光:

LightAreaAngle = atan(AreaLightFraction / LightDistance)
ACos = acos(CosAngle) 
CosAngle = cos(ACos + LightAreaAngle)

能量守恒(*似值):

SpecularLighting /=  pow(ACos + LightAreaAngle, 2) * 10


新的后处理图:

  • 图形:创建每一帧,没有用户界面,依赖项定义执行顺序,按需RT、引用计数、lazy释放。
  • 节点:种类多但功能固定,多个输入和输出,定义输出纹理格式。

SSAO:

  • 经典SSAO [Kajalin09]。环境遮挡计算为后期处理,只需要z缓冲区和3d点样本,很少有样本以小屏幕对齐模式排列。

  • UE的技术基于二维点样本。基于角度的类似于HBAO[Sainz08],使用GBuffer法线进一步提高质量,用高频细节补充体素照明。

  • 采样:在半分辨率z缓冲区中使用6个样本对 = 12个样本,16次旋转,以4x4模式交错的模式:

  • 逐像素法线进一步限制角度:

    A) Given: z buffer in the sample direction
    B) Get equi-distant z values from samples
    C) AO (so far) = min((angle_left+angle_right)/180,1)
    D) Clamp against per pixel normal
    E) AO (per pixel normal) = (angle_left+angle_right)/180AO ~= 1-saturate(dot(VecA,Normal)/length(VecA))
    

    效果对比:

HDR直方图:

  • 64个Bucket,对数,无原子。

  • Pass 1:并行生成屏幕局部直方图(CS)。

    清除组共享直方图float4[64][16]
    同步
    并行累积直方图
    同步
    将多个直方图累积到一个float4[16]
    在16个纹素中每行输出一个直方图
    
  • Pass 2:将所有行合二为一,64个Bucket存储在16个ARGB中。

人眼自适应:

  • 从直方图计算*均亮度(蓝线),仅考虑明亮区域(例如 >90%),拒绝少数非常明亮的区域(非常明亮的发射区,例如 >98%)。
  • 计算整个视口的单个乘数,与最后一帧*均值*滑融合(白条),绑定在用户指定区域(绿色),应用于色调映射器(白色曲线)。
  • 在色调映射VS中读取结果,作为插值器传递给PS。

GPU加速的粒子:

  • CPU:生成粒子(任意复杂的逻辑),固定大小缓冲区中的内存管理(单位:16 个粒子),发射器管理(索引缓冲区、绘图调用排序)。
  • GPU:牛顿力学的运动(固定函数),来自非定向体积级联的照明(3D 查找),如果需要,GPU 基数深度排序 [Merrill11] [Satish09],渲染,来自矢量场的附加力(3D 查找),粒子曲线来调整粒子属性(一维查找)。

Bloom:

  • 目标:大型、优质、高效

  • 下采样:

    A = downsample2(FullRes)
    B = downsample2(A)
    C = downsample2(B)
    D = downsample2(C)
    E = downsample2(D)
    
  • 下采样期间的模糊可避免锯齿:

  • 重组(随着分辨率的增加):

    E’= blur(E,b5)
    D’= blur(D,b4)+E’
    C’= blur(C,b3)+D’
    B’= blur(B,b2)+C’
    A’= blur(A,b1)+B’
    
  • 上采样时模糊,提高质量,几乎不影响模糊半径:

    blur(blur(X,a),b) ~= blur(X,max(a,b)) 
    
  • 组合脏纹理(dirt texture):

     J*const + tex2d(Dirt,ScreenUV)*const
    

GBuffer模糊:

  • 智能模糊:*均5个像素,按正常加权,按深度差加权。

  • 应用:减少镜面反射材质的锯齿(运动中明显),减少环境光遮挡中的高频抖动伪影,可以使用IBL或体素照明提高性能。

  • 尽可能对深度、AO使用 Gather() 。

  • 输出:SpecularPower、Normal、AmbientOcclusion。

  • 降低镜面反射功率 [Toksvig05] [Bruneton11]:

    L = saturate(length(SumNormal) * 1.002)
    SpecularPower *= L / (L + SpecularPower * (1 - L))
    

图的辅助:

  • 后处理体积:线性混合后期处理属性,优先级取决于相机位置,具有混合半径的软过渡,可以远程控制权重。
  • 渲染目标池(Render Target Pool):按需分配,引用计数,延迟释放,查看中间缓冲区的工具。

Destiny: From Mythic Science Fiction to Rendering in Real-Time描述了Destiny引擎的渲染技术和特效。

当时的Destiny引擎支持和的特性有:矢量地形移动树,河流和湖泊,可定制的齿轮、实时布料、面部技术、高质量的实时阴影、特效、公告板系统、曲面细分、高质量的大型AO (GI)、可见性、一天的动态时间,还有很多很多......

Destiny引擎的目标是在艺术风格选择和现实主义之间取得良好的*衡,高质量的视觉效果,但成为通用图形引擎不少目标。渲染关键领域有高效的内容创建流程和管道、可信而复杂的角色高度互动、动态的世界处理复杂问题的能力、充分利用下一代硬件的强大功能、可扩展到当前一代的控制台。引擎技术包含基于作业的多线程、数据并行和缓存一致性、执行在任意类型的核心上。

为了加速渲染,减少了几何通道,保持小的渲染目标尺寸,统一的光照+材质模型,简化着色器。Halo: Reach采用了混合延迟渲染管线:

Pre-pass延迟渲染管线:

Destiny引擎的延迟渲染管线:

其GBuffer数据布局如下:

材质库,材质模型参数存储在表中在G-Buffer中,只在表中存储单个索引,可表达且可定制:表被存储为一个纹理,指定使用创作的曲线或绘制的纹理。

镜面反射瓣指数(10 位存储在法线旁边),控制镜面高光的形状,4位指定了艺术家绘制的波瓣形状,并且6位指定粗糙度变化,这些变化在导入过程中自动计算。



光照和着色过程如下系列图:




各种延迟渲染的性能对比如下:

Destiny透明光照的目标是光线不透明的一致性,阴影与不透明的一致性,应用于大气中。*似方法是动态地将光探头放置在透明的地方,每一帧都为探测器构建一个低阶球面谐波,要考虑光线和阴影。

传统的辐照度体积在CPU进行,不过Destiny移到了GPU。处理如下:

  • 每帧构建探针列表。在处理可见透明对象时,CPU会构建一个光探测点列表,点被写入一个快速的线程安全的无锁缓冲区,在任务中构建对象列表。XYZW每个组件都是32位的浮点数,光源探针半径存储在W中。

  • 每帧提交光源探针到GPU缓冲区。限制数量为1024个,编码进64x64的RGBA32F纹理,使用双缓冲以阻止停顿。

  • 光源探针GPU生成。设置MRT与SH光照环境表面:3 x 64 x 16 RGBA16F渲染目标,每个渲染目标为一个颜色通道编码4个SH系数。为太阳渲染一个四边形到SH表面,将定向光映射到SH系数。

    • 阴影。采样级联阴影映射以确定每个探针的阴影,在光照点缓冲位置执行PCF,使用基于探针半径的样本半径。
    • 光照环境。光源可以被艺术家标记为影响透明度,光源只是艺术家可选择的一组着色器组件。对每一个影响透明的光源:渲染一个四边形到SH表面,将光源的参数映射到给定的光源探针点缓冲位置的SH系数。
  • 渲染半透明。半透明在后置的主要延迟通道被渲染,在应用延迟光照和阴影之后。当渲染一个半透明物体时,采样SH表面,应用光源到像素颜色。环境光照模型低开销且且之后再计算。

限制:SH不是不透明照明的完美搭配,但是合理的足够的,当物体是透明的时,大多数瑕疵不明显。需要很多样本来获得*滑的阴影响应,由于每个物体的阴影因素,有利的一面是在64x16的缓冲区上操作速度很快。

粒子是超低(Über-Low)分辨率的VDM(方差深度图)粒子。

左:全分辨率;中:使用双线性上采样的1/4分辨率;右:使用VDM的1/4分辨率

2014年,Solving Visibility and Streaming in The Witcher 3: Wild Hunt with Umbra 3阐述了巫师3利用Umbra组件实现可见性和流式的功能。Umbra是2007年成立的专门做遮挡剔除的小型公司,它的工作流程如下:

巫师3的要求是大型开放世界→ PVS,手动操作是不可能的,Umbra是自动的,流数据,LOD。数据流的挑战有独立区块、边界匹配、速度。Umbra3中的LOD,之前的场景由单个对象实例组成,问题有需要多个LOD级别、关卡*间的自遮挡、LOD层次结构?解决方案如下:

LOD挑战;距离参考点、LOD选择的其他标准、更智能的LOD遮挡体。

遮挡数据流:

  • 从特定的摄像机位置确定所需的tile组。
  • 如果新确定的集合与当前使用的集合不同,则异步计算开始。
  • 流式输入预烘焙缓冲区(仅适用于尚未流式输入数据的tile)。
  • 创建Tome对象(仅适用于尚未创建此对象的tile)。
  • 一旦所有的Tome都存在,就会从中创建TomeCollection对象。
  • 新创建的集合将被发送到渲染器以替换当前使用的集合。
  • 不再需要的tile会破坏其Tome对象,并取消流(unstream)预烘焙缓冲区的tile,销毁之前的收集对象。

《天涯明月刀》引擎开发是腾讯北极光工作室的安柏霖在GDC中国2014上的一个分享,讲解了水银(QuickSilver)引擎的场景、材质、光影等方面的技术。

在场景方面,要满足4kmx4km的可玩区域和12kmx12km的可视区域,挑战是开发&运行效率。在运行效率上采用了可见性检测和剔除、LOD、实例化、多线程渲染,而开发效率则使用了管线工具、协助编辑、外部互通工具:

在可见性检测方面,主线程跳过粒子、动画等计算,使用基于Actor/Entity级别的检测。渲染线程跳过DrawCall,执行通用可见性检测(场景管理、视锥体剔除、软光栅剔除、贡献度剔除)、反射剔除、阴影剔除等。

在通用可见性检测上,场景管理使用了两个层级的Cell/BVGrid,以获得快速的内存访问,采用了SSE2指令集、软件光栅化(地形、内包围盒),且剔除占屏幕非常小的物件(角色和普通物件使用不同的阈值)。

通过以上手动,场景的Draw Call提升了47.8%。

在执行反射剔除时,将水面上的地形当作遮挡体,以剔除镜像相机不可见的地上物体:

对于阴影剔除,计算量很大,范围大(2kmx2km的范围),静态物件和植被会产生投影。由于光的方向变化是固定的,可以离线计算不需要投影的情况,已经被阴影覆盖的地形、静态物件和植被,投影但是没有物体接受。


反射进一步简化,避免水中的反射非常清晰的情况,只在Z方向投射,不再有多向投射,MipmapBias = 2。


光照和材质方面,支持通用、头发、眼睛、皮肤的材质,使用了PBR光照。光照拆分为漫反射和镜面反射项,镜面反射使用了两层高光:主高光、次高光。

主镜面反射使用了主流的Cook-Torrance模型,其中D : blinn-phong、F : Schlick’s approximation、G : Neumann-Neumann GAF。次镜面反射使用了离线烘焙的IBL。头发使用了Kajiya-Kay,皮肤使用了5S,眼睛使用了基于Jimenez12的改进和优化版本。环境光使用3个半球光(Sky、Ground、SunScatter)。

角色光照进行了特殊处理,因为需要任何情况下看起来都很棒。光照参数和场景分开,由角色美术单独调试,渲染是两个pass,不同参数,使用stencil区分。增加摄像机灯光—Camera View方向的一个光照,头发在阴影中DirectLighting保留%30到%40。

光照管线采用了Deferred/Forward混合模式。

级联阴影有4个cascade,保证*滑过渡,Pack到一个LightViewBuffer中,前3个Cascade的每帧更新,覆盖大约200米,第4个cascade覆盖超过2km。没有*滑过渡的话,为了保证完整性,Frustum需要完整包含Cascade的包围球,而Frustum最外侧部分只是为了很小部分cascade椎体。有了*滑过渡,可以把Frutum缩小到原来的0.85x0.85=0.72,少部分覆盖不到的地方由下一个cascade混合处理:

第4级阴影覆盖超过2km,分帧更新,每20秒一切换(随光的方向,位置的移动),切换过程中,*滑过渡。阴影缓存可以节省约%70的static mesh&terrain sector的shadow中渲染,用在第三个Cascade里,在第1、2两个cascade中意义不大,值不回显存的成本。

此外,使用了Snapping处理抖动,以及Shadow Mask 对ShadowMap构建pass达到%10到%60的优化。

Rendering in Codemasters’ GRID2 and beyond Achieving the ultimate graphics on both PC and tablet陈述了游戏超级房车赛2在兼容PC和*板设备的渲染技术,包含像素着色顺序、OIT、自适应体积光照、可编程混合、粒子光照等等新兴渲染技术。

2014年的显示屏分辨率和GPU都有较大的变动,分辨率变得越来越高清,1080p占据了主导地位,而Intel的集成显卡依然是主导地位,GTX次之:

当时的行业目标是使中等设置与当前控制台匹配,从控制台质量向上和向下扩展:

像素着色顺序(Pixel Shader Ordering):屏幕位置的像素着色器互斥,速度很快,因为只有冲突的线程被序列化。保证执行顺序类似于Alpha混合规则,以V_PrimitiveID顺序写入的像素,像素着色器排序将这种保证的排序移动到像素着色器中。可以用它做任何想要读修改写每像素数据结构的东西。

左:没有像素着色顺序,重叠像素可以并行执行;右:使用了像素着色顺序,重叠的像素不能再并行地执行,变成了串行。

OIT(顺序无关的透明度):可以表示多个透明层,而不存在排序问题,如密集/柔和的叶子(尤其是在距离较远的地方,因为mips),改进了其它Alpha测试几何体。

在UAV表面上,将可视性函数存储为顺序的固定大小节点数组,每个红色节点对应一对深度和透射率值:为了压缩可见性,移除了产生最小面积变化的节点:

GRID2中的透明度覆盖率(Alpha Coverage):叶子上有很多半透明的部分(下图左红色区域),Alpha混合不是一个选项,最初的系统使用Alpha 2x覆盖率,但需要4xMSAA才能看起来不错。在没有混合或A2C的情况下,会得到带锯齿的结果:

自适应体积阴影(Adaptive Volumetric Shadow Mapping,AVSM):可以用一个类似的想法来*似穿过参与介质的透光率吗?从光源的角度渲染OIT,可用于渲染体积烟雾效果,例如下图的轮胎被雾气覆盖:

发光粒子:最初的研发专注于优化阴影图读/写,粒子的每像素照明时间>=10毫秒…逐顶点照明太粗糙,带有屏幕空间细分的逐顶点实际上看起来更好!每个顶点的细分速度快2-3倍。

可编程混合(Programmable Blending):对数编码到R10G10B10A2后缓冲区的HDR照明值,编码值的固定函数alpha混合无效,结果是在透明物体后面失去了高动态范围,解决方案是融入线性空间。

GPU和CPU优化:不同于使用离散图形优化系统,有些优化是违反直觉的,优化功率和带宽在获得预期性能方面发挥了重要作用。CPU和GPU共享系统的热设计功率(TDP)额定值。CPU和GPU有最大允许频率,可以得到一个或另一个,而不是同时得到两个!在图形基准测试中,负载共享如下所示:

游戏看起来更像下图!TDP在CPU和GPU之间的共享更加均匀。音频、人工智能和更高的图形API开销都会导致更高的CPU使用率,更高的CPU要求意味着可能很难达到最大Gfx频率。

降低TDP,更积极的权衡。在较低的TDP下,最大CPU和GPU频率可能不会有太大变化,但不能同时获得这两个频率

那么优化图形时会发生什么呢?如果分析告诉你是GPU受限的,那么优化GPU将提高性能,对吗????节省20%的GPU工作量,你有20%的额外FPS吗??额外的FPS通常需要更多的CPU来驱动工作负载。

GPU受限?优化CPU!!听起来很疯狂,但越来越普遍。GPU和CPU共享电源预算,在运行时根据工作负载动态调整频率,优化其中一个会给另一个带来更多动力,基本CPU频率可能会产生误导……

博主注:文中的有些描述是带有时间性的,不知道现在的移动或*板设备是否还如此,需要另外查资料或针对性地实测!

能量不是唯一共享的东西!共享的还有高达1.7G的系统内存,通过环形总线连接到CPU,共享LLCache,CPU和GPU之间共享的系统带宽。

好戏还在继续,随着TDP的增加,外部带宽变化不大,增加GPU或CPU工作负载会增加带宽需求:

你能足够快地给系统供电吗?如果更高的TDP不能提供更高的性能,请检查GPU有多忙。EU暂停通常可以直接由等待RAM造成,也可以通过采样器间接造成。Intel GPA可以检测相关数据。

SSAO改进前,当研发规模没有缩小时的中等设置,仅有15-20%的帧数,成本过高。基于CS,难以跨多个硬件供应商进行优化,内存非常密集,每个遮挡结果2个深度样本,智能交叉双边模糊从深度读取以确定边缘,以1/2 x 1/2屏幕分辨率工作。

SSAO改进后,基于图像空间地*线的环境遮挡(Based on Image-Space Horizon-Based Ambient Occlusion),完全基于PS,仍然是1/2 x 1/2分辨率,法线和边缘检测的基本成本+一个遮挡结果的一个深度样本,智能交叉双边模糊使用上一个过程中的边缘,不读取深度。下图是改进前(上)后(下)的性能对比,:

可见改进后的SSAO的性能有巨大的提升,普遍提升了好几倍。

MSAA性能:像素着色器每个采样运行一次,覆盖率和遮挡率更高。子样本级别所需的存储增加了带宽和内存需求,成本因硬件和工作量而异,但从来都不是免费的。

后处理AA作为替代方案,评估了两种最常见的方法:SMAA 1x和FXAA 3.11用于GRID2。FXAA 3.11的性能良好,但开发人员发现它太模糊:(文本和高频纹理上的细节丢失);SMAA 1x在正向渲染方面,成本比MSAA高一点,仍然有点模糊;从“形态抗锯齿”开始,后期处理,通过分析颜色不连续(边缘)来检测锯齿,并应用智能模糊来减少锯齿。

输入保守形态AA(CMAA):基于MLAA,但仅求解对称Z形,而不是U形、Z形和L形,更好地保存*均图像颜色和时间稳定性。确定和修剪边缘的保守方法,“如果不确定,不要模糊”,与FXAA 3.11相比,总体损伤更小,AA质量更高,为英特尔Haswell量身定制:快于FXAA 3.11,是SMAA 1x的两倍。

全屏阴影通道:像素着色器上的块,EU像素着色器停顿 = 42.3%,最初在4种阴影纹理中读取,一个着色器用于所有质量设置,在较低的设置下清理纹理读取。添加模板遮罩以移除选定区域,例如天空,在中等及以下级别使用不同的着色器,从粒子阴影纹理中删除读取。

其它优化方法:可以删除高端PC功能,*板电脑GPU的性能最初为每帧约53ms,但有更大的空间进行积极的改进,可扩展性(更多图形菜单选项!),更选择性地使用镜面反射和法线贴图等。更便宜的着色器,想在主场景中使用环境贴图着色器?该渲染过程本质上是主颜色通道的低质量版本,在某些情况下,质量太低,但可节省20毫秒的GPU时间!

纹理LOD偏差,视觉质量下降非常快,目前的测试显示,增益微乎其微。较低的几何细节层次,更*的绘制距离,树木/人群的广告牌LOD,降低顶点成本和照明成本,简化后处理:只需要色调映射(需要bloom),运动模糊、镜头光斑等都消失了。

低分辨率粒子渲染:对控制台/PC进行有效优化,以较低的分辨率渲染粒子,并将其与主帧缓冲区相结合,从而降低填充率,1/4的宽度和高度,*板电脑的固定成本管理费用要高得多,创建下采样深度缓冲区,上采样下采样颜色缓冲区,更高效地以全分辨率渲染粒子,并牺牲粒子数,高粒子数仅在碰撞或偏离轨道时出现。

总结:新的扩展允许视觉差异化,并且节能;现有算法可以显著优化;正常的GPU优化规则是微妙的不同,带宽和功率意味着事情并不总是直观的;CMAA是一种适用于所有硬件的低成本后处理抗锯齿解决方案,适用于担心模糊/图像退化的情况,与SMAA相比,AA的效果不如SMAA(尤其是与更昂贵的变体相比);你是否在电源受限的硬件上做出了最佳的视觉权衡?

Authoring Tools Framework: Open Source from Sony's Worldwide Studios提到了一种特殊的设计模型——文档物体模型(Document Object Model,DOM),内存中可观察的类XML的数据库,DomNode树的根通常是一个文档,DomNodes有属性和子节点,由DomNodeType指定(类似于模式类型),与XML中的属性一样,属性是简单类型(int、float、string、引用)或简单类型的数组。节点是可观察的,例如子节点添加事件或移除子事件、属性更改事件等。

DomNode层级:每个DomNode都有特定的属性和子节点,由DomNode的DomNodeType指定,可以通过编程或加载模式文件来创建DomNodeType,事件从子节点传给父节点。

节点适配器:客户端的“业务类”源于DomNodeAdapter,是为特定的DomNodeTypes定义的。首先创建DomNode,然后自动创建其DomNodeAdapter,但会根据需要进行初始化。在根DomNode上调用InitializeExtensions,初始化整个树的所有DomNodeAdapter。

上下文:通常每个文档一个。SelectionContext跟踪用户的选择并有更改事件;HistoryContext跟踪DOM对子树的更改以进行撤消/重做;TransactionContexts是HistoryContext的基类,跟踪一组更改的时间开始和结束,以便可以在正确的时间执行验证逻辑。InstancingContext实现复制、粘贴和删除。

注册表,每个应用程序各有一个。DocumentRegistry–跟踪文档,公开文件清单,添加和删除文档,活动文档。ContextRegistry–跟踪上下文,可用“上下文”列表,添加和删除上下文,活动上下文。IControlRegistry、IControlHostService客户端注册控件,以便它们出现在停靠框中,跟踪主动控制。

Introduction to PowerVR Ray Tracing阐述了2014年的PowerVR的移动端GPU的光线追踪硬件架构。

没看错!2014年!移动端GPU!!超前的技术和探索!

下图是PowerVR的硬件架构图:

上图的架构中,和光线追踪有关的单元是:Ray Tracing Unit、Scene Hierarchy Generator、Ray Data Master等。下图是渲染效果:

当时主要应用于AR、VR及轻量级游戏。其中VR涉及透镜畸变和像差校正(aberration correction):

游戏中的混合渲染:阴影,反射,透明度,使用多个动态光源进行更好的缩放,光栅化的实时光照图更新,易于集成。

完全的光线追踪图形:蛮力的路径追踪,轻松实现照片真实感,几乎需要对所有3D内容进行光线跟踪,在当今的控制台/桌面技术中,使用向导是可能的,对于未来几代人来说,在移动设备中完全实时使用可能不太现实,适合亚实时使用。2014年的功率和带宽曲线图:

Imagination的移动端图形能力如下:

实例是顶点、像素或OpenCL线程,任务共享计划中的所有实例、uniform、参数等。

核心内的ALU、寄存器、通用存储区结构如下:

每个统一着色簇(Unified Shading Cluster)有12个周期的延迟,其内部结构如下:

整体能力:Series6为最新移动设备提供超过100Gflops的着色效果,OpenGL ES 3.0支持,巨大的性能提升,允许使用数学重着色器,移动图形是真正的创新所在。

新着色器类型:当光线与三角形相交时,会调用光线着色器,着色器类型可以发射任意数量的光线,OpenRL有一个框架着色器来发射主光线,现有片段/像素着色器也可以发射光线!GLSL编程模型相同,拥有内置函数、参数等。

图元对象:封装一个网格的渲染状态,包括VBO、制服、着色器程序、纹理绑定等。光线追踪单元将光线分类到具有公共图元对象的任务中,帧间持久,客户提供的可变对象。

限制:着色器不能等待单个光线跟踪操作的结果,着色器必须提供有关子光线数的最坏情况估计,必须仔细管理每根射线用户数据有效载荷。

*行度取决于光线而不是像素:


光线的AABB测试:6条线条构成了AABB的轮廓,射线原点和每个边向量的6个*面,*面法线和射线方向向量的点积,6个标志必须匹配且为负数。

USC指令组打包,下图显示了26条指令,如果使用了压缩数据格式则是32条:

此功能的面积减少44倍:

光线追踪单元和一致性引擎(Coherency Engine):

下图显示了升序虚拟内存地址存储了大约100兆字节的数据,用于1百万个三角形,包括变量:

图元物体的层级结构如下:

体素化的图元物体:

一致性队列:

自动查找一致性路径:

场景层次生成器:

局限性:场景由三角形表示——与常规一样,BVH是一种为构建和便利而优化的定义格式,三角形顺序通常必须遵循空间连贯的流程,需要大致的场景规模估计,几何体着色器不与光线跟踪管线内联。

优势:着色簇工作负载不高于顶点着色器,只需处理在世界空间中实际移动的几何体,独特的算法仅将工作集限制为内部寄存器,单程操作:与顶点着色器执行一致,很好地处理“长而瘦的三角形”问题,流式写入外部存储器,由于构建算法,输出格式被无损压缩,精简逻辑。建在稀疏的log2八叉树形的层级上:

光线追踪的三角形处理流程如下:

处理一些三角形之后,结构体图例:

组装父节点之后更复杂:

在组装一个级别的所有父节点之后:

硬件单元的各个性能指标如下:

Practical Techniques for Ray Tracing in Games也是Imagination探索游戏中光线追踪的实用技术。

之前关于光线追踪的谬论:光线跟踪仅用于照片级真实感/物理精确渲染,光线跟踪与光栅化图形不兼容,光线跟踪是渲染给定数量像素的效率较低的方法。实际上,光线跟踪是一个对象的着色能够感知其它对象的几何体的技术,利用光线追踪可以方便地实现更加真实的阴影、反射、折射、AO、GI等效果:

光线跟踪允许模拟光线的行为:

如何将光线追踪添加到游戏中?使用混合的游戏引擎。在世界空间场景相交处的光线追踪细节:

基于光栅的现代游戏引擎,大多数现代游戏引擎都使用延迟着色:

混合渲染使用G缓冲区设置光线:

光线追踪可以实现软阴影,但半影渲染要求每个像素有多条光线,为每个表面点发射几条光线(而表示1条),每条光线的行为与硬阴影情况相同,*均每个像素的所有光线的结果:如果所有光线都被遮挡,则表面将被完全遮挡,如果所有光线都到达光源,则光源表面将完全照亮,如果一些光线被遮挡,一些光线到达光线,则表面处于半影区域。

选择光线方向:如果某个区域中的光源发光,则将光线分布在从表面可见的光源横截面上,要使用无限远的*行光*似日光,请从表面选择一个锥形光线:为了表示完全晴朗的一天,圆锥体的立体角为零,表示云层较大的日光时,立体角变大。估计到达表面点的入射光,为了得到好的估计,样本应该均匀地覆盖域。

GBuffer连续性:需要大量光线才能准确地对软阴影进行采样。对于大多数图像,从一个像素到相邻像素,表面属性变化很小,因此,从G缓冲区的一个像素发送的光线可能会与从相邻像素发送的相同光线击中同一对象。当然有一种方法可以利用这一事实来减少光线数量,同时保持视觉准确性?

交错采样(Interleaved Sampling),利用相邻像素的阴影光线数据。在帧缓冲区上*铺\(N^2\)射线方向的二维正方形阵列,基于网格发射阴影光线,生成的图像具有一个关键特性,即对于图像的任何NxN区域,都可以表示整个\(N^2\)射线方向数组,所以使用一个盒子过滤器来去除图像中的噪声,每个输出像素是\(N^2\)个相邻输入像素的*均值。必须处理图像中的不连续性。

反射:当光线击中一个完美的镜面时,它会以与入射角相同的角度反射,公元前3世纪,欧几里德首次编纂了基本物理定律。在现实世界中,反光物体很常见,而不仅仅是金属球!

光线追踪反射:从反射表面发射一条额外的光线,反射光线的方向根据入射光线的方向使用反射定律计算,当光线击中场景中的对象时,使用与直接可见表面相同的照明计算对该表面进行着色。

光线追踪透明度,“真实”透明度不是alpha混合!在现实光学中,当光线通过半透明物体时,有些光线被吸收,有些则不被吸收。从表面的背面发射光线,像反射光线一样的着色,顺序无关。

透明度和阴影:当阴影光线击中一个透明的物体时,它会继续朝向光线。投射到透明对象上的阴影光线应进行着色并重新发射,就像它是非阴影光线一样,阴影光线将穿过表面的完全透明区域,阴影光线从半透明对象获取颜色,阴影光线与透明物体相互作用!

总之,光线追踪很容易,使逼真的光源模拟变得简单,与现有基于光栅的引擎轻松结合。

Next Generation Post Processing in Call of Duty分析了COD上的次世代后处理流程、技术、应用和优化。其后处理管线包含了运动模糊、Bokeh DOF、次表面散射、泛光、阴影采样等。

COD的方法是基于后处理特效过滤作为输入,如颜色缓冲区、深度缓冲区、速度缓冲器,没有屏幕空间后处理DOF可以完全准确。信息缺失:在运动模糊中,当对象移动时,背景会显示出来,在DOF背景中,从不同的视点积累视图。

使用分散技术自然地实现,收集即分散(scatter-as-you-gather)允许基于过滤器的方法,但其存在的问题有:如何确定采样区域,检测哪些样本真正起作用,如何恢复背景。

收集即分散的运动模糊案例。在这种情况下,采样速度足够大,以实际覆盖当前像素,样本也比当前像素更接*相机。所以,这两种情况都意味着样本会起作用。

对于运动模糊的如何确定采样区域的问题,可以采用3个Pass:

第1个Pass[Tile最大值]:以Tile为单位计算最大速度(20x20像素)。

第2个Pass[Tile邻域]:计算每个Tile在3x3内核中的最大速度。

第3个Pas[运动模糊]:在Tile速度方向上应用全分辨率的可变宽度模糊。

下面是不同方法的运动模糊效果图:

从左到右:McGuire2013方法、准确的样本贡献、准确的样本贡献+镜像背景重建。

在采样上,尝试了几种方法,包括:均匀、白噪声、抖动、时间抖动。

运动模糊的优化:计算每个瓷砖的最小值/最大值(与最大值相比),如果[max-min]很小,可以使用更简单的版本,如果[max-min]等于零,则该算法收敛到常规颜色*均值。

实现提示:使用可分离方法计算最大速度(更快),从[width x height]到[20 x height]缓冲区的水*通道,从[20 x高度]到[20 x 20]的垂直通道。与[Sousa2013]的启发类似,将速度整合到一个深度,在COD中,是一个R11G11B10缓冲区,将每个样本的点击次数从3(颜色、速度、深度)减少到2(颜色、速度/深度)。与[McGuire2013]类似,抖动用于访问tile的纹理坐标,有助于减少tile瑕疵,使用点采样以避免溢出。

DOF概览:运动模糊的许多想法适用于DOF,收集即分散对双方都有用。呈现的“聚集即散射”的运动模糊方法仅覆盖单个层(单个方向),许多可能发生并重叠,但在COD中,运动模糊不是什么大问题,它们更容易看到,移动对象会隐藏问题。因此,开发了一种两层方法。想要一直都好看的东西,使用真实的相机控件(光圈、焦距…)会使创作中的DOF问题更难解决,由玩家触发的DOF,不受创作控制。

传统的DOF问题:

  • 收集即分散问题。
    • 和运动模糊一样,邻域像素可以渗入当前像素,类似的解决方案。计算tile中的最大COC,主二维滤波器半径与之成比例。
    • 计算每个样本的贡献,对样本进行分类和混合,背景重建。
  • 过滤质量问题。欠采样,需要正确使用双线性过滤。
  • 性能问题。很多样本,半分辨率渲染。

收集即分散的理想算法:

  • 检测一个样本COC是否和当前像素重叠。
  • 检测它是否前景。
  • 对重叠和前景像素进行从后向前排序。
  • 分配一个透明度给每个样本,\(\alpha = \cfrac{1}{\pi c^2}\),其中\(c\)是COC半径。
  • 混合它们。
  • 排序的开销异常大。

DOF样本的侧视图(左)和顶视图(右)。

COD的排序方法:

  • 与[Lee2008]类似,性能简化。
  • 将样本分为两层:背景、前景。
  • 对每层:
    • 叠加\(\alpha\)混合*均值。
  • 计算前景Alpha。
  • Alpha混合前景/背景。
  • 松散的前景/背景分类渐变。
    • 距离0英寸:前景。
    • 距离100英寸:背景。

COD的前景、后景、当前样本的侧视图和顶视图。

用双线性进行采样时,会产生颜色溢出(下图右):

通常使用COC预乘求解,适用于极端情况,介于两者之间的情况可能会失败,特别适用于高动态范围图像。需要RGBA16缓冲区,更多内存,与只使用颜色的循环不兼容(tile优化)。以较小的数字编码的颜色会降低精度。

双线性过滤的具体方法:

  • 预过滤。
    • 颜色缓冲区用双线性采样。
    • 深度缓冲区用Gather4 + 计算最小双边权重。
  • 主通道。
    • 颜色缓冲区用点采样+随机偏移。
    • 预排序缓冲区用点采样+随机偏移。
    • 但对于快速tile(仅彩色循环)用双线性采样+随机偏移。

在阴影方面,由于需要60FPS,阴影过滤的限制非常具有挑战性的,因为没有很多样本的预算。COD用每像素随机旋转的泊松圆盘做实验,发现它们的结果质量不高,样本数量适中(8个),运动时非常不稳定。

通过实验和优化噪点发生器,发现了一个噪点函数,可以将其分类为介于抖动和随机之间的一半,称之为交错梯度噪声(Interleaved Gradient Noise)

float3 magic = float3( 0.06711056, 0.00583715, 52.9829189 );
return -scale + 2.0 * scale * frac( magic.z * frac( dot( sv_position, magic.xy ) ) );

用于旋转样本:

sincos( 2.0 * PI * InterleavedGradientNoise( sv_position ), rotation.y, rotation.x );
float2x2 rotationMatrix = { rotation.x, rotation.y, -rotation.y, rotation.x };
...
float2 sampleOffset = mul( offsets[i], rotationMatrix );

两全其美:产生丰富的值范围,类似于随机噪声,产生时间上一致的结果,类似于抖动方案。这种噪声会产生交错的梯度,意味着以恒定速度移动的对象将*滑地旋转样本,要使其适用于静态图像,可以滚动水*位置:

sv_position.x += scale * time;

交错梯度噪点和交错梯度。

交错梯度噪点的空间一致性允许通过模糊更容易地*滑,在COD的例子中不能模糊它,但它在其它情况下可能有用。

把以上所有概念放在一起,获有一个随时间*滑旋转的螺旋,考虑到高帧速率,它比实际拥有的样本更多:

Crafting a Next-Gen Material Pipeline for The Order: 1886讲述了游戏《秩序:1886》的次世代材质管线。

《秩序:1886》引擎的光照管线:使用了分块前向渲染,透明物体使用漫反射+镜面反射的完整照明。静态几何使用光照图,使用Optix在GPU农场上烘焙,基于H-方向变化的基。SH动态探针使用三阶(9个系数),预卷积镜面探针,立方体贴图在引擎中渲染,在计算着色器中卷积。

环境光遮蔽:定向AO地图(H基)烘焙角色和静态几何,静态几何仅用于阻挡探头的镜面反射,动态几何将AO应用于SH探针的扩散。

核心着色模型:默认镜面反射BRDF为Cook Torrance,D项是Walter等人的GGX分布(匹配在同一篇论文中导出的Smith G项)、Schlick对菲涅耳的模拟、兰伯特漫反射(与镜面反射强度*衡)。GGX+Cook Torrance==大量数学运算,可以优化,不要使用三角形,将Smith的G项折叠成分母,让艺术家使用sqrt(roughness),更直观,更适合混合。

// Helper for computing the GGX visibility term
float GGX_V1(in float m2, in float nDotX) 
{
    return 1.0f / (nDotX+ sqrt(m2 + (1 -m2) * nDotX* nDotX));
}

// Computes the specular term using a GGX microfacetdistribution. m is roughness, n is the surface normal, h is the half vector, and l is the direction to the light source
float GGX_Specular(in float m, in float3 n, in float3 h, in float3 v, in float3 l) 
{
    float nDotH= saturate(dot(n, h));
    float nDotL= saturate(dot(n, l));
    float nDotV= saturate(dot(n, v));
    float nDotH2 = nDotH* nDotH;
    float m2 = m * m;
    // Calculate the distribution term
    float d = m2 / (Pi * pow(nDotH* nDotH* (m2 -1) + 1, 2.0f));
    // Calculate the matching visibility term
    float v1i = GGX_V1(m2, nDotL);
    float v1o = GGX_V1(m2, nDotV);
    float vis= v1i * v1o;
    // Multiply this result with the Fresnel term
    return d * vis;
}

其它可用的BRDF:Beckmann、各向异性GGX、头发(Kajiya Kay)、皮肤(预集成漫反射)、布料。

皮肤:最昂贵的着色器!基于\(N \cdot L\)的逐光源的纹理查找,多个镜面反射波瓣。没有使用基于着色器梯度的曲率,瑕疵太多,使用曲率贴图。

头发着色:不以身体为基础,使用Kajiya Kay光照模型。调整菲涅耳曲线,使用切线方向进行SH漫反射,各向异性表面的分析切线辐照度环境贴图[Mehta 2012]。次级镜面反射叶沿切线向尖端移动,采用反照率颜色。

偏移贴图以分解高光,沿切线方向额外移动:

定义切线方向的流向图:

布料着色:参考数码相片的观察结果:柔和的镜面反射波瓣,具有较大的*滑衰减,边缘因粗糙度散射而产生绒毛,前向角度的低镜面反射贡献,有些面料有两种色调的镜面反射颜色。

用于粗糙度散射的反向高斯,从原点进行*移,以在前向角度提供更多镜面反射,没有几何项:

普通的Cook-Torrance+法线贴图会产生明显的高光锯齿,修改粗糙度贴图以减少锯齿,使用基于Han等人“频域正态图滤波”的技术。

将NDF表示为球形高斯分布(vMF分布),以SH表示的*似BRDF为高斯分布,两个高斯函数的卷积是一种新的高斯函数,使用关系计算新的粗糙度。


为了获得逼真的材质,使用了3D扫描的技术:

材质 = 文本资源,使用自定义数据语言(radattr),语言支持:类型、继承、用户界面布局、元数据。材质创作是基于功能的,没有着色树,用于实时编辑的材质编辑器工具,也可以托管在Maya内部。

用于制作模板的radattr继承,基础材质中共享的通用参数,衍生材质仅存储基础材质的变化,更快地创建资产,全局变化可以在单一资产中进行。

着色器:手写的Uber着色器,很多#if,主要材质特征=宏观定义,硬编码到着色器中的参数(规格强度、粗糙度等),动画或合成时除外,一些代码是自动生成的,用于处理纹理和动画参数。着预定义的“排列”,排列=宏定义+入口点,蒙皮、混合形状、实例化、光照贴图等的排列,可视化/调试的调试排列,基于排列+材质在构建管线中编译的着色器。

优点:着色器针对材质进行了优化,优化器具有完全访问权限,游戏本身没有运行时编译(由工具使用)。缺点:要编译的着色器太多了!缓存了所有内容,但迭代可能会很慢,整体着色器很难调试,大量依赖编译器。

材质组合:主要是离线流程,材质资产指定组合栈,堆栈中的每一层都有:引用材质、混合Mask、混合参数,递归构建+组合参考材质,逐层逐组合使用像素着色器。从材质和混合贴图生成参数贴图,支持合成BRDF的子集,布料、GGX和各向异性,合成布料需要应用2层BRDF。

// Compositing pixel shaderrun on a quad covering the entire output texture
CompositeOutputCompositePS(in float2 UV : UV) 
{
    CompositeOutputoutput;
    float blendAmt= BlendScale* BlendMap.Sample(Sampler, UV);
    float4 diffuseA= DiffuseTintA* DiffuseMapA.Sample(Sampler, UV);
    float4 diffuseB= DiffuseTintB* DiffuseMapB.Sample(Sampler, UV);
    float diffuseBlendAmt= blendAmt* DiffuseContribution;
    output.Diffuse= Blend(diffuseA, diffuseB, diffuseBlendAmt, DiffuseBlendMode);
    // Do the same for specular, normals, AO, etc.
    return output;
}

材质层:最多4层,源自基本材质,每层单独的合成链,由顶点颜色驱动。

LayerParamscombinedParams;

[unroll]
for(uinti= 0; i< NumLayers; ++i) 
{
    // Build all layer paramsfrom textures and hard-coded
    // material parameters
    LayerParamslayerParams= GetLayerParams(i, MatParams, Textures);
    // Blend with the previous layer using vertex data and blend masks
    combinedParams= BlendLayer(combinedParams, layerParams, vtxData, BlendMode, Textures);
}

// Calculate all lighting using the blended params
return ComputeLighting(combinedParams);

Rendering Techniques in Ryse讲述了游戏Ryse: Son of Rome所用的引擎CryEngine采纳的各类渲染技术,包含PBS、遮挡、SSDO、反射遮挡、AO颜色溢出、图像稳定性、几何锯齿、高光锯齿、LOD选择、粒子着色、阴影(太阳、点光源阴影、角色阴影、静态阴影图、粒子阴影、影视级阴影)、大规模AO等内容。

PBS方面,基于真实世界行为建模光与物质的相互作用,注重一致性,一切都遵循一个定义良好的规则集,从图形编程中排除了很多猜测,对几个领域的重大影响。材质模型:为资产定义清晰的规则,从而实现更高的艺术/内容一致性,强制执行合理的材质参数,并阻止不切实际的设置。光照模型:更复杂的BRDF、菲涅耳、镜面高光标准化、总体能量守恒,微*面BRDF仍然只是有限的现实模型。照明模型:必须小心保护整个管线的材质完整性,基于物理的着色只有在所有区域都得到尊重的情况下才能很好地工作。

着色模型的限制:为模型的数学正确性付出了大量努力,主要是在学术研究方面,普通分析镜面微*面BRDF只是现实的有限模型,不考虑多次光反弹,忽略粗糙表面上任何与波长相关的吸收。

光照模型注意事项:需要注意保持材质的完整性,如果光源可以在不影响镜面反射的情况下随机添加漫反射贡献,那么真实世界中的反射比将毫无用处,添加漫反射而不添加相应数量的镜面反射将显著*坦材质,因为它会有效降低材质反射率F0。删除了Ryse的所有仅漫反射常量和半球形环境项,对纯镜面反射表面(金属)无影响,由具有漫反射和镜面反射立方体贴图的局部环境探测器捕获的所有间接照明,探头延伸到更大的区域可能会缺少局部光强度变化,从而导致环境*坦。引入了倍增光(“环境光”)作为实用工具,供灯光艺术家设置反弹照明和遮挡[SCHULZ14],环境光同样影响间接漫反射和镜面反射,并保持反射率。

遮挡是全局照明的重要组成部分,AO是理解物体之间空间关系的重要线索,小范围遮挡:SSDO、屏幕空间反射,大规模遮挡:带有负环境光的局部探头、基于简单阴影图的遮挡系统。特殊解决方案:用于眼睛的预烘焙遮挡贴图、一些资产的预烘焙AO图。

SSDO:用统一的屏幕空间定向技术完全取代SSAO,基于简单类SH基的方向遮挡编码,两个独立使用的波段:恒定部分和方向部分,应用于间接漫反射照明的常数项(如SSAO中),用于直接照明的方向项,提供简单的接触阴影,使用光源方向进行查找,应用于灯光的漫反射和镜面反射贡献,这两部分都用于反射遮挡,使间接镜面反射变暗,使用视图反射向量进行查找。

反射遮挡开启(左)和关闭(右)的对比图。

AO颜色溢出:去除环境光的主要部分可能看起来太暗,在吸收率低的明亮表面(包括白种皮肤)尤其明显,AO没有考虑到发生多次光反弹,限制最大AO变暗有助于,但AO在深色表面上会变得太弱,需要根据表面反照率调整遮挡量。

简单颜色溢出*似:最后一分钟的功能,必须非常便宜,生成低频版本的反照率缓冲区,作为非常粗糙的局部散射*似,作为gamma函数应用于遮挡值,Ryse中夸张的颜色变化,给人一种轻微的GI印象:

float3 occlColor = pow(occlTerm, 1 - min( bleedColor * bleedColor * bleedColor * 3, 0.7 ));

颜色溢出关闭(上)和开启(下)的对比图。开启后,物体凹槽处会有轻微的提亮。

图像稳定性:干净且暂时稳定的图像对感知质量至关重要,视觉噪点会分散注意力,需要最小化的各种形式的锯齿,包含几何体锯齿着色锯齿镜面锯齿

着色锯齿

  • 由于欠采样、过采样、多分辨率渲染等原因,如射线行进(SSR)、阴影图采样等。
  • 要由算法级别的特定解决方案解决,通常倾向于使用时间稳定的方法,而不是在静态图像中提供稍高质量的方法。

几何锯齿

  • 空间锯齿。臭名昭著的锯齿状/楼梯瑕疵,基于后处理的方法,如MLAA、FXAA和SMAA,效果非常好,Ryse[JIMENEZ12]中使用的SMAA。

  • 时间锯齿。更严峻的挑战之一,亚像素大小的三角形在一帧中光栅化,但在另一帧中不光栅化,导致闪烁/微光。每像素需要更多样本(MSAA、超级采样),在Ryse中完全支持超级采样,用于预录制的电影,可在PC版本中使用。

  • 时间几何锯齿。

    • MSAA在Xbox One上不是延迟着色的选项,带宽大幅增加,遇到ESRAM大小问题,2倍MSAA不够高质量,需要依赖前一帧的数据。
    • 最终使用的解决方案是新的SMAA 1TX[SOUSA13],累积多个帧以获得更好的时间稳定性,跟踪前一帧中的几何图形,但限制信号变化,避免动态对象中的像素无法准确重投影时出现重影,避免图像过于*滑的关键是根据信号频率选择累积采样数,图像低频部分的采样数越少,高频部分的采样数越多。

镜面锯齿

  • 基于物理的着色非常容易出现镜面反射锯齿,标准化BRDF的高亮度值与高频正常信息相结合。
  • Ryse中的法线和粗糙度严格耦合。从概念上讲,法线代表宏观尺度上的表面凹凸,微观尺度上的粗糙,粗糙度存储在源资源的法线贴图alpha通道中,按引擎拆分为2个纹理(BC5表示法线,BC4表示粗糙度),粗糙度贴图中mips的法线方差[HILL12],用于估计方差和推导新粗糙度的Toksvig因子[TOKSVIG04]。
  • 在GBuffer中修改粗糙度时仍存在问题(贴花、雨水湿度),通过在屏幕空间中应用法线方差滤波器解决[SCHULZ14]。

LOD选择:改进的LOD选择,计算LOD转换的最佳观察者距离,通过避免小三角形有助于减少锯齿(还可以提高性能)。计算*均屏幕空间三角形大小,当屏幕上的投影尺寸低于阈值时,网格过于详细,应使用下一个LOD网格,离线预计算的*均网格三角形面积,各种可能的指标:*均值、中位数、几何*均值等。选择几何*均值(对数*均值),许多小三角形会减少值,而少数大三角形几乎不会增加值。刚刚为第一个LOD过渡计算的开关距离,后续LOD使用该距离的倍数,防止完全跳过某些LOD网格。

上:使用物体尺寸的LOD选择;下:使用三角形尺寸的LOD选择。

太阳和点光源阴影:用于太阳的级联阴影贴图,*似对数分裂格式,点光源展开为单个投影器,每个投影器从大型阴影地图集中渲染成块,块分辨率取决于投影仪的重要性。阴影遮挡在全屏过程中进行评估,并累积到“阴影遮罩”纹理数组中。每个灯光一个颜色通道,如果可能,通道共享,允许对阴影接收区域进行有效的模板剔除,减少tile着色过程中的GPR压力,通过逐像素旋转的泊松盘进行过滤。

角色阴影:第三人称视角,主角始终处于焦点,高质量的自阴影,自定义阴影贴图集中在紧密的边界框上,使用“max”操作符混合到阴影遮罩(shadow mask)中。

静态阴影图:

  • 在早期的开发中,Draw Call是一个主要的瓶颈,尤其是阴影,是项目的主要风险。大多数阴影绘制通道都花在远距离的级联上,世界覆盖率呈指数级增长,甚至在高度优化的资产上也是如此,聚合距离剔除,尽可能禁用阴影投射。优化折衷是远处没有动态阴影。
  • 简单方法:用“静态阴影贴图”替换最远的级联,8192 x 8192像素,每像素16位: 128MB视频内存,每个对象只渲染一次,零阴影绘制需要在静态阴影贴图填充后替换级联,世界空间分辨率(每米像素)匹配或超过第一级,在分辨率方面没有质量损失,由于偏离对数纹素分布而产生新的欠采样失真,在Ryse中相当次要。
  • 选择哪种级联?对数分割方案导致固定世界空间面积的指数存储需求,CryEngine选择了cascade 4作为最佳选择,能够覆盖大约1.3公里×1.3公里的区域,而不会造成质量损失。

1km x 1km区域所需的纹理大小,保持第一次替换级联的分辨率不变(对数比例)。

  • 由设计师和灯光艺术家控制的地图放置和更新,关卡检查点,在区域之间切换。在XBox One上进行大约10-15毫秒的完整更新,避免帧速率峰值的时间切片更新策略,每帧的绘制调用数上限,流选项:渲染对象完全流化后。在优化的资产上节省大约40%-60%的阴影绘制调用。

影视级阴影:太阳阴影需要高质量的阴影解决方案,从样本分布阴影图开始[LAURITZEN11]。有很好的结果,但如何摆脱回读?上一帧数据中的瑕疵太多,避免回读需要使用几何体着色器(速度慢!),或者在GPU上全部剔除和绘制调用,但GPU的做法太危险。最终采取了非常务实的解决方案:在cutscene创作工具中暴露*距离/远距离阴影,*/远之间的完全对数分割,2048 x 2048阴影贴图将超过屏幕分辨率。

大规模AO:最终使用了类似于[SWOBODA10]的方法,将场景自上而下渲染到阴影贴图中,生成高度贴图*似值。全屏通道:每个像素
投影到阴影贴图空间,对周围像素进行采样,并使用所选AO算法计算遮挡。

主要关注捕捉最大规模的特征,低分辨率阴影贴图(大约每像素0.5米),2048 x 2048足够覆盖1km x 1km的区域。AO内核采样半径:7.5米,Ryse没有那么大比例的移动对象,与静态阴影贴图相同的更新策略。非常低频的效果,每像素4个(交错)样本,可以很容易地在一半或四分之一的时间内完成,通过与常规SSDO通道合并,获得了略好的性能。

大规模AO关闭(左)和开启(右)的对比。

效果出奇地好,相当低开销,在XBox One上0.4毫秒。在天空大部分被遮挡的区域,适当地降低立方体贴图中天空的贡献,例如透过窗户看房子。不过只能在户外,当高度贴图表示与场景不匹配时会出现问题,重叠结构。

Achieving the Best Performance with Intel Graphics Tips, Tricks, and Clever Bits阐述了在Intel的GPU芯片上进行性能优化的技巧。

高效的GPU编程需要充分利用管线,如IA软件堆栈中的优化、特定于应用程序和通用的,应用程序优化的最大影响。

GPU编程优化包含应用程序、驱动和GPU三层。

绘图调度和资源更新:注意已调度操作的内存访问模式,如三维/二维作业调度、状态/着色器更改、资源位置。

排序绘制调用时,影响因素最大的且具有相同资源的Draw Call排在一起。例如,RT影响最大,故而图下将0和1安排在一起;然后是引用了相同资源的(图中)。

图形只是谜题的一部分,独特的架构特征:动力与性能、内存层次结构,成对*台:中央处理器、系统存储器,其它制约因素:热量、功率等。

CPU/GPU之间的关系,CPU或GPU瓶颈,CPU可以限制GPU......

横坐标是总功率,竖坐标是各个硬件单元的功率。左:随着总功率的提升,GPU的功率先下降后提升;CPU相反,而未被使用的功率先略微提升,后下降。右:随着总功率提升,GPU频率先降低接着持*后提升,CPU则相反。

以上图表只针对2014年前后的Intel硬件架构,目前及其他GPU厂商是否依然如此有待查证。

缓存位置是王道:优化CPU和GPU的内存访问,内存带宽限制,层次结构因*台而异,可选的CPU+GPU缓存:末级缓存(LLC)、嵌入式DRAM(eDRAM)。

架构组件:

  • 非切片。
    • 固定函数:变换、裁减
  • 切片。
    • 普通切片:光栅化、着色器调度、颜色后端
    • 子切片:着色器执行

架构扩展/扩展组件:

  • 切片:并行图元处理。
  • 子切片:并行宽度(span)处理。

采样器:每个子切片1个采样器本地纹理缓存,由L3缓存作为后盾支持。

采样器性能:还记得缓存位置吗?吞吐量:格式、采样模式,糟糕的访问模式会增加内存带宽和延迟。

填充率:逐切片通用,如像素后端、颜色缓存(RCC$)。

填充率性能,输出颜色:

  • 吞吐量。
    • 格式。
    • 维度+区域。
  • 其它因素。
    • 光栅化。
    • 提前Z/S。
    • 像素着色器执行。
    • 后期Z/S。
    • 混合函数+模式。

表面格式:为颜色范围选择适当的格式,中间/最终渲染目标,不必要选择更高精度的格式,否则会降低填充率,增加内存带宽。

算术逻辑:逐子切片的块,包含执行单位(EU)、指令缓存(IC$)。

算术逻辑性能:算法复杂性,如控制流、数学、扩展数学、最大并发寄存器数。

着色器优化:基于意图的最优代码,着色器伸缩,通用着色器的情况,产生未使用的产出。

几何着色器:单个非切片,固定函数:VS、HS、TE、DS、GS、SOL,剪切,设置前端。

优化几何以提高算法复杂度,单个几何体的最佳定义,基于*台的质量扩展,意图:灯光,深度,动画…

几何体的软边、硬边的做法和效果对比。

内存带宽:都是关于内存,因*台而异,关注的原因是从内存中读出,写入内存。

采样器吞吐量:不同的架构和*台,测量所有用例,包含维度数、格式、过滤模式。

填充率:多种表面类型,包含渲染目标:格式、维度、混合/非混合,深度:读/写,模板:读/写。

几何吞吐量:固定功能带宽与算术逻辑,固定功能(剪辑/剔除、光栅化),几何变换:ALU。

时间来到了2015年,这一年,以DirectX 12、Vulkan为代表的新一代图形API正式发布,紧接着,有不少文献阐述了它们的特点和应用。D3D12 A new meaning for efficiency and performance就是其中之一。文中对Command List、Root Signature、资源同步、屏障、并发、多线程、多队列、渲染应用和性能分析等等方面阐述得由浅入深、鞭辟入里,适合入手和进阶。想了解更多的可查阅原文或剖析虚幻渲染体系(13)- RHI补充篇:现代图形API之奥义与指南

Visual Effects in Star Citizen分享了游戏星际公民的视觉效果,该游戏由CryEngine研发。

该游戏具有非常高端的视觉效果,长期关注质量,高系统规格,当时仅限DX11。舰体拥有复杂的网格数量、材质和贴花效果。

其中航舰的破坏效果的渲染流程如下:

添加破坏效果的具体步骤如下:

由于游戏中的MMO部分,需要很多环境,因此选择了模块化方法。模块化的“套件”易于组装,简化艺术管道(例如外包),对于关卡设计师来说非常灵活。

期间遇到了许多性能问题,由于期望的保真度、多边形数量、纹理密度太高,无法烘焙纹理,*铺纹理意味着每个网格有许多绘制调用,大量的网格来建造一个房间,空间站需要更多的网格和绘制调用。

纹理数组是一种潜在的解决方案,分辨率限制意味着流数据很困难,相反,只对LOD使用低分辨率纹理数组,无需流式传输单个纹理–256x256的整个纹理数组的级别小于15Mb,只需一次绘制调用即可渲染LOD!顶点缓冲区按材质ID排序,因此如果需要,仍然可以使用高分辨率纹理。

类似KillZone的网格合并解决方案,为每个单独的模块化资产构建LOD,迭代启发式算法,结合LOD,构建具有最小绘制调用和内存的层次结构。依赖于积极的LOD,无需艺术家手动工作即可大幅减少绘制调用。

Rendering the World of Far Cry 4分享了Fay Cry 4的综合渲染技术,包含材质、光照、植被、抗锯齿、地形等。

在光照方面,FC4支持天空遮挡、环境图、间接光照等特性。天空光使用Bruneton天空模型和Preetham太阳模型,生成三阶SH照明。对于天空遮挡,将直接天空照明与间接照明分离高分辨率“自上而下”天空遮挡,从高度场创建可见性二阶SH,使用了类似SSAO的方法来计算遮挡。

FC4为了让单个cubemap在一天中的每个时间都有合适的强度,对cubemap每帧进行重照明,主流程如下:

在间接光方面,使用延迟辐射传输体积[Stefanov2012],存储辐射传输信息的光照探针。目标是上一代和当前一代使用相同的光照探针,扩大间接照明的范围,通过将CPU工作转移到GPU来加快更新速度。主流程如下:

  • 离线:烘焙探针,二阶SH中的辐射传输信息。
  • CPU:流化探针数据,上传到GPU并更新页面表。
  • GPU:计算辐射传输,将探针插入剪辑图并在延迟照明中采样。

其中在整个单元格列表执行辐射传输的过程如下:

植被是FC4的主要渲染关注点,使用了和以为完全完全不同的数据集。目标是*距离视觉逼真度,改进的LOD和替代物(imposter)以及各类模拟。其中骨骼和物理模拟的流程如下:

对于替代物(imposter),从九个角度截图,八根垂直于这棵树,另外一个自顶向下:

G-Buffer屏幕截图:捕捉反照率、法线和材质属性。

深度公告板:公告板几何图形细分为16x16网格,在GBuffer截图期间捕获深度,根据原始树深度置换顶点。深度数据和渲染图如下:

此外,还要为植被生成AO体积:尺寸范围从16x16x16到64x64x64,从体积周围的32个方向捕获阴影贴图,采样阴影和*均值。

从视觉上看,植被很难完全正确,模拟了部分特性:光在草叶间反射,光在树叶中散射等等,需要TA的魔法。

抗锯齿上,使用了HRAA,详见14.4.3.5 特殊技术Hybrid Reconstruction Anti Aliasing部分。

SIMD at Insomniac Games分享了Insomniac公司的游戏所使用的SIMD技术,包含SSE、技巧、最佳实际等。

SIMD编程在Insomniac公司的工作室中有悠久历史,如PS2 VU、PS3 SPU+Altivec、X360 VMX128、SSE(+AVX),关注本周期的SSE编程,当PC+主机共享ISA时,更大的激励,当使用SIMD时,PC工作站的速度快得离谱,许多旧的最佳实践不适用于SSE。

当时的趋势是都是GPGPU,但许多问题太小,无法转移到GPU,并且不息在控制台上浪费x86内核。永远不要低估暴力+线性访问,CPU SIMD可以大大提高性能,不能只把性能留在PC上。当时SSE和AVX SIMD的选项有:

  • 编译器自动向量化。乌托邦式的想法在实践中并不奏效,编译器是工具不是魔杖,在维护期间经常中断,只获得一小部分性能提升!编译器支持/保证=糟糕,VS2012中没有支持,VS2013中有些支持,不同的编译器有不同的怪癖。
  • 英特尔ISPC。SSE/AVX类着色器编译器,编写标量代码,ISPC生成SIMD代码,需要在另一个抽象层次上进行投入,注意:容易生成低效的加载/存储代码。主要优点:SSE/AVX自动切换,例如英特尔的BCT纹理压缩器, 在AVX工作站上自动运行速度更快。
  • 内部函数。掌握控制权,而不必去汇编,Insomniac游戏中编写SIMD的首选方式,可预测,没有无形的性能退化,灵活地公开所有CPU函数。难以学习和实践,但不是真正的反对理由,所有好的编程都很难(而糟糕的编程很容易)。
  • 汇编。永远是一个选择!64位VS编译器上没有内联汇编,需要外部汇编程序(例如yasm)。对于初学者来说,有很多陷阱:在OS之间保持ABI可移植性很难、相对稳定(Non-volatile)的寄存器、64位Windows的异常处理、堆栈对齐、调试…

为什么SSE没有被更多地使用?对个人电脑领域碎片化的恐惧,每个x64 CPU都支持SSE2,但通常支持更多, “它不符合我们的数据布局”,传统上,PC引擎不太重,在OO设计中嵌入SIMD代码很尴尬,“我们试过了,但没用”。

// SSE版Vec4声明
class Vec4 
{
    __m128 data; // 有X/Y/Z/W,是4D向量
    
    operator+ (…)
    operator- (…)
};

// 【不正确】的SSE版Vec4点积
Vec4 Vec4Dot(Vec4 a, Vec4 b)
{
    __m128 a0 = _mm_mul_ps(a.data, b.data);
    __m128 a1 = _mm_shuffle_ps(a0, a0, _MM_SHUFFLE(2, 3, 0, 1));
    __m128 a2 = _mm_add_ps(a1, a0);
    __m128 a3 = _mm_shuffle_ps(a2, a2, _MM_SHUFFLE(0, 1, 3, 2));
    __m128 dot = _mm_add_ps(a3, a2);
    return dot; // WAT: the same dot product in all four lanes
}

// 【良好】的SSE版Vec4点积
__m128 dx = _mm_mul_ps(ax, bx); // dx = ax * bx
__m128 dy = _mm_mul_ps(ay, by); // dy = ay * by
__m128 dz = _mm_mul_ps(az, bz); // dz = az * bz
__m128 dw = _mm_mul_ps(aw, bw); // dw = aw * bw
__m128 a0 = _mm_add_ps(dx, dy); // a0 = dx + dy
__m128 a1 = _mm_add_ps(dz, dw); // a1 = dz + dw
__m128 dots = _mm_add_ps(a0, a1); // dots = a0 + a1

不要把时间浪费在SSE类上,试图用AOS数据抽象SOA硬件,注定是笨拙和缓慢的。SSE代码想要自由,实现无包装器或框架的最佳性能,只需根据需要编写小的助手例程。“它不符合我们的数据布局”,空粒子(float pos[3],…),存储在结构粒子{float pos[3];…},在SSE中使用粒子数组很难,所以避免这样做。保留spawn函数,更改内存布局,问题在于结构粒子,而不是SSE。内存中的结构粒子(AOS):

内存中的粒子(SOA):

数据布局选择:对于SSE代码来说,SOA形式通常要好得多,自然映射到指令集,SOA SIMD代码与标量参考代码紧密映射。AOS形式通常更适合于标量问题,尤其是对于查找或索引算法,单缓存未命中以获取一组值。如果需要,在转换中局部地生成SOA数据,通过调整输入/输出来*衡SIMD效率。

举个具体的案例——门。自动打开的门,当右翼演员“忠诚”在某个半径范围内时,想想《星际迷航》,典型博弈问题,最初是作为面向对象解决方案实现的,开始出现在性能雷达上,约100扇门 x 约30个角色测试 = 3000次测试!下面是有性能问题的版本及解析:

上图的原始更新中输入数据的内存关系如下:

SIMD准备工作:将门数据移动到中心位置,实际上只是SOA形式的一包价值观,很好的方法,因为门很少被创建和破坏,每个门都有一个进入中央数据仓库的索引,在更新中本地构建参与者表,每次更新一次,而不是100次, 隐藏在堆栈上的简单数组中(分配用于可变大小)。

// 门更新数据设计

// In memory, SOA
struct DoorData 
{
    uint32_t Count;
    float *X;
    float *Y;
    float *Z;
    float *RadiusSq;
    uint32_t *Allegiance;
    // Output data
    uint32_t *ShouldBeOpen;
} s_Doors;

// On the stack, AOS
struct CharData 
{
    float X;
    float Y;
    float Z;
    uint32_t Allegiance;
} c[MAXCHARS];

SIMD门新的更新可以一次完成所有的门,在内部循环中测试4个门和1个参与者,数据布局带来的巨大好处,所有的计算都会自然而然地以SIMD操作的形式出现。

// 外循环
for (int d = 0; d < door_count; d += 4) 
{
    // 加载4扇门的属性,清除4个“打开”累积器
    __m128 dx = _mm_load_ps(&s_Doors.X[d]);
    __m128 dy = _mm_load_ps(&s_Doors.Y[d]);
    __m128 dz = _mm_load_ps(&s_Doors.Z[d]);
    __m128 dr = _mm_load_ps(&s_Doors.RadiusSq[d]);
    __m128i da = _mm_load_si128((__m128i*) &s_Doors.Allegiance[d]);
    __m128i state = _mm_setzero_si128();
        
    // 内循环
    for (int cc = 0; cc < char_count; ++cc) 
    {
        // 加载1个角色的属性,广播到所有4个线程(lane)
        __m128 char_x = _mm_broadcast_ss(&c[cc].x);
        __m128 char_y = _mm_broadcast_ss(&c[cc].y);
        __m128 char_z = _mm_broadcast_ss(&c[cc].z);
        __m128i char_a = _mm_set1_epi32(c[cc].allegiance);
        
        // 计算角色和四扇门之间的*方距离
        __m128 ddy = _mm_sub_ps(dy, char_y);
        __m128 ddz = _mm_sub_ps(dz, char_z);
        __m128 dtx = _mm_mul_ps(ddx, ddx);
        __m128 dty = _mm_mul_ps(ddy, ddy);
        __m128 dtz = _mm_mul_ps(ddz, ddz);
        __m128 dst = _mm_add_ps(_mm_add_ps(dtx, dty), dtz);
            
        // 对比开门半径和忠诚=>或进入状态
        __m128 rmask = _mm_cmple_ps(dst, dr);
        __m128i amask = _mm_cmpeq_epi32(da, char_a);
        __m128i mask = _mm_and_si128(_mm_castps_si128(amask), rmask);
        
        state = _mm_or_si128(mask, state);
    }
    
    // 为这4扇门存储“应该开门”,为下一组4扇门做好准备。
    _mm_store_si128((__m128i*) &s_Doors.ShouldBeOpen[d], state);
}

内循环的汇编代码生成如下:

vbroadcastss xmm6, dword ptr [rcx-8]
vbroadcastss xmm7, dword ptr [rcx-4]
vbroadcastss xmm1, dword ptr [rcx]
vbroadcastss xmm2, dword ptr [rcx+4]
vsubps xmm6, xmm8, xmm6
vsubps xmm7, xmm9, xmm7
vsubps xmm1, xmm3, xmm1
vmulps xmm6, xmm6, xmm6
vmulps xmm7, xmm7, xmm7
vmulps xmm1, xmm1, xmm1
vaddps xmm6, xmm6, xmm7
vaddps xmm1, xmm6, xmm1
vcmpps xmm1, xmm1, xmm4, 2
vpcmpeqd xmm2, xmm5, xmm2
vpand xmm1, xmm2, xmm1
vpor xmm0, xmm1, xmm0
add rcx, 10h
dec edi
jnz .loop

结果获得了20-100倍加速比,更可能的是,现在可以对数据进行推理,蛮力SIMD代表“合理的事物”,游戏中有很多“合理的”问题!删除缓存未命中+SIMD ALU可能是一个巨大的胜利,解决“千刀之死”(death by a thousand cuts)问题,这种类型的转换通常会让它远离雷达。

文中还例举了过滤数据的案例。

最佳实践:分支。一般避免分支,预测失误的分支在大多数H/W上仍然非常昂贵,不想在内部循环中很难预测分支,如果非常可预测,可以进行分支,分支应正确预测99%以上才能有意义,例如,数据海洋中的一些昂贵的东西。如果在SSE2上,请_mm_movemask_X(),还可以考虑m_testz_si128()和SSE4.1+。

分支的替代方案:GPU风格的“计算两个分支”+选择,用于许多较小的问题,每个问题单独输入数据+内核,尽可能产生最佳性能,考虑对索引集进行分区,运行fast内核将索引数据划分为多个集合,在每个子集上运行优化的内核,除非访问了大多数索引,否则预取可能很有用。

最佳实践:预取。对上一代硬件来说绝对必要,盲目地将其推广到x86不是一个好主意,准则:不要预取线性数组访问,在某些硬件上可能存在严重的TLB未命中成本机会,芯片已经在缓存级别免费预取。指南:可能预取即将发布的PTR/索引,如果知道他们之间的距离足够远/不规则,AMD/Intel之间的预取指令有所不同,仔细测试是否从所有硬件中受益。

最佳实践:展开。在VMX128/SPU样式代码中常见,为了让机器隐藏延迟,很有意义,也有很多寄存器!对于SSE/AVX来说,通常不是个好主意,只有16个(命名)寄存器——硬件内部有更多寄存器,无序的执行在一定程度上为你展开。指南:仅展开至整个寄存器宽度,例如展开2x 64位循环以获得128位循环,但不能再展开,可以根据需要对非常小的循环进行例外。

最佳实践:流式读写。一定要使用流式读取(>SSE 4.1)和写入,有助于避免缓存垃圾,特别是对于使用大型查找表的内核,但别忘了围栏!!针对不同体系结构的不同选项_mm_fence()总是有效,但速度很慢,流绕过了强大的x86内存模型,如果不加以限制,微妙的数据竞争就会发生。

结论:SIMD不是魔法,所有人都可以成为性能的英雄!小投资可以带来巨大的收益,现代SSE好处多多!

Strategies for efficient authoring of content in Shadow of Mordor分享了游戏Middle-earth: Shadow of Mordor中的内容创作策略和效率,包含同步、加载、性能、资产处理、内容依赖等内容。

随着游戏日益复杂,游戏的资源尺寸、数量、数据记录等都呈数十倍的增长曲线,其中纹理占据约55%,音频占据35%,其它约10%。而纹理和音频中,占用尺寸从高到低依次是动画、关卡、模型、数据记录、行为、特效、着色器。(下图)

文中采用了智能加载的策略,检查源文件格式,强调最小占用、最大加载速度,如数据进入内存(磁盘)的速度有多快,数据的解释速度有多快(CPU)等。LTA(LTA–Lith Tech ASCII)源文件格式,类似xml的文本,人类可读、文件大、解读缓慢,使用编码、压缩的ASCII码,磁盘上更小,更快地进入内存,解读速度较慢。压缩二进制表示法,磁盘上更小,速度快得多,无需解析文本,独立压缩的文件树根(Zlib),并行或部分加载/解压缩,CRC检查暴露文件损坏,提供转换为人类可读格式的实用程序。对于压缩格式,占用小10倍,加载快10倍!

按需加载:在需要之前不加载大多数数据,需要用户操作来提示额外加载,非常适合独立的工作流程,树控制对此很有效。需要适当的源文件粒度,很难处理单个文件。延迟加载时间(快15倍):

在后台加载,仅提前加载部分数据,让用户在加载初始块后开始编辑,需要数据,但不是立即需要,如有必要,请用户屏蔽,例如视觉辅助、Visual Studio智能提示。后台加载可以提升5倍的速度。

千刀之死:需要加载大量小文件,硬盘在这方面的性能很差,异步IO将有所帮助,建立一个“检查点”,单个文件,压缩后占用最小,在其过期的情况下进行修补,90k文件/800m到30m的检查点。

磁盘SSD可以提速序列化,但容量更小。CPU线程池实现并行化,不适用于任何地方,复杂有开销。

线程池可以减少CPU等待时间,可以提高线性速度,如果合适容易丢弃,缺点是不会抵消糟糕的算法选择,核心竞争,更复杂,只和最慢的作业一样快。

文中提及的资源构建管线如下:

还采用了数据继承:

Piko: A Framework for Authoring Programmable Graphics Pipelines分享了一种可编程的GPU渲染管线。文中提到当前市面上高效的图形管线有:

渲染器 *台 算法
Unreal Engine 4 GPU 延迟着色的光栅化
Unity 5 GPU 前向和延迟着色的光栅化
Disney Hyperion 多核CPU 延迟着色的路径追踪
Pixar RenderMan 多核CPU 光线追踪的Reyes
Solid Angle Arnold 多核CPU 路径追踪
Media Molecule Dreams GPU 基于点的延迟着色渲染

但问题是高效的图形管道实现很难编写,设计空间也很难探索。

GPU上的软件光线有:

引入灵活的图形管线,在类型中抽象各个阶段,通过队列抽象通信。

高性能的基础是并行、执行局部性、数据局部性、生产者消费者局部性。而Piko框架真是解决以上问题的桥梁:

Piko的运行流程如下:

管线中的每个阶段都有三个步骤:


Piko管线易于表达和定制:

利用空间分块,可以提升并行度和局部性,从而提升图形管线效率:


Rendering the Alternate History of The Order: 1886分享了游戏The Order: 1886的渲染迭代历程。

该游戏引擎使用了深度预通道、顶点法线和速度,按分块列表计算,使用深度缓冲区剔除,透明材料的单独列表,异步计算->基本免费,更低开销的MSAA。生成每材质像素着色器,完全优化的材质+照明管道,更难手动优化所有案例。存在一些GPR问题,照明所用参数的函数。

造成画面闪烁的原因有高频信号、欠采样(时间或空间)、移动采样、不良的过滤方法(需要考虑频率响应,可以使用后处理抗锯齿)。

重建过滤器是重采样(上采样、下采样、过采样)的重要部分,生成输出(影响渐变、影响感知的细节、影响锯齿)。有时无法选择重建过滤器,因为它是整个系统固有的东西,其中一个例子是显示器。显示器会采集离散采样信号,并将其转换为连续信号。通过查看屏幕上的实际物理模式,可以了解正在使用什么样的过滤器。在LCD显示器中,矩形像素图案的普遍使用是我们有时认为像素是“小正方形”的原因之一。

以下是常见的几种重建过滤器,分别是盒子、三角形和Sinc:



锯齿的来源:光栅化(几何锯齿)、镜面反射(镜面锯齿)、阴影、纹理、SSAO、后处理特效和采样!

几何锯齿的来源:几何采样不足(通常频率非常高,无法预过滤三角形),光栅化器是一个阶跃函数,二进制-开/关,丑陋的楼阶梯图案,相机移动时的时间瑕疵,改变覆盖范围=闪烁!可以用MSAA进行过采样。

镜面锯齿的来源:低粗糙度=非常高的频率,移动采样点将闪烁,采样不足的几何体+法线贴图变得更糟,可以使用LEAN/CLEAN/Toksvig进行预过滤,很难解释所有法线方差的来源,但你绝对应该这么做!

The Order使用的抗锯齿是EQAA(2倍片元、4倍覆盖率、质量与性能/内存的*衡)、自定义解析(高阶过滤、用一些细节换取稳定)、TAA(进一步减少闪烁,与MSAA解决方案整合)。

使用了MSAA的中间通道有:延期贴花,累积到非MSAA RT;低分辨率透明和AO,放大每深度子样本,合成一次;Alpha测试,按深度子样本测试,或使用A2C;景深,使用所有子样本中最小的CoC。

带MSAA的DOF。

MSAA自定义解析:使用计算着色器而不是硬件解析,样本颜色片元和覆盖率样本,2像素宽的立方滤波器,覆盖相邻像素的子集,没有负波瓣–HDR的振铃(ringing)太多。更多请参阅:MSAA Resolve Filters

The Order想要选择更宽、更*滑的重建滤波器,对比了点、盒子和高斯重建滤波器之后,最终选用了后者,因为它更宽更*滑,在时域中转换为更*滑的过渡,从而提供更好的稳定性。

HDR的MSAA:非线性色调映射,在极端情况下“杀死”AA,夹紧(clamp)特别糟糕!需要在postFX之后进行色调映射,后处理需要HDR,昂贵的解决方案:MSAA分辨率的后处理,在色调映射后立即解析。廉价的解决方案:色调映射子样本,解析,然后逆转色调映射,复杂的运算符不容易逆转。更便宜的解决方案:使用简单运算符的*似色调映射(Reinhard),基本上按1 / (1 + Luminance)来计算样本权重,好处是抑制小高光以减少闪烁。

左:HDR下的MSAA;右:逆转亮度过滤器。

还需要需要考虑曝光!不想过度渲染高光,以更好地匹配最后的色调映射步骤,仍然可以将曝光偏移1或2档,*衡过度压暗和抑制高光。

左:逆转亮度过滤器,中:逆转亮度过滤器(曝光+10),右:修复的逆转亮度过滤器(曝光+10)。

// 解析过滤样本代码
float3 sample = InputTexture.Load(uint2(samplePos), subSampleIdx).xyz;
float weight = Filter(sampleDist);  // Bicubic, Gaussian, etc.
float sampleLum = Luminance(sample);
sampleLum *= exposure * exp2(ExposureOffset); // ExposureOffset ~= -2.0
weight *= 1.0f / (1.0f + sampleLum);
sum += sample * weight;
totalWeight += weight;

TAA的主要目标是减少镜面闪烁,预过滤还不够,主要启发是TXAA、SMAA 1TX[Sousa13]、Dust 514[Malan12]、Killzone: Shadow Fall [Valient14]。积累多个样本,指数移动*均数,使用速度缓冲区重新投影前一帧,使用最小/最大邻域进行加权和夹紧,MSAA解析期间计算的最小/最大值,没有抖动(主要是引擎团队没有时间集成,不管怎样,摄像机总是在移动)。

后处理AA锐化:宽的解析导致“柔和的外观,与视觉风格一致,可以增加后处理AA锐化,不锐化的遮罩非常简单,但注意不要太极端!

从左到右的锐化值是0.0、0.5、1.0。

阴影:16个聚光灯阴影投射,带4个级联的1个*行光,最多支持2个*行光。保持简单:1个用于聚光灯的纹理数组,1个用于级联,前向通道中采样。缓存聚光灯阴影,基于距离的不同频率更新,如果没有移动,就不会重新生成阴影。

预计算阴影可见性:用于主要可见性的系统扩展,在关卡构建期间预计算,仅适用于非移动光源。对于聚光灯,从光源POV栅格化网格ID,使用模板标记投射者(计数>=2)。对于*行光,在光源空间中将关卡拆分为NxN的tile,确定潜在的阴影投射者,对于每个摄影机采样点:渲染摄影机可见网格,渲染潜在的阴影投射者,如果模板值>=2,则添加到最终阴影可见性列表中。

用于所有阴影的EVSM,好处是没有偏倚、更少条纹,硬件过滤(三线性、各向异性),预过滤非常适合正向渲染,有助于降低GPR压力,非常适合缓存。

采样了SDSM来实现CSM,分析深度缓冲区,基于可见曲面约束级联。计算视图空间的的最小/最大XYZ,带优化原子的单通道,理想情况下,逐联计算光源空间中的AABB。在回读时附带1帧延迟,可以使用GPU路径,减少瑕疵的“时间扭曲”技巧:计算每个像素的速度,预测下一帧位置,展开AABB以覆盖下一帧位置。不稳定,需要进一步探讨。

左:普通CSM,右:SDSM。

文中还探讨了一种改良的贴花渲染方法——混合贴花:延迟通道累加,使用深度进行投影,添加混合到fp16渲染目标中,主前向通道读取贴花缓冲区,修改材质属性。

Far Cry 4 and Assassin’s Creed Unity: Spicing Up PC Graphics with GameWorks由Nvidia呈现,讲述了Far Cry 4和刺客信条两款游戏利用GameWorks在PC上增强了不少图形特性,如HBAO+、PCSS、TXAA、角色渲染、光照等。

Far Cry 4的喜马拉雅山脉场景。

刺客信条的中世纪宏伟场景。

NVIDIA GameWorks包含多个组件,如ShadowWorks、PostWorks、Godrays、HairWorks等等。其中,ShadowWorks和PostWorks非常适合Far Cry 4和刺客信条,HairWorks发型和Godrays非常适合Far Cry 4。

NVIDIA ShadowWorks由不同的技术组成,以提供电影级阴影、HBAO+、高级软阴影等。基于地*线的环境遮挡+(Horizon-Based Ambient Occlusion+,HBAO+)是当时最先进的SSAO方法,有最佳性能,可伸缩。调整HBAO+:半径用HBAO内核的大小,偏移隐藏低细分瑕疵,指数遮挡衰减,细节遮挡是高频遮挡组件的权重,粗糙遮挡是低频遮挡分量的权重。

高级软阴影:最先进的软阴影,基于更*的软阴影百分比(PCS),支持级联阴影图,简单但功能强大的界面。调整高级软阴影的参数:光源尺寸、最大阈值、最低百分比、混合百分比、边界百分比。修复漏光:当灯光尺寸过大时,会发生灯光泄漏,PCSS内核太宽,在级联之外采样,调整边界百分比以限制内核,如果仍存在漏光,需减小灯光大小和最大阈值。

时间抗锯齿(TXAA)专为减少时间锯齿而设计的胶片式抗锯齿技术,NVIDIA PostWorks家族成员。

NVIDIA HairWorks使用户能够模拟和渲染毛发,以提供真正的交互式游戏体验,运行时库和内容创建工具的组合。

HairWorks集成流程:

HairWorks允许自定义着色模型,支持正向着色和延迟着色。在Far Cry 4中,依靠Dunia渲染机制来执行着色,使用定制材质,HairWorks参数存储在GBuffer中。

HairWorks的GBuffer数据:压缩的漫反射、法线、镜面指数及缩放、切线、最终成像。

抗锯齿:最好的解决方案是在一个单独的、启用抗锯齿的过程中渲染HairWorks毛发。在Far Cry 4中,毛发在主管线中渲染,并依赖于全局抗锯齿,以对抗闪烁。

左:无AA,中:4xMSAA,右:4xTXAA。

NVIDIA Godrays可以渲染出逼真的太阳光束,巨大的调整空间,可扩展的性能,首次整合使用了游戏中的烟雾颜色,使用太阳色显示出最好的效果。

Godrays需要找到*衡点:场景看起来完全模糊了,事实上,只是增加了太多的密度,使光线强度依赖于白天

<br/ >

14.4.3.2 光影技术

2010年,A Real Time Radiosity Architecture for Video Games阐述了Frostbite引擎实现的实时辐射光照的架构,包含介绍Enlighten的概述、架构及如何集成到Frostbite中。

文中提到Enlighten有4个特点:分离的光照管线、带回馈的单次反馈、光照图输出和来自目标几何体的重建光照。Enlighten的管线如下图,预计算阶段包含分解场景到系统、投影细节几何到目标几何以重建光照、提取目标几何以实时计算辐射,运行时阶段包含GPU渲染直接光、CPU异步生成辐射、在GPU组合直接和非直接光。

运行时的管线如下图,

上图涉及的各个节点和最终组合效果如下系列图:





光照图输出如下所示:

Frostbite集成Enlighten的因素包含工作流和工作时间、动态环境、灵活的架构。Frostbite的预计算流程如下:

  • 收集静态和动态物体。静态物体接受和反弹光照,而动态物体只接受光照。

  • 生成辐射系统。并行处理和更新,输入依赖关系控制光传输,用于辐射粒度。

  • 参数化静态几何。静态网格使用目标几何,利用目标几何图形来计算辐射,投影细节网格到目标网格以获得uv,系统打包成单独的uv图集。

  • 生成运行时数据。每个系统一个数据集(流友好),使用Incredibuild的XGI进行分布式预计算,数据只依赖于几何形状(不是光或反照率)。

渲染时,分离直接光照和辐射度光照,CPU计算辐射度,GPU计算直接光。Frostbite使用延迟渲染,所有光源都可以动态反弹辐射度。分离光照图和光照探针渲染,光照图在前向Pass中渲染,光照探针被添加到3D纹理中,并在延迟渲染中执行。运行时管线分为三步:

  • 辐射度Pass(CPU)。更新非直接光照图和光照探针,将光照探针注入到3D纹理中。
  • 几何Pass(GPU)。增加非直接光照图到单独的GBuffer中,使用模板缓冲遮蔽掉动态物体。
  • 光照Pass(GPU)。渲染延迟光源,从GBuffer增加光照图,从3D纹理中增加光照探针。

以上几个阶段的效果图如下:





无独有偶,Pre-computing Lighting in Games也探讨了游戏引擎中的预计算光照。文中提到使用烘焙光的原因有3个:

  • 光照工作流。烘焙照明是一种让艺术家访问全局照明 (GI) 的方式,根据实际光源定义照明,没有人工补光灯,将光照与几何体/材质分离。烘焙照明还允许更丰富的光源集,基于物理的软阴影,阴影投射HDR光探头。

  • 质量。允许最高质量的光模拟算法,GI效果,多次反弹,允许高质量的直接照明。

  • 性能。运行时性能非常好,独立于灯光设置,独立于GI算法,好看的光照贴图与不好看的光照贴图具有相同的性能。

    • 性能可预测。运行时性能往往非常强大,艺术家可以根据需要添加任意数量的灯光,实时阴影贴图性能和GI难以预测,光照角度和位置影响阴影渲染的性能,玩家位置会影响灯光需要的分辨率。
    • 性能可伸缩。可在Quake 1、手持设备、高端游戏中使用。

烘焙光面临的挑战有:

  • 更改灯光设置。可以烘焙一天中的不同时间,如果引入更多可变灯,则组合会爆炸。可将移动和强度变化的灯光视为普通运行时灯光,适合组合爆炸或闪烁的灯光,没有间接照明。

  • 移动/变形几何。区分局部和全局更改,局部的包含在房间里移动的角色、小家具、弹孔,全局的包含被毁的建筑物、被毁的墙壁。

    局部几何体改变时存在两个问题:物体如何受环境影响?物体如何影响其环境?

    朴素的方法只是为移动的物体添加直接照明,但会使使角色看起来格格不入,同样在没有直射光的区域,角色是完全黑色的。Light probes优雅地解决了这些问题,并为照亮角色和其它移动物体提供了一个很好的管线(下图)。一些游戏将关键灯置于光探头之外,并将它们添加为更传统的直射灯。

    对于移动物体上的入射光,在房间里烘焙光照探头,使用最*的来照亮物体,将入射照明*似为整个物体的一个光照探头,与环境相比,适用于较小的物体,非常大的物体可能需要特殊处理。编码可采用球面谐波(通常为3阶),每个面使用1个像素的立方体贴图,只有单一的环境色。

    移动物体也可以影响环境,光照探头中的直接照明照明可选,允许对动态对象进行自阴影,允许对象在环境上投射阴影,也可以从光探头中提取最强的光方向,也可能可以提供间接照明的自我阴影,有些方法只为环境上的角色阴影很重要的灯光烘焙间接光,角色的间接照明通常微不足道。

    对于全局几何体改变,高动态游戏倾向于避免全局烘焙光照,其它子系统也倾向于依赖静态几何或在静态几何上表现更好(路径寻找,碰撞检测,游戏故事通常需要玩家遵循某些路径)。

  • 内存占用。照明是全局性的,包含材质纹理(实例共享材质纹理,多个对象可以共享纹理,纹理可以*铺和镜像)、照明纹理(每个实例必须是唯一的,不能*铺、镜像等,可根据分辨率要求优化分辨率)。

    法线贴图非常适合增加几何细节级别,法线贴图在光照中引入高频细节,高频照明需要高纹理分辨率。

    对于定向光照图,细节在几何体中,而不是在入射光中,将每个纹素的入射光半球存储在光照贴图中,允许*似不同法线方向的照明。定向光照图的典型编码有辐射度法线贴图 (RNM)、SH(一般为 2 个波段,4 个分量)、每像素环境光和定向光、SH基,允许使用真正的BRDF,半球会模糊的,但也可以从中获得合理的镜面反射效果。



    上:烘焙光;中:法线贴图;下:低分辨率定向光照贴图与法线贴图相结合。

  • 灯光重建时间。可以采用混合的方案:

    • 仅烘焙间接照明。间接光通常比直接照明更*滑,锐利的阴影需要更高的纹理分辨率。

    • 太阳的特殊处理。阳光往往是对户外场景影响最大的光线,阳光直射通常是锐利阴影和动态范围差异的来源,仅从太阳烘焙间接光,直接光作为运行时光添加。

该文提出的烘焙光管线如下:

管线的影响有:

  • 光源构建阶段可能很耗时。在CPU小时为单位的数量级,取决于算法、分辨率、关卡大小、灯光设置、反弹次数等。
  • 加快速度的工具,选择性灯光构建,预览质量构建,预览工具,相机渲染工具,渐进式光照贴图生成,分布。
  • 自动重建以确保照明始终是最新的。
  • 用于管理GI特定光源属性的工具。直接和间接照明的比例因子,以放大和分离光的贡献。
  • 用于管理GI特定材质属性的工具。在屏幕上产生良好发光效果和正确外观的东西并不一定会在环境中产生所需的光发射,增加或减少场景的整体反射率。
  • 纹理烘焙形状需要唯一的UV。可以在一定程度上实现自动化,易于展开的内容更可取,如果可能,将细节保留在法线贴图层中。
  • 顶点烘焙很常见。由于纹理分辨率不足而没有接缝,法线贴图和定向光照贴图有助于在低分辨率光照下提供细节,不适合多边形内的阴影和其他照明不连续性。

Real-time Diffuse Global Illumination in CryENGINE 3提出了级联光照传播体积(Cascaded Light Propagation Volumes,CLPV)的技术。CLPV的核心思想在于:

1、采样照明表面,将它们视为辅助光源。为GI采样场景时,使用面元(又名point、disk, Surfel == 表面元素),所有光照面元都可以在光源空间中展*为2D映射图,使用反射阴影贴图(RSM)进行照明,RSM是在GPU上对光照面元进行采样的最快方法,甚至过度快O_O!

2、将样本分簇成一个统一的粗糙3D网格(grid),累加并*均每个单元格(cell)的辐射亮度(Radiance)。分簇面元时,以虚拟点光源(VPL)表示的光照面元,将每个面元分布到最*的单元格中(类似于PBGI, light-cuts and radiosity clustering),将所有VPL转换为输出辐射分布,以较低频带的球面谐波表示,在拥有者的单元格的中心进行累加,使用光栅化完全在GPU上完成。

3、迭代地将辐射亮度传播到相邻单元格(仅适用于漫反射)。RSM是一组从灯光位置规则地采样的场景VPL,通过规则网格和SH离散地初始VPL分布,将光照从一个单元格迭代传播到另一个单元格。(下图)

跨3D网格的局部单元格到单元格的传播,类似于参与媒体照明的SH离散坐标法[GRWS04],6个轴向方向,轮廓面作为传播波前(wave front),将得到的SH系数累加到目标单元格中以进行下一次迭代。

4、用生成的网格点亮场景。使用LPV进行最终场景渲染,使用硬件三线性插值在特定位置查找生成的网格3D纹理,将辐照度与被照表面法线的余弦波瓣进行卷积,应用抑制因子(dampening factor)以避免自溢出(self-bleeding),计算朝向法线的方向导数,基于与强度分布方向的梯度偏差进行抑制。

注入后迭代8次的效果:

为了稳定光照结果,可以采用以下方法:

  • 空间稳定。将RSM捕捉一个像素以进行保守光栅化,通过一个网格单元捕捉LPV以实现稳定注入。
  • 自发光(Self-illumination)。在RSM注入期间偏移半单元格VPL到法线方向。
  • 时间连贯性和重投影。对RSM注入执行重投影的时间SSAA。

此方法的局限性:仅漫反射相互反射,稀疏空间和低频角度*似(光扩散:光传输溅射在各个方向,空间离散化:对于遮挡和非常粗糙的网格可见),次级AO信息不完整。

可以采用多分辨率方法,以不同的分辨率渲染多个嵌套的RSM,受级联阴影贴图技术的启发,在GPU上模拟不均匀的多分辨率渲染,根据对象的大小将对象分配到不同的RSM。将RSM注入相应的LPV,创建绑定RSM视锥体的嵌套LPV网格,独立进行传播和渲染,从内部LPV传播到外部。

LPV还可以扩展到:

  • 透明物体。
  • 用于大规模光照*似的光照缓存,将分析辐射注入被光线覆盖的网格单元。
  • 具有附加遮挡网格的二级遮挡,使用相同的技巧可以多次反弹。
  • LPV中部分匹配的光泽反射。
  • 参与介质照明,来自传播过程的本质。

CLPV效果这么好的原因有:

  • 人类对间接照明的感知。对接触照明非常敏感(角落、边缘等),间接照明主要是低频,即使是间接阴影,*滑渐变而不是阴影中的*坦环境,*似为参与媒体的扩散过程。
  • 级联:基于重要性分簇。发射器根据其大小分布在级联中。

离线的PBRT和实时的LPV的对比如下图,两者差异不太明显:

光照图、PRT、LPV在图像质量、内存、动态光照支持、动态物体支持、次级遮蔽、多反射、区域光等参数的对比如下表:

LPV还可以和其它技术相结合:

  • 与SSAO相乘以添加微遮挡细节。
  • 延迟环境探针。结合后可增强远距离GI。
  • 间接光源和延迟光源。在某些地方使用间接光灯模拟GI,对GI风格化的艺术家很重要。

总之,LVP是全动态方法,改变场景/视图/照明,GPU和控制台友好,极快(在PlayStation 3上大约需要1毫秒/帧),符合生产要求(用于实时调整的丰富工具集),高度可扩展,与质量成正比,稳定、无闪烁,支持复杂的几何形状(例如树叶)。

Physically-Based Shading Models in Film and Game Production是Naty Hoffman等人在Siggraph上分享的PBR实时化的演讲,演讲中详细地阐述了PBR的物理理论基础和数学化建模,以及如何在GPU中实现出来。

不同物质对光的吸收和散射的表现。

基于微观几何建模的光照模型。

漫反射和次表面散射的转变关系。

经典的Cook-Torrance BRDF公式。

Crafting Physically Motivated Shading Models for Game Development也是Naty Hoffman的演讲,涉及了PBR在游戏引擎的实现、改进和优化等内容。文中说到使用PBR的原因有:更容易实现照片写实/超写实,在照明和观察变化下保持一致,更少的调整和“捏造因素”,为艺术家提供更简单的材质界面,更容易排除故障,更容易扩展。

PBR需要一些前置基础,包含伽玛校正渲染、支持HDR值,良好的色调映射(最好是电影)。伽玛校正渲染的特点是着色输入(纹理、浅色、顶点颜色等)自然创作、预览和(通常)使用非线性(伽马)编码存储,最终帧缓冲区也使用非线性编码,这样做是有充分理由,感知一致等于有效使用比特,还有历史遗留原因(如工具、文件格式、硬件)。

如果着色默认为Gamma空间,着色结果不正确,产生“1+1=3”的效果:

高动态范围 (HDR) 可以产生逼真的渲染,但需要处理远高于显示白色 (1.0) 的值,着色前:光照强度、光照贴图、环境贴图,着色产生影响光晕、雾、景深、运动模糊等的高光,存在廉价的解决方案。

文中对比了Phong和Blinn-Phong的效果,发现在某些情形Blinn-Phong的效果更真实:


文中提到对镜面高光,除了菲涅耳项之外,还引入了归一化因子\((\alpha_p+2)/8\),归一化因子非常重要,若没有它,镜面反射亮度会从4倍太亮到数千倍太暗,具体取决于\(\alpha_p\)的值,误差如此之大,菲涅耳因子变得无关紧要。没有归一化使得创建看起来逼真的材质变得非常困难,尤其是当每个像素的\(\alpha_p\)变化时。下面分别是有无归一化的曲线和效果对比图:


笔者的另一篇文章已经详细深入地探讨过PBR:由浅入深学习PBR的原理和实现

OIT And Indirect Illumination Using Dx11 Linked ListsReal-Time Order Independent Transparency and Indirect Illumination Using Direct3D 11讲述了使用DirectX 11的特性来实现间接光的效果。文中提到了没有间接阴影的间接光方案:

1、绘制场景G-Buffer。

G-Buffer需要允许重建:世界/相机空间位置、世界/摄影机空间法线、颜色/反照率,DXGI_FORMAT_R32G32B32A32_FLOAT位置可能需要用于间接阴影的精确光线查询。

2、绘制反射阴影图(RSM)。RSM显示从光源接收直射光的场景部分。

RSM需要允许重建:世界/相机空间位置、世界/摄影机空间法线、颜色/反照率,仅绘制间接光源的发射器,间接阴影的光线精确查询可能需要DXGI_FORMAT_R32G32B32A32_FLOAT的位置。

3、以1/2的分辨率绘制间接光缓冲区。RSM纹素用作G-Buffer像素上的光源,用于间接照明。步骤如下:

  • 延迟渲染1/2分辨率的间接光(IL)。
  • 将G-Buffer像素转换为RSM空间。
    • G-Buffer像素的空间转换顺序:Screen Space -> Light Space -> 投影到RSM纹素空间。
  • 使用RSM纹素的内核作为光源。
    • RSM纹素也称为虚拟点光源 (VPL)。
    • 内核大小取决于所需速度、想要的效果外观、RSM分辨率。

在G-Buffer的一个像素上计算IL,然后累加内核中所有VPL的贡献:

下面的计算项与辐射度形状系数(form factor)计算中使用的项非常相似:

*滑IL的简单解决方案需要考虑四个中心位于t0、t1、t2 和t3的VPL内核:

大的VPL内核的计算速度很慢:

可以采用下图的技巧:

4、上采样间接光 (IL)。

间接光缓冲为1/2的分辨率,执行双边上采样步骤,结果是全分辨率的IL。

5、绘制添加IL的最终图像。

组合直接照明、间接照明和阴影。

左:没有间接光;右:组合了间接光。

添加间接阴影的步骤:

  • 使用CS和链表技术。

    • 将IL的遮挡几何图形(使用遮挡者的三角形)插入到3D列表网格中,查看备用数据结构的备份。

  • 再读取一个VPL的内核。

  • 只累加被遮挡者三角形遮挡的VPL的光。

    • 通过3d网格追踪光线以检测被遮挡的VPL。
    • 仅渲染低分辨率缓冲区。
  • 从IL缓冲区中减去被遮挡的间接光。

    • 使用了低分辨率遮挡的IL的模糊版本,模糊是双边模糊/上采样的组合。

上排:3D网格、非直接光缓冲区、被遮挡的非直接光;下排:非直接光缓冲区、减去被遮挡的非直接光、最终成像。

[Uncharted 2: Character Lighting and Shading](http://advances.realtimerendering.com/s2010/Hable-Uncharted2(SIGGRAPH 2010 Advanced RealTime Rendering Course).pdf)阐述了神秘海域2使用的角色渲染技术,包含皮肤、头发、布料等材质的渲染。

神秘海域2中不同角色的渲染效果。

其中皮肤采用了次表面散射模型。其中下图是NV使用了纹理空间的模糊来*似次表面散射效果:

然后通过RGB分量各不相同的卷积核来累加获得最终的次表面散射效果:

// 直接光:像素自身的权重最大,并且B > G > R.
diffColor = direct*float3(.233,.455,.649);

// 散射光:lm1~lm5就是上图模糊后的纹理.
diffColor += lm1 * float3(.100,.336,.344);
diffColor += lm2 * float3(.118,.198,.0);
diffColor += lm3 * float3(.113,.007,.007);
diffColor += lm4 * float3(.358,.004,.0);
diffColor += lm5 * float3(.078,0,0);

从左到右:仅直接光、仅散射光、组合了两者。

由于NV在模糊的过程使用了太多通道,性能和销毁无法满足要求,为此,神秘海域2采用了12-Tap的*似方法:

12-Tap抖动的权重如下:

float3 blurJitteredWeights[13] = 
{
    // 像素自身的权重.
    { 0.220441, 0.437000, 0.635000 }, 
    // 12-Tap的权重.
    { 0.076356, 0.064487, 0.039097 }, 
    { 0.116515, 0.103222, 0.064912 }, 
    { 0.064844, 0.086388, 0.062272 }, 
    { 0.131798, 0.151695, 0.103676 }, 
    { 0.025690, 0.042728, 0.033003 }, 
    { 0.048593, 0.064740, 0.046131 }, 
    { 0.048092, 0.003042, 0.000400 }, 
    { 0.048845, 0.005406, 0.001222 }, 
    { 0.051322, 0.006034, 0.001420 }, 
    { 0.061428, 0.009152, 0.002511 }, 
    { 0.030936, 0.002868, 0.000652 }, 
    { 0.073580, 0.023239, 0.009703 }, 
}

12-Tap有两种实现方法:

  • 分离模糊(Separate Blur)。
    • 渲染到光照贴图。
    • 12-Tap模糊光照贴图。
    • 渲染最终场景。

  • 组合模糊。
    • 渲染到光照贴图
    • 渲染最终场景,从光照贴图中使用12-Tap采样。

以上两种方法都有不错的渲染效果:

左:分离模糊;右:组合模糊。

Uncharted 2还尝试了弯曲法线(Bent Normal),以伪装R/G/B各分量来自不同的法线,R更接*几何,G/B更接*法线贴图(下图),漫反射计算3次。

但是,对R/G/B使用不同的法线似乎会导致一些蓝色斑点:

出现蓝色斑点的原因是点积在极端角度,可能会遇到diffuseR=0diffuseB=1的情况,或相反亦然。

另一种方法是混合法线(Blended Normal),对几何和法线映射法线进行漫反射计算,并在它们之间从几何法线中获取更多红色,从法线映射法线中获取更多绿色/蓝色。具体做法是:Diffuse(L, G)、Diffuse(L, N) 然后Lerp,蓝色/绿色保持不变、红色溢出,可以有红色但没有蓝色/绿色,不能有蓝色/绿色但没有红色。这种方法可以显著降低蓝色斑点:

在头发渲染上,Uncharted 2使用了Kajiya-Kay的光照模型,实现的细节和特点如下:

  • 轻微环绕漫反射(Slight Wraparound Diffuse)。
  • Kajiya-Kay镜面反射。
  • 没有自阴影。看起来像具有额外偏差的最大级联。
  • 漫反射贴图作为高光遮罩。部分去饱和。
  • 使用Blinn-Phong的延迟光照。

在布料上,Uncharted 2使用了边缘光 + 内部光 + 漫反射的组合:

布料光照。从左到右:边缘光、内部光、漫反射、组合光。

布料光照的伪代码如下:

VdotN = saturate( dot( V, N ) );
Rim = RimScale * pow( VdotN, RimExp );
Inner = InnerScale * pow( 1-VdotN, InnerExp );
Lambert = LambertScale;

ClothMultiplier = Rim + Inner + Lambert;
FinalDiffuseLight *= ClothMultiplier;

但以上布料的实现方式忽略了光线方向,因此布料光照不能随光源方向的改变而改变。

CryENGINE 3: reaching the speed of light主要是CryENGINE 3在控制台上纹理压缩和延迟照明的改进。

纹理压缩改进:

  • 颜色纹理。创作精度、最佳色彩空间、DXT块压缩的改进。

    建议根据直方图选择正确的颜色空间,按照经验是如果75%以上的像素高于中值(线性空间是116/255=0.45),则使用线性空间。

  • 法线贴图纹理。法线精度、3Dc法线贴图压缩的改进。

    以前,艺术家将法线贴图存储到8bpc纹理中,导致法线从一开始就被量化了!将工作流更改为始终导出16bpc法线贴图!修改工具以默认导出,对艺术家透明。

    上:8bpc法线纹理;下:16bpc法线纹理。显然后者的高光更细腻*滑。

    可以改进用于法线贴图的3Dc编码,3Dc比ARGB8好很多,在多数GPU上以16位精度生成插值!

    常规的3Dc编码器:将x和y独立压缩为两个alpha通道——不将x-y视为法线!

    建议改进3Dc编码器:将两个alpha块视为一个整体x-y法线,计算正常而不是“色差”的误差:

    \[\triangle N = \arccos\bigg(\cfrac{(N_c \cdot N)}{||N_c|| \ ||N||} \bigg) \]

    为了加速压缩,可以采用自适应方法:压缩为2个alpha块,测量法线的误差。如果误差高于阈值,则运行高质量编码器。

    a:原始纹理;b:常规编码;c:建议编码;d:误差。

CryEngine 3的遮挡剔除使用软件z缓冲区(又名覆盖缓冲区),步骤如下:

  • 在控制台上缩小前一帧的z缓冲区。使用保守遮挡避免错误剔除。
  • 创建mip并使用分层遮挡剔除,类似于Zcull和Hi-Z技术,使用AABB和OOBB测试遮挡。
  • 在PC上:手动放置遮挡物并在CPU上光栅化,CPU和GPU之间的延迟使z缓冲区无法用于剔除。

SSAO的改进包含将深度编码为2通道16位值 [0;1],作为有理数的线性深度:depth=x+y/255。以半屏分辨率计算SSAO,将SSAO渲染到同一个RT(另一个通道),双边模糊同时获取SSAO和深度。具有4个样本的体积遮挡,简单重投影的时间累积,整体性能是在X360上1ms,PS3上1.2ms。

对于颜色分级(color grading),将所有全局颜色转换烘焙到3D LUT,事实证明16x16x16 LUT已足够,尽量使用硬件3D纹理,颜色校正通道是一种查找:newColor = tex3D(LUT, oldColor)

CryEngine 3使用Adobe Photoshop作为色彩校正工具,从Photoshop读取转换后的颜色LUT:

文中谈到了延迟渲染管线的问题包含

  • 不支持抗锯齿,MSAA对于延迟管线来说过于繁重,后处理抗锯齿不会完全消除锯齿,大多数情况下需要超采样。
  • 有限的材料变化,无各向异性材料。
  • 不支持透明对象。

延迟渲染的GBuffer的每像素数据越小越好,CryEngine 3最小化GBuffer到64 bits / pixel,其中RT0存储Depth 24bpp和Stencil 8bpp,RT1存储Normals 24bpp和Glossiness 8bpp。用于标记照明组中的对象的模板:门户/室内、自定义环境反射、不同的环境和间接照明。光泽度不可延迟,照明累积通道需要,否则镜面反射是非累积的。这种G-Buffer布局的问题:仅Phong BRDF(正常 + 光泽度)、没各向异性材质、24bpp的法线过于量化、照明带状/低质量。

对于着色的法线精度,24bpp的法线过于量化,光照质量低。24bpp的精度本应该足够了,为什么会出现光照质量低?原因是存储了标准化的法线!立方体是256x256x256个单元格 = 16777216个值,在这个立方体中只使用单位球体上的单元格:16777216个中的约289880 个单元格,即约1.73%!!

我们有一个包含\(256^3\)个值的立方体!最佳拟合是找到一条光线误差最小的量化值,可以离线执行,使用[3D-DDA](光线追踪小记:空间加速结构Regular Grid与3DDDA)中的约束优化。将其烘焙到结果的立方体贴图中,立方体贴图应该足够大(显然 > 256x256)。

提取这个对称立方体贴图最有意义和唯一的部分,保存为2D纹理,在G-Buffer生成期间查找它,缩放法线,将调整后的法线输出到G-Buffer。

法线的最佳匹配支持Alpha混合,尽管最合适的会被破坏,但通常不是问题,重构只是一种归一化!可以应用于一些选择性*滑的物体,例如禁用带有细节凹凸的对象,不要忘记为结果纹理创建mip-maps!

几种法线存储技术的对比如下表:

法线存储技术 有效单元格 有效单元格占比
Normalized normals 约 289880 / 16777216 约 1.73 %
Divided by maximum component 约 390152 / 16777216 约 2.33 %
Proposed method (best fit) 约 16482364 / 16777216 约 98.2 %

标准化法线(上)和最佳匹配(下)法线的渲染对比图。

标准化法线(上)和最佳匹配(下)法线的对比图。

该文还谈到了一种用于光照的计算:裁剪体积(Clip volume)。没有阴影的延迟光照往往会溢出,但阴影开销很大。解决方案:使用艺术家定义的剪裁几何体——剪裁体积,除了光照体积遮罩之外,还由遮罩模板。非常低开销,提供四倍的模板标记速度。

裁剪体积示例。左上:裁剪体积几何体;右上:模板标记;左下:光照累积缓冲;右下:最终成像。

CryEngine 3为了高效地实现各项异性材质,还将BRDF复杂度与光照复杂度解耦,BRDF复杂性完全从光照通道中消除。(下图)

在抗锯齿上,CryEngine 3使用了混合抗锯齿的解决方案。

  • *处物体用后处理AA。不超采样,适用于边缘,使用MLAA。
  • 远处物体用TAA。进行时间超级采样,不区分表面空间阴影变化。
  • 用模板和无抖动相机将它们分开。

距离分离保证了远处物体的视图矢量的微小变化,减少反向时间重投影的基本问题:着色域中的视图相关变化。原因是重投影是基于深度缓冲区的,因此,不可能考虑物体的着色空间局部变化。如果将它应用到特写物体上,可能会导致重影、反射等。

在逐物体基础上分开,一致的物体空间着色行为,使用模板标记物体以进行时间抖动。


上:用于远处物体的TAA;下:用于*处物体的后处理AA。

Sample Distribution Shadow Maps是Intel提出的一种改进的阴影渲染方法。

Sample Distribution Shadow Maps(SDSM)是样本分布阴影图,通过分析阴影样本分布,找到紧凑Z的最小值/最大值,基于紧凑Z边界的对数分区,无需调整即可适应视图和几何形状。计算紧凑的光源空间边界,每个分区都有紧凑的轴对齐边界框,大大提高有用的阴影分辨率。


上:PSSM(*行阴影图)的分区、光源空间、光源空间分区;下:SDSM的分区、光源空间、光源空间分区。

分区变体有:

  • K均值(K-means)的分簇。在Z中有大量样本的地方放置分区,*均误差的好结果,但有玻璃钳口(glass jaw)。
  • 自适应对数。与基本对数类似,但要避免Z中的间隙,只适用于特殊情形,通常不值得尝试。

上面的方案需要深度直方图。

SDSM有两种不同的实现:

  • 对数的简单“减少”实现。可以在DX9/10硬件上的像素着色器中实现。
  • 一般深度直方图实现。共享内存原子使这成为可能,太慢且依赖于DX11之前的硬件。

SDSM产生更紧密的光源空间截锥体,渲染到阴影贴图中的几何体更少。

在GPU上生成的分区边界数据,CPU不能用于截锥体剔除!阻塞并读回分区边界数据(非常小),听起来糟糕,但就是当时所做的,而且速度相当快。未来在GPU上进行截锥剔除。

关于时间一致性的说明:

  • 改变分辨率会导致时间锯齿。
  • 量化光源空间中的分区边界仅适应于定向灯?根本无法移动或调整分区大小,存在一些相机变换的问题,过于限制和次优。
  • 将分区量化为2的幂大小?可以工作,但苛刻,且浪费了很多分辨率。
  • 以亚像素阴影分辨率为目标。需要足够的分区分辨率(约等于屏幕分辨率),使用良好的过滤和阴影贴图抗锯齿!

未来的改进方向:

  • 更好的分区方案?虽然尝试了很多方法,但可能有更好的算法在实践中运行良好。
  • 解决投影锯齿的混合算法。在误差高的地方使用更昂贵的算法。

Toy Story 3: The Video Game Rendering Techniques是介绍了迪斯尼的游戏玩具总动员3所使用的SSAO、环境光及阴影等渲染技术。

文中提到,SSAO面临的挑战及对应的解决方案如下:

  • 如何以及在哪里采样。

使用线性积分。

蓝点仍然是正在采样的像素,想象一个围绕它的概念体积球体,是在2D中采样而不是在3D中采样。每个样本都有相应的体积,并且根据样本的深度,该体积的一小部分将被遮挡。

使用线积分,每个样本都会产生一小部分遮挡与非遮挡,因此遮挡量会*滑地变化。

线性积分的算法过程:采样 (x,y) 坐标,计算沿 [0,1] 的距离,样本的对应行是将该数量乘以相应的体积以获得样本的遮挡贡献。

  • 如何伪造以获得更多样本。

使用2D随机旋转。先创建具有2D旋转的纹理(使用4x4 G16R16F纹理来编码每个角度的正弦和余弦)。

以4x4纹理编码的旋转:

具有4x4偏移的顺序旋转:

为了避免旋转的样本过于集中或规则,可以随机旋转:

随机旋转后的效果:

还可以对旋转添加抖动(下图左没有抖动,右添加了抖动):

有没旋转样本的对比(左无右有):

  • 如何处理大距离的深度差。

以往(如CryTek)的做法是对于大距离深度差的AO直接设为0,但问题是如下图所示的*面应该被1/2遮挡,因此将不可用的样本设置为零会使结果偏向过于未遮挡,从而导致光晕。

当*面与视图*面*行时,使用0.5效果很好,但会因倾斜表面而失效。在下图中,结果将是遮挡太少,如果遮挡物在另一侧,则会导致遮挡过多。

为了能够以估算丢失的样本,需要对采样模式施加约束,即每个样本都是一对的一部分(配对采样),解释了之前看到的奇怪的“猎户座”采样模式:

在环境光方面,辐照度光源使用了SH,每个轴沿 +/- 的一个定向光,单色环境光,仅用于环境照明。可实时调整,负光源(negative light),SH可以实时混合。其中负光源主要用于从负y方向指向上方的光,使光源的底部变暗,并给一切带来了轻微的阴影。

渲染Wii上的环境光采用SH,且每一帧在视图空间中生成一个可以用法线查找的球面贴图。环境存在局部区域的问题,某些地方漏光导致过亮,原因是每个世界只有一个环境配置(ambient rig),预期的效果是无论位置如何,一切都呈现在相同的氛围中。可能的解决方案是在两个环境配置之间混合,基于相机距离的混合,根据位置切换环境配置,烘焙环境照明,实时辐射度或全局照明,指定的环境光。

但没有采取以上方法,而是增加约束:只有两种类型的光源,即暗光源和亮光源。

该解决方案的工作原理是将体积渲染到延迟缓冲区中,并根据缓冲区的值在明暗环境绑定之间进行混合。主要优点是艺术家可以更好地控制环境照明。

延迟技术的步骤:

  • 使用体积。
  • 将体积渲染到单独的渲染目标中。
    • 输出颜色表示从体积中心到像素的距离。
  • 在主场景通道中混合亮的和暗的环境色。

上述的体积包含了多种几何类型和操作:立方体、球体、旋转和缩放。

下面是有无使用体积的对照图:


在阴影方面,文中还分享了没有光照贴图的动态阴影、用于主角的投射阴影(Drop Shadow)、柔和并保留阴影的形状、以牺牲整体距离为代价*距离获得更高质量的阴影。

在柔和并保留阴影的形状方面,传统的方案是以绝对最高分辨率渲染阴影贴图、添加过滤以减少瑕疵。但存在需要更柔和的阴影,ToyStory 3包含多达300万个顶点的场景,在300m的距离上拉伸4个级联非常昂贵,有限的LOD和遮挡剔除技术。ToyStory 3也考虑过虚拟阴影图(Virtual Shadow Map,VSM),但VSM也在当时也存在诸多限制,如模糊高分辨率阴影贴图过于昂贵,没有 2x 深度写入,艺术家不喜欢的视觉效果,漏光很难管理等,最终未被采纳。最终采用了组合解决方案:3个640x640阴影贴图、4x4的高斯PCF、5x5交叉双边滤波。(下图)

此外,ToyStory 3还采用了延迟阴影(Deferred shadow)的技术,R通道存储SSAO,G通道存储世界阴影,B通道存储角色阴影。

延迟阴影着色步骤如下:

  • 渲染全屏四边形。
  • 从视图空间深度缓冲区重新生成世界位置。
  • 包围盒级联选择。
  • 动态深度偏差计算。
  • 4x4 高斯PCF到最终阴影值。

文中还对阴影的条纹、深度偏差等瑕疵进行了优化和改善。

Real-Time Order Independent Transparency and Indirect Illumination Using Direct3D 11分享了基于DX11的OIT透明渲染和带有间接阴影的全局光照技术。间接阴影可以帮助感知场景中发生的细微动态变化,为深度感知添加有用的提示,场景像素上的间接光贡献更准确,当环境光线昏暗或动作发生在远离直射光的情况下,这对于视觉体验和游戏玩法尤其重要。

Dynamic lighting in GOW3讲述了游戏战神3(God of War III)所使用的动态光照技术,包含环境光、点光源、定向光等。

其中环境光被组合成一个RGB插值器,该文并不涉及。点光源和定向光表示为混合顶点光源(Hybrid vertex light)

混合顶点光源可支持1个与像素灯相同的光源,也支持多光源:计算每个顶点的距离衰减,每个顶点组合成一个聚合光,插值每个像素的聚合光位置,在片元程序中执行\(N\cdot L\)\(N\cdot H\)等,就好像有一个单像素光一样。

在插值任意三角形两个点的位置时,使用默认的插值会产生错误的结果,需要特别处理光源方向:

对于光源的衰减函数,希望它是光滑的、便宜的,希望一阶导数接*0,因为函数本身接*0。下图是相同的定向灯直射而下的衰减,右边是文中采纳的衰减,衰减函数设置为在相同距离处达到零:


为了更好地表示聚合的光源,从每个世界光位置减去世界顶点位置以创建相对向量,计算长度和重量(记住两个灯的光强度都是 1),将相对向量乘以权重以进入方向域,添加灯光方向,并累积权重,将聚合方向乘以累积权重以返回位置域,最终得到了聚合光的相对光向量,将顶点世界位置添加到它以获得聚合光的世界位置。

相关的计算公式、符号说明、图例如下:

解决计算聚合光位置的方法,选择了合适的衰减函数之后,需要解决背面光源,因为光源是在不考虑阴影的情况下聚合的,消除背向顶点的灯光的贡献很重要。采用以下公式:

接下来还需要解决不同方向的两个光源的过渡问题。假设下图是完全对称的,在片元程序中计算的N dot L将正好是1,比A或B处的N dot L值高得多,因此将在P处看到意想不到的亮紫色高光:


修复以上问题的过程是:在片段程序中,得到从顶点插值的光位置,然后计算光向量并在计算 N dot L 之前对其进行归一化,如果插值光向量短于阈值,则停止归一化,可以很好地解决上述问题。

接下来处理聚合光源颜色的问题。用于计算“物理上正确”值的数据丢失,需要解决一些在片段程序中插值时会给出合理结果的东西。计算聚合光位置,计算归一化光方向,计算点积:



相加多个向量,其结果的长度等于投影的总和:

最后拟合出的公式如下:

扩展到RGB:

GOW3实现时,在EDGE作业中将每个顶点的光照计算作为自定义代码运行,高度优化,仍然保持PPU版本运行以供参考和调试。

[Physically-based Lighting in Call Of Duty: Black Ops](http://advances.realtimerendering.com/s2011/Lazarov-Physically-Based-Lighting-in-Black-Ops (Siggraph 2011 Advances in Real-Time Rendering Course).pptx)陈述了在不断发展的使命召唤图形的背景下基于物理的照明和阴影以及经验教训。

COD的运行时光照策略是:所有主要照明都在着色器中计算,每个主要的运行时阴影贴图会覆盖相机周围半径中的烘焙阴影。因此,主要可以改变颜色和强度,移动和旋转小范围,仍然看起来正确,静态和动态阴影很好地融合在一起。

对于漫反射,主要漫反射使用经典的兰伯特项,由阴影和漫反射反照率调制。次级漫反射由具有逐像素法线的光照贴图/光照网格二次辐照度重建,由漫反射反照率调制。

对于镜面反射,主要镜面反射使用微*面BRDF,由阴影和“漫反射”余弦因子调制。次级镜面反射从具有逐像素法线和菲涅耳项的环境探针重建,也与二次辐照度相关,基于与主要高光相同的BRDF参数。

采用模块化方法,早期实验性使用Cook-Torrance,然后尝试了不同的选项以获得更逼真的外观和更好的性能,由于BRDF的每个部分都可以单独选择,因此尝试了各种“乐高积木”(即组合)。

其中,D(法线分布)采用了Beckmann方程:

F(菲涅尔)采用了以下方程:

G(几何遮蔽)采用了Schlick-Smith联合公式:

对于环境贴图,以前有几十个环境探针来匹配照明条件,由于内存限制,分辨率低,过渡问题、镜面反射流行、大型网格的连续性。对于Black Ops,希望解决这些问题,并拥有更高分辨率的环境贴图来匹配高镜面反射指数。解决方案:

  • 归一化(Normalize)——通过捕获点的*均漫射照明来划分环境贴图。

  • 去归一化(De-normalize)——将环境贴图乘以从光照贴图/光照网格中重建的每个像素的*均漫反射光照。

归一化允许环境贴图更好地适应不同的光照条件,户外区域只需一张环境图就可以逃脱,室内区域需要更多特定位置的环境贴图来捕捉次级镜面光照。使用AMD/ATI的CubeMapGen预过滤和生成Mipmap,HDR角度范围过滤,面边修正。

根据材质光泽度选择mip:

texCUBElod(uv, float4(R, nMips - gloss*nMips));

对于非常光滑的表面,可能会导致纹理损坏,某些GPU具有获取硬件选择的mip的指令。环境贴图“菲涅耳”:

但会导致高光过多,可以采用法线方差(Normal Variance)解决。方差贴图可以直接对来自mipping法线贴图的丢失信息进行编码,方差图需要高精度和额外的成本才能在着色器中存储、读取和解码,如果离线将它们与光泽贴图结合起来会怎样?

可以从法线贴图中提取投影方差,总是从顶部mip中提取,最好使用NxN加权滤波器:

添加创作的光泽,转换为方差:

将方差转换回光泽度:

这种方法解决了大部分的高光强度问题,也可以用于镜面反射的抗锯齿,在对环境贴图的mip进行光泽控制时,最大限度地减少纹理损坏的机会。

基于物理的着色相对更昂贵(ALU *均增加10-20%),使用特殊情况着色器对性能有所帮助,对于纹理绑定着色器,可以隐藏额外的ALU成本,为特定情况使用快速的Lambert着色器仍然是个好主意。

基于物理的着色是完全值得的,使镜面反射真正成为“下一代”,准备好在工程和艺术方面付出相当大的努力以获得收益。

Lighting the Apocalypse: Rendering Techniques for RED FACTION: ARMAGEDDON分享了游戏红色兵团(Red Faction)使用的光照技术,如推断光照。

推断光照(Inferred Lighting)是延迟光照的一个变种,也叫光照预通道渲染(Light Pre-Pass Rendering)。将照明与场景复杂性隔离开来,对于处理场景破坏至关重要,推断光照 = Light Pre-Pass++,可调照明分辨率、支持MSAA、Alpha照明。

1年后,Lighting & Simplifying Saints Row: The Third分享了推断照明的最新迭代,新增了几项优化和功能,以及自动化LOD管线,包含网格简化和实际执行问题。

原来的推断照明支持许多完全动态的光源、集成Alpha照明(无前向渲染)、硬件MSAA支持(即使在 DX9上)。而本文在此基础上新增了雨滴照明(需要IL)、更好的树叶支持(仅适用于IL)、屏幕空间贴花(由IL增强)、径向环境光遮蔽 (RAO)(由IL优化)。详见14.4.4.1 Inferred lighting小节。

对于LOD,以前的方法是主要由艺术家创作,耗时,实际创建的LOD并不多,大多选择淡入“细节集”。新方式实现了全功能网格简化器,在crunchers中运行,而不是在DCC应用程序中运行。大部分是自动生成的LOD,但艺术家可以调整:建筑物、人物、车辆,完全自动化(无艺术家干预):地形,还使用简化器生成地形碰撞船体、构建阴影代理。

网格简化主要使用误差度量,误差度量衡量网格*似的“糟糕”程度,用于计算收缩误差,确定哪个边先收缩,放置结果顶点的位置。二次误差度量概述:

实现的过程中可能出现UV边界的拉伸问题:

原因是UV不连续:

可以采用UV镜像,以便让边界不那么明显:

更好的做法是保持边界,以相同方式保留任何类型的边界,通过边界边缘添加“虚拟”*面:

连续区域:在每个顶点,跟踪具有连续UV的区域。连续区域问题是UV可能在顶点处是连续的……即使地区是分开的

材质数量:随着LOD变得更简单,材料成本占主导地位:

减少材质数量:积极寻找“小”面积材质,更换为同一网格上使用的较大材质,数量有所减少,但不会有大的节省。

补充细节层次,可将每个可流区域烘焙成单个网格,更加简化(大约是原始顶点的 5%),用顶点着色替换几乎所有材质。

CSM Scrolling: An acceleration technique for the rendering of cascaded shadow maps讲述了一种阴影优化技巧,通过滚动CSM和区分动态、静态物体来优化CSM的渲染。

对于阴影,大多数时候,相机不会跨帧进行彻底的改变,大多数几何图形在帧之间是相对静态的。可以识别从前一帧发生变化的几何图形,跨帧的光线方向相对稳定,空间查询的结果可以与阴影渲染在同一帧中使用,几何被分成小实例。CSM滚动步骤可结合下图加以说明:

1、在缓存阴影图中存储来自上一帧的静态几何体。

2、滚动缓存阴影图以匹配相机视图的变化。

3、在滚动过程中暴露的边缘中渲染额外的静态几何体(比如数字3旁边阴影图右上侧的圆柱体),然后在缓存区域中渲染新的静态几何体(比如数字3旁边阴影图左上侧的圆形)。

(以上阶段都是在持久的缓存阴影图中操作,以下阶段则是在临时的当前帧阴影图中操作)

4、复制缓存阴影图到当前帧的最终阴影图。

5、渲染非静态几何体到当前帧的最终阴影图。

现在假设相机没有移动或移动很少,则涉及4个主要阶段(下图数字标注)。

  • 将前一帧的“静态”几何图形存储在缓存地图中(“静态” = 在 t 时间内没有移动,例如5秒)。(1和2)
  • 每帧将非静态几何图形渲染到缓存副本。(3和4)
  • 但是,上一帧的阴影图缓存在相机移动、相机FOV变化、“静态”几何体移动的情况下会失效。涉及阶段1。
  • 解决方法是针对阶段2中新的静态几何体(红色圆形):
    • 在缓存区域中渲染新的“静态”几何图形。
    • 查询“静态”几何体的状态,区分当前“静态”与以前“静态”查询结果。
    • 使用动态遮挡系统。
  • 创建复制新的阴影图缓存以使用此帧。(3)
  • 渲染动态”几何体到临时阴影图。(4)

现在假设相机移动了很多(但很慢),此时将涉及5个阶段(下图数字标注)。

  • 插入CSM缓存:滚动阴影图,渲染到暴露的边缘。(2、3)

  • 滚动缓存阴影图以考虑相机视图的变化。(2)

  • 从前一帧采样阴影纹理。(2)

  • 滚动区域是钳制到边框的(下图白色区域)。(3)

  • 由于相机运动是3D,涉及横向滚动、深度滚动。(2)

    • 横向滚动:垂直于光线的*移。

      // input是已在delta相机在光源坐标系中转换的UV。
      float ScrolledDepth_LateralOnly(float3 input)
      {
          float2 uv = input.xy;
          // 简单的纹理查找(点采样)
          return SampleShadow(uv);
      }
      
    • 深度滚动:*行于光线的*移。

      // input是已在delta相机在光源坐标系中转换的UV。
      float ScrolledDepth(float3 input)
      {
          float2 uv = input.xy;
          // 深度滚动需要额外的处理. input.z是光源坐标系中的相机深度的差异值。
          float depth_offset = input.z;
          float old_depth = SampleShadow(uv);
          // 抵消所有先前的深度(滚动深度)
          float new_depth = old_depth + depth_offset;
          // 防止深度超出远*面。
          return (old_depth < 1.0f) ? new_depth : 1.0;
      }
      
  • 在滚动过程中暴露的边缘中渲染额外的静态几何体(右上侧红色圆柱体)。(3)

    • 滚动区域被划分为板块(薄的OBB,下图)。

    • “静态”几何体具有重叠边界体积:

    • 几何体相对于视图的粗糙度:

    • 有的具有大量的重叠体积(下图左),有的只有少量的重叠体积(下图右),有的具有明显的锯齿(下图中):

  • 在缓存区域中渲染新的“静态”几何体。(3)

  • 复制阴影图以用作当前帧的最终阴影图。(4)

CSM的滚动涉及2、3、4,渲染效果如下:

总之,直接添加到CSM缓存,关键是像2D位图一样滚动,可以减少约70%静态几何体渲染到CSM。

Practical Physically Based Rendering in Real-timeBeyond a simple physically based Blinn-Phong model in real-time详细阐述了实时渲染领域的PBR的理论、依赖知识、特点、实现及优化。

实时PBR使整个渲染管道基于当前控制台的物理基础,包含以下几点:

  • 基于物理的着色模型。基于物理的BRDF模型
  • 基于物理的光照。基于物理的量,胶片模拟(基于频谱的色调映射)。
  • 基于物理的相机模拟。基于真实相机系统的镜头模拟。

基于物理的光照要求使用的物理量:

  • 正确的色彩空间。
    • 在光谱域 (380nm – 1000nm) 处理胶片模拟(色调映射)。
    • 基于真实电影资料的电影数据库,曝光、显影、复制、打印和投影。
  • 基于瓦特。
    • 其他单位(勒克斯、流明、色温)在引擎中转换为瓦特。
    • 光源面积。用于延迟和前向光源的基于图像的照明和伪光源大小,金属或光泽物体不再需要环境贴图着色器。

基于物理的相机模拟要求基于真实相机系统:

  • 基于镜头数据库的光学模拟:
    • 真正的散景模拟。
      • 基于透镜方程。真实的相机参数和镜头,对焦模拟。
      • 孔径模拟。叶片数、圆形光圈、光圈机制。
      • 暗角。桶形暗角,光学暗角。
    • 支持其它光学效果。

基于物理的着色模型(已实现或正在研究的模型):

  • 基于物理的 Blinn-Phong。
    • 各向同性、各向异性、光谱。
    • 布林-贝克曼。
  • Ashikhmin。
  • 分层材质。
  • Oren-Nayar、改良的Oren-Nayar。
  • 逆向反射材料。
  • 其它特殊材料。Marschner、金属、玻璃、印刷、NPR。

基于物理的Blinn-Phong:


文中还详细地剖析了基于物理的IBL的理论、公式、推导及实现。


基于物理的IBL公式推导及*似。


辐射率环境图(REM)的生成过程。


IBL效果。


镜面AO及效果对比。

不同漫反射模型的效果和性能对比。

Realtime global illumination and reflections in Dust 514讲解了利用特殊的高度场光线追踪来完成间接光和反射的计算。下面三幅图像从左到右分别显示了原始环境、理想的卷积环境和实时*似:

理想的间接项是使用前面描述的朗伯卷积离线计算的,计算需要几秒钟。右侧是锥形轨迹*似,在像素着色器中实时评估。过渡是基于表面法线的Z/up分量的天空颜色和地面颜色之间的线性混合;底色是通过对具有大量mip偏移的地面纹理进行采样获得的。毫无疑问,可以通过花费更多周期来建立更好的*似值。

无论如何,关键是有一种方法可以为空间中的任何点和任何表面法线方向提供间接项,为所有环境提供了一个解决方案,可以用一个*面水*面和一个天空立方体来*似。

在对高度场进行光线追踪时,将光线与一个水**面相交,该*面的高度是通过对光线原点下的高度场进行采样来确定的。(下图)

另一种方法是使用单独的偏向光线进行向上和向下跟踪,因此从不显示水*光线跟踪的结果。

单样本光线追踪步骤在大多数情况下运行良好,但在某些情况下完全失败,例如下图的横截面。黑色矩形表示一个物体,例如一座桥; 橙色线是从下方渲染场景时生成的高度场。左下角的箭头表示一些样本射线,例如从下方经过的车辆顶部的反射。问题是当光线原点在屋顶下传播时,交点会不连续地变化,在该车辆的顶部,桥边缘下方的反射会出现明显的不连续性。虽然仍然可以对不准确的交点进行合理的反映,但不连续性非常明显并且显然是错误的。

解决方案是对高度场应用后处理,以确保高度不会突然变化,将产生下图显示的横截面。意味着反射永远不会有不连续性,因此会得到更合理的反射,还具有一步光线跟踪*似的好处。高度场过程是增量完成的; 高度场需要多次通过才能收敛到此处显示的结果, 这是它的工作原理。

高度场细化过程:

整个跟踪过程:计算上/下偏向光线向量,在射线原点采样压缩高度场,计算每一层的交点,计算每一层的mip偏差,采样四层纹理和天空纹理,合成结果以产生上下颜色(天空、天花板、下面的桥梁、地板、上面的桥),根据查询光线方向混合向上/向下颜色。

细化:时间切片分层更新,为阴影重用CSM,边缘淡出,量化运动。

总之,提供通用间接项,提供具有可变模糊的通用反射项,快速,不支持任意高度复杂度的场景,一般情况下,无法应对墙壁和垂直表面,动态对象不会促成间接或反射,反射质量有限。

三重缓冲的目标是GPU永远不会停止等待页面翻转发生,并且翻转发生在vblank上,因此不会出现撕裂。

两个全高清缓冲区A和B之间的放大和累积步进乒乓球。由于以30fps运行,并且只在帧的最后写入全高清缓冲区,因此要求没有限制写入之间至少有16.6毫秒的间隔。一旦完成了对其中一个缓冲区的写入,就请求在下一个vblank上翻转。在正常的事件过程中,它会在主720p渲染期间的某个时间点发生,当准备开始放大下一帧时,可以确信翻转将会发生。

需要一个栅栏来强制执行这种延迟,因为在RSX几乎无事可做的情况下,例如菜单和加载屏幕。从三个720p缓冲区变为两个1080p缓冲区和一个720p缓冲区,因此所需的额外内存约为9MB。

累积步骤:如果确定正在处理的当前像素是静态的,那么需要找出所有低分辨率样本(下图的红点)落在正在处理的高分辨率像素的区域内。如果有,希望将其混合到运行*均颜色中,如果没有,则保持当前像素不变。

累积的效果对比:


[Rock-Solid Shading](http://advances.realtimerendering.com/s2012/Ubisoft/Rock-Solid Shading.pdf)阐述了PBR的基础理论,分析了导致着色失真的问题,以及如何提升材质的真实可信度。

让东西看起来不错的因素有稳定、干净、没有锯齿,材质类型的表现力,简单、直观的模型。主要工具:Blinn-Phong、Banks、Ashikhmin-Shirley,大多数材质无论是否风格化都可以用简单的BRDF表示。

当前的着色模型存在的问题:物体太亮。锯齿:采样问题可能会导致法线突然成为亮点,闪闪发光,在 HDR 绽放过程中导致失真,亮点可能完全错过,防止使用高的镜面指数,经常会看到使用环境贴图完成的规范以获得清晰的亮点。

为什么离线渲染不会出现以上问题?采样率经常被锁定——例如REYES,即使是错误的,样本也是帧到帧的,从方程中消除了大部分时间锯齿。一切都过采样,每个像素一百个样本并不少见,在无穷大采样大多数问题都会消失。解决了吗? 不,但蛮力使之减缓。

没有达到目标的原因:不稳定——分辨率对大尺度效果影响很大,锯齿——时间和空间,缺乏表现力——无法使用广泛的力量。如何正确实现Blinn-Phong?可以做一个基于纹理的照明方法——一个la REYES,可以找到一个类似的BRDF,真实地表现自己吗?

LEAN映射的机制。LEAN(Linear Efficient Anti-aliased Normal Mapping)是线性高效抗锯齿法线贴图,在Sid Meier的文明V中应用过,为未来的所有资产生产进行部署。好处:时间稳定,分辨率稳定,可以使用高镜面指数(例如10000+),Blinn-Phong内容可以轻松转换,自动各向异性。

给定凹凸贴图的法线:N = (N.x, N.y, N.z),然后创建另一个图M:M = (B.x^2, B.x*B.y, B.y^2),其中: B = (N.x/N.z,N.y/N.z)。不是冗余数据,需要这些项的线性过滤版本!

存储5个通道:X、Y与中心凹凸的偏移量,可以是8位,\(X^2\)\(Y^2\)\(X*Y\),如果想要很好的高镜面反射功率,需要16位。压缩也许可行,但由于使用了线性过滤,不能牺牲它。接下来要介绍的中间解决方案。

从Blinn Phong初始化内容:沿\(X^2\)\(Y^2\)项添加的基本镜面反射功率s为1/s,所以M映射$ (X^2, Y^2, XY) $变为 \((X^2 + 1/s, Y^2 + 1/s, XY)\),存储逆幂意味着需要16位精度来获得大于256的幂,观察:即使使用Blinn-Phong,将功率存储为1/s也会导致MIP滤波器正常运行。

它与Blinn-Phong的*似程度如何?对于低power(例如 < 16),LEAN映射的响应与Blinn-Phong不同,可能需要重新调整一些内容

清晰映射:5个值可能开销大:修改,丢弃各向异性:

存储3个值:X、Y、(X^2 + Y^2)/2,只有 (X^2 + Y^2)/2需要以高精度存储。

对于高光过亮的问题,在最小化滤镜下高光更稳定;对于之前提出的锯齿问题,无锯齿。

达到目标。稳定:渲染的分辨率不影响大尺度效果,抗锯齿:线性硬件滤波器工作正常,表现力:可以使用大功率,并支持各向异性。还有一些问题:在低功率下与Blinn-Phong的分支,存储空间要求更高。

接下来阐述着色器锯齿匿名(Shader Aliasing Anonymous)。选项:纹理空间着色,关键思想是MIP照明!但开销大,用虚拟纹理缓存?可选项1: 拟合,关键思想是找到最合适的参数,例如法线、粗糙度、反射率,缓慢、脆弱、不连续。可选项2:直接方差估计,关键思想是估计方差 -> 新的粗糙度:





烘焙方差过程:对于每个MIP:

  • 对双线性应用小过滤器。
  • 计算方差。
  • 存储结果:
    • 直接,或调整光泽图。
    • 允许编辑?

调整光泽度:多种多样……镜面AA “无处不在”!

  • 动态反射:生成MIP,MIP偏移的查找,或是DX11:可变高斯? 图像空间收集?
  • 体素锥追踪 [Crassin 11]
  • 反射公告牌 [Mittring11]
  • 反射遮挡?

选项:预过滤(LEAN),更好:更准确的结果,各向异性效果。

缺点是内存、额外的着色成本、切线空间。

LEAN的双变量法线分布:

可视化图:

LEAN的内存,烘焙,和Toksvig 一样……双线性模拟仍然很重要!存储协方差矩阵:[Σx, Σ y, Σz]?

可能有精度问题。可改成存储两个光泽值:

可以使用BC5或DXT5,可选择存储相关性:\(\rho = \cfrac{\sum_z} {\sqrt{\sum_x \cdot \sum_y}}\)

除了LEAN,还涉及详细法线贴图、几何物体、漫反射、环境贴图。

其中几何物体锯齿(Geometry AA)是另一个方差来源!

想法1——预过滤几何法线:扩大,生成MIP,使用Toksvig,需要图集!

想法2——像素四边形消息传递 [Penner11A],访问邻居⇒*均⇒方差,*均代码:

float2 dir = 0.5 - frac(vpos*0.5 - 0.25)*2;

float3 n0 = N;
float3 n1 = ddx_fine(n0)*dir.x;
float3 n2 = ddy_fine(n0)*dir.y;
float3 n3 = ddy_fine(n1)*dir.y;

float3 nn = n0 + n1 + n2 + n3;

想法3——来自 Kaplanyan & Valient:结合法向锥(曲率)和镜面反射波瓣锥,将规格功率转换为锥角,添加曲率角:

float3 dN = fwidth(N);
float3 new_normal = normalize(N + dN);
float curvature = acos(dot(new_normal, N))/(pi*0.5);

转换回新的power,已投入实际应用中!优化后的代码:

float3 dN = fwidth(N);
float3 new_normal = normalize(N + dN);
float curvature = sqrt(1 - dot(new_normal, N));

float angle = 4.11893/sqrt(power) + curvature;
power = 16.9656/(angle*angle);

类似的结果。下面是不同方法的着色效果对比:

当前的漫反射存在误差,原因是完整的漫反射积分方程是:

而实际上,目前使用的漫反射积分方程是:

也就是忽略了\((n_a\cdot \bold l)\)的积分。实际上,如果要正确计算漫反射,可以使用完整积分的等同变体:

漫反射积分的解决方案:法线方差⇒圆锥,*均法线 (Na) 周围法线锥的光照积分,锥角公式:cos(θ) = 2*length(Na) - 1

可以预计算积分,就像预积分的皮肤着色 [Penner11B]一样:

float len = length(Na);
float3 N = Na/len;
tex2D(LUT, float2(dot(N, L)*0.5 + 0.5, len));

结果样例:

缩小LUT,重要区域:0~25度(下图)。

也可以避免 LUT,使用曲线贴合:\(x^2\)

float DiffuseAA(float3 N, float3 L)
{
    float a  = dot(N, L);
    float w  = max(length(N), 0.95);
    float x  = sqrt(1.0 - w);
    float x0 = 0.373837*a;
    float x1 = 0.66874*x;
    float n  = x0 + x1;
    return w*((abs(x0) <= x1) ? n*n/x : saturate(a));
}

约18 条指令 (fxc) ,可以说太合适了,获得了大致*似。但有类似Toksvig的问题:压缩法线,存储预过滤的长度/方差。

另外,LEAN还可用于环境图中:

Calibrating Lighting and Materials in Far Cry 3阐述了育碧的Far Cry 3的基于物理的光照模型及优化。

为了获得更加接*物体本身的基础色(反照率),Far Cry 3使用数码相机对物体进行扫描,然后删除光照和镜头畸变,获得了更加物理的反照率贴图。

捕捉漫反射反照率时,使用麦克白颜色检测器(Macbeth ColorChecker)作为参考,由X-Rite制作,有24个已知sRGB值的色块。

ColorChecker旁边的照片材质,照明必须一致,使用ColorChecker的块寻找变换。

变换有两种:仿射变换(Affine Transform)和多项式变换(Polynomial Transform)。仿射变换的优点是消除通道间的串扰,缺点是线性变换。多项式变换的优点是精确调整级别,缺点是通道独立。

颜色校正工具:命令行工具,由Photoshop脚本启动,在xyY颜色空间中运行,应用以下变换[Malin11]:

下面是校正前(左)后(右)的对比:

天空的着色模型采用了CIE的模型:

光照模型也是Cook-Torrance,其中D项是:

F项有两种:Schlick*似和球面高斯*似:

可见性项:

然后简化了G项:

为了减少镜面反射的锯齿和保留其细节,采用了Toksvig的公式来缩放镜面power:

将这些缩放数据存储在纹理中,理想情况下,用于调整光泽贴图。添加额外纹理的成本太高,不是每个着色器都使用光泽贴图,无法将Toksvig贴图与现有光泽贴图组合。DXT5压缩法线贴图中存在自由通道,艺术家在法线贴图的alpha通道中绘制光泽,将光泽与Toksvig组合储存在R通道中:

法线的y分量的压缩受到影响:

光泽贴图是可选的,如果有,便与Toksvig结合,如果没有,将Toksvig*均为单个值。

Deferred Radiance Transfer Volumes: Global Illumination in Far Cry 3详细介绍了FarCry 3中使用的动态全局照明的*似值,分为两部分:首先是理论概述和离线预计算,其次是实时渲染实现和着色细节。使用稀疏体积的辐射传输探针,每个探针存储球谐系数矩阵,可以在运行中重新照亮探针,从而支持在一天中的时间变化下的照明,以及枪口闪光和爆炸等局部照明。使用重新照亮探针的着色是在屏幕空间中的GPU上完成的。使用混合CPU/GPU实现来确保系统在当前一代控制台上运行良好,在内存和性能方面都是高效的,提供详细的性能统计数据以及代码片段来说明系统的内部结构。

延迟辐照亮度传输体积的特点是*似全局光照,轻量级、主机友好,实时重照明,混合CPU/GPU。其中全局光照是低频的辐照亮度传输,支持太阳和天空反弹及天空直接光。

实时重照明是用太阳/天空颜色更新的全局光照,支持一天的周期时间,直接的艺术家反馈。

整个系统的概述。

离线时,将探针放置在世界中并为它们预先计算辐射转移,这一过程也称为烘焙。在游戏中,每当光照环境发生变化时,都会实时重新点亮探测器。将动态生成新的辐照度值插入到许多体积纹理中,然后GPU使用这些来遮蔽屏幕空间中的所有内容。探针烘焙时,预计算辐射传输和天空的能见度。

PC的局部辐射亮度传输:来自动态光源的全局照明,假设光源与探针处于同一位置,在每个探针位置存储白光PRT。

实时重照明:由太阳和天空驱动的照明,投影到二阶SH,艺术家创作梯度。

计算每个探针的光贡献,结果是颜色/强度数组,每个基本方向都有一种颜色,可以添加更多的基础方向,以获得更好的准确性。

体积纹理:GPU快速滤波,可以逐像素完成,适用于大型对象。连接到第一人称摄像机,体素是四个基础强度,96x96x16的RGBA,完全更新约7ms,占用5个SPUS。时间摊销:只更新那些没有数据可用的,使用包装避免偏移现有的切片。

体积纹理环绕:环绕采样器状态昂贵,模拟具有frac()的环绕,用于正确过滤的重复边界。

环境照明方面考虑了室内、远距离环境等因素。

下图是PS3的实现概览:

Practical Clustered Shading介绍了分簇着色的特点和实现。

分簇着色(Clustered Shading)是Olsson、Billeter、Assarsson等人在HPG 2012的论文Clustered Deferred and Forward Shading提出的一种扩展了分块着色的技术。它的特点是实时、鲁棒性好,支持的光源多,与视图低相关,可以处理嘈杂的深度分布。

分簇着色带来的海量光源,可带来全局光照、复杂的光源类型(如面光、体积光)、艺术家无约束、带光照的特效等。假设有以下的样例场景:

对于分块着色,支持延迟或前向,简单,某些情况快速,2D分块。

分块着色(Tiled Shading)的最大问题在于游戏是3D,而它是在2D*面上分块,导致单个块会和改块在深度上的所有光源相交,即便较远的光源被前面的物体遮挡!简而言之,分块是2D,几何样本、片元/像素是3D,光密度也是3D,视图依赖,不可预测的着色时间。

分簇着色(Clustered Shading)的核心思想是增加第三维,也在深度方向分块 = 分簇,也可大于3维(例如法线)。

分块和分簇在空间上的分布如下两图:


分簇着色的步骤如下:

  • 光栅化G缓冲区。前向:pre-z通道。
  • 分簇分配。

分簇键:$ck = (i, j, k) \(的整数元组,i, j = 2D分块的id,即gl_FragCoord.xy,\)k = \log(z_\text{viewSpace})$。

  • 寻找唯一的簇。

全屏通道,标记使用的分簇,读取深度,计算ck,将网格中的单元格设置为1。向前:具有副作用的几何通道。

精简非零的分簇值,获得非空簇的列表,并行前置和Compute Shader。

  • 将光影分配给簇。

许多簇和光源,可分层方法:光源的层次结构、32叉树(匹配GPU的SIMD),在GPU上动态重建,用BV测试给每个簇遍历光源树。

  • 着色视图样本,延迟:全屏通道,前向:几何通道。

以下是在Crytek Sponza场景有树有10000+光源的情况下,不同方法的性能对比图:

上图显示,Tiled着色受着色时间支配,即使使用非常简单的像素着色器也是如此,意味着提高tile速度不会太大改变结果。另一方面,分簇着色在着色和算法的其它部分之间更加*衡。图中还显示了三个基于法线的更复杂剔除的变体,然而测试实现中没有得到回报,因为更复杂的剔除和更多簇的成本超过了着色成本的降低。随着更昂贵的着色和更好的分簇和剔除实现,它们可能仍然值得。

分块前向着色(Tiled Forward Shading)可用于透明物体,存在的问题是视图依赖、退化为二维、全屏不连续!

分簇前向着色(Clustered Forward Shading)在预几何通道执行,标记使用的分簇,片元着色器中的副作用,将网格中的单元格设置为1。

以下是分簇前向着色的性能数据:

总之,分簇着色高性能,低视图依赖性,良好的最坏情况性能,全动态,支持透明度,支持前向或延迟,或两者兼而有之。潜在的优势有:样本的快速体素化,如阴影、锥体追踪的起点、*似阴影、自适应着色及其它用途。

Moving Frostbite to Physically Based Rendering 3.0阐述了Frostbite引擎改进了PBR关照,系统地梳理了其理论基础、相关技术、推导、优化及应用。PBR的范围包含了光照、材质和相机(之前的关注点只在光照和材质上)。

80%的外观类型是标准材质,镜面反射用带有GGX NDF的微面模型,漫反射用迪士尼模型,其它材质类型有次表面材质、单层涂层材料。其中镜面反射的公式如下:

下图是GGX-Smith和Height-Correlated Smith的G项对比效果:

上图显示差异很小,但对于高粗糙度值(右侧)来说很明显:

漫反射项上,不再使用兰伯特,用迪斯尼漫反射取而代之,因为后者使用了漫反射和镜面反射之间的耦合粗糙度和反向反射(retro-reflection)。

它们的漫反射对比如下图,在低粗糙度时有点暗,在高粗糙度时更亮,很微妙,但可以带来不同。

最初的迪士尼漫反射项存在一个问题,即能量不守恒:在某些情况下,反射光可能高于入射光。Frostbite应用了一个简单的线性校正,以确保当镜面反射和漫反射项相加时,半球方向的反射率小于1。


对于镜面反射和漫反射的输入值,Frostbite再次使用了Burley的*似方法,解耦了金属和非金属,更易于资产创作。

Frostbite选择向艺术家展示“*滑度”而不是粗糙度,因为白色的*滑对他们来说更直观。还尝试了各种重新映射函数,以获得感知线性度,最后,再次使用了Burley的方法来*方化粗糙度。


在光照方面,争取光照一致性——所有BRDF必须与所有光源类型正确集成,所有光源都需要管理直接照明和间接照明,所有照明均正确组合(SSR/本地IBL/...),所有光源之间的比例都正确。光照存在多种单位和参考系,常见的光度单位制如下:

而Frostbite使用了以上4种单位,它们的应用场景具体如下:

Frostbite支持四种不同的形状:球体、圆盘、矩形和管状。每种光源都可以有一个更简单的版本,但只有点和点使用准时(punctual)光照路径,因为它们更频繁,成本较低。

下面是关于准时光、光度光、区域光的描述:




对于IBL,关注点在*距离光照探针和远距离光照探针。其单位和光照来源和公式如下:


在运行时,不使用表面的镜像方向,而采用略有偏移的主导方向(dominant direction,下图青色箭头),有助于提高积分*似的精度。


对于摄像机,依旧考量了基于物理的模拟,纳入将场景亮度转换为像素值、光圈、感光元件、镜头、快门等因素:

到达传感器的场景亮度将由曝光确定,然后将曝光转换为像素值:

过渡到PBR的步骤:

1、标准材质 + 观察者优先 + 培养关键艺术家。

2、PBR/非PBR并行,自动转换。

3、向游戏团队宣传PBR + 验证工具。

Real-time lighting via Light Linked List阐述了使用光源链表来实现和优化海量光源的技术。使用Light Linked List(光源链表,LLL)可以获得半透明顺序正确的光照效果:

使用Light Linked List的前(左)后(右)对比。

将光源存储在逐像素链接列表中,其结构体如下:

struct LightFragmentLink
{
    float m_LightDepthMax; // 光源最大深度
    float m_LightDepthMin; // 光源最小深度
    int   m_LightIndex;    // 光源索引
    uint  m_Next;          // 下一个光源
};

// 压缩版本
struct LightFragmentLink
{
    uint  m_DepthInfo; // 深度信息
    uint  m_IndexNext; // 下一个光源
};

分辨率越低越好:四分之一、八分之一等等…内存消耗:4个缓冲区,分辨率为八分之一:2个RWByteAddressBuffer、1个RWStructuredBuffer、1个深度缓冲区(可选),*均每像素预分配40个光源。总成本:900P:约7.25megs,1080P:约10.15megs。

Insomniac引擎的渲染流程如下:

其中填充链表的消耗如下:

LLL的深度缓冲:生成较小的深度缓冲区,使用保守的深度选择,使用GatherRed

LLL的着色步骤:软件深度测试、获取最小和最大深度、分配一个LLL片元。

LLL的深度测试:正面通过深度测试,背面未通过深度测试,禁用硬件深度剔除。

// 软件测试正面.
// 如果正面Z测试失败,跳过片元.
if((pface = true) && (light_depth > depth_buffer))
{
    return;
}

如果两种深度都穿越,哪个深度优先?边界的RWByteAddressBuffer,编码深度+ID(16位ID、16位深度),

uint new_bounds_info = (light_index << 16) | f32tof16(light_depth);

使用InterlockedExchange交换新旧的边界值:

使用一个RWStructuredBuffer来存储:

struct LightFragmentLink
{
    uint  m_DepthInfo; // 深度信息,高位是最小深度,低位是最大深度。
    uint  m_IndexNext; // 下一个光源。
};
RWStructuredBuffer<LightFragmentLink> g_LightFragmentLinkedBuffer;

// 增加当前的计数
// 分配.
uint new_lll_index = g_LightFragmentLinkedBuffer.IncrementCounter();
// 不要越界
if(new_lll_index >= g_VP_LLLMaxCount)
{
    return;
}

// 填充链接光源的片元并保存。
// 最终输出
LightFragmentLink element;
element.m_DepthInfo = (light_depth_min << 16) | light_depth_max;
element.m_IndexNext = (light_index << 24) | (prev_lll_index & 0xFFFFFF);
// 存储光源链表信息
g_LightFragmentLinkedBuffer[new_lll_index] = element;

计算光照的步骤:绘制全屏四边形、访问LLL、应用光源。访问LLL时,获取第一个链接元素偏移:第一个链接元素以较低的24位编码。

uint src_index = LLLIndexFromScreenUVs(screen_uvs);
unit first_offset = g_LightStartOffsetView[src_index];
// 解码首个元素索引
uint elemen_index = (first_offset & 0xFFFFFF);

启动照明循环:等于0xFFFFFF的元素索引无效。

while(element_index != 0xFFFFFF)
{
    LightFragmentLink element = g_LightFragmentLinkedView[element_index];
    element_indx = (element.m_IndexNext & 0xFFFFFF);
}

解码光源的最小和最大深度,比较光源的深度。

// 解码光源边界
float light_depth_max = f16tof32(element.m_DepthInfo >> 0);
float light_depth_min = f16tof32(element.m_DepthInfo >> 16);
// 执行深度边界检测
if((l_depth > light_depth_max) || (l_depth < light_depth_min))
{
    continue;
}

// 获取完整的灯光信息
uint light_index = (element.m_IndexNext >> 24);
GPULightEnv light_env = g_LinkedLightsEnvs[light_index];
switch(light_env.m_LightType)
{
    // ......
}

对于阴影,使用纹理数组,分配子区域。

Multi-Scale Global Illumination in Quantum Break说明了游戏Quantum Break的多种规模的全局光照,包含大规模光照和屏幕空间光照。

文中提到可能的全局光照解决方案有:

  • 动态方法:

    • Virtual Point Lights (VPLs) [Keller97]

    • Light Propagation Volumes [Kaplaynan10]

    • Voxel Cone Tracing [Crassin11]

    • Distance Field Tracing [Wright15]

  • 基于网格的预计算:

    • Precomputed Radiance Transfer (PRT) [Sloan02]
    • Spherical Harmonic Light Maps
  • 无网格预计算:

    • Irradiance Volumes [Greger98]

文中经过对比之后,选用了Irradiance Volumes。

辐照度体积原理示意图。

全局照明体积的好处是没有UV,适用于LOD模型,体积光照,与动态对象一致,但不适用于镜面,由于数据量太大。混合反射探头图例如下:




自动化放置探针,最大化可见表面积,尽量减少到地面的距离,选择K个最佳探针位置:

对于全局光照数据的存储(如镜面探针图集),可选的方案有:

  • GPU体积纹理:由于压缩,无法使用原生插值。

  • GPU稀疏纹理:对于细粒度树结构,页面太大,可能无法在未来游戏的目标*台上使用。

  • 自适应体积数据结构有:

    • Irradiance Volumes [Greger98, Tatarchuk05]
    • GigaVoxels [Crassin09]
    • Sparse Voxel Octrees [Laine and Karras 2010]
    • Tetrahedralization, e.g., [Cupisz12], [Bentley14], [Valient14]
    • Sparse Voxel DAGs [Kämpe13]
    • Open VDB [Museth13]

自适应体素树:隐式空间划分,64的分支因子,多尺度数据。

体素树结构:

树遍历:

体素树可视化:

无缝插值:

文中的SSAO基于Line-Sweep Ambient Obscurance(LSAO)[Timonen2013],LSAO定位了最有贡献的遮挡体。

扫描36个方向,长步(~10px)和短线间距(相隔约2倍),GPU的调度友好,在Xbox One上,720p的扫描速度为0.75毫秒。对样本增加抖动,额外的*场样本(距离约2倍),样本垂直于加紧的遮挡体。

36个方向太贵,无法按像素采集,在3x3邻域上交错(4个方向/像素),使用深度和法线感知3x3盒过滤器进行收集。

屏幕空间漫反射照明,LSAO样本是“最可见的”,很适合对入射光进行采样,无法根据定义进行遮挡(提供自遮挡)。

效果对比:

屏幕空间反射:GGX分布的每像素1条光线,针对所有表面进行评估,线性搜索(7步),步进形成一个几何级数。

深度缓冲样本的处理,需要支持不同的粗糙度,计算圆锥体覆盖率,需要适应遮挡和颜色采样,还可以找到单色样本位置。深度厚度=a+b*(沿射线的距离),深度场延伸至/自摄影机,而不是沿视图z!

将线性项匹配视图空间的步长,否则,匹配实心几何体上的孔:

对于遮挡,计算圆锥体的最大覆盖率,将圆锥体的下限夹紧到曲面切线!

对于颜色,需要一个样本位置,首先选择覆盖大部分圆锥体的样品。将反射光线对准覆盖的中心,并与最后两个样本之间的直线相交,低采样密度:向相机方向插值(蓝色)。

光线上方的上一个示例:不插值。

优化交叉点,如果相邻光线的方向相同,交叉搜索,采取最*的命中距离。

Hybrid Ray-Traced Shadows阐述了混合光线追踪的阴影技术,以获得高质量更接*基准真相的阴影效果。常规的阴影图存在粉刺、彼得*移和锯齿等瑕疵:

传统的边界体积层次结构可以跳过许多光线三角形命中测试,需要在GPU上重建层次结构,对于动态对象,树遍历本身就很慢。

存储用于光线跟踪的图元,而无需构建边界体积层次!对于阴影贴图,存储来自光源的深度,简单而连贯的查找。同样地存储图元,一个深层图元图,逐纹素存储一组正面三角形。深度图元图绘制(N x N x d)包含3个资源:

  • 图元数量图(Prim Count Map):纹理中有多少个三角形,使用一个原子来计算相交的三角形。
  • 图元索引图(Prim index Map):图元缓冲区中三角形的索引。
  • 图元缓冲区(Prim Buffer):后变换的三角形。

d够大吗?可视化占用率:黑色表示空的,白色表示满了,红色则超出限制,对于一个已知的模型,很容易做到这一点。

GS向PS输出3个顶点和SV_PrimitiveID:

[maxvertexcount(3)]
void Primitive_Map_GS( triangle GS_Input IN[3], uint uPrimID : SV_PrimitiveID, inout TriangleStream<PS_Input> Triangles )
{
    PS_Input O;
    [unroll]
    for( int i = 0; i < 3; ++i )
    {
        O.f3PositionWS0 = IN[0].f3PositionWS; // 3 WS Vertices of Primitive
        O.f3PositionWS1 = IN[1].f3PositionWS;
        O.f3PositionWS2 = IN[2].f3PositionWS;
        O.f4PositionCS = IN[i].f4PositionCS; // SV_Position
        O.uPrimID = uPrimID; // SV_PrimitiveID
        Triangles.Append( O );
    }
    Triangles.RestartStrip();
}

PS哈希了使用SV_PrimitiveID的绘制调用ID(着色器常量),以生成图元的索引/地址。

float Primitive_Map_PS( PS_Input IN ) : SV_TARGET 
{ 
    // Hash draw call ID with primitive ID 
    uint PrimIndex = g_DrawCallOffset + IN.uPrimID; 
    // Write out the WS positions to prim buffer 
    g_PrimBuffer[PrimIndex].f3PositionWS0 = IN.f3PositionWS0;     
    g_PrimBuffer[PrimIndex].f3PositionWS1 = IN.f3PositionWS1; 
    g_PrimBuffer[PrimIndex].f3PositionWS2 = IN.f3PositionWS2; 
    // Increment current primitive counter uint CurrentIndexCounter; 
    InterlockedAdd( g_IndexCounterMap[uint2( IN.f4PositionCS.xy )], 1, CurrentIndexCounter ); 
    // Write out the primitive index 
    g_IndexMap[uint3( IN.f4PositionCS.xy, CurrentIndexCounter)] = PrimIndex; return 0; 
}

需要使用保守的光栅来捕捉所有与纹素接触的图元,可以在软件或硬件中完成。硬件保守光栅化:光栅化三角形接触的每个像素,在DirectX 12和11.3中启用:D3D12_RASTERIZER_DESC、D3D11_RASTERIZER_DESC2。

软件保守光栅化:使用GS在裁减空间中展开三角形,生成AABB以剪裁PS中的三角形,参见GPU Gems 2-第42章。

光线追踪:计算图元坐标(与阴影贴图一样),遍历图元索引数组,对于每个索引,取一个三角形进行射线检测。

float Ray_Test( float2 MapCoord, float3 f3Origin, float3 f3Dir, out float BlockerDistance )
{
    uint uCounter = tIndexCounterMap.Load( int3( MapCoord, 0 ), int2( 0, 0 ) ).x;
    [branch]
    if( uCounter > 0 )
    {
        for( uint i = 0; i < uCounter; i++ )
        {
            uint uPrimIndex = tIndexMap.Load( int4( MapCoord, i, 0 ), int2( 0, 0 ) ).x;
            float3 v0, v1, v2;
            Load_Prim( uPrimIndex, v0, v1, v2 );
            // See “Fast, Minimum Storage Ray / Triangle Intersection“
            // by Tomas Möller & Ben Trumbore
            [branch]
            if( Ray_Hit_Triangle( f3Origin, f3Dir, v0, v1, v2, BlockerDistance ) != 0.0f )
            {
                return 1.0f;
            }
        }
    }
    
    return 0.0f;
}

左:3k x 3k的阴影图;右:3k x 3k的阴影图 + 1K x 1K x 64的PM。

为了抗锯齿,使用额外的光线可行吗?开销太大了!简单技巧——应用屏幕空间AA技术(FXAA、MLAA等)。

混合方法;将光线跟踪阴影与传统的软阴影相结合,使用先进的过滤技术,如CHS或PCS,使用阻挡体距离计算lerp系数,当阻挡体距离->0时,光线跟踪结果普遍存在。插值因子可视化:

L = saturate( BD / WSS * PHS ) 

L: Lerp factor 
BD: Blocker distance (from ray origin) 
WSS: World space scale – chosen based upon model 
PHS: Desired percentage of hard shadow 

FS = lerp( RTS, PCSS, L ) 

FS: Final shadow result 
RTS: Ray traced shadow result (0 or 1) 
PCSS: PCSS+ shadow result (0 to 1)

使用收缩半影过滤,否则,光线跟踪结果将无法完全包含软阴影结果,将导致在两个系统之间执行lerp时出现问题。

效果对比:

不同图元复杂度的效果、消耗及性能如下:

局限性:目前仅限于单一光源,不会扩大到适用于整个场景,存储将成为限制因素,但最适合最接*的模型:当前的焦点模型、最*级联的内容。总之,解决传统的阴影贴图问题,AA光线跟踪硬阴影的性能非常好,混合阴影结合了这两个世界的优点,无需重新编写引擎,今天的游戏速度足够快!

Advancements in Tiled-Based Compute Rendering阐述了基于分块的计算着色器的渲染,如当前的技术、剔除改进、分簇渲染等。

文中改进的目标有Z-Prepass(前向+)、深度边界、光源剔除、颜色通道。

首先来分析深度边界,以往的做法是根据每个tile确定深度缓冲区的最小和最大界限,原子操作的最小、最大值。以往的实现往往存在不少性能上的问题:

可以改成并行规约(Parallel Reduction),原子有用,但不是高效的,需要计算友好算法,当前已经有了很好的资料:

  • Optimizing Parallel Reduction in CUDA [Harris07]
  • Compute Shader Optimizations for AMD GPUs: Parallel Reduction [Engel14]

实现细节:第1个pass读取4个深度样本,需要单独的通道,写入边界到UAV,也许对其它操作也有用。

效率对比:

显卡 Atomic Min/Max Parallel Reduction 变化
AMD R9 290X 1.8 ms 1.6 ms 提升11.1%
NVIDIA GTX 980 1.8 ms 1.54 ms 提升14.4%

在3840x2160分辨率和2048个光源情况下的深度边界和光源剔除的综合成本,并行规约过程约需0.35ms,比测试的GPU上的原子最小值/最大值更快。

接下来分析光源剔除。

光源剔除需要涉及球体-视锥体的相交检测,存在以下几种方式:

上:裁剪*面;中:围绕长视锥体的AABB;下:围绕短视锥体的AABB。

除了以上几种,还有Arvo[Arvo90]相交测试方式,其代码如下:


左:球体-视锥体相交测试;右:Arvo相交测试。明显Arvo的更紧凑精确。

剔除聚光灯时,不要在聚光灯原点周围放置边界球体,在半径为r的球体内P处的紧凑包围聚光灯:

在深度裁剪时,存在2.5D和HalfZ方式,而文中使用了改良的HalfZ方式:像往常一样计算最小和最大Z,然后计算HalfZ,分别使用HalfZ和Max&Min的第二组最小值和最大值,测试*边界和远边界,写入其中一个列表或者两者,在深度边界通道重复一次,最坏情况收敛于HalfZ。

从上到下:2.5D、HalfZ、改进的HalfZ。

Unreal Engine 4的Infiltrator演示和不同方法的光源剔除可视化。

剔除结论:带有AABB的改良HalfZ通常效果最好,尽管生成MinZ2和MaxZ2会增加一些成本,即使在两个AABB而不是一个AABB中剔除每个光源,32x32的tile在剔除阶段节省了大量时间,以推送更多光源时的颜色通道效率为代价。

对于分簇渲染的光源剔除,视图空间AABB在二维网格上工作得最好,但在16切片时很糟糕,视图空间视锥体*面更好,逐tile*面计算,然后测试每个切片的*距离和远距离,(可选)然后测试AABB。

VRAM的使用:16x16像素的2D网格需要numTilesX x numTilesY x maxLights,1080p:120 x 68 x 512 x uint16 = 8MB,4k: 240 x 135 x 512 x uint16=32MB,每种灯光类型(点和聚光)的列表:64MB,32片:仅点光源为1GB,或者使用更粗的网格,或者使用压缩列表。

压缩列表的选项1:在CPU上进行所有剔除,但其中一些灯光可能是由GPU产生的,CPU是宝贵的资源!选项2:在GPU上剔除,跟踪TGSM中每个切片的灯光数量,在灯光列表标题中写入偏移表,每个分块只需要maxLights x “安全系数”。

Z Prepass非常依赖场景,通常被认为开销太大,DirectX12有助于降低提交成本,应该已经有一个超级优化的阴影深度路径!仅位置流,索引缓冲区和材质一起批处理,部分Prepass确实有助于减轻几何体负载。

结论:并行规约比原子最小值/最大值更快,AABB球体测试结合改良的HalfZ是一个不错的选择。分簇着色可能会大大节省tile的剔除,低光源数量的开销更小,与2D分块相比,它还提供了其它好处。聚合裁剪是非常值得的,为昂贵的场景颜色提供了最佳的优化。

如何打造一款秒级别的全局光烘培软件由网易研究院在GDC中国2015呈现,讲述了实现快速烘焙的全局光照渲染。文中提到需要自己研发的原因是当时市场上的商业烘培软件烘培慢、包体大、材质有限,提出了CloudGI的烘焙流程:

使用了基于点云的GI:

基于点云的GI主要有3个步骤:点云生成、构建八叉树、计算间接光照。

点云实际是用的面元,包含了法线、位置、辐射率、面积等信息,使用了DX1的UAV 和原子相加的指令。对于面元的面积,在几何着色器中执行,公式如下:

\[\text{Area} = \cfrac{\text{TriangleArea}}{\text{UVArea/UVPerPixel}} \]

构建八叉树使用了GPU,高度并发,无需同步,不浪费显存。创建叶节点,3位一层编码,32位表示10层:

计算间接光照的过程:遍历八叉树,得到id映射表,然后从ID计算辐射率,最后用下面的公式获得*均值并乘以PI:

另外,采用了分块烘焙的方法来提升速度。


14.4.3.3 移动*台

The Benefits of Multiple CPU Cores in Mobile Devices阐述了移动设备对多核的需求、SMP多处理器、Tegra 2、Dual Core ARM Cortex A9架构等内容。

移动设备执行各种各样的任务,例如We浏览、视频播放、移动游戏、SMS文本消息和基于位置的服务。由于高速移动设备和Wi-Fi网络可用性的增长,移动设备也将用于以前由传统PC处理的各种性能密集型任务。下一代智能手机(称为“超级手机”)和*板电脑将用于各种任务,例如播放高清1080p视频、基于Adobe Flash 的在线游戏、基于Flash的流式高清视频、视觉丰富的游戏、视频编辑、同步高清视频下载、编码和上传以及实时高清视频会议。

当前这一代移动处理器并非旨在应对这种高性能用例的浪潮。当用户同时运行多个应用程序或运行性能密集型应用程序(如游戏、视频会议、视频编辑等)时,基于单核CPU的设备的体验质量会迅速下降。为了提高CPU性能,工程师采用了多种技术,例如使用更快和更小的半导体工艺、提高内核工作频率和电压、使用更大的内核以及使用更大的芯片缓存。

增加CPU内核或高速缓存的大小只能将性能提高到一定水*,超过该水*的热量和散热问题使得进一步增加内核和高速缓存大小变得不切实际。从基本的半导体物理学中我们知道,提高工作频率和电压可以成倍地增加半导体器件的功耗,即使工程师可以通过增加频率和电压来挤出更高的性能,但性能的提高会大大缩短电池寿命。此外,消耗更高功率的处理器将需要更大的冷却解决方案,从而导致设备尺寸的意外扩大。因此,提高处理器的工作频率以满足移动应用不断提高的性能要求,从长远来看并不是一个可行的解决方案。

为了满足移动设备对性能和外形时尚度快速增长的需求,业界已开始采用更新的技术,例如对称多处理(Symmetrical Multiprocessing,SMP)异构多核(Heterogeneous Multi-core)计算。 NVIDIA Tegra是当时世界上最先进的移动处理器,从头开始构建为异构多核 SoC(片上系统)架构,具有两个ARM Cortex A9 CPU内核(下图)和其它几个专用内核来处理特殊任务,例如音频、视频和图形。

ARM Cortex A9 CPU内核架构图。

与用于音频、视频和图形处理等任务的通用处理内核相比,专用内核需要的晶体管更少、工作频率更低、性能更高、功耗更低。(下图)

双核CPU的电压和频率扩展优势。

对称多处理(SMP)技术使移动处理器不仅能够提供更高的性能,而且还能满足峰值性能需求,同时保持在移动电源预算之内。 具有SMP的多核架构由以下特征定义:

  • 架构由两个或更多相同的CPU内核组成。
  • 所有内核共享一个公共的系统内存,并由一个操作系统控制。
  • 每个CPU都能够在不同的工作负载上独立运行,并且尽可能与其它CPU共享工作负载。


单核(上)和双核(下)运行网页浏览的对比图。

搭载ARM Cortex A9 CPU的移动设备和搭载其它芯片的移动设备的性能表现。图中表明前者有2.5倍的提升。

游戏Dungeon Defender开启双核之后,FPS是单核的2倍多。

总之,芯片制造商意识到,频率和内核尺寸的不断增加导致功耗呈指数级增长和过度散热, 因此,CPU制造商开发了多核CPU架构,以继续提供更高性能的处理器,同时限制这些处理器的功耗。智能手机和*板电脑等移动设备比PC设备从多核架构中获益更多,因为电池寿命和续航的收益巨大。该文预测双核处理器将在2011年成为标准,四核将在不久的将来出现。

为了进一步提高性能并保持在移动电源预算内,所有移动处理器最终都将不可避免地具有多核处理器。Android、Windows CE和Symbian等移动操作系统能够在多核环境中运行,并具有有效利用底层硬件的多个处理核心所需的功能。此外,流行的Web浏览器和大多数PC游戏已经是多线程的,如果将这些应用程序移植到基于多核CPU的移动处理器上运行,用户将看到性能的巨大改进。

NVIDIA Tegra旨在利用对称多处理的强大功能,提供非凡的Web浏览体验、响应速度快的用户界面、有效的多任务处理以及极大的电池续航时间。

在2012年的GDC中国上,《调教三国》产品研发及技术感悟分享了国内具有代表性意义的移动端游戏的研发过程、技术和经验教训。下图是该游戏的服务端使用的架构:

下图是该游戏使用的引擎架构:

在移动端游戏的引擎选择上,文中给的建议是不盲目选择商业引擎(Unity、Unreal、Flash),流行的开源引擎会更加符合实际,也不要盲目跨*台,iOS和Android足够。当时的Cocos2D-X是多少移动端游戏的首选,并建议不要自己重新造轮子写引擎。下图是当时移动端游戏引擎的常见模块和架构:

对客户端技术及语言的选择,建议在Windows开发环境,减少对于Object C的依赖(iOS),减少对于Java的依赖(Android),C/C++作为主要编程语言。

在游戏品质分级上,建议的分辨率是高端1136x640(iPhone 5)、中端960x640(iPhone 4S)、低端480x320(iPhone 3GS),内存控制在150M以内,包体大小在高中低端上分别是小于50M、约80M、大于100M。

用户体验上注重特殊操作方式(触摸点大小、操控范围)、适应新兴屏幕特点(小屏幕、高清晰)、性能优化(加载时间、内存局限)、网络优化(时延、断线及压缩)。

iOS and Android Development with Unity3D讲述了2012年用Unity开发移动*台游戏的攻略。当时付费下载过移动端app的情况如下图,其中40%多ios用户选择了是,而安卓30+%:

为了以后*台迁移,选择解决方案要考虑代码最少的大多数*台、不会吃掉利润的许可模式、广泛的社区支持等因素。同时对比了H5、Cocos2D、UDK、Flash、Corona等开发套件。说明选择Unity的原因:对关键*台的最佳支持,包含移动设备(iOS、Android)、网页(NaCL、Flash、网页播放器)、桌面(Steam、Mac App Store)、主机及原生插件、广泛的社区等。

2012年的Unity游戏画面。

插件方面,主要使用跨*台插件,访问*台特定功能(游戏中心等)。重构*台特定代码只花了几天,更换了适用于Android的iOS插件,运行时*台检查和#IF 编译器指令的组合,AndroidJava类。具备跨*台的导出工具,每个*台的资产设置,如压缩设置、过滤、缓存服务器、多*台工具包、特定于*台的资产、构建时资产更改。

总之,Unity有最佳商业模式,最广泛的*台支持,最佳社区支持,非常简单的移植过程。

Bringing AAA Graphics to Mobile Platforms阐述了移动端硬件的幕后工作原理及案例研究,软件如何应用这些知识将控制台图形引入移动*台。

移动图形处理器的功能有着色器、渲染到纹理、深度纹理、MSAA,性能也在慢慢变好。

移动GPU架构:基于Tile的延迟渲染(TBDR),与台式机或控制台截然不同,常见于智能手机和*板电脑,ImgTec SGX GPU属于这一类,还有其它基于tile的GPU(例如 ARM Mali)以及其他移动GPU类型(NVIDIA Tegra更传统)。

基于Tile的移动GPU:TLDR将屏幕分割成tile(例如16x16或32x32像素),GPU适合整块芯片,处理一个tile的所有绘制调用,对每个tile重复以填满屏幕,每个tile在完成时被写入RAM。

ImgTec处理过程。

顶点前端:顶点前端从GPU命令缓冲区读取,将顶点图元分布到所有GPU核心,将绘制调用拆分为固定的顶点块,GPU核心独立处理顶点,持续到场景结束。

顶点处理如下图:

各阶段描述如下:

  • 顶点设置。从顶点前端接收命令。
  • 顶点预着色。获取输入数据(属性和uniform)。
  • 顶点着色器。通用可扩展着色器引擎,执行顶点着色器程序,多线程。
  • 参数缓冲区。存储在系统内存中,但不能溢出这个缓冲区!

像素前端:读取参数缓冲区,将像素处理分配给所有内核,一次一整块tile,在一个GPU内核上完整处理一个tile,tile在多核GPU上并行处理。

像素处理(每个GPU核心)如下图:

其中:

  • 像素设置。从Pixel Frontend接收tile命令,从参数缓冲区获取顶点着色器输出,三角光栅化,计算插值器值,深度/模板测试, 隐藏表面剔除(HSR)。
  • 像素预着色。填充插值器和统一数据,启动非依赖纹理读取。
  • 像素着色器。多线程ALU,每个线程可以是顶点或像素,每个GPU核心中可以有多个USSE。
  • 像素后端。当tile中的所有像素都完成时触发,执行数据转换、MSAA下采样,将完成的tile的颜色/深度/模板写入内存。

着色器单元注意事项:没有动态流控制的着色器程序:每条指令4个顶点/像素,具有动态流控制的着色器程序:每条指令1个顶点/像素, 着色器中的Alpha混合:不分离专用硬件,切换状态时可能会发生着色器修补。

手机是新的PC,广泛的功能和性能范围,可伸缩的图形回归,用户图形设置回归,低/中/高/超,渲染缓冲区大小缩放,100个SKU测试回来了。

渲染目标已死,MSAA开销低且使用更少的内存,只有在内存中的解析数据,MSAA大约消耗0到5ms,当心缓冲区(颜色或深度)回存!Alpha混合没有带宽消耗,低开销的深度/模板测试。

“免费”去除隐藏表面(ImgTec SGX GPU专用),消除所有背景像素,消除过绘制,仅用于不透明。

移动与控制台:OpenGL ES API 的CPU开销非常大,100-300个绘图调用时的最大CPU使用率,避免每个场景的数据过多,顶点和像素处理之间的参数缓冲区,节省带宽和GPU刷新,着色器打包,某些渲染状态会导致着色器被驱动程序修改和重新编译,例如alpha混合设置、顶点输入、颜色写入掩码等。

Alpha测试/丢弃:有条件的z写入可能非常慢,在像素着色器确定当前像素的可见性之前,“像素设置”(PDM) 不会提交更多片段,而不是提前写出Z。使用alpha-blend而不是alpha-test,使几何体适合可见像素。

渲染缓冲区管理:

  • 每个渲染目标都是一个全新的场景,避免来回切换渲染目标!

  • 可能导致完全恢复(restore):在场景开始时将全部颜色/深度/模板从RAM复制到Tile Memory。

  • 可能导致完全解析(resolve):在场景结束时将完全的颜色/深度/模板从Tile Memory复制到RAM。

  • 避免缓冲区恢复。清除一切:颜色/深度/模板,清除只是在寄存器中设置一些脏位。

  • 避免缓冲区解析。使用丢弃扩展(GL_EXT_discard_framebuffer)。

  • 避免不必要的不同FBO组合,不要让驱动程序认为它需要开始解析和恢复任何缓冲区!

纹理查找:不要在像素着色器中执行纹理查找!让“pre-shader”提前排队,即避免依赖纹理查找。不要用数学操作纹理坐标,将所有数学运算移至顶点着色器并向下传递。不要将.zw组件用于纹理坐标,将作为依赖纹理查找处理,仅使用.xy并在.zw中传递其它数据。

移动端材质系统:完整的虚幻引擎材质太复杂,初步构想是预渲染为单个纹理,目前的解决方案是将组件预渲染为单独的纹理,添加特定于移动设备的设置,由艺术家推动的功能支持。

移动材质着色器:一个手写的uber shader,所有功能都有很多#ifdef,在艺术家UI中显示为固定设置,如复选框、列表、值等。

着色器离线处理:离线运行C预处理器,减少游戏内编译时间,在离线时消除重复。

着色器编译:启动时编译所有着色器,避免在运行时挂起在GL线程上编译,在Game线程上加载,编译还不够,必须触发虚拟draw call!记住某些状态如何影响着色器!可能需要尝试避免着色器补丁,例如alpha混合状态、颜色写入掩码。

文中谈及了耶稣光的渲染和具体步骤。其移动端优化包含:

  • 将所有数学运算移至顶点着色器。没有依赖纹理读取!
  • 通过插值器传递数据。但插值器数量有限。
  • 将径向过滤器拆分为4个绘制调用,4x8 = 总共32次纹理查找(相当于 256 次)。
  • 从30毫秒缩短到5毫秒。

文中还涉及了角色阴影。投影、调制的动态阴影,相当标准的方法,生成阴影深度缓冲区,模板潜在像素,比较阴影深度和场景深度,使受影响的像素变暗。具体步骤:

  • 从光源角度投影角色深度。
  • 重新投影到相机视图中。
  • 与SceneDepth进行比较并进行调制。
  • 在顶部绘制角色(无自阴影)。

阴影优化:

  • 帧中阴影深度优先。避免渲染目标切换(解析和恢复!)

  • 解析阴影之前的场景深度:

    • 将tile深度写入RAM以作为纹理读取。
    • 保持在同一个tile中渲染。
    • 不幸的是,OpenGL ES中没有这方面的API。
  • 优化阴影的颜色缓冲使用:

    • 只需要深度缓冲区!
    • 不必要的缓冲区,但在OpenGL ES中是必需的。
    • 清除(避免恢复)并禁用彩色写入。
    • 使用glDiscardFrameBuffer()避免解析。
    • 可以用F16/RGBA8颜色编码深度。
  • 绘制屏幕空间四边形而不是立方体,避免依赖纹理查找。

工具提示:在PC上使用OpenGL ES包装器。几乎“所见即所得”,在Visual Studio中调试。Apple Xcode GL调试器,iOS 5,完整捕获一帧,在单独的窗格中显示每个绘制调用、状态,显示每个drawcall使用的所有资源,显示着色器源代码+所有统一值。

ImgTec 6xxx 系列:100+ GFLOPS(可扩展到 TFLOPS 范围),DirectX 10、OpenGL ES “Halti”,PVRTC 2,提高内存带宽使用率,改进的延迟隐藏。

Accelerate your Mobile Apps and Games for Android™ on ARM由Arm呈现,讲解了在Android系统上如何优化App的技术。

移动应用程序需要特殊的设计考虑,这些考虑因素并不总是很清楚,解决日益复杂的系统的工具也很有限。动画和游戏丢帧,联网、显示、实时音视频处理耗电,应用程序不适合内存限制。幸运的是,谷歌、ARM 和许多其他公司正在开发这些问题的分析工具和解决方案。应用程序是CPU/GPGPU受限、I/O 或内存受限或省电的吗?能用什么方式来修复它?

Java SDK Android应用分析:使用SDK Lint工具进行静态分析,使用DDMS进行动态分析(分配/堆、进程和线程利用率、Traceview(方法)、网络),层次结构查看器,系统跟踪。

这个性能瓶颈是否可并行化?是Java还是Native? 反过来会更好吗?以前有这样做过吗? 不要重新发明轮子。对资源使用很智能吗?应该针对哪个版本的Android?静态分析:LINT(下图):

超越静态分析Dalvik Debug Monitor Server (DDMS),DDMS 线程分析(类似“top”但更好):

DDMS:Traceview追踪每种方法消耗多少CPU时间。

分配和HEAP是否以高频方法进行分配:

Dumpsys gfxinfo:将dumpsys数据列放入电子表格并可视化……例如动画是否会掉帧:

Systrace:已经尽所能在应用程序内部进行分析,但仍然找不到瓶颈,Systrace来救援!Systrace.py将生成一个5秒的系统级快照:


ARM DS-5社区版免费Android原生分析器和调试器:

流线型的概览:

Mali GPU的图形分析工具:

应用程序资源优化器 (ARO),免费/开源以网络为中心的诊断工具(由AT&T提供,但不需要AT&T设备),pcap/数据收集需要root,设备上的APK,用于捕获数据分析的Java桌面应用程序。

网络资源:

  • 关闭连接。80% 的应用程序在完成后不会关闭连接,LTE功率增加38%(3G功率增加18%)。
  • 缓存数据。17 的移动流量是重复下载相同的未更改HTTP内容 ,“这只是一个6 KB的徽标”——6KB * 3DL/会话 * 10000个用户/天 = 3.4GB/月,从本地缓存读取比从Web下载快75-99%,即使支持缓存 - 默认情况下它是关闭的。
  • 管理每个连接。将连接分组,节省电池,加快应用程序。

缓存方法:每个文件都有一个唯一标签,在服务器上为每个请求重新验证。高性能网站:规则1 – 减少HTTP请求 ,添加连接会耗尽电池电量,增加500-3000毫秒的延迟。仔细分配Max-Age时间很重要,在达到Max-Age之前,应用程序不会检查服务器上的文件,检索是严格的文件处理时间。

分组连接:下图,红色:每60秒下载一张图片,蓝色:每60秒下载一个广告,绿色:每60秒向服务器发送一次分析。

未分组:使用了38J的能量,分组:使用了16J的能量,节省58%的电量!!

其它网络优化:删除对文件的重定向,它们每次请求大约2-3秒,预取常用文件,线程文件下载而不是串行下载,不应出现4xx、5xx的http响应错误代码,小心定期连接,定期3分钟轮询更新可能会在一天中保持连接1.2 小时,消耗大约20%的电池电量。

适用于ARM的原生开发套件 (NDK):NDK是一个全面的工具包,使应用程序开发人员能够直接为ARM处理器编写。

SMP和并行化:当今市场上几乎所有的Android和移动设备都是多核的,而且这一趋势将继续下去——设计多线程应用程序Davlik Java线程和IPC,AsyncTask通常是将任务快速推送到后台工作线程的最简单方法,且IPC复杂性极低,仿生C库实现了Pthreads API的一个版本,大多数pthread和sem_函数都已实现,但没有SysV IPC,如果它在pthread.h 或 semaphore.h中声明,它将大部分按预期工作。

OpenGL ES 2.0支持可编程嵌入式GPU的完全可编程3D图形,免版税的跨*台API,2D和3D图形,支持Android的框架API和NDK。

为移动/嵌入式/电池编写java:新的方法,应避免,至少不在CPU密集/频繁的活动中,尝试使用静态变量或仅预先分配或在活动的自然停顿时分配,避免触发垃圾收集(使用DDMS)。在JellyBean中为图形使用新功能,例如用于垂直同步脉冲的android.view.Choreographer、myView.postInvalidateOnAnimation(),不要画不会显示的东西c.quickReject(items...)、Canvas.EdgeType.BW。

SIMD: NEON——适用于许多应用的通用SIMD处理,支持用于互联网应用的最广泛的多媒体编解码器,许多软编解码器标准:MPEG-4、H.264、On2 VP6/7/8、Real、AVS、……在软件中支持所有互联网和数字家庭标准。更少的周期,NEON将在复杂的视频编解码器上提供 1.6x-2.5x 的性能,单个简单的DSP算法可以显示更大的性能提升 (4x-8x),处理器可以更快地休眠 => 整体动态节能。直接编程,干净的正交向量架构,适用于广泛的数据密集型计算。不仅适用于编解码器 - 适用于2D/3D图形和其他处理,32个寄存器,64位宽(双视图为16个寄存器,128位宽),现成的工具、操作系统、商业和开源生态系统支持。

线程文件下载(左)与串行下载(右)的对比。

OpenGL ES 3.0 - Challenges and Opportunities分享了OpenGL ES 3.0在移动端的特性、应用和优化。

OpenGL ES是嵌入式系统的开放式图形库,图形硬件的底层软件接口,OpenGL的子集,OpenGL ES驱动的GPU的各种使用,用于智能手机/*板电脑、电视机、汽车等等。桌面GPU计算在移动设备中还需要多长时间?计算能力和带宽速度曲线及趋势预测如下:


OpenGL ES 3.0于2012发布规范,基于OpenGL3.3/4.x的功能集,减少扩展需求,完全向后兼容OpenGL ES 2.0。着色语言GLSL ES 3.00模式如下:

ETC2纹理压缩是标准纹理压缩,支持Alpha、1或2个通道,消除ETC1的限制(不支持Alpha、质感差),理论上不再需要专有纹理格式,更小的文件大小,没有不同的资产包。

布尔遮挡查询:用于基于硬件的可见性测试的软件接口:

glGenQueries
glDeleteQueries
glBeginQuery
glEndQuery
glGetQueryObjectuiv
...

int qid[NUM_OBJECTS];
unsigned int result = 0;
// 生成查询
glGenQueries(NUM_OBJECTS, &qid[0]);

for (int i = 0; i < NUM_OBJECTS; ++i) 
{
    // 开始查询
    glBeginQuery(GL_ANY_SAMPLES_PASSED, qid[i]);
    // 以低细节渲染物体.
    RenderOccluders();
    // 结束查询.
    glEndQuery(GL_ANY_SAMPLES_PASSED);

    // 等待查询结果有效.
    // 注意:这种同步等待结果的方式会导致【严重的性能问题】!!
    while (result == GL_FALSE) 
    {
        glGetObjectuiv(qid[i], GL_QUERY_RESULT_AVAILABLE, &result);
    }
    
    // 获取结果。
     glGetObjectuiv(qid[i], GL_QUERY_RESULT, &result);
    if (result == GL_TRUE) 
    {
        // 以全精度渲染物体。
        RenderObjects();
     }
 }

... 

遮挡查询的CPU和GPU时序图。

ES3比2的遮挡查询能够有明显的提升。

实例化渲染:尽量减少绘图调用,强大的具有大量相同几何图形的场景,相关接口:

glDrawArraysInstanced(GLenum mode, Glint first, GLsizei count, GLsizei primcount)
glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei primcount)
glVertexAttribDivisor(GLuint index, GLuint divisor)
gl_InstanceID 

可能难以在现有(2013年)渲染管道中实现。

// OpenGL ES 2.0
for ( int i = 0; i < numInstances; i++ ) 
{
    // set for each instance the model-view-projection matrix
    glDrawElements(GL_TRIANGLES,mesh->indx_count,GL_UNSIGNED_SHORT,mesh->indx);
} 

// OpenGL ES 3.0
glDrawElementsInstanced(GL_TRIANGLES,mesh->indx_count,GL_UNSIGNED_SHORT,mesh->indx, numInstances); 
// 非实例化渲染
// Vertex shader
#version 100
uniform mat4 u_matViewProjection;
attribute vec4 a_position;
attribute vec2 a_texCoord0;
varying vec2 v_texCoord;

MVP = glGetUniformLocation( programObj, "u_matViewProjection" );
glUniformMatrix4fv(MVP, 1, GL_FALSE, &mvpMatrix ); 

// 实例化渲染
// Vertex shader
#version 100
// uniform -> attribute
attribute mat4 u_matViewProjection;
attribute vec4 a_position;
attribute vec2 a_texCoord0;
varying vec2 v_texCoord;

// glGetUniformLocation -> glGetAttribLocation
MVP = glGetAttribLocation( programObj, "u_matViewProjection" );
// 填充实例化数据.
for (int i = 0; i < 4; i++) 
{
    glEnableVertexAttribArray(MVP + i);
    glVertexAttribPointer(MVP + i,
        4, GL_FLOAT, GL_FALSE, // vec4
        16*sizeof(GLfloat), // stride
        &(matArray + 4*i*sizeof(GLfloat)); // offset
    glVertexAttribDivisor(MVP + i, 1);
}

#define LTP_ARRAY     0
#define VERTEX_ARRAY 4

layout(location = LTP_ARRAY)    in highp mat4 inLocalToProjection;
layout(location = VERTEX ARRAY) in highp vec3 inVertex;

void main()
{
    gl_Position = inLocalToProjection * inVertex;
}

多重渲染目标(MRT)在一次绘制调用中渲染到多个缓冲区,提供在下一代中执行视觉效果的可能性,如延迟照明、细胞阴影、延期贴花、实时局部反射......

// C++
...
unsigned int fb;
unsigned int initializedTexture2D_1;
unsigned int initializedTexture2D_2;
GLenum buffs[] = {GL_COLOR_ATTACHMENT0,
GL_COLOR_ATTACHMENT1};
glGenFrameBuffer(1, &fb);
glBindFramebuffer(GL_FRAMEBUFFER, fb);
glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0, GL_TEXTURE2D, initializedTexture2D_1, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0, GL_TEXTURE2D, initializedTexture2D_2, 0);
glDrawBuffers(2, buffs);
// render calls
...
    
// fragment shader
#version 300 es
layout(location = 0) out lowp vec4 color;
layout(location = 1) out highp vec4 normal;
in lowp vec4 v_color;
in highp vec4 v_normal;

main() 
{
    color = v_color;
    normal = v_normal;
} 

OpenGL ES 3.0特性的消耗收益比。

挑战:

  • 现有引擎中的实现并非易事。
  • 需要对产品管线进行改造。
  • 在某些情况下,OpenGL ES 3.0特性不会获得更好的性能。
  • 图形部门也需要理解MRT。
  • OpenGL ES 3.0设备目前比较少同时支持ES2/ES3。

机会:

  • 更好的性能。
  • 更小的能耗。
  • OEM喜欢看到开发人员使用的最新技术。
  • 当前控制台和移动设备之间的差距越来越小。
  • 通过扩展,一些3.0功能可在当前的通用硬件上使用。

2013年,大型多人在线游戏 (MMOG) 等视频游戏已成为文化媒介,手机游戏为应用市场带来大量下载和潜在收益。尽管移动设备的处理能力增加了带宽传输,但网络连接不佳可能会成为游戏即服务 (GaaS) 的瓶颈。为了提高数字生态系统的性能,处理任务分布在瘦客户端设备和强大的服务器之间,An Architecture Approach for 3D Render Distribution using Mobile Devices in Real Time基于“分而治之”的方法,即使用游戏中场景序列的树KD对体积曲面进行细分,从而将曲面缩减为小点集。提高了重建效率,因为数据的搜索是在局部和小区域中进行的。流程通过一组有限状态建模,这些状态使用隐马尔可夫模型构建,域由启发式配置。进行了六个控制每个启发式状态的测试,包括间隔数,以验证所提出的模型。该验证得出的结论是,所提出的模型在一系列交互中优化了每秒的响应帧。

节点的架构全连接(遍历)模型。

桌面虚拟现实(Virtual Reality,VR)历来是消费级3D计算机图形的主要显示技术。*来,立体视觉和头戴式显示器等更复杂的技术已变得更加普及。然而,大多数3D软件仍然仅设计用于支持桌面VR,并且必须进行修改以在技术上支持这些显示器并遵循其使用的最佳实践。Virtual Reality Capabilities of Graphics Engines评估了现代3D游戏/图形引擎,并确定了它们在多大程度上适应不同类型的负担得起的VR显示器的输出,表明立体视觉得到了广泛的支持,无论是原生还是通过现有的适应。其它VR技术,如头戴式显示器、头部耦合透视(以及随之而来的鱼缸VR)很少得到原生支持。但是,该文确定并描述了一些方法,例如重新设计,通过这些方法可以添加对这些显示技术的支持。

2013年虚拟现实显示技术有桌面VR(串流)、立体视觉、头部耦合透视、头戴式显示器等几种,它们在模拟模型和用户感知方面的差异如下图:

立体视觉(Stereoscopy)是适用于双目视觉的桌面VR范式的扩展。立体镜通过两次渲染场景来实现这一点,每只眼睛一次,然后以这样的方式对图像进行编码和过滤,使每张图像只能被用户的一只眼睛看到。这种过滤最容易通过特殊的眼镜实现,眼镜的镜片设计为选择性地通过匹配显示器产生的两种编码之一。当前的编码方法是通过色谱、偏振、时间或空间。这些编码方法经常被分类为被动、主动或自动立体。被动和主动编码之间的区别取决于眼镜是否是电主动的:因此被动编码系统是颜色和极化,而唯一的主动编码是时间。自动立体显示器是不需要眼镜的显示器,因为它们在空间上进行编码,这意味着眼睛之间的物理距离足以过滤图像。

消费者立体显示器与计算机的接口方式与桌面VR显示器相同(通过VGA或DVI等视频接口)。由于这些接口中的大多数都没有特殊的立体观察模式,因此将两个立体图像以显示硬件可识别的格式打包成一个图像。此类帧封装格式包括交错、上下、并排、2D+深度和交错。由于这些标准化接口是软件将渲染图像传递给显示硬件的方式,因此软件应用程序不需要了解或适应编码系统的显示硬件。相反,图形引擎支持立体透视所需要的只是它能够从不同的虚拟相机位置渲染两个具有相同模拟状态的图像,并将它们组合成显示器支持的帧封装格式。

头部耦合透视(Head-coupled perspective,HCP) 的工作原理与桌面VR和立体视觉略有不同, 定义了一个虚拟窗口而不是虚拟相机,其边界是虚拟的窗口映射到用户显示器的边缘。因此,显示器上的图像取决于用户头部的相对位置,因为来自虚拟环境的对象会沿用户眼睛的方向投影到显示器上。这种投影可以使用桌面VR中使用的投影数学的离轴版本来完成。

为了做到这一点,必须实时准确地跟踪用户头部相对于显示器的位置。用于此目的的跟踪系统包括电枢、电磁/超声波跟踪器和图像- 基于跟踪。HCP的一个限制是,由于显示的图像取决于用户的位置,因此任何其他观看同一显示器的用户将感知到失真的图像,因为他们不会从正确的位置观看。

头戴式显示器(Head-mounted display,HMD)是另一种单用户VR技术,将立体视觉的增强功能与类似于HCP的大视场和头部耦合相结合。HMD背后的感知模型是完全覆盖用户眼睛的视觉输入,并将其替换为虚拟环境的包含视图。通过将一个或两个小型显示器安装在非常靠*用户眼前的镜头系统来实现的,以实现更自然的聚焦。由于显示器非常靠*用户的眼睛,显示器的任何部分只有一只眼睛可见,使系统具有自动立体感。

头饰中还嵌入了一个方向跟踪器,允许跟踪用户头部的旋转,允许用户通过将虚拟相机的方向绑定到用户头部的方向来使用自然的头部运动来环顾虚拟环境。它与HCP不同,HCP跟踪的是位置,而不是方向。支持HMD的软件要求与立体观察相同,但附加要求是图形引擎必须考虑HMD的方向,以及要校正的镜头系统引起的任何失真。

通过确定可以使用哪些扩展机制来实现所需的VR显示技术来衡量支持级别,已经结合了差异可以忽略不计的扩展机制(例如脚本和插件),并引入了两个额外的级别,不需要扩展(本机支持)和没有引擎内支持(重新设计)。扩展机制按引擎代码相对于实现VR支持的非引擎代码的比例排序,产生的支持级别及其排序如下:

5、原生支持。在原生支持VR技术的引擎中,引擎的开发人员特意编写了渲染管线,使用户只需最少的努力即可启用VR渲染。所需要做的就是检查开发人员工具中的选项或在引擎的脚本环境中设置变量。除了轻松启用该技术外,这些引擎还旨在避免常见的优化和快捷方式,这些优化和快捷方式在桌面VR显示器中并不明显,但随着更复杂的技术变得明显,一个常见的例子是渲染具有正确遮挡但深度不正确的对象,会导致立体镜下的深度提示冲突。

4、通过引擎内图形定制(包括节点图)。一些引擎的设计方式使得可以使用具有图形界面的自定义工具来更改渲染过程,一种方法是通过节点图,其中渲染管道的不同组件可以在多种配置中重新排列、修改和重新连接。根据支持的节点类型,有时可以配置节点以产生某些 VR 技术的效果。下图显示了虚幻引擎的材质编辑界面,该界面配置为将红青色立体立体渲染作为后处理效果。

3、通过引擎内编码(脚本或插件)。每个引擎都可以使用自定义代码进行扩展,使用定义明确但受限制的扩展点。两种常见的形式是在受限环境中运行的脚本,以及引擎加载并运行外部编译的代码插件,两种形式都可以访问引擎功能的子集,但是,插件也可以访问外部API,而脚本不能。由于通常实现特定于应用程序功能的机制,因此可用于自定义代码的引擎功能可能更多地针对人工智能、游戏逻辑和事件排序,而不是控制确切的渲染过程。

2、通过引擎源代码修改。除了免费的开源引擎,一些商业引擎通过适当的许可协议向用户提供其完整的源代码。通过访问完整的源代码,可以实现任何VR技术,尽管所需的修改量可能很大。

1、通过工程改造。对于不提供上述任何定制入口点的引擎,仍然可以通过重新设计进行一些更改。工程改造是逆向工程的一种形式,除了学习程序的一些工作原理之外,还修改了它的一些功能。对渲染管道进行完全逆向工程所需的工作量可能很大,因此更可取的是微创形式的再工程。其中一种方法是函数挂钩,即内部或库函数的调用被拦截并替换为自定义行为。由于很大一部分实时图形引擎使用OpenGL或Direct3D库进行硬件图形加速,因此这些库为通过函数挂钩实现纯视觉VR技术提供了可靠的入口点。事实证明,这种方法可以有效地将立体视觉添加到3D游戏。本文还展示了以这种方式实现头耦合透视也是可能的,通过挂钩加载投影矩阵(glFrustum和glLoadMatrix)的OpenGL函数,并用头部耦合矩阵替换原始程序提供的固定透视矩阵。

影响用户体验的因素有很多,虽然质量因素本质上与显示硬件相关,但适当的软件设计可以缓解这些问题,而粗心的设计可能会引入新问题。可以通过软件减轻的硬件质量因素的示例是串扰(立体)、A/C故障(立体)和跟踪延迟(HCP和HMD)。由于这些因素对于它们各自的显示技术来说是公认的,因此有众所周知的技术可以最大限度地减少它们引起的问题。解决方案分别是降低场景对比度、降低视差和最小化渲染延迟。

不正确的软件实现也会影响VR效果的质量,可能是由于粗心或桌面VR优化的结果。这方面的一个示例是任意位置的特殊图层(例如天空、阴影和第一人称玩家的身体)不同通道的深度。虽然在桌面VR中产生正确的遮挡,但在立体镜下添加双目视差提示会显示不正确的深度,并在这两个深度提示之间产生冲突。由于桌面VR的主导性质,这不是一个不常见的问题,并且可以作为另一个例子,说明简单的第三方实现可能不如原生VR支持。从这些方面应该注意到,虽然非原生VR实现可能满足必要的技术要求,但也必须考虑其它因素。

下表是2013年的主流引擎对VR的支持情况:

引擎 立体视觉 头部耦合透射 头戴式显示器
UDK 4:图形定制。可以使用Unreal Kismet创建双摄像头装备,并使用材质编辑器打包输出。 1:工程改造。无法从引擎访问自定义相机投影,因此如果无源代码访问权限,则需要工程改造。 3:引擎编码。通过自定义实现立体化,可以通过自定义DLL获得头部方向并通过脚本绑定到相机。
Unity 3:引擎编码。 3:引擎编码。 3:引擎编码。
CryENGINE 5: 原生。 3:引擎编码。 3:引擎编码。
OGRE 3:引擎编码。 3:引擎编码。 3:引擎编码。

Developing Virtual Reality Experiences with the Oculus Rift详细地分享了在Oculus Rift上进行VR开发的技术、过程及实操。

2014年的Rift技术包含开发工具包2,1920x1080 OLED屏幕,每只眼睛一半,广角圆形透镜,90-110度FOV,GPU可校正镜头变形,低持久性–每个像素每帧的亮度小于3毫秒,1000Hz陀螺追踪朝向,60Hz位置跟踪:外部摄像头可以看到HMD上的LED阵列,软件融合、方向和位置预测。

善待VR玩家:虚拟现实开发者每天花数小时观看HMD,大部分时间,到处都会有bug,大脑很快就会学会忽视疯子,但玩家不会!他们的大脑新鲜而天真,他们希望事情是真实的,希望你已经调试了所有东西,并且拥有真正的“存在感”。如果你把每件事都推到11,你会给他们带来创伤,他们会停止玩游戏,给你一星的评价!

每个人都大相径庭,有些人无法忍受的事情,而其他人甚至看不见,没有一个“VR公差”滑块,对一个方面非常敏感的人可能会容忍另一个方面,例如上下楼梯,宽容不仅仅是一种可以学习的技能,可能会有负面反馈:人们对暴露的容忍度会降低。最佳实践指南包含当前已知道的内容,将其用作至少需要认真思考的事项清单。

温和错误,过于激烈的虚拟现实让人更难理解剧情和游戏机制,让紧张体验成为可选,尽量避免“在你脸上”的粒子和爆炸,更少、更慢的移动。默认低难度,让更有经验的VR用户“选择加入”,而不是让新手“选择退出”,便于随时更改,在受“VR打击”之后,允许降低强度以实际玩游戏。

前庭光学反射(Vestibulo-Optical Reflex,VOR):眼球和肌肉、反射神经元、耳内半圆管等部位和交互如下图:

用于“固定”,如静止物体、移动头部,通过耳朵检测头部旋转,<10毫秒后,眼睛转动顺畅,不是扫视!非常*滑,卓越的视觉质量。

VOR增益是耳朵运动和眼睛反应之间的比率,通常给予1:1的补偿,+10⁰头部运动 = -10⁰眼动,在固定过程中获得微调,尝试产生零“视网膜流”,调整速度非常慢。

如果视图被压缩怎么办?一副新眼镜,VR中的渲染比例不正确,10⁰头部运动现在需要-5⁰保持注视的眼球运动,VOR增益现在导致视网膜流,导致定向障碍,增益适应需要1-2周(假设持续使用!)。

保持VOR增益:显示器上的游戏通常有一个“FOV”滑块,监视器上可接受–不会直接影响VOR增益,显示器不会随头部移动——不会发生“虚拟注视”,房间周边视觉提供真实光流真实性检查…但即便如此,它也会给一些人带来问题。在Rift中,唯一需要关注的是虚拟现实,VR对象的视网膜流必须与真实世界的运动相匹配。虚拟现实中的视场比例不是任意选择!它必须与HMD+用户特征相匹配,“医生,当我这样做的时候,会伤害我的玩家的大脑……”

Rift显示屏有一个物理间距,即“每可见度像素数”,准确值取决于失真、用户的头部和眼睛位置等。通过用户配置工具找到,SDK将帮助您精确匹配此音高,对于给定的设备和用户大小,它将提供正确的视野和比例,避免任何改变视野或“缩放”效果,头部旋转10度必须产生10度光流,即使每度像素的微小变化也会给大多数用户带来问题。

IPD——瞳孔间距,实际上每只眼睛有两种成分:从鼻子到瞳孔——“半IPD”,视距——从透镜表面到瞳孔的距离,与HMD的尺寸无关!从中心到眼向量,在用户配置期间设置,存储在用户配置文件中。很少对称,有点人的视距可能相差2毫米,下图的那个家伙的鼻子和瞳孔相差1个像素:

中央瞳孔-SDK报告的位置,头盔显示器的中心线,左右视距的*均值。大致是玩家“感觉”到自己的位置,音频侦听器位置,视线检查,十字线/十字线光线投射的原点。

保持帧率:存在是一个相当二元的东西——有或没有,坚如磐石的高FPS对虚拟现实中的存在感至关重要,以75FPS的速度进行立体声显示很有挑战性,积极删除细节和效果,以保持帧速率和低延迟,保持状态给玩家带来的乐趣远远超过额外效果,主要成本是绘制调用和填充率。

对于绘制调用,双倍的眼睛,双倍的调用,新的API应该可以降低多次提交的成本:Mantle(Vulkan的前身)、DX12等。有些事情只需要做一次:剔除——使用包括双眼的保守*截体、动画、阴影缓冲区渲染、一些远距离反射/光泽贴图/AO渲染——但不是全部!一些延迟照明技术。

对于填充率,更改虚拟相机渲染的大小,而不是帧缓冲区大小,例如对于DK2,帧缓冲区始终为1080x1920–不要更改此设置!但相机眼睛通常每只眼睛渲染1150x1450,取决于用户面部形状和眼睛位置——由个人资料和SDK设置。缩放此渲染效果非常好,失真校正过程将对其进行重新采样和过滤,每帧动态缩放也很好——几乎看不见。如果该帧中有大量粒子/爆炸,请降低大小。使用相同的RT,只需使用一小部分即可,SDK明确支持此用例。

Virtual Reality and Getting the Best from Your Next-Gen Engine阐述了如何在游戏引擎种集成VR渲染。文中提到可调节的眼镜佩戴者:

无需调整即可耐受瞳孔间距的变化:

下图则是关键的(上)和可容忍的参数示意图:

对于立体3D而言,在摄影立体和头盔显示器的固定设置下的所需的焦距要求:

结合下图,(a)大多数HMD的视野都很窄,(b)实现宽视场需要更高分辨率的显示器,(c)或更大的像素。(d)如何做到两全其美?利用眼睛的可变敏锐度,(e)使用扭曲着色器压缩图像的边缘,(f)光学元件应用反向失真,使边缘看起来再次正确,(g)中心像素较小,边缘像素较大。

VR开发需要注意以下事项:

  • 不要控制玩家的头部!
  • 注意第一人称动作。
  • 照片现实主义是没有必要的。
  • 不要使用电影级的渲染效果!比如可变焦距、过滤器、镜头光斑、泛光、胶片颗粒、暗角、景深等。

立体渲染质量检查:

  • 左右方向正确吗?
  • 双眼中的元素相同吗?
  • 两幅图像代表同一时间吗?
  • 刻度正确吗?
  • 深度一致吗?
  • 避免快速深度变化了吗?

如果要在游戏引擎集成VR功能,需要注意的是每个引擎都是不同的,但总会有一些相似之处。什么是良好的虚拟现实引擎?答案如下:

  • 高质量的视觉效果。

    所说的高质量视觉效果是什么意思?没有任何东西会分散你的注意力,让你沉浸在游戏中,良好的着色效果(但不一定是真实照片),通常意味着良好的抗锯齿。

    为什么良好的抗锯齿至关重要?人类感知的本质意味着我们很容易被高频噪点分心,分心会降低存在感,使用立体渲染时,锯齿伪影可能会更严重,它们会导致视网膜竞争,良好的抗锯齿比本机分辨率更重要。

    抗锯齿方法有:边缘几何AA,通常硬件加速;图像空间AA,非常适合大多数渲染管线,如FXAA、MLAA、SMAA等;时间AA,使用再投影进行时间超采样。


    MSAA在高频几何体、几乎垂直的线条、对角线看起来更好,但内部纹理/着色仍然有锯齿。


    FXAA在边缘几何体、纹理/着色细节看起来更好,但有时会丢失高频数据中的细节。

    超采样反走样渲染到更大缓冲区的效果良好……如果负担得起的话,与良好的下采样过滤器一起使用。

    抗锯齿结果:镜面AA也可以大大改善图像,一个很好的起点是研究LEAN、Cheap LEAN (CLEAN)和Toksvig AA,扭曲着色器减少边缘锯齿,在某些游戏中,可能需要更多地关注LOD。

    几种AA方法的组合可能会产生更好的结果,每种不同的AA解决方案都能解决不同方面的锯齿问题,使用最适合引擎的方法。

  • 一致的高帧速率。

    为什么一致的高帧速率至关重要?在虚拟现实中,低帧速率看起来和感觉都很糟糕,如果没有高帧率,测试就很困难。在整个开发过程中保持高帧率,缺少V-sync也更为明显,因此,请确保启用了V-sync。

    在当前的引擎中,“通道”的概念被广泛接受,如反射渲染、阴影渲染、后处理等,每个通道都有不同的要求,每个通道都要找出瓶颈所在。CPU?DrawCall?状态设置?资源设置?GPU?顶点处理受限?几何处理受限?像素处理受限?

    绘制调用、状态设置或资源设置时CPU受限?考虑如何使用几何着色器,可以减少绘制调用的总数,阴影级联渲染:drawCallCount/n,其中n是层叠的数量,立方体贴图渲染:drawCallCount/6。降低资源设置成本,它还有其它特性可以帮助将处理从CPU上移开。

    几何渲染单元将一个图元流转换为另一个可能更大的图元流,在像素着色器之前发生,即在直接顶点像素绘制调用中的顶点着色器之后,如果启用了细分,则在Hull着色器之后。

    几何体着色器功能,渲染目标索引/视口索引,用于单程立方体贴图渲染、阴影级联、S3D、GS实例,允许逐图元运行同一几何体着色器的多次执行,而无需再次运行上一个着色器阶段。

    用于立体3D渲染的几何体着色器,一种使引擎立体3D兼容的简单方法,为每种材质添加一个GS(或调整已有材质的GS),如下所示:

    [maxvertexcount(3)]
    void main(
        inout TriangleStream<GS_OUTPUT> triangleStream,
        triangle GS_INPUT input[3])
    {
        for(uint i = 0; i < 3; ++i)
        {
            GS_OUTPUT output;
            output.position = (input[i].worldPosition , g_ViewProjectionMatrix);
            triangleStream.Append(output);
        }
    }
    

    顶点/几何体受限?通过压缩属性来减少顶点大小,在着色器阶段之间打包所有属性,如果正在使用用于放大或细分管道的几何体着色器,这一点很重要。考虑使用延迟获取(late fetch)法:将顶点属性数据绑定为使用的着色器阶段中的缓冲区,高度依赖硬件,始终调试性能,看看是否有影响!减少在GPU周围移动的数据。

    像素受限?降低像素着色器的复杂性,减少每帧着色的像素数,一个使用较小渲染目标的实验上采样与高质量视觉冲突,引入光晕、微光和视网膜冲突。

    考虑使用重新投影来加速立体3D渲染的方面,在PlayStation 3立体声3D游戏中获得巨大成功,然而,它只能在视差较小的情况下成功使用。

  • 出色的跟踪和标定。

    项目Morpheus SDK处理跟踪,使用SDK提供的跟踪矩阵。游戏定义的默认观看位置和方向:

    追踪玩家头部与摄像机的偏移量:

    玩家眼睛相对于头部矩阵的偏移量:

    跟踪器重置功能:设置头部位置,使其与游戏摄像机的位置和偏航对齐。

    标定:重置位置和方向跟踪,重新调整游戏世界与现实世界的关系,以便固定的玩家位置,传递和游戏(Pass-and-play),匹配不同身高的玩家。

  • 低延迟。

    为什么减少延迟如此重要?延迟是输入和响应之间的时间间隔,重要的不仅仅是始终如一的高帧率。不仅用于虚拟现实头部跟踪,提高响应能力在游戏中至关重要,游戏编程人员了解响应控制的必要性,网络程序员了解对响应性对手的需求等等。

    多上下文渲染:从引擎的角度来看,减少延迟的一种方法是使用延迟上下文在多个线程上异步构建命令列表(又称命令缓冲区),作为即时上下文,在命令缓冲区中将命令排队时会产生渲染开销,相比之下,在回放期间,命令列表的执行效率要高得多,适用于“通道”的概念。多上下文渲染允许GPU在帧中更早地开始处理,从而减少延迟。

    单上下文(上)和多上下文(下)渲染的对比。

    多上下文渲染最简单的测试用例:为每只眼睛的视图并行创建和提交命令列表,立即减少CPU帧时间,如果引擎受CPU受限,意味着帧延迟会立即减少。如果引擎是GPU受限,但GPU现在在帧中完成得更早,因为它启动得更早,意味着帧延迟立即减少。

    虚拟现实的延迟考虑:是否有任何特定于虚拟现实的方法来应对延迟?采样跟踪数据和使用该数据渲染帧之间的时间需要尽可能短,不要使用超过两倍的缓冲,使用最新的方向数据重新投影图像可以改善明显的延迟和帧速率。尽可能降低被跟踪外围设备的延迟,是否有任何特定于*台的方法来应对延迟?

    预测(Prediction):带有Project Morpheus的PlayStation 4是一个已知的系统,硬件中存在任何延迟,库/软件中存在任何延迟,需要想方设法减少这些延迟。提供CPU和GPU性能分析工具,使开发者能够计算并减少游戏中的延迟,可以用它来预测图像显示时HMU的位置。减少引擎延迟是关键,但使用预测来掩盖任何微小的剩余延迟都可以很好地发挥作用,指定的预测量越小,其质量越好。

假设现在拥有一个非常高效、高帧率、低延迟、超高质量的下一代引擎中拥有了出色的跟踪功能,该引擎针对虚拟现实进行了优化……引擎的工作完成了吗?当然不是!还有特定于*台的优化、跟踪外围设备、社交方面、游戏性/设计元素等工作。

异步计算:仍受CPU限制?也许Compute可以帮助将可并行任务卸载到GPU上。仍然受限于GPU?Compute允许你从不同的、更通用的角度来思考GPU任务,在GPU未被充分利用的地方使用它,阴影渲染通常需要顶点/几何体,因此它是安排异步计算任务的好地方。

Diving into VR World with Oculus阐述了Oculus下的VR开发攻略。

虚拟现实中最重要的因素有:短余辉(Low Persistence)、延迟、现实。

Oculus SDK 0.4.x的特色:支持DK1和DK2,新款C接口; 用新款SDK来对游戏进行重新编译。位置跟踪,位置原点目前距离摄像头1米,SDK用预测的Pose状态来报告传感器状态,包括方向、位置和导数,超出跟踪范围时,给出旗标提示。依靠头部模型,Direct-to-Rift和扩展模式,OVR配置工具。

OVR软件堆栈如下图,C接口: 容易与其它语言连接,驱动程序DLL: 自动支持硬件和功能的变更,OVR服务: 在各应用之间的Rift分享和虚拟现实转换。

SDK渲染与游戏渲染的比较:SDK 0.2未做任何渲染,只提供适当渲染所需的参数;SDK 0.4中的新渲染后端,延续了关键的渲染特色,Game(App) layer gives层将SDK左、右眼纹理ovrHmd_EndFrame()。

SDK的一般工作流程:

  • ovrHmd_CreateDistortionMesh。通过UV来转换图像,比像素着色器的渲染效率更高,让Oculus能更灵活地修改失真。
  • ovrHmd_BeginFrame。
  • ovrHmd_GetEyePoses。
  • 基于EyeRenderPose(游戏场景渲染)的立体渲染。
  • ovrHmd_EndFrame。

用后面的特性在SDK 0.4上完成渲染:利用色差和时间扭曲来实现桶形畸变,内部延迟测试及动态额预测,低延迟垂直同步(v-sync)和翻转(用direct-to-rift,甚至会更好),健康与安全警告。

SDK易于集成,无需创建着色器和网格,通过设备/系统指针和眼睛纹理,支持OpenGL和D3D9/10/11,必须为下一帧重新申请渲染状态。好处:与今后的Oculus硬件和特性更好地兼容,减少显卡设置错误,支持低延迟驱动显示屏访问,例如前前缓冲区渲染等,支持自动覆盖:延迟的测试、摄像头指南、调试数据、透视、*台覆盖。

支持Unreal Engine 3、Unreal Engine 4、Unity等主流游戏引擎使用SDK渲染。

扩展模式:头戴设备显示为一个OS Display,应用程序必须将一个窗口置于Rift监视器上,图标和Windows在错误的位置,Windows合成器处理Present,通常有至少一帧延迟,如果未完成CPU和GPU同步,则有更多延迟。

Direct To Rift:输出到Rift,显示未成为桌面的一部分。头戴设备未被操作系统看到,避免跳跃窗口和图标,将Rift垂直同步(v-sync)与OS合成器分离,避免额外的GPU缓冲,使延迟降到最低,使用ovrHmd_AttachToWindow,窗口交换链输出被导向Rift,希望直接模式成为较长期的解决方案。

延迟:Motion-to-photon的延迟,涉及多阶段:动作、传感器、处理与合并、渲染、Scanout、传输、像素变化时间、像素余辉。

将延迟保持在低值是提供良好虚拟现实体验的关键,目标是< 20毫秒,希望接*5毫秒。

渲染延迟- 时间扭曲(TimeWarp):将渲染重新延迟到后面一个时间点,与变形同时进行,减少感受到的延迟,负责DK2滚动快门,SDK 0.4处理方向、位置。在帧结束前,使用传感器是否有其它方式?时间扭曲– 预测的渲染(John首创)。



Programming for Multicore & big.LITTLE阐述了ARM的大小核心的特殊CPU并行架构,可区别地处理不同计算密集度的任务,以达到省电和性能的均衡。

多核和big.LITTLE之多处理的情况:*台趋势:从中端到高端的四核+内核明显增加,一切都在变大——LTE、GPU、摄像头、显示屏,单线程性能改善正在减少——关注多核,这不仅仅关乎性能——热量约束用例现在已经司空见惯。软件趋势:操作系统供应商更多地利用多核,更广泛地了解多处理支持库,增加设备的组合使用,例如增强现实。

利用并行性,在核心内可以使用NEON、SIMD,使用并行的工具(OpenMP、Renderscript、OpenCL等),尽可能地多线程,从来都不容易,但越来越有必要。

2014-2015年的多核心趋势:Cortex-A15/Cortex-A7 big.LITTLE的2014年的高端产品,芯数范围:4(2+2)、6(2+4)和8(4+4)核心,Cortex-A17/Cortex-A7(32b)将于2015年上市。2014年,ARMv8-A(64b)芯片组在所有细分市场中崭露头角,四核和八核Cortex-A53进入入门级和中端,高端移动设备预计将在2015年向A57和A53 big.LITTLE迁移,多个big.LITTLE预期的拓扑结构。新的小型处理器提供与Cortex-A9类似的性能,使用大处理器(如Cortex-A15)显著提升性能。

从程序员的硬件视角看big.LITTLE系统:高性能Cortex-A57 CPU集群,节能Cortex-A53 CPU集群,CCI-400保持集群之间的缓存一致性,GIC-400提供透明的虚拟中断控制。

big.LITTLE:来自4+4MP系统与Quad Cortex-A15的证据:

big.LITTLE开发 / 关于全局任务调度(GTS)的一般建议:

  • 相信调度程序。Linux将为性能和效率制定时间表,所有新任务都从大屏幕开始,以避免延迟,快速适应任务的需要。
  • 除非你知道线程是密集的,但不是紧急的,可以在小核执行则永远不要使用大核,例如可能将小核用于在单独的线程上加载资产。
  • 小核心是伟大的。你会经常使用它们,Cortex-A53的性能比Cortex-A9高20%,大多数工作负载将运行在很少的服务器上,为其它SoC组件提供更多热量空间。
  • 大核心是重要的动力。把它们想象成短脉冲加速器,例如基于物理的特效,在设计过程中考虑权衡。

需要避免:

  • 共享公共数据的不*衡线程。集群一致性很好,但不是免费的。
  • 如果有实时线程,请注意实时线程不是自动迁移的,实时线程是一个设计决策,请仔细考虑亲和性。
  • 避免在大内核上执行长时间运行的任务。很少需要长时间的处理能力,这个任务可以并行化吗?

在2014年的big.LITTLE和Global Task Scheduling(或HMP)设备:精彩的巅峰表现,针对长时间运行的工作负载的节能、可持续计算;多处理:超越单线程性能的限制,避免对性能的热量约束。

NEON是一种广泛的SIMD数据处理体系结构,ARM指令集的扩展,32个寄存器,64位宽(双视图为16个寄存器,在ARMv7中为128位宽),NEON指令执行“打包SIMD”处理,寄存器被视为相同数据类型元素的向量,数据类型:有符号/无符号8位、16位、32位、64位、单/双精度、浮点还是整数。指令在所有线程上执行相同的操作。

通用SIMD处理适用于许多应用:

  • 支持用于互联网应用的范围最广的多媒体编解码器。许多软编解码器标准:MPEG-4、H.264、ON2VP6/7/8/9、Real、AVS…,在软件中支持所有互联网和数字家庭标准。
  • 需要更少的周期。NEON将在复杂的视频编解码器上提供1.6x-2.5x的性能,单个简单的DSP算法可以显示更大的性能提升(4x-8x)
    ,处理器可以更快地休眠=>整体动态节能。
  • 易于编程。清晰正交向量结构,适用于广泛的数据密集型计算,不仅适用于编解码器——适用于2D/3D图形和其它处理,现成的工具、操作系统、商业和开源生态系统支持。

NEON的优化路径:

  • 开源库,例如OpenMAX、libav、libjpeg、Android Skia等,免费提供的开源优化。
  • 矢量化编译器。利用现有源代码自动利用NEON SIMD,状态:发布(在DS-5 armcc、CodeSourcery、Linaro gcc和现在的LLVM中)。
  • NEON指令集。NEON操作的C函数调用接口,支持NEON支持的所有数据类型和操作,状态:发布(在DS-5和gcc中),LLVM/Clang正在开发中。
  • 汇编程序。对于那些真正想在低级别上进行优化的人,状态:已发布(在DS-5和gcc/gas中)。
  • 商业渠道。优化并支持现成的软件包。

Arm的各代架构图:

ARMv7-A到ARMv8-A的演变:

异常级别和交互处理:

OpenGL ES 3.0 and Beyond: How To Deliver Desktop Graphics on Mobile Platforms阐述了用OpenGL ES 3.0在移动端开发出桌面般的图像特性的说明。

OpenGL ES 3.1在2014年发布,当时的安卓应用占比达62%,ES 3.0的使用率达到8%:

OpenGL ES 3.0的新功能:

  • 主要新功能:
    • 多重渲染目标
    • 遮挡查询
    • 实例渲染
    • 统一缓冲区对象(UBO)和统一块
    • 变换反馈
    • 基本重启
    • 序二进制
  • 增强的纹理功能:
    • Swizzle、3D纹理、2D阵列纹理、LOD/MIP级别夹具、无缝立方体贴图、不变纹理、NPOT纹理、采样器对象
  • 新的渲染缓冲区和纹理格式:
    • 浮点格式
    • 共享指数RGB格式
    • ETC/EAC纹理压缩
    • 深度和深度/模板格式
    • 单通道和双通道纹理(R和RG)
  • ES着色语言3.00版:
    • 完全支持32位整数/浮点数据类型(IEEE754)
    • 输入/输出存储限定符,复制到/来自后续/上一管道阶段的值。
    • 数组构造函数和操作
    • 新的内置函数

其中实例化和非实例化的对比:

英特尔Bay Trail*台上的OpenGL ES 3.1:

OpenGL ES 3.1-计算着色器模型:

OpenGL ES 3.1 EXT Extensions–细分着色器:

OpenGL ES 3.1英特尔扩展–像素同步:

  • 概念

    • 英特尔OpenGL | ES扩展:GL_INTEL_fragment_shader_ordering

    • 允许从着色器中同步无序的内存访问

    • 在同步点向着色器添加单个内置项:beginFragmentShaderOrderingINTEL();

  • 好处

    • 使用无序内存访问映射到同一像素的片段可能会导致数据竞争
    • 片元可以按顺序进行阴影处理
  • 应用

    • OIT
    • 可编程混合
    • 自适应体积阴影贴图
    • 等等

Assassin's Creed Identity: Create a Benchmark Mobile Game!讲述了如何制作一个高性能的移动端游戏,包含引擎选择、内容创作、架构概述、游戏逻辑、统一编辑器扩展等内容。

使用的引擎要求可以快速原型化支持:易于学习的环境,支持动画驱动的游戏,编辑器框架应该易于理解。移动端友好型,艺术家驱动,专注于可扩展工具,灵活的许可条款(例如iOS和Android仅适用于部分工程师)。

在设计架构时,遵循几条优先级。优先事项1:协作促进跨部门工作;优先事项2:让人们继续工作,不要破坏构建;优先级3:保持敏捷,解耦计划和流线处理。

前提条件:尽可能将游戏逻辑从Unity逻辑分离,控制和调试Unity引擎的Mono行为被认为是一项挑战。首先,是优化游戏逻辑实体的更新时间:

(重新)使用有限数量的3D角色:

为什么要重写Unity引擎的实体组件模型?启动、唤醒、更新和修复更新不允许刺客信条团队想要的控制粒度,复合实体只能通过预置进行克隆,并且需要像下面这样:

public class PlayerCharacter: AssassinCharacter, IPlayerCharacterStateMachinesCarrier,IPlayerEventHandlerCarrier, IParticipantCarrier, IVisionCarrier
{
    public PlayerCharacter() : base(entity=> newPlayerCharacterStateMachines(entity)) 
    {
        ...

文中还对Unity的C#提出一些深层次的优化建议和注意视线,感兴趣的童鞋可阅读全文。

Frostbite on Mobile分享了Frostbite在移动端化的相关技术和经验,包含从GL迁移到Metal、着色器、光照等。

在GL迁移到Metal过程中,有两大挑战:1、引擎在内存消耗方面已经开始与xbox 360时代有所不同;2、许多着色器都是用纯HLSL编写的,使用YACCGL作为着色器转换器。

利用Metal经验改进OpenGL ES 3.0后端,花时间将Metal和GL后端与控制台/PC对齐。管理磁贴内存:glInvalidateFramebuffer,glClear,所有*台上的延迟渲染/正向渲染,ES上的大多数功能,但性能较低。

在光照方,许多光源都支持使用灯光分块优化,所有游戏都转移到基于物理的渲染。光源类型有点光源、聚光灯、区域光源、阴影投射等效物、*面反射、局部反射体积等。光照分块的向前vs延迟如下图:


交叉编译许多复杂着色器,计算用于光源剔除/装箱(binning)的着色器,在Deferred / Forward / Forward+之间切换。用于局部反射的立方体贴图数组unw(ra/ar)ped到2d lat-long纹理数组,采样时有些alu开销,但支持硬件寻址/过滤/MIPMAP。

将延迟光积累从cs重写为vs/ps,在tile内存中累积光照,在Metal上没有间接的drawcalls/Dispatch,使用早期的顶点着色器模拟。相关优化:

  • 后端优化。公开tiler提示api并大量使用(非tiler上的nop:s),合并尽可能多的渲染过程,减少状态变化。
  • 着色器代码。尽可能多地使用内部函数/内置函数,使用标量数学,仔细打包、对齐数据。

总之,在深入细节之前先了解全貌,今天的移动硬件和api支持完整的引擎功能集,许多特定于tile内存的优化都可以在不偏离桌面/控制台代码基础的情况下完成,如果为多个*台构建,请使用交叉编译器。新API如Vulkan/ES 3.1、spir-v,特定于tile的着色器优化(延迟着色),使用tile本地存储进行高效渲染,特定于移动设备的着色器优化(fp16/fp32使用、alu/带宽*衡),未来可以考虑Tesselation、异步计算、间接绘制等。

Advanced VR Rendering阐述了Valve的VR*台上的尝试和改进,包含立体绘制、时序(调度、预测、VSync、GPU气泡)、镜面锯齿和各向异性照明以及其它VR渲染主题。

Valve的VR有3年多的研究,联合了硬件和软件工程师,专为VR设计的定制化光学元件,显示技术——低持久性、全局显示,跟踪系统(基于基准的位置跟踪、基于点的桌面跟踪和控制器、激光跟踪耳机和控制器),SteamVR API–跨*台、OpenVR。

HTC Vive开发者版规格:刷新率是90赫兹(每帧11.11毫秒),低持久性,全局显示,帧缓冲区的分辨率是2160x1200(每只眼睛1080x1200),离屏渲染的宽高约1.4倍:每只眼睛1512x1680 = 254万个着色像素(蛮力),FOV约为110度,360⁰ 房间尺度跟踪,多个跟踪控制器和其它输入设备。

光学与变形:Warp通道分别为RGB使用3组UV,以考虑空间和颜色失真。

可视化1.4倍的渲染目标。其中上图是扭曲前,下图是扭曲后。

每秒着色可见像素数:30赫兹时720p:2700万像素/秒,60Hz时1080p:1.24亿像素/秒,30英寸监视器2560x1600@60赫兹:2.45亿像素/秒,4k监视器4096x2160@30赫兹:2.65亿像素/秒,90赫兹时的VR 1512x1680x2:4.57亿像素/秒,我们可以将其降低到3.78亿像素/秒,相当于非虚拟现实渲染器在100赫兹时的30英寸监视器。

没有“小”的影响:跟踪允许用户接*跟踪体积中的任何内容,无法实现超昂贵的效果,并声称“这只是角落里的一个小东西”,即使是最低画质也需要比传统创作的更高的逼真度,如果在跟踪体积中,必须是高保真的。

虚拟现实渲染目标:最低GPU最低规格,希望虚拟现实取得成功,但需要客户,最低规格越低,客户就越多,客户不应注意到锯齿,客户将锯齿称为“闪烁”,算法应该扩展到多个GPU上。

立体渲染(单GPU):强力运行CPU代码两次(错误),使用几何体着色器放大几何体(错误),重新提交命令缓冲区(很好的解决方案),使用实例将几何体加倍(更好,API调用减少一半,提高了VB/IB/texture读取的缓存一致性),来自High Performance Stereo Rendering For VR。

立体渲染(多GPU):AMD和NVIDIA都提供DX11扩展以加速跨多个GPU的立体渲染,AMD实现的帧速率几乎翻了一番,但还没有测试NVIDIA的实现。非常适合开发人员,团队中的每个人都可以在他们的开发盒中使用多GPU解决方案,在没有不舒服的低帧率的情况下打破帧率。

预测(Prediction):目标是尽可能缩短HMD和控制器变换的预测时间(渲染为光子)(精度比总时间更重要),低持久性全局显示:在11.11毫秒帧中,面板仅点亮约2毫秒。

上面的图像不是最佳的VR渲染,但有助于描述预测。

管线架构:渲染当前帧时模拟下一帧:

在提交之前,会重新预测转换并更新全局cbuffer,由于预测限制,虚拟现实实际上需要这样做,必须保守地在CPU上减少大约5度。

等待VSync:最简单的VR实现,在VSync之后立即预测,模式#1:Present(),清除后缓冲区,读取像素;模式#2:Present(),清除后缓冲区,在查询上自旋转(spin)。非常适合初始实现,但避免这样做,GPU不是为此而设计的。

“运行开始”的VSync:怎么知道离VSync有多远?很棘手,图形API并不直接提供这一点。Windows上的SteamVR/OpenVRAPI在一个单独的进程中,在调用IDXGIOutput::WaitForVBlank()时旋转,记录时间并递增一个帧计数器。然后,应用程序可以调用getTimeSincellastVsync(),该函数也会返回一个帧ID。GPU供应商、HMD设备和渲染API应该提供这一点。

“运行开始”的细节:要处理坏帧,需要与GPU部分同步,在清除后缓冲区后注入一个查询,提交整个帧,在该查询上旋转,然后调用Present(),确保在当前帧的VSync的正确一侧,现在可以旋转直到运行开始时间:

为什么查询题很关键?如果有一帧延迟,查询将在下一帧的VSync右侧,确保预测保持准确(下图橙色部分):

开始运行总结:具有一个稳定的1.5-2.0毫秒GPU性能增益!正常情况,可以分别在NVIDIA Nsight和微软的GPUView中看到下图所示:

锯齿是VR的头号敌人:相机(玩家的头)永远不会停止移动,因此,锯齿会被放大。虽然要渲染的像素更多,但每个像素填充的角度比以前做的任何事情都大,以下是一些*均值:2560x1600 30英寸显示器:约50像素/度(50度水*视场),720p 30英寸显示器:约25像素/度(50度水*视场),VR:约15.3像素/度(110度视场,是非VR的1.4倍),必须提高像素的质量。

4xMSAA最低质量:前向渲染器因抗锯齿而获胜,因为MSAA正好有效,如果性能允许,使用8xMSAA,必须将图像空间抗锯齿算法与4xMSAA和8xMSAA并排进行比较,以了解渲染器将如何与业内其它渲染器进行比较,使用HLSL的“sample”修饰符时,抖动的SSAA显然是最好的,但前提是可以节省性能。

法线贴图依然可用,大多数法线贴图在虚拟现实中效果都很好。无效的情况:跟踪体积内大于几厘米的特性细节不好,以及被跟踪体积内的表面形状不能在法线贴图中。有效的情况:无法*距离查看的被跟踪体积外的远处物体,以及表面“纹理”和精细细节。法线贴图映射错误:

任何只生成*均法线的mip过滤器都会丢失重要的粗糙度信息:


用Mips编码的粗糙度:可以存储一个各向同性值(可视为圆的半径),是所有2D切线法线与促成该纹理的最高mip的标准偏差,还可以分别存储X和Y方向标准偏差的二维各向异性值(可视化为椭圆的尺寸),该值可用于计算切线空间轴对齐的各向异性照明


添加艺术家创作的粗糙度,创作了2D光泽=1.0–粗糙度,带有简单盒过滤器的Mip,将其与每个mip级别的法线贴图粗糙度相加/求和,因为有各向异性光泽贴图,所以存储生成的法线贴图粗糙度是免费的。

左:各向同性光泽度;右:各向异性光泽度。

切线空间轴对齐的各向异性照明:标准各向同性照明沿对角线表示,各向异性与任一相切空间轴对齐,只需要2个附加值与2D切线法线配对=适合RGBA纹理(DXT5>95%的时间)。

粗糙度到指数的转换:漫反射照明将Lambert提高到指数(\(N\cdot L^k\)),其中\(k\)在0.6-1.4范围内尝,试了各向异性漫反射照明,但不值得这么做,镜面反射指数范围为1-16384,是具有各向异性的修改的Blinn-Phong。

void RoughnessEllipseToScaleAndExp(float2 vRoughness, out float o_flDiffuseExponentOut,out float2 o_vSpecularExponentOut,out float2 o_vSpecularScaleOut)
{
    o_flDiffuseExponentOut=((1.0-(vRoughness.x+ vRoughness.y) * 0.5) *0.8)+0.6;// Outputs 0.6-1.4
    o_vSpecularExponentOut.xy=exp2(pow(1.0-vRoughness.xy,1.5)*14.0);// Outputs 1-16384
    o_vSpecularScaleOut.xy=1.0-saturate(vRoughness.xy*0.5);//This is a pseudo energy conserving scalar for the roughness exponent
}

各向异性的光照计算过程:

几何镜面锯齿:没有法线贴图的密集网格也会产生锯齿,粗糙度mips也无济于事!可以使用插值顶点法线的偏导数来生成*似曲率的几何粗糙度项。

float3 vNormalWsDdx = ddx(vGeometricNormalWs.xyz);
float3 vNormalWsDdy = ddy(vGeometricNormalWs.xyz);
float flGeometricRoughnessFactor = pow(saturate(max(dot(vNormalWsDdx.xyz, vNormalWsDdx.xyz), dot(vNormalWsDdy.xyz, vNormalWsDdy.xyz))), 0.333);
vRoughness.xy=max(vRoughness.xy, flGeometricRoughnessFactor.xx); // Ensure we don’t double-count roughness if normal map encodes geometric roughness

flGeometricRoughnessFactor的可视化。

MSAA中心与质心插值并不完美,因为过度插值顶点法线,法线插值可能会在轮廓处导致镜面反射闪烁。下面是文中使用的一个技巧:

// 插值法线两次:一次带质心,一次不带质心
float3 vNormalWs:TEXCOORD0;
centroid float3 vCentroidNormalWs:TEXCOORD1;

// 在像素着色器中,如果法线长度*方大于1.01,请选择质心法线
if(dot(i.vNormalWs.xyz, i.vNormalWs.xyz) >= 1.01)
{
    i.vNormalWs.xyz = i.vCentroidNormalWs.xyz;
}

法线贴图编码:将切线法线投影到Z*面上仅使用2D纹理范围的约78.5%,而半八面体编码使用2D纹理的全部范围:

缩放渲染目标分辨率:事实证明,1.4x只是HTC Vive的一个建议(每个HMD设计都有一个基于光学和面板的不同建议标量),在较慢的GPU上,缩小建议的渲染目标标量,在速度更快的GPU上,放大建议的渲染目标标量,尽量利用GPU的周期。

各向异性纹理滤波:提高了显示器的分辨率(别忘了,VR的每度只有更少的像素),对于颜色和法线贴图,强制启用此选项,默认使用8x。禁用其它所有功能,仅三线性,但需要测量性能。如果在其它地方遇到瓶颈,各向异性过滤可能是“免费的”。

噪点是你的朋友,在虚拟现实中,过渡很可怕,带状(banding)比液晶电视更明显,当像素着色器中有浮点精度时,可在帧缓冲区中添加噪点。

float3 ScreenSpaceDither(float2vScreenPos)
{
    // Iestyn's RGB dither(7 asm instructions) from Portal 2X360, slightly modified for VR
    float3 vDither = dot(float2(171.0, 231.0), vScreenPos.xy + g_flTime).xxx;
    vDither.rgb = frac(vDither.rgb / float3(103.0, 71.0, 97.0)) - float3(0.5, 0.5, 0.5);
    return (vDither.rgb / 255.0) * 0.375;
}

环境图:无穷远处的标准实现 = 仅适用于天空,需要为环境图使用某种类型的距离重新映射:球体很便宜,立方体更贵,两者在不同的情况下都很有用。

模板网格(隐藏区域网格):用模板屏蔽掉实际上无法透过镜头看到的像素,GPU在提前模板拒绝时速度很快。或者,可以渲染到接*z的深度缓冲区,以便所有像素都可启用提前z测试,透镜会产生径向对称变形,意味着可以有效地看到投影在面板上的圆形区域。

模板网格图例。从上到下从左到右依次是:扭曲视图、理想扭曲视图、浪费的空间、无扭曲视图、无扭曲视图(屏蔽无效像素)、最终无扭曲视图。

模板网格(隐藏区域网格):SteamVR/OpenVRAPI提供此网格,填充率可以降低17%!无模板网格:VR 1512x1680x2@90Hz:4.57亿像素/秒,每只眼睛254万像素(总计508万像素),带模板网格:VR 1512x1680x2@90Hz:3.78亿像素/秒,每只眼睛约210万像素(总计420万像素)。

扭曲网格,依次是:镜头畸变网格、暴力、剔除0-1之外的UV、剔除模板网格、收缩扭曲。

需要性能查询!总是保持垂直同步,禁用VSync查看帧率会让玩家头晕,需要使用性能查询来报告GPU工作负载,最简单的实现是测量从第一个到最后一个draw调用。理想情况下,测量以下各项:从Present()到第一次绘图调用的空闲时间、从第一次绘图调用到最后一次绘图调用、从上次绘图调用到现在的Present()的空闲时间。

总结:立体渲染、预测、“运行开始”(每帧节省1.5-2.0毫秒)、各向异性照明和Mipping法线贴图、几何镜面抗锯齿、模板网格(节省17%的渲染像素)、优化的扭曲网格(降低15%的成本)。


14.4.3.4 并行技术

UFO Invasion: DX11 and Multicore to the Rescue讲解了DirectX 11下的多线程特性。文中对比了多线程和单线程的任务执行模式,多线程下不再串行地执行任务,而是划分成若干各子任务,并在多帧直接重叠:

在多线程模式下,当有的工作线程处于饥饿(空闲)状态时,应该可以从其它满负载的线程偷取任务执行:

文中提及了Entity的概念和性质,Entity包含两种数据:State和Mind。State是公开给游戏的其它系统的数据,Mind是Entity的私有数据。Entity更新时,可以并行地更新State和Mind,但需遵循的规则如下:

  • 永远没有其它实例读取Mind。
  • 更新Mind时不要改变State。
  • 更新Mind时不要关注其它实例。

Entity更新允许依赖,有时上一帧的信息不够,但知道想知道的,Entity可以声明自己依赖于另一个(或其它)Entity。

Render是完全并行的,因为每个线程都在自己的队列中收集数据,排序稍后会将所有内容放在一起。每个条目的权重为128位(64位密钥、32位实体ID、32位参数),对于4096个条目,需要排序的64K数据,勉强足以证明并行排序的合理性……许多昂贵的渲染部分同时发生在此,最显著的是剔除。

渲染本质上是在一个线程上连续发生的,查看当前密钥与前一个密钥之间的差异,引擎可以非常有效地更新管线的状态。从一个越界的“Last Key”开始,它使引擎选择正确的渲染目标、视口和各种状态。Entity被回调以绘制它需要绘制的任何内容,当前键成为最后一个键,重复直到完成。实例化实际上只是累积具有相同实例化id的键并使用结果列表回调Entity的问题。

DirectX 11的多线程非常简单:

  • 多线程渲染到延迟上下文。
  • 延迟上下文生成命令列表。
  • 主线程将它们提交给立即上下文。

适应DirectX 11多线程模型的渲染流程如下:

最巧妙的是,这种方法还处理命令列表不依赖于当前管线状态的要求:总是从一个超出范围的“最后一个键”开始,即每个命令列表都以完整的管线状态开始。

Shears - Squeeze the Juice Out of the CPUs: Post Mortem of a Data-Driven Scheduler介绍了一种通过解决多线程环境中的数据争用来安排引擎循环的创新方法。这种数据驱动的调度最大限度地减少了数据竞争,并最大限度地增加了包括Cell的SPU在内的硬件占用。使用无锁算法实现,与更传统的调度程序相比,实现了更好的性能。

当前引擎在循环上主流的做法是采用重叠,利用多线程并行:

执行分布在多个线程上,然后收集有关同步点的数据,即使它可以很好地在微观和宏观上扩展,但有数据访问问题:处理的只读权限或写访问但需要锁同步原语,需逐项目调整。

下图的架构即使它的伸缩性比同步点好,但和上一个问题一样,仍然需要同步点和/或锁,逐项目的调整:

可以做些什么来避免这些问题?使用剪刀(Shear):将大任务切割成更小的块。结合下图看一下前面的任务序列,推送一堆数据,任务C锁定直到A&B完全完成,所以,识别2个数据流D0&D1。

改变视角,从数据流看。数据流表示自上而下,任务仍然存在,添加了重要元素:任务访问,任务访问定义调度。现在放入数据流,任务A&B先运行然后C,结果和以前一样。

现在将数据倒入数据流中,结果 => 任务C可以更早开始,不等待任务A&B,调度程序的输入意味着数据访问声明而不是任务序列声明。

以游戏循环引擎为例:数据从顶部 => 流向底部,一切都与数据流有关,没有数据 => 没有进程 => 只会执行必要的代码。

文中还给出了详细的动画说明Lock-free的执行过程(下图是静态的,无法展示动画):

下表是在Intel Core 2 Quad Processor Q6600测试临界区和无锁原子的性能对比,单位是操作数/毫秒,2个线程:不是20%或50%,快36倍 - 超过3000%,57%的空闲时间调用锁函数,即使锁成功。

随着硬件开发人员远离创建更快的处理器来代替多核架构,游戏开发人员必须利用多线程技术来利用这些新设备。 对于多核移动设备,对基于网络的多线程游戏引擎的需求已成为现实。Building a Multithreaded Web Based Game Engine Using HTML5, CSS3 and JavaScript讨论各种多线程Web引擎架构的设计,这些架构允许使用线程控制器在线程中动态处理请求。利用WebWorkers、WebSockets和WebGL等HTML5和JavaScript API可以在基于浏览器的3D游戏中建立一个新标准,这些游戏功能齐全,真正跨*台支持移动和桌面设备,无需插件。


Web端的应用架构和OS内核之间的关系。

嵌套和共享类型的工作。

Web端的多线程架构和运行模型。

应用层、OS、第三方扩展的层次结构。

我们知道CPU擅长任务并行,GPU擅长数据并行。而GPU Task-Parallelism: Primitives and Applications偏偏剑走偏锋,不按套路出牌,尝试在GPU上引入任务并行,阐述其概念、重要性、实践等技术。

什么是任务并行?

  • 任务:在单个上下文中执行的一组逻辑相关的指令。
  • 任务并行:任务并行处理。调度组件确定如何将任务分配给可用的计算资源。
  • 示例:Cilk、英特尔TBB、OpenMP。

GPU是数据并行的,围绕数据并行处理构建的GPU硬件,CUDA是一种数据并行抽象,基于任务的工作负载被忽略(到当时为止)。

GPU任务并行性:扩展了GPU编程的范围,许多任务并行问题仍然表现出大量的并行性,将GPU编程为任务并行设备,分为两部分:原语(primitive)和应用程序。

原语的目标是构建一个任务并行系统,它可以处理不同的工作流程,处理不规则的*行度,遵从任务之间的依赖关系,对所有这些进行负载*衡。

原语并行:

  • 任务粒度。处理任务的正确并行粒度是多少?每个线程一个任务是好的做法,每个warp一个任务更好。重视SIMD,将warp视为具有 32宽矢量通道的MIMD线程。

  • 任务管理器(启动、退出)。如何继续处理任务直到没有任务为止?持久线程编程模型:

    while(stillWorkToDo)
    {
        // 运行任务 
    }
    

    将启动范围与工作量分离,谨防死锁!

  • 任务通信。如何在SM之间*均分配任务?具有工作捐赠程序的分布式队列。也可用单个块队列,因为原子现在足够快,也很简单。

  • 任务依赖。如果任务有依赖关系怎么办?如何增强当前的系统以尊重依赖关系?

    依赖决策:放入队列的所有任务都在没有依赖关系的情况下执行,依赖关系影响哪些任务可以放入工作队列,维护一个任务依赖映射,每个warp必须在排队其它工作之前检查该映射。

    while(Q is not empty)
    {
        task t = Q.pop()
        Process (t)
            
        Neighbors tnset = dependencyMap(t)
        For each tn in tnset
            tn.dependencyCount--;
        if(tn.dependencyCount == 0) 
            Q.push(tn);
    }
    

对于应用并行,有多种场景需要任务并行:Reyes渲染、延迟照明、视频编码,只使用必要的原语。

对于Reyes渲染,需要任务并行的原因是不规则并行、动态通信。需要的原语是持久线程、动态任务队列。

Reyes的任务并行。

对于延迟光照,不同的灯光影响屏幕的不同部分,所以我们用太多的灯光细分tile。需要任务并行的原因是不规则并行、动态通信。需要的原语是持久线程、动态任务队列。

总之,任务并行性很重要,包含多种应用场景。几种基本原语:调度任务粒度、持久线程、动态排队、依赖解析。

Killzone Shadow Fall: Threading the Entity Update on PS4分享了PS4游戏Killzone Shadow Fall使用的线程化更新Entity的技术。

实体是大多数游戏对象的基类,例如玩家、敌人、武器、门,不用于静态世界,具有组件,例如模型、移动器、破坏性,以固定频率更新(15、30、60Hz),几乎所有频率均为15Hz,玩家更新频率更高,以避免延迟。实体和组件具有代表性,控制渲染、音频和VFX,在实体未更新的帧中插入状态,插入比更新更便宜,但会引入延迟,始终与上次更新保持一致。

在PS3中,一个Entity = 1个纤程,大部分时间花在PPU上,没有明确的并发模型,读取部分更新状态,实体相互等待。

在PS4上,一个Entity = 一个作业,无纤程,实体整体更新,如何解决竞争条件?

有依赖的实体不能并发更新,但没有依赖的可以。无(间接)依赖=无访问权限,工作方式有两种:武器也可以接*士兵,创建依赖项有1帧延迟,全局系统需要锁。

但是,有少量实体会导致大量的瓶颈:

非排它依赖项,进入“子弹系统”的通道必须有锁保护。

还可以使用弱引用,两个坦克相互开火(下图),循环依赖发生时,更新顺序颠倒,不经常使用(每帧<10)。

非更新实体,实体可以跳过更新(LOD),实体可以在其它帧中更新,正常调度!下图是各种依赖的总结:

调度算法:具有独占依赖关系的实体合并到一个作业中,依赖关系决定排序,非排它性依赖成为作业依赖,先开启开销大的作业!

边界情况:非周期性依赖变成周期性作业依赖,作业1和作业2需要合并。

跨帧*衡实体,防止所有15Hz实体在同一帧中更新,实体可以移动到其它帧,1次更新的增量时间更短,将父子关联的实体保持在一起,如士兵的武器、骑枪士兵、锁定*距离战斗。

性能问题:内存分配互斥,消除了许多动态分配,使用堆栈分配器,锁定物理世界,主模拟世界的R/W互斥,第二次“子弹碰撞”宽相位+锁定,大量依赖实体,玩家更新非常大开销等。

可以采用切分场景(Cut Scene)的策略。切分场景实体需要依赖关系,切分场景中的10多个角色创造了巨大的作业!

以上问题的解决方案是为非交互实体创建子切分场景,主切分场景决定时间和流程,在时间线中向前扫描1帧以创建依赖关系。

使用对象,不可能依赖可用对象(太多),获取可用对象的列表,受锁保护的全局系统,“使用”图标出现在屏幕上,玩家选择,建立依赖关系,启动“使用”动画,1帧后开始交互(依赖关系有效),隐藏1帧延迟!

以上各类更新方式的性能对比如下:


整帧的时间线和实体更新的时间线如下两图:


总之,易于在现有引擎中实现,游戏程序员可以像单线程一样编程,几乎没有多线程问题。

Multithreading for Gamedev Students讲述了多线程编码的相关技术,如硬件支持、常见游戏引擎线程模型、竞争条件、同步原语、原子与无锁、危险。

多处理器(Multiple processors):成本高、功耗高、芯片间延迟、缓存一致性问题,通常仅限于高端台式机和超大型计算机。

多核(Multiple cores):多核可更有效地利用可用硬件资源,内核可能共享二级/三级缓存、内存接口,台式机和游戏机最常见的设置。

多个硬件线程(Multiple hardware threads):同步多线程(SMT),英特尔领地上的“超线程”,线程共享core的资源,如执行单元、一级缓存等。更有效地利用核心资源,暂停的线程不会浪费资源,与单硬件线程相比,通常快10%-20%,但变化很大。

多个软件线程(Multiple software threads):操作系统可以创建多个进程,游戏通常作为一个进程运行。一个进程可以产生多个线程,共享内存地址空间。线程可以在多个线程之间迁移,或固定到具有线程关联的特定线程。

硬件样例:

  • Intel Xeon E5-1650:

    • 6 hyper-threaded cores
      • 12 ‘logical processors’
    • L1 & L2 per-core
    • Shared L3
  • Xbox 360:

    • IBM Xenon CPU
      • PowerPC
    • 3-core SMT
      • 6 hardware threads
    • L1 per core, shared L2

  • PlayStation 4 / Xbox One
    • AMD Jaguar architecture
    • 2 quad-core 'modules'
      • 8 hardware threads
      • L1 cache per core
      • L2 cache shared by all cores in module

  • AMD GCN GPUs
    • Used by both PS4 & Xbox One
      • 18 & 12 compute units respectively
    • Rendering is inherently parallel
      • Hardware exploits this to achieve high speed & throughput
    • Extensible to non-graphics workloads
      • Compute & async compute

游戏内的多线程:30/60fps目标,必须使用所有可用资源,许多相互作用的系统,有限的共享数据集,一些常见的线程模型。游戏内常见的几种并行方式:



竞争条件:系统的输出取决于时序,时间受到很多因素的影响,未定义的行为-所有赌注均已取消,随机=不可预测=错误结果,调试噩梦。下图是典型的竞争条件案例:

同步:

  • 自旋锁(Spinlock)

    • 紧密旋转,试图获得锁定,通常通过原子变量。
    • 可能会造成问题,CPU和内存带宽使用。
    • 正确使用时是轻量级。
  • 互斥(mutex)

    • 锁定/解锁配对。
    • 保护代码的关键部分,提供单线程访问(互斥)。
  • 信号(Semaphore)

    • 维护内部计数器。
    • 等待(递减)和信号(递增)操作。<= 0个线程睡眠,大于0个等待线程继续。
    • 信号可以唤醒线程。
    • 用于线程之间的信令,或者控制可以执行任务的线程数。
  • 条件变量(Condition variable)

    • 线程等待条件满足。
    • 监视器:互斥+条件变量。
    • 游戏中使用的*台特定事件。
  • GPU围栏(Fence)

    • 用于CPU和GPU交互。知道共享数据何时产生或使用。
    • 特定于*台的API,DX12和OpenGL自3.2以来的核心。

编写多线程代码时,还需要注意内存顺序(Memory ordering),以下面代码为例:

// global
int data = 0;
int readyFlag = 0;

// thread A
data = 32;
readFlag = 1;

// thread B
if(readFlag == 1)
{
    Output(data); // 这里的data并非唯一的值,可能是0或32!!
}

以上错误的出现正是多线程之间的内存顺序问题。编译器可以重新排序指令,CPU可以重新排列指令,CPU可以重新安排内存访问顺序。内存模型确定哪些读写操作可以相对于其它操作重新排序,硬件和软件:处理器只有一个内存模型,语言可能还有另一个原因。

顺序一致性(Sequential consistency):内存访问所见即所得,没有明显的重新排序,对可能的优化的后续限制,除非性能另有要求,否则请使用。

内存屏障:用于在编译器和CPU上强制执行内存排序,隐含在某些函数中,如std::atomic<>操作不是memory_order_relaxed的操作。明确获取和释放栅栏。

无锁编程:实现无锁、无阻塞的多线程算法,没有线程可以通过被中断来阻止全局进程。使用无锁的原因是免于锁争用、可扩展性、性能(无约束锁的性能非常好)。但无锁的缺点是复杂!

多线程的危险(Hazard)包含:

  • 死锁。获得了两把锁,但顺序不同,一个线程锁定A并等待B,另一个锁B并等待A。
  • 活锁(Livelock)。多线程在局部处理,每个线程的活动都会导致其它线程多次无法取得全局处理。经典类比:走廊里有两个人。
  • 优先级反转(Priority inversion)。低优先级线程获取高优先级线程所需的锁,然后由于其优先级而进入睡眠状态,系统性能最终由低优先级线程而不是高优先级线程决定。
  • 虚假分享(False sharing)。多个线程在同一缓存线中修改内存,导致持续缓存失效和不必要的内存流量,会显著影响性能。
  • ABA问题

多线程的复杂性:只知道一点知识是危险的。错误容易犯,但很难调试。没有不可能的事情, “百万分之一”,每帧50次,每秒30帧…~11分钟,如果你运气好,坏事就会发生。即使是简单的事情也会引起问题:

enum{EValueA, EValueB};
// ...
Assert(foo==EValueA || foo==EValueB);

上面代码乍一看,不会有太多问题,但它开始偶尔会触发断言。可能本能会想到是内存损坏或其它一些内存问题,比如对齐(GPU通过栅栏设置foo的值),因为该值只能是这两个值中的一个。更让人困惑的是,无论何时报告断言,foo实际上等于EValueA。实际上,在断言逻辑的中间,foo被从EValueB改为EValueA——在第一次测试之后,在第二次测试之前!这表明,当你放松警惕时,事情很容易出错!!

调试性:考虑到所有这些复杂性,提前计划,始终尝试保持单线程路径处于活动状态,所以你知道问题是逻辑还是线程,运行时可切换(如果可能),有时,仅仅思考比调试更好。

[Concurrent Interactions in The Sims 4](https://www.gdcvault.com/play/1020190/Concurrent-Interactions-in-The-Sims#:~:text=For The Sims 4%2C we,in to perform the interaction)讲述了The Sims 4(模拟人生4)架构的并发技术,包含互动、约束、交互队列、转换、社交等。

The Sims的世界是用游戏对象建立起来的,游戏对象提供交互,模拟人生也是对象!模拟人生运行交互,互动是行为的基本单位。多任务是很自然的事情,人们同时做多种事情,频繁请求的功能,系统方法是有价值的,临时实现需要大量工作,结果不一致。

不是真正地并发执行会很棘手,可能导致诸如死锁、竞争条件等,多任务涉及上下文切换和协作等。


角色多任务的串行和并行图例。

多任务中使用了子动作(Sub Action)的概念:

规则:我能执行一个动作吗?状况→ 行动。如何执行动作?行动→ 条件。避免重复逻辑。

约束:数据驱动的规则,运行互动的先决条件。回答问题:我可以进行互动吗?如何运行互动?

约束创作:数据驱动。动画:位置、姿势、携带,XML调优:几何、方向、表面,脚本:评分功能、视线。

约束组合:多任务组合约束,支持操作:交集、并集。

交互队列:每个模拟都有一系列激活的互动和等待交互的有序队列。互动具有优先级:高(用户导向)、低(自动)、空闲(已完成但仍在运行)。


队列处理和交互处理。

生成行为:约束定义了执行交互的先决条件,可以生成性地使用,需要能够找到到约束的转换。

转换图:每个对象上的约束都存储在一个抽象图中,边是状态变化,搜索图形以生成转换序列。

使用转换图:

图搜索:多个节点可以满足需求,边缘按成本加权,按*似距离加权的路线,搜索决定最佳路径。

搜索优化:双向搜索,使用携带(carry)、插槽(slot)简化,节点查询索引。

Parallelizing the Naughty Dog Engine Using Fibers讲述了Naughty Dog引擎利用纤程来实现并行化的技术。

新作业系统的设计目标是允许将无法移动到SPU的作业化代码,作业可以在执行过程中让渡给其它作业,例如玩家使用kick更新并等待光线投射,游戏编程人员易于使用的API,用户没有内存管理,同步/链接作业的一种简单方法,性能仅次于API的易用性。

纤程(Fiber)就像一个局部的线程,用户提供堆栈空间,包含纤程状态的小型上下文和节省寄存器。由线程执行,协作多线程(无抢占),纤程之间的切换是明确的(PS4上的sceFiberSwitch),其它操作系统也有类似的功能。最小化开销,在纤程之间切换时没有线程上下文切换,只有注册保存/恢复。(程序计数、指针堆栈、gprs…)

Naughty Dog引擎的作业系统有6个工作线程,每个都锁定在一个CPU内核上,线程是执行单元,纤程是上下文,作业始终在纤程的上下文中执行,用于同步的原子计数器。有160个纤程(128 x 64k堆栈,32 x 512k堆栈),3个作业队列(低/正常/高优先级),没有作业窃取。

一切都是作业:游戏对象更新、动画更新和骨骼混合、射线投射、命令缓冲区生成,除了I/O线程(套接字、文件I/O、系统调用…),这些是系统线程,像中断处理程序一样实现(读取数据、发布新作业),总是等待,从不进行昂贵的数据处理。

新作业系统的优点:极易更新现有游戏玩法,深度调用堆栈没有问题,让一个作业等待另一个作业是直截了当的:WaitForCounter(...)。超轻量,可更换纤程,系统支持的操作,如PS4上的sceFiberSwitch(),保存/恢复程序计数器和堆栈指针和其它所有的寄存器。缺点是无法再使用系统同步原语:互斥、信号量、条件变量…锁定到特定的线程,纤程在线程之间迁移。同步必须在硬件层面上完成,原子自旋锁几乎无处不在,特殊作业互斥锁用于持有时间较长的锁,如果需要,将当前作业置于睡眠状态,而不是旋转锁定。

对纤程的支持:可以在调试器中查看纤程及其调用堆栈,可以像检查螺纹一样检查纤程,纤程可以命名/重命名,指明当前作业,异常处理,纤程调用堆栈与线程一样保存在核心转储中。纤程安全线程本地存储(TLS)优化,问题是TLS地址允许在测试期间缓存,默认情况下,该函数将运行,在功能中间切换纤程用错误的TLS指针醒来。目前不受Clang支持,解决方法:对TLS访问使用单独的CPP文件。在作业系统中使用自适应互斥体,可以从普通线程添加作业,旋转锁->死锁,在进行系统调用之前,旋转并尝试抓住锁,解决优先级反转死锁,由于初始旋转,可以避免大多数系统调用。

引擎的管线如下,游戏逻辑向渲染逻辑向GPU依次发送任务:

以帧为中心的设计,每个阶段都是完全独立的,不需要同步,一个阶段可以立即处理下一帧,简化了引擎设计的复杂性,由于并行性,几乎不需要锁,锁仅用于在大量作业的阶段更新中进行同步。下面是新旧设计的对比图:


Naughty Dog引擎对帧的定义:“经过处理并最终显示在屏幕上的一段数据”,要点是“一段数据”,时间不长,帧由数据成为显示图像所经过的阶段定义。

帧参数(FrameParams)是每个显示帧的数据,最终显示的每个新帧的一个实例,通过引擎的各个阶段发送,包含每帧状态:帧序号、增量时间、蒙皮矩阵,每个阶段访问所需数据的入口点。无竞争资源,由于每个阶段都在一个独特的实例上工作,因此不需要锁,状态变量会在每一帧复制到此结构中,例如增量时间、摄像机位置、蒙皮矩阵、要渲染的网格列表,存储每个阶段的开始/结束时间戳:游戏、渲染、GPU和翻转。如果帧已完成特定阶段,则易于测试:HasFrameCompleted(frameNumber),现在可以很容易地跟踪生命周期,如果在第X帧中生成GPU要使用的数据,则等待HasFrameCompleted(X)为真,有16个帧参数,可以在它们之间旋转,但只能跟踪最后15帧的状态。

内存生命周期:单游戏逻辑阶段(临时内存)、双游戏逻辑阶段(低优先级光线投射)、渲染逻辑阶段的游戏(对象实例数组)、游戏到GPU阶段(蒙皮矩阵)、渲染到GPU阶段(命令缓冲区)及同时用于CPU和GPU的内存!

内存不足:许多不同的线性分配器,许多不同的生命周期,所有尺寸都适合最坏的情况,从未同时遇到所有分配器的最坏情况,100-200 MiB的浪费内存。

标记堆(Tagged Heap)是基于块的分配器,2M的块大小,2MiB是PS4上的“大页面”–>1 TLB条目,每块都有一个标记(uint64_t),没有“Free(ptr)”接口,只能释放与特定标记关联的所有块(下图)。

所有分配器都使用标记堆,从共享标记堆中分配2M块,并在分配器中局部存储,99%的分配都小于2MB,大于2M的分配会从标记的堆中连续分配2M的块,在此局部块中进行分配,直到为空,像这样共享一个公共块池允许动态调整分配器的大小。

分配器给多个工作线程分配标记堆的示意图。如果单个2M的块被多个线程同时使用,则需要加锁保护,防止竞争。

优化:在分配器中为每个工作线程存储一个2M块,使用工作线程索引来选择要使用的块,该线程上的所有分配都将进入该线程的块,从而避免竞争,99.9%的内存分配不需要锁,实现高容量、高性能分配器。

逐线程块的分配器示意图。

总结:纤程很棒,以帧为中心的设计简化了引擎,使用FrameParams之类的方法可以大大简化数据生命周期和内存管理,在处理多帧引擎设计时,基于标记的块分配器非常有用。


14.4.3.5 特殊技术

Advanced Screenspace Antialiasing描述了抗锯齿的各类技术,包含形态抗锯齿、基于屏幕空间的抗锯齿及实现细节。该文总结了当时的主流抗锯齿技术:

文中关于删除瑕疵的描述如下:

  • 已经丢失的信息无法被恢复。例如下采样瑕疵无法被删除。

  • 半透明瑕疵比较棘手。深度剥离、模板路径的K-Buffer可能可以解决此问题,避免透明几何图形(如窗口和框架)的交叉,在透明发生的地方添加不透明边缘。

  • Alpha Test几何体和几何边缘可以被处理。

形态抗锯齿(Morphological Antialiasing)使用颜色不连续检测器,可能会错过重要的边(其中颜色差异很小),可能过于敏感(检测纹理细节),会检测透明,不需要额外的缓冲区(如法线、纹理坐标和深度)。常用的边缘检测卷积核如下:

检测各种类型的线段,包含S形和由2个L形组成的U形:

所需的实际长度和像素位置覆盖率可以使用截距定理(intercept thorem)计算得到:

对于水*计数,2个额外的缓冲区用于边缘检测(仅显示一个):


4个额外缓冲区的计数技术(上、下、左、右):

总是拒绝所有不在垂直不连续缓冲区中的像素:

形态抗锯齿总是以明确的方式处理U形,Biri.v的实现占用了大量的内存:2个ARGB计数缓冲区、1个argb混合缓冲、1个RG间断缓冲器、1512×512像素的浮点覆盖率计算查找表。通过巧妙的封装和缓冲区共享,可以显著减少总体内存消耗(同时增加计算成本)。该算法在像素着色器中使用了大量的条件分支,使用了大量的通道,没有使用Early-Stencil。因此,它不适合当时的主机。

文中提出了改进版本,称作Advanced Screenspace Antialiasing(ASAA)。ASAA实现没有使用颜色来进行不连续检测,因为深度会更精确,深度显示并不是所有的边缘和细粒度细节丢失。添加法线和纹理坐标作为额外的边缘检测提示,这两个额外的缓冲区可以安全地与场景颜色共享,因为它们正在被使用。使用拉普拉斯卷积核进行不连续检测→检测边缘两侧的不连续:

通过改进边缘检测、不连续检测、计数、覆盖率计算等过程,ASAA获得了以下优点:

  • 内存占用率适中。1个RGB缓冲区用于技术和不连续(比2xMSAA低),2个G16R16用于法线和纹理坐标,可共享,建议压缩这些值,因为边缘检测着色器是纹理绑定的。
  • 没有条件分支。
  • 适合当时的主机。
  • 其它特点:对U形的模糊处理,在Xbox360和PS3上,计数可以转移到CPU上。

ASAA的实现步骤和流程如下:

其中混合可以优化,XBox360上可以使用线性纹理,可以在GPU上计数。ASAA的效果对比如下:

Destruction Masking in Frostbite 2 using Volume Distance Fields分享了Frostbite 2引擎使用体积距离场来渲染破坏物体的效果。

有符号体积距离场的使用是将球体放置在几何体上,标记破坏面罩的位置,从球体组计算距离场,结果存储在体积纹理中,使用低分辨率:约2米/像素,每个遮蔽的几何体一个纹理。

点采样、三线性过滤、放大+细节等技术可以获得更高细节的效果:

// 高细节的距离场计算代码
float opacityFromDistanceField(float distanceField, float detail)
{
    distanceField += detail * g_detailInfluence;
    return saturate(distanceField * g_distMultiplier + g_distOffset);
}

体积纹理注意事项,Xbox 360体积纹理要求32×32×4的尺寸倍数,许多小纹理会浪费内存,使用纹理图集,每个维度一个图集简化了打包,需要填充以防止边界泄漏。

绘制破坏遮罩时,与几何图形一起绘制,在像素着色器中使用 [branch] 进行优化,RSX上的动态分支效率不高,6个循环命中分支,粗分段大小(800-1600 像素),将遮罩绘制为延迟贴花。延期贴花的绘制步骤:

  • 绘制场景几何。
  • 在贴花区域周围绘制凸体积。
  • 获取深度缓冲区并转换为体积内的局部位置。
  • 使用局部位置来查找不透明度,例如体积纹理。
  • 在几何图形之上混合。

对于投影细节/法线纹理,需要切线向量,G-Buffer法线不适合,包含来自法线贴图的数据。需要的切向量数量有限,使用主要几何将索引写入G-Buffer,使用索引对包含切线向量查找表的纹理进行采样。

对于Alpha混合,固定功能混合在混合到G-Buffer时会导致问题,用于混合和目标alpha的输出alpha。受限于贴花的输出alpha,也受限于G-Buffer布局中,可编程混合是个不错的用例。

对于纹理mipmap的选择,由于四边形mipmap选择会导致边界周围的伪影,可在着色器中计算mip级别,使用tex2Dlod进行采样。

\[\text{lod} = \log_2\cfrac{\text{pixelPerMeter} \ \times\ \tan(\text{fov}) \ \times\ \text{distToNearPlane}}{\text{screenRes} \ \times\ \bold v \cdot \bold n} \]

在四边形内的纹理坐标中存在不连续性时,该四边形的mipmap选择将是错误的。在右侧放大的图片中,四边形上部的纹理坐标来自砖墙纹理,底部来自地板纹理,这些纹理坐标位于一个纹理图集中的不同位置,GPU将为这组像素选择最低的mipmap。解决这个问题的方法是手动计算每个单独像素的正确 mip 级别,并使用tex2Dlod显式采样。可以通过使用输入v.n和distToNearPlane创建2D查找表纹理来优化计算。

处理距离场三角剔除时,使用逐三角形的分支,针对距离场测试每个三角形,输出两个索引缓冲区,发出两个绘图调用。缓存剔除结果,距离场变化时更新。

基于体积距离场渲染的破坏效果。

Advanced Material Rendering介绍了几种渲染高级材料的技术,例如皮肤、水晶、玻璃、海水、沼泽水和泥水。提出的算法针对当前一代的游戏机,同时考虑到有限的内存和计算能力。涵盖了几个重要的材质特性,例如:次表面散射、半透明、透明度、水散射、动态表面等。此外,还讨论了延迟渲染器中的功能、性能、美学和实现问题,为上述材质渲染提供了久经考验的解决方案。

文中提到抖动技巧,抖动是以某种模式进行采样以掩盖更合理噪声中的欠采样,通常使用样本偏移的“旋转盘”完成分布,包含均匀和泊松。

使用旋转盘抖动,预先计算好的偏移分布表,使用磁盘分布的标准化空间中的N个点。对于每个阴影像素,获得随机法线向量N,对于每个样本,将圆盘分布中的点旋转N,使用该点作为缩放偏移进行采样。由于非离散采样点,线性采样很重要。

抖动的使用案例:阴影。双抛物面软阴影,仅4个Tap,最小的额外开销,似是而非的噪音,更大的柔软度需要更多的图案。


未使用(上)和使用(下)抖动的阴影效果对比。

对于透明度,延迟架构的透明度很棘手,常用的情形有:简单透明度(照亮)、全透明材料、半透明材质(照亮)、半透明材质(始终照亮)。

对于简单透明度,可以使用纱窗(screen door)效果,计算/查找抖动模式,使用它们“杀死”像素,根据透明度值在图案之间交替。4级透明度在带宽受限时易于计算,记得检查编译器是否在剔除像素——应该尽快做。代码如下:

float jitteredTransparency(float alpha, float2 vP)
{
    const float jitterTable[4] =
    {
        float( 0.0 ),
        float( 0.26 ),
        float( 0.51 ),
        float( 0.76 ),
    };
    
    float jitNo = 0.0;
    int2 vPI = 0;
    vPI.x = vP.x % 2;
    vPI.y = vP.y % 2;
    int jitterIndex = vPI.x + 2 * vPI.y;
    
    jitNo = jitterTable[jitterIndex];
    if (jitNo > alpha)
        return -1;
    
    return 1;
}

抖动的透明度在720p中看起来很糟糕,想要模糊那些讨厌的抖动像素,但负担不起另一个会检测到它们并模糊的通道。已在边缘AA的pass中执行。

自定义边缘AA是延迟渲染器中的常用技术,是全屏通道,根据深度/法线数据查找边缘,然后模糊它们,只需提示边缘AA过滤器即可找到“介于”被剔除像素之间的边缘,可以免费获得漂亮的混合,可以通过改变边缘检测的来源(将不连续性深入)来使用标志或更hacky的方式来完成。

左:没有自定义边缘AA;右:有自定义边缘AA。

完全透明物体不需要照明,只是反射/折射光线,适用于玻璃、水、扭曲粒子,视为后效,需要后缓冲作为纹理,便于在Alpha通道中获取深度信息。

半透明材质,需要照明以保证正确,与整个场景一致,带阴影。因此希望它处于延迟模式,最好具有单一的照明和着色成本,在样本重建中使用抖动模式。可以使用2通道渲染:

  • 第1个通道:使用抖动模式将半透明材料写入G-Buffer。

    图案覆盖基本渲染四边形(即 2x2),图案选择取决于被覆盖的透明材料层的数量,一个2x2四边形可以覆盖,每增加一层都会导致照明质量下降。

  • 第2个通道:材质在光累积后完全渲染,使用样本重建来获得正确的照明值,需要排序和alpha混合。

    重叠的半透明材质从后到前排序(半透明首先被渲染),对于每种重叠材质,以正确的模式对光缓冲进行采样以获得原始光照值,使用全分辨率纹理和重建照明渲染材质,透明度是通过与后缓冲区进行alpha混合来处理的。

    光照重建时,只采集一个样本会导致严重的锯齿,必须采集多个样本进行重建。检查被着色的像素是否是原始像素,如果为假,则对邻域进行采样以获取有效样本,对它们进行加权并*均以进行样本重建,如果为真,请保持不变。在运动过程中减少锯齿并提高稳定性,对超过2种材质使用2x2四边形=大量纹理缓存垃圾和锯齿。

  • 类似的方法是推断渲染(Inferred Rendering)。

还存在具有单一透明度的延迟渲染器:

  • 半透明几何图形被渲染到具有棋盘格图案的g-buffer。
    • 反照率设为1。
      • Pass 1是特性权重 – 仅法线和镜面反射。
    • 延迟着色后。
      • 累积缓冲区包含半透明几何照明信息和底层阴影几何的交替像素。
      • Pass 2重建两者:照明数据,着色背景。
    • 以全质量渲染材质。
    • Alpha混合手动完成。

除了半透明,该文还涉及了皮肤、头发、水体等特殊材质的渲染。其中水体的光学原理包含表面法线、反射、折射、光散射、消光、焦散、固体表面贴花、镜面反射等(下图)。

针对水面的以上光学原理,文中给出了对应的渲染解决方法。

[Adaptive Volumetric Shadow Maps](http://advances.realtimerendering.com/s2010/Salvi-AVSM(SIGGRAPH 2010 Advanced RealTime Rendering Course).pdf)是Intel的图形研究人员针对烟、雾、云、头发等体积性的材质能够获得可信的阴影而研发。

AVSM可以流式简化算法,使用较小的固定内存占用量生成自适应体积光衰减函数,具有固定数量的节点,可变和无限的误差,易于使用的方法,不对光线遮挡体的类型和/或其空间分布做出任何假设。

一个单独的AVSM纹素编码N个节点,每个节点由一个深度和一个透射率值表示。节点始终按排序(从前到后)的顺序存储。通过将纹素中的所有节点初始化为相同的值来清除AVSM,将深度设置为远*面,将透射率设置为 1(无遮挡)。传入的光线遮挡体由光视图矢量对齐的段表示,一段由两个点(入口点和出口点)和出口点的透射率(入口点的透射率隐式设置为1)定义。假设入口点和出口点之间的空间由均匀致密的介质填充,通常会生成形状为分段指数曲线的透射率曲线,可以使用线条来简化问题(在大多数情况下没有太大的视觉差异)。

第一个和最后一个节点永远不会被压缩/删除,因为它们提供了非常重要的视觉提示。最后一个节点非常重要,因为它对投射在位于体积块后面的任何接收器上的阴影信息进行编码。 例如,一些香烟烟雾投射在桌子上的阴影总是正确的(没有压缩伪影)。删除节点后,就不会更新剩余节点的位置以更好地拟合原始曲线(deep shadow maps)。事实上,当节点在压缩*面上执行随机游走时,在十几次插入压缩迭代中更新节点位置可能会产生一些不可预测的结果。

基于DirectX 11的实现时,为流式简化而设计的算法,但映射到同一像素的运行的片元会导致数据竞争,对像素着色器当前不可用的结构的原子RMW操作。有两个实现版本:

  • 基于计算着色器,速度较慢但内存固定,粒子的软件管线原型几乎没有优化工作,比可变内存实现慢约2倍。
  • 基于像素着色器,速度更快但内存可变。

AVSM的性能方面表现在:竞争性能,更高的图像质量,阴影查找占主导地位,通常<30%的AVSM相关渲染时间花费在插入代码中,DSM比AVSM慢20-40倍。

总之,AVSM的优点是通过自适应采样提高图像质量,避免基于定期采样或可见性函数系列扩展的方法的常见缺陷,固定且易于使用,不需要任何关于遮光剂类型和空间分布的先验知识,易于权衡图像质量以换取速度和存储。缺点是快速的固定内存实现需要图形硬件在帧缓冲区上添加对读-修改-写操作的支持。

Per-Pixel Linked Lists with Direct3D 11介绍了基于DX11实现的逐像素链表的特点、实现、优化和应用等内容。

链表对编程有用的数据结构,使用以前的实时图形API很难有效实现,DX11允许高效地创建和解析链表,逐像素链表,枚举属于同一屏幕位置的所有像素的链表集合。链表涉及两步处理:

  • 链表创建。将传入的片段存储到链表中。

“片元和链接”缓冲区包含所有片元的数据和链接以存储,必须足够大以存储所有片元,使用计数器支持创建,UAV视图中使用D3D11_BUFFER_UAV_FLAG_COUNTER标志。声明如下:

struct FragmentAndLinkBuffer_STRUCT
{
    FragmentData_STRUCT FragmentData; // Fragment data
    uint                uNext;        // Link to next fragment
};
RWStructuredBuffer <FragmentAndLinkBuffer_STRUCT> FLBuffer;

“起始偏移”缓冲区包含在每个像素位置写入的最后一个片元的偏移,屏幕尺寸:宽 * 高 * sizeof(UINT32) ,初始化为魔法值(例如 -1),魔法值表示不再存储片元(即列表末尾)。声明如下:

RWByteAddressBuffer StartOffsetBuffer;

链表创建时,没有颜色渲染目标绑定, 也还没有渲染,只是存储在LL。如果需要,绑定深度缓冲区,OIT将在后面需要它,绑定UAV作为输入/输出:StartOffsetBuffer (R/W) 、FragmentAndLinkBuffer (W)。

Per-Pixel Linked List创建示意图。图中的黄色三角形占了Start Offset Bufer的两个像素,每个像素指向了Fragment and Link Buffer的一个位置。注意计数器是一直累积的,黄色之前已经有绿色和橙色的像素被处理和存储。

链接创建代码如下:

float PS_StoreFragments(PS_INPUT input) : SV_Target
{
    // 计算片段数据(颜色、深度等)。
    FragmentData_STRUCT FragmentData = ComputeFragment();
    
    // 检索当前像素数并增加计数器。
    uint uPixelCount = FLBuffer.IncrementCounter();
    
    // 在StartOffsetBuffer中交换偏移量。
    uint vPos = uint(input.vPos);
    uint uStartOffsetAddress= 4 * ( (SCREEN_WIDTH*vPos.y) + vPos.x );
    uint uOldStartOffset;
    StartOffsetBuffer.InterlockedExchange(uStartOffsetAddress, uPixelCount, uOldStartOffset);
    
    // 在片段和链接缓冲区中添加新的片段条目。
    FragmentAndLinkBuffer_STRUCT Element;
    Element.FragmentData = FragmentData;
    Element.uNext = uOldStartOffset;
    FLBuffer[uPixelCount] = Element;
}
  • 从链表渲染。链表遍历和存储片段的处理。渲染像素的步骤:

1、将“起始偏移”缓冲区和“片段和链接”缓冲区绑定为SRV:

Buffer<uint> StartOffsetBufferSRV;
StructuredBuffer<FragmentAndLinkBuffer_STRUCT>
FLBufferSRV;

2、渲染全屏四边形。

3、对于每个像素,解析链表并检索此屏幕位置的片段。

上图在第2行第个位置检索到了有效数据,根据Start Offset Buffer去获取Fragment and Link Buffer的数据。

上图在Fragment and Link Buffer获取黄色像素的数据时,发现该像素还存在其它数据(橙色),于是根据索引去读取下一个像素数据。

4、根据需要处理片元列表。取决于算法,例如排序、查找最大值等。

读取像素链表的所有数据之后,就根据需要处理片元列表,图中是*均像素的颜色。

float4 PS_RenderFragments(PS_INPUT input) : SV_Target
{
    // 计算UINT对齐的起始偏移缓冲区地址
    uint vPos = uint(input.vPos);
    uint uStartOffsetAddress = SCREEN_WIDTH*vPos.y + vPos.x;
    // 获取当前像素的第一个片段的偏移量
    uint uOffset = StartOffsetBufferSRV.Load(uStartOffsetAddress);
    // 解析该位置所有片段的链表
    float4 FinalColor=float4(0,0,0,0);
    while (uOffset!=0xFFFFFFFF) // 0xFFFFFFFF是魔法数字
    {
        // 在当前偏移处检索像素
        Element=FLBufferSRV[uOffset];
        // 根据需要处理像素
        ProcessPixel(Element, FinalColor);
        // 检索下一个偏移量
        uOffset = Element.uNext;
    }
    
    return (FinalColor);
}

通过逐像素链表可以实现OIT(与顺序无关的透明度),将透明片元存储到PPLL,渲染阶段按从后到前的顺序对像素进行排序,并在像素着色器中手动混合它们,混合模式可以是每个像素唯一的!MSAA支持的特殊情况。

链表结构通过减少向UAV写入/读取的数据量来优化性能(例如uint而不是float4的颜色),OIT的示例数据结构:

struct FragmentAndLinkBuffer_STRUCT
{
    uint uPixelColor; // 打包的像素颜色
    uint uDepth;      // 像素深度
    uint uNext;       // 下一个链接地址
};

也可以将颜色和深度打包到同一个uint中(如果相同的Alpha),使用16位颜色 (565) + 16位深度,性能/内存/质量的权衡。

使用仅可见片元,在Linked List创建像素着色器前使用 [earlydepthstencil],可确保仅存储通过深度测试的透明片元(即可见片元),可以节省性能和渲染正确性!

[earlydepthstencil]
float PS_StoreFragments(PS_INPUT input) : SV_Target
{
    (...)
}

对像素进行排序,就地排序需要对链表的R/W访问权限,稀疏的内存访问 = 慢!更好的方法是将所有像素复制到临时寄存器数组中,然后进行排序,临时数组声明意味着对每个屏幕坐标的像素数的硬性限制,性能所需的权衡。排序的具体过程见下面一组图:





// 存储像素以进行排序
(...)
static uint2 SortedPixels[MAX_SORTED_PIXELS];
// Parse linked list for all pixels at this position
// and store them into temp array for later sorting
int nNumPixels=0;
while (uOffset!=0xFFFFFFFF)
{
    // Retrieve pixel at current offset
    Element=FLBufferSRV[uOffset];
    // Copy pixel data into temp array
    SortedPixels[nNumPixels++]=
    uint2(Element.uPixelColor, Element.uDepth);
    // Retrieve next offset
    [flatten]uOffset = (nNumPixels>=MAX_SORTED_PIXELS) ?
    0xFFFFFFFF : Element.uNext;
}
// Sort pixels in-place
SortPixelsInPlace(SortedPixels, nNumPixels);
(...)


// PS中的像素混合
(...)
// Retrieve current color from background texture
float4 vCurrentColor=BackgroundTexture.Load(int3(vPos.xy, 0));
// Rendering pixels using SRCALPHA-INVSRCALPHA blending
for (int k=0; k<nNumPixels; k++)
{
    // Retrieve next unblended furthermost pixel
    float4 vPixColor= UnpackFromUint(SortedPixels[k].x);
    // Manual blending between current fragment and previous one
    vCurrentColor.xyz= lerp(vCurrentColor.xyz, vPixColor.xyz,
    vPixColor.w);
}
// Return manually-blended color
return vCurrentColor;

通过支持MSAA的逐像素链表实现OIT时,若将单个样本存储到链接列表中需要大量内存,性能会受到影响!解决方案是像以前一样将透明像素存储到PPLL中,但也包括样本覆盖数据!需要与MSAA模式一样多的位,在PS结构中声明SV_COVERAGE:

struct PS_INPUT
{
    float3 vNormal : NORMAL;
    float2 vTex    : TEXCOORD;
    float4 vPos    : SV_POSITION;
    // 像素覆盖数据
    uint uCoverage : SV_COVERAGE;
}

链表结构与之前几乎没有变化,深度现在被打包成24位,8位用于存储覆盖范围:

struct FragmentAndLinkBuffer_STRUCT
{
    uint uPixelColor; // Packed pixel color
    uint uDepthAndCoverage; // Depth + coverage
    uint uNext; // Address of next link
};

样本覆盖率示例如下图,第三个样本被覆盖,此时uCoverage = 0x04(二进制的 0100):

打包深度和覆盖数据,然后存储:

Element.uDepthAndCoverage = ( In.vPos.z*(2^24-1) << 8 ) | In.uCoverage;

渲染阶段需要能够写入单个样本,因此PS以采样频率运行,可以通过在输入结构中声明SV_SAMPLEINDEX来完成,解析链表并将像素存储到临时数组中以供以后排序,类似于非MSAA案例,区别在于仅在覆盖数据与被光栅化的样本索引匹配时才存储样本。渲染代码如下:

static uint2 SortedPixels[MAX_SORTED_PIXELS];
// Parse linked list for all pixels at this position
// and store them into temp array for later sorting
int nNumPixels=0;
while (uOffset != 0xFFFFFFFF)
{
    // Retrieve pixel at current offset
    Element=FLBufferSRV[uOffset];
    // Retrieve pixel coverage from linked list element
    uint uCoverage=UnpackCoverage(Element.uDepthAndCoverage);
    if ( uCoverage & (1<<In.uSampleIndex) )
    {
        // Coverage matches current sample so copy pixel
        SortedPixels[nNumPixels++]=Element;
    }
    // Retrieve next offset
    [flatten]uOffset = (nNumPixels>=MAX_SORTED_PIXELS) ?
    0xFFFFFFFF : Element.uNext;
}

Texture Compression in Real-Time Using the GPU介绍如何在当前控制台和DirectX 10.1视频卡上使用GPU来执行DXT纹理压缩,提出了一种不依赖GPU计算API或存在按位数学运算的方法,涉及每个*台的实现细节和优化。

使用GPU压缩的理由是游戏使用更多运行时生成的内容(混合地图、动态立方体贴图、用户生成内容),CPU压缩速度较慢,并且需要额外的同步和延迟。下图的性能对比来自实时DXT压缩论文的CPU性能数据:

DXT1/BC1是代表4x4纹素的64位块,其中有4个颜色值、2个存储值、2个插值:

颜色索引的伪代码如下:

Index00 = color_0;
Index01 = color_1;
Index10 = 2/3 * color_0 + 1/3 * color_1;
Index11 = 1/3 * color_0 + 2/3 * color_1;

if (color_1 > color_0)
{
    Index 10 = 1/2 * color_0 + 1/2 * color_1;
    Index 11 = “Transparent”;
}

DXT压缩的基本步骤:

  • 获得一个 4x4 的纹素网格。代码如下:
float2 texel_size = (1.0f / texture_size);
texcoord -= texel_size * 2;
float4 colors[16];

for (int i = 0; i < 4; i++) 
{
    for (int j = 0; j < 4; j++) 
    {
        float2 uv = texcoord + float2(j, i) * texel_size;
        colors[i*4+j] = uv;
    }
}
  • 找到想用作存储颜色的颜色。此操作可能非常昂贵,但是后面会提到一些方法,查找端点颜色或非常便宜!建立端点值,需要小心Alpha。

  • 将每个4x4纹素匹配到最合适的颜色。查找纹素指数的代码如下:

float3 color_line = endpoints[1] - endpoints[0];
float color_line_len = length(color_line);
color_line = normalize(color_line);
int2 indices = 0;
for(int i=0; i<8; i++) 
{
    int index = 0;
    float i_val = dot(samples[i] - endpoints[0], color_line) / color_line_len;
    float3 select = i_val.xxx > float3(1.0/6.0, 1.0/2.0, 5.0/6.0);
    index = dot(select, float3(2, 1, -2));
    indices.x += index * pow(2, i*2);
}

​ 重复接下来的8个像素。

  • 创建块的二进制表示。
dxt_block.r = max(color_0_565, color_1_565);
dxt_block.g = min(color_0_565, color_1_565);
dxt_block.b = indices.x;
dxt_block.a = indices.y;
return dxt_block;
  • 将结果放入纹理中。方法因*台而异,渲染目标应该是源的1/4尺寸,1024x1024的源 = 256x256的目标,使用16:16:16:16的unsigned short格式。

漫反射贴图的运行时压缩和离线压缩的对比及它们的颜色差异如下:


可以采取一些措施调整性能,着色器编译器很聪明,但并不完美,确保测试比较过展开(unrolling)与循环(looping)的性能,为目标格式创建变体着色器,如果只使用2个组件,法线贴图会更便宜。法线贴图的运行时压缩和离线压缩的对比及它们的颜色差异如下:


[An Optimized Diffusion Depth Of Field Solver (DDOF)](http://www.klayge.org/material/4_2/DoF/An Optimized Diffusion Depth of Field Solver.pdf)分享了CoC景深的几种深度优化技术。DOF的解算涉及三对角系统(下图),其中橙色是源自CoC的每个像素的输入行/列,绿色是生成模糊的行/列,红色是输入图像的行/列:

文中提及的之前的解算器(Solver)有混合的GDC2010解算器、圆形缩减 (CR) 解算器。以下是Vanilla CR解算器的处理过程:

但是上图的Stop at size 1阶段会阻碍并行,引起性能下降。可以以合理尺寸停止,随后以足够大的并行工作负载解算Y:

在内存优化上,可以将rgba32f改成rgba16f(无明显瑕疵)。此外,上图的abc纹理再一次保存大量的内存,因为它是求解器使用的最大表面,可以跳过abc构造过程,并在第一个reduce通道期间动态计算abc。(下图)

第一个reduce之后的纹理又会显著保存大量内存,可以reduce的4个通道减少成成1个特殊的reduce通道,在一个特殊的替换过程中替换1到4。

在DX11中,可以将abc和X打包进一张rgba_uint纹理,可以使用SM5的数据打包:

DX11内存优化还可以对解算器进行水*和垂直通道,低分辨率RT链需要出现两次,水*减少/替代链,垂直减少/替代链。UAV允许将水*链的数据重用于垂直链,概念验证实现表明这样做很有效,但会显著影响运行时性能(帧率降低约 40%)。保留RT,因为内存已经很低了,仅在真正关心内存时使用。

经过以上方式优化之后,4-to-1 Reduction + SM5 Packing的方法获得了最好的性能,且占用内存最低:

Five Rendering Ideas from Battlefield 3 & Need For Speed介绍了来自Frostbite 2引擎的5种渲染技术:可分离的Bokeh DOF、Hi-Z//Z-Cull、色度亚采样图像处理、基于*铺的延迟着色、时间稳定的屏幕空间环境遮挡。

可分离的Bokeh DOF的模糊过程有以下几种方法:

  • 高斯模糊。常见于DX9游戏中。
  • 2D区域采样。纹理tap爆炸限制了内核大小。
  • GS扩大的点精灵。繁重的填充率,CryEngine3和UE3采纳的方法。

图像空间中的任意模糊是\(O(N^2)\),高斯模糊可以是可分离的\(O(N)\),可以被分离的2D模糊有:高斯、方框、倾斜的方框。

六角模糊(Hexagonal Blur)可以把一个六边形分解成三个菱形,每个菱形都可以通过分离的模糊计算,总共7个pass:3个形状 × 2个模糊+ 1个组合。

使用可分离滤镜的六边形模糊,但7个通道和6次模糊并不具有竞争力,需要减少通道。

下图显示了总共只需要2个通道,但通道1必须输出两个MRT,并且通道2必须读取2个MRT。现在总共有5个模糊,也减少了1个,最后的组合通道是通道2的一部分。

下图展示的方法在通道1中,像往常一样将向上模糊输出到MRT0,但在输出到MRT1之前,还将它添加到左下模糊的结果中。在通道2 中,彻底的模糊将同时在菱形2和3上运行。只需要1次模糊!!

模糊效果如下:

高斯和六边形的对比:高斯总共有2个模糊;六边形2个通道(3个解析),总共4个模糊,但每个模糊只需要一半的tap数,因此虽然相同的tap数,但每个tap贡献一样(高斯的不一样),所以需要更少的tap,以满足一个给定的美学过滤器的内核宽度。

因为有相等权重的模糊,可以对模糊使用迭代优化,多个通道填充欠采样,双重迭代模糊需要总共5个通道、8次半模糊。

伪分散过滤器(Pseudo Scatter filter),适当的散景应该有它的模糊分散到它的邻居。然而,像素着色器是用来收集结果的,它们不能分散结果。典型的模糊默认过滤器内核为像素的CoC,而默认用大的CoC,并根据采样纹素的CoC拒绝,额外的方法可以避免溢色瑕疵,并可以锐化*滑的梯度。

文中提到了专用于X360的Hi-Z/Z的反向重新加载技巧。在现有深度缓冲区上添加渲染目标的重叠,初始重叠的RT成D3DHIZFUNC GREATER EQUAL,绘制全屏矩形(PS设为NULL、Zfun==Never),此时Hi-Z颠倒了:

反向Hi-Z用于CSM渲染:定向光的每个级联都以世界空间中的一个长方体为界,只有长方体内部的世界空间像素会投射到阴影图上,通过绘制长方体背面,只有这些像素将通过反向Z测试。非常适合CSM,CSM的设计使它们包围的体积相交并将相机包围在*面附*,虽然后面的级联不是这样,但它们会包含前面的级联。

作为单独的通行证进行评估,输入是深度缓冲区,创建一个L8掩码纹理输入到定向光通道。可以做一个之前的全屏通过标签背面像素写到在模板的光源,启发式的太阳与相机的角度。潜在的1/4分辨率双边上采样,模板被更新以表示已经处理过的像素。

基于反向Hi-Z的CSM。从上到下依次是级联0到级联3。

后续还可以用Min/Max深度来实现PCF软阴影(下图),具体过程可参阅原文。

色度亚采样(Chroma Sub-Sampling)不是一个新想法,已用于电视广播。Jpeg / Mpeg压缩将图像分解为亮度和色度,只用全分辨率存储亮度(因为人眼对亮度更敏感),用低分辨率存储色度

最顶部是正常的图片,下面三幅分别是分解出来的Y、U、V分量图,其中Y是亮度,U和V是色度。

后处理需要大量的带宽,如果用普通的格式存储数据,对GPU产生巨大的压力和瓶颈。相反,如果使用Luma方法,可以将所需的带宽减少到原始的1/4,但需要对颜色进行额外处理,可以是1/4分辨率的2个通道(原始大小的1/8)。

采用色调亚采样之后,着色器可以有4倍的速度提升吗?答案是否定的,因为受限于ALU:纹理单元和ALU是为4组件分量(如float4)的SIMD设计的,只使用了一个组件,需要打包4个亮度值一起处理。

只需要1次纹理读取就可以获得4个亮度值,所以1280x720亮度缓冲区是320x720的ARGB缓冲区,用压缩缓冲区执行双线性过滤是不正确的,必须手动使用DOTP水*过滤

在打包数据时使用了蝴蝶打包(Butterfly Packing)的技巧,覆盖每个象限到ARGB,镜面周围的图像中心点,现在双线性除了跨越边界外都是有效的,以额外的混合和重组重新绘制一个strip(条带),水*方向为R<-->g、B<-->A,垂直方向为R<-->B和G<-->A,径向模糊有效。蝴蝶解包的过程如下图:

文中提到的TBDR技术除了传统的渲染过程,还使用了GPGPU裁剪,过程如下:

  • 屏幕被划分为920个32x32像素的块(tile)。
  • 下采样并将场景从720p划分到40x23(1像素 = 1 tile)。
    • 找到每个tile的最小/最大深度。
    • 找到每个tile的材质排列。
    • 下采样是通过多通道和MRT完成的。

下图是材质分类的示例,假设场景有3个材质:默认材质(红色)、皮肤着色(绿色)、金属(蓝色)。

当下采样(通过手动双线性tap)时,将材质组合成最终输出颜色。可以在下图右边看到头部周围的下采样是如何产生黄色的,这是红色和绿色的组合,红色和蓝色类似,呈现洋红色。

跳过几个步骤后得到下图,下图是40x23的纹理,将准确地提供每个tile中的材质组合。在并行MRT中,还下采样并存储了最小/最大深度。有了这些信息,现在可以使用GPU来剔除光源,因为已经知道相机视锥体中的所有灯光和每个tile(和天空)的最小/最大深度和排列组合(迷你的视锥体)。

  • 为每个tile构建一个迷你的视锥体。
  • 在着色器中剔除忽略天空的tile的光源。
  • 将裁剪结果存储在纹理中(Column == Light ID、Row == Tile ID)。
  • 实际上,可以一次处理4个光源(A-R-G-B)。
  • 在CPU上回读贡献结果,准备计算光照。

计算光照的伪代码如下:

// 分析纹理上的裁剪结果, 在CPU上执行.
ParseCullingResultsTexture();

For each light type:
    For each tile:
        For each material permutation:
            // 重新组合并设置PS常量的光照参数.
            RegroupAndSetLightParametersForPSConstants(...); 
            // 设置着色器循环计数器.
            SetupShaderLoopCounter(...);
            // 使用单个绘制调用累加地渲染灯光(到最终的HDR照明缓冲区).
            RenderLights(...);

时间稳定的SSAO使用了线段采样,线段采样基本上是占用函数的解析积分乘以圆盘上每个采样点的球体深度范围。由于所有2D样本都将投影到z缓冲区中的不同点,因此线采样也更加有效和稳定,而在3D空间中随机点采样的常用方法,两个点样本可能会投影到同一位置,导致样本数量下降。

SSAO渲染过程还涉及了快速灰度模糊,其工作原理如下:

Pre-Integrated Skin Shading阐述了皮肤渲染涉及的相关技术,包含SSS、各种*似方法,提出了运行时效率极高的预积分皮肤渲染,在工业界产生了深远的影响。文中先总结了之前研究出的几种*似方法:

基于纹理空间扩散(TSD)的几种皮肤渲染方法。

快速次表面散射方法。

屏幕空间的次表面散射。

但是,以上方法都或多或少存在一些问题或限制。文中提出了Pre-Integrated Skin Shading,它的目标是只用一个简单的像素着色器,所有输入本地存储在纹理/凹凸贴图中(无模糊通道)。关键观察:散射并非随处可见,发生在入射光照变化附*(光照梯度)。策略是查找光照梯度和预积分散射。

纹理空间扩散的几种情况,如上图数字标识。数字1的区域表示入射光是常量,不需要做任何扩散;数字2处是小表面凹凸,表面曲率产生了强烈的漫反射衰减;数字3处通过皱纹和凹凸等特征扩散;数字4处光照散射在阴影内,形成独特的皮肤外观。

以上4处可以总结成3个待处理的问题:

  • 表面曲率(Surface Curvature)。预积分基于曲率的BRDF。

对于普通的漫反射、环绕光照和距离衰减函数,都无法适用于表面曲率光照计算。但是,可以在环绕光照(wrap lighting)基础上做改进,用预模糊漫射BRDF与皮肤轮廓来解决不是基于真实的皮肤扩散剖面的问题,使用曲率参数化 BRDF来解决不考虑曲面曲率的问题。

经过上述分析之后,就可以进行常规的漫反射光计算,并准确计算特定尺寸(或特定曲率)球体上的散射情况,通过收集来自球体(或环,更容易)上所有点的所有漫射光来做到这一点。可以使用任何昂贵的技术,因为是离线计算,只会记录结果。对于漫反射衰减的每个点(下图右显示法线和光线之间的一个角度),整合从整个球体散射而来的所有光,这一步非常昂贵,所以记录这个值,以便以后可以使用它。

下面分别是不同参数的漫反射预计算结果:

现在已经使用准确的皮肤轮廓来捕捉各种表面曲率上的散射外观,可以将所有这些数据存储在由\(N\cdot L\)和曲率参数化的简单2D纹理中:

事实证明,可以通过使用法线向量的变化率和在世界空间中位置的变化率之间的简单相似三角形关系来获得曲率的一阶估计,可以使用导数指令得到这些无穷小的变化(下图左)。下图右是将其应用于头部模型的结果。注意:此处为了可视化曲率,但应该使用扩散轮廓中的正确单位,然后校正查找纹理中的曲率范围。

  • 表面凹凸。预积分弯曲法线。

BRDF适用于光滑的曲面,法线贴图中的小凹凸呢?光照应该散射通过几个表面凸起,使用曲率在小尺寸时会失效,现在只关注法线。

使用法线贴图,可以对邻域进行采样(可以使用许多样本,光源、权重和累积)。或者,可以预先过滤法线贴图吗?预过滤点\(N\cdot L\)是正确的,预过滤max(0, dot(N, L))并不严格正确(但很接*),LEAN/CLEAN映射对镜面反射执行此操作。

切线可以获取法线贴图,一种应用光照梯度度来捕捉法线的技术,不同的光照颜色产生不同的捕捉法线。下图使用了4个法线贴图,其中每个法线贴图分别用于计算红、绿、蓝漫射光和镜面光。

不需要捕获的法线来使用这种技术,从“真正的”法线贴图(高光)开始,使用R/G/B皮肤轮廓预过滤新的法线贴图,最佳情况下,需要4个法线:R/G/B 和高光。看起来很棒,但占用很多内存!

优化弯曲法线的方法有:

  • 使用几何和高光法线(混合其余部分)。仅适用于某些艺术,法线贴图必须只包含细节(皱纹/毛孔)。
  • 使用两个正常样本(混合其余样本)。适用于所有艺术,一个法线贴图,两个采样器,将一个采样器截取在模糊的mip阈值处。

有很好的解决方案:弯曲但光滑的表面(预集成的BRDF),*坦但凹凸不*的表面(预集成的法线贴图)。它们相得益彰,从主要曲率(几何)中选择的BRDF,来自主表面的广泛散射,弯曲法线填补了空白,提供局部细节的细粒度散射。

  • 阴影。预积分阴影半影。

事实证明可以应用一个非常相似的技术,就是来自盒子过滤器的阴影。给定阴影值,反转半影模糊功能,找到阴影内的位置。(下图)

如果衰减函数中有位置/距离,基本上可以指定一个新的衰减函数,一个为散射留下一堆额外空间的,然后就可以使用皮肤扩散配置文件将散射效果预先积分到阴影中。

下图是预先积分到阴影得到的结果。类似于漫反射计算,此处有一个2D查找。第二个维度代表了在世界空间中的半影大小,由于表面的坡度或阴影的模糊程度,可能会有不同的尺寸。这个宽度可以通过多种方式计算,例如使用光的表面斜率,甚至可能使用阴影本身的导数。注意,如果有一些疯狂的东西(比如抖动阴影贴图),就不会有硬边,而且可能会得到不正确的散射。此外,在2D中,距离不会完全正确,但外观才是最重要的。

下俩图是纹理空间扩散和预积分的对比:


阴影还可以结合PCF、VSM进行效果改进。下图是通过2的幂来改变尺寸和强度的效果矩阵,其中横坐标是尺寸,纵坐标是强度:

Practical Occlusion Culling on PS3分享了用于PS3的遮挡剔除技术,包含SPU运行时、创建遮挡体、调试工具、性能优化等。PS3游戏杀戮地带的渲染管线见下图:

SPU、PPU和内存之间的协作和交互如下图:

在遮挡剔除方面,计划如下:

  • 离线创建几何遮挡。
  • SPU每一帧渲染遮挡到720p深度缓冲。
  • 将缓冲区分割成16像素高的块用于光栅化。
  • 下采样缓冲到80x45(16x16最大滤波器)。
  • 在场景遍历期间测试这个边界框。
    • 准确:光栅化+深度测试。
    • 粗糙:某种恒定时间点测试。

添加遮挡剔除阶段的管线如下:


遮挡体查询作业:

  • 在(截断的)视锥体中查找遮挡体。
  • 遮挡体是正常的渲染图元,使用标记位标识的可绘制对象的其余部分。

遮挡体设置作业:

  • 解码RSX风格的顶点和索引数组。
  • 输出剪切+投影三角形到暂存区域。
  • 隐藏DMA延迟的内部管道。

光栅化作业:

  • 启动一个光栅每条工作。
  • 使用列表DMA从暂存区加载三角形。
  • 在LS中绘制三角形到一个640x16深度的浮点缓冲区。
  • 压缩深度缓冲到uint16并存储。

过滤作业:

  • 光栅化完成后运行。
  • 生成粗糙的剔除数据。在查询期间用于剔除小对象。
  • 回写拒绝缓冲区(相邻且闭塞的缓冲区)。

遮挡查询作业:

  • 测试工作在两级层次结构中。对象存在于kd树中,并包含多个部件。
  • 测试对象以避免提取部件。
  • 测试部件,避免绘制它们。
  • 最终的查询结果写入主存,并用于建立显示列表。

提高裁剪率:

  • 尽量使用球体测试,以得到足够的RSX剔除。
  • 要确保对所有物体都进行全面测试。这使得优化变得更加重要。

文中还涉及了裁剪各个阶段的实现细节和优化建议,可点击原文查看。

Analytic Anti-Aliasing of Linear Functions on Polytopes阐述了多面体上基于线性函数的特殊解析抗锯齿。

采样(Sampling)是计算机图形学中非常常见的核心技术,应用广泛,最主要的用例之一是将数据采样到常规网格。

普通输入网格经过某种采样方式之后,利用有限的样本值重建网格。本文只涉及从原始网格采样的领域。

已知有一张具有空间高频的图片:

利用不同的滤波将上图下采样到半分辨率:

由此可知,Box、Hat滤波都存在一些问题,而高斯滤波效果最佳。为了更好地采样,通常还需要加入随机抖动:

解析采样的主体流程和步骤如下:

其卷积公式和图例如下:


获得的结果是复杂场景的无锯齿采样:


总之,本文提出了多边形和多面体的分析抗锯齿,允许网格上的线性函数(例如颜色、密度……)、高阶径向滤波器函数、常规和非常规采样网格。

Water Technology of Uncharted分享了神秘海域的水体模拟、实现和优化。水有多种形式,从小到大的水体,与水的相互作用,可弄湿衣服,快速移动,缓慢而难以导航。水体的着色模型如下:

水流模拟图如下:

基于流量的位移,每个顶点以不同的相位φ在圆形图案上移动:

海洋是渲染的挑战,开阔的海洋,大浪(100 米以上),海浪驱动船只和驳船,动画循环被考虑但未使用,可以游泳。

波系统,包含程序化、参数、确定性、LOD等。有两种波浪:Gerstner波,简单但高频细节不够,在高开销之前只能使用几个。FFT波比较真实,更多细节,频谱让艺术家难以控制,在低分辨率网格上*铺视觉失真。

还有波粒子(Wave Particle),来自点源的波,神秘海域不使用点源,相反,在环形域中随机分布,粒子来*似开放水域的混沌运动,一定速度范围内的随机位置和速度,产生一个可*铺的矢量位移场。波粒子的特点是艺术家可以直观地控制,没有*铺失真,速度快, 适合SPU向量化。时间确定性,无需移动粒子,新位置来源于初始位置、速度和时间。

波场是4个Gerstner波+波粒子(使用4次):

流网格是在网格中编码流、泡沫、幅度乘数:

添加更简单的波之后:

对于细节层次,有多种方法创建水体网格,屏幕投影网格 → 锯齿失真,准投影网格 → 处理大位移的问题。

不规则几何剪贴图基于Geometry Clipmaps: Terrain Rendering Using Nested Regular Grids,修改成水渲染。不同的分裂来固定环水*的T形接头,关卡之间的动态混合,小块(patch)可提高SPU利用率。



水体裁剪图运行机制。(只选取了部分步骤)

不同LOD级别的水体网格边界会出现裂痕,需要修复:


需要对水体网格Patch进行剔除,使用Frustum-bbox测试剔除截锥体之外的Patch。

用天空光照亮场景时,用视锥体-包围盒测试剔除视锥体之外的patch,用*面和包围盒测试剔除天窗外的patch:

计算天空光光照时,对于渲染使用着色器丢弃操作来进行*面裁剪(下图左),用于评估大厅包围盒内的钳位点(下图右)。

对于漂浮对象,采样点和最适合定位的*面,在相交区域,将幅度相乘。附加对象,采样点和最适合定位的*面,在相交区域,将幅度相乘。

网格计算时,对于每个环,运行一个SPU作业来处理patch (i % 3):

J1: (0,3,6,9,12,15) 
J2: (1,4,7,10,13,[16]) 
J3: (2,5,8,11,14)

最小化环级计算,双缓冲网格输出。

由于每个作业都会创建一个看起来完美的网格,因此无需缝合网格。海洋的最终网格,由多个网格组成。(下图)

需要注意时间,剪辑图需要特定时间的波粒子,只要一个波粒子工作,此作业生成位移网格。为了同步作业,设置了一个屏障来等待波粒子作业完成生成其网格。(下图)

渲染效果截图:

Graphics Gems for Games - Findings from Avalanche Studios描述了游戏中的一些特殊渲染技术,如粒子修剪、合并实例、电话线抗锯齿、第二深度抗锯齿。

文中提到GPU越来越强大,ALU涨麻了!TEX相当不错的增长,BW(带宽)有点迟钝,ROP(渲染输出单元)冰川般的速度:

如果ROP受限,想方设法绘制更少的像素。对于粒子修剪,典型的ROP密集案例包含粒子、云、广告牌、图形用户界面元素。解决方案:渲染到低分辨率渲染目标、滥用MSAA。本文的解决方案:修剪粒子多边形以减少浪费。粒子使用的网格常可见大量alpha=0的区域,浪费填充率,调整粒子的网格以减少浪费,可使用自动化工具

裁剪之后可以节省大量填充率,更多顶点⇒更大的节省,但收益率递减(下图),Just Cause 2使用4个顶点用于云、8个顶点用于粒子效果。

粒子裁剪有两种方式:

  • 手动修剪。乏味,但证明了这个概念,可用于云图集。2倍性能,数十个图集粒子纹理。

  • 自动工具。输入纹理、Alpha阈值、顶点数,输出优化的封闭多边形。

粒子裁剪算法:

  • 阈值化Alpha。
  • 将所有实心像素添加到凸边形。通过潜在角测试进行优化。
  • 减少Hull顶点数。替换最不重要的边,重复直到最大船体顶点数。
  • 蛮力遍历所有有效边缘的排列,选择面积最小的多边形。

粒子裁剪算法过程。1:原始纹理;2:阈值化纹理;3:将所有实心像素添加到凸边;4:减少凸边形的面积;5:最终4个顶点的多边形(60.16%);6:最终6个顶点的多边形 (53.94%);7:最终8个顶点多边形 (51.90%)。

粒子修剪的问题:

  • 多边形延伸到原始四边形之外。常规纹理没问题, 使用CLAMP。可能会切入相邻的图集tile,首先计算所有外壳,拒绝与另一个外壳相交的解决方案,如果没有有效的解决方案,则恢复为对齐的矩形。
  • 性能。蛮力,保持凸包顶点数合理地低。
  • 过滤。添加像素的所有四个角(更快),或插入子像素alpha值(准确)。
  • 处理“奇怪”的纹理,例如在纹理边缘alpha != 0。

接下来是合并实例化(Merge-Instancing)

实例化是一个网格多个实例,有多个绘制调用;合并是多个网格,每个网格一个实例,有顶点数据的重复;合并实例化是一次绘制调用,没有顶点重复。下面是实例化和合并实例化的对比代码:

// 实例化
for (int instance = 0; instance < instance_count; instance++)
  for (int index = 0; index < index_count; index++)
    VertexShader( VertexBuffer[IndexBuffer[index]], InstanceBuffer[instance] );

// 合并实例化
for (int vertex = 0; vertex < vertex_count; vertex++)
{
    int instance = vertex / freq;
    int instance_subindex = vertex % freq;

    int indexbuffer_offset = InstanceBuffer[instance].IndexOffset;
    int index = IndexBuffer[indexbuffer_offset + instance_subindex];

    VertexShader( VertexBuffer[index], InstanceBuffer[instance] );
}

合并奇数大小的网格,选择常用频率,根据需要复制实例数据,根据需要使用退化三角形填充。例子——Mesh0:39个顶点,Mesh1:90个顶点,选择频率 = 45,用2个退化三角形(6个顶点)填充Mesh0。

Instances[] = {
    ( Mesh0, InstanceData[0] ),
    ( Mesh1, InstanceData[1] ),
    ( Mesh1 + 45, InstanceData[1] ) }

接下来聊电话线抗锯齿。锯齿的来源:

  • 几何边缘。主要由MSAA解决,后处理AA通常也有效,用细几何体分解。
  • 着色。通过mipmapping解决的排序,缺乏研究/理解,一些实用的游戏技巧,如LEAN映射

电话线是常见的游戏内容,通常是亚像素大小,MSAA有帮助但不多,在子样本大小处中断。其实可以不要亚像素大小!

电话线是长圆柱形状,由中心点、法线和半径定义。避免进入亚像素,将半径钳制为半像素大小,以半径减小比淡化。

// Compute view-space w
float w = dot(ViewProj[3], float4(In.Position.xyz, 1.0f));

// Compute what radius a pixel wide wire would have
float pixel_radius = w * PixelScale;

// Clamp radius to pixel size. Fade with reduction in radius vs original.
float radius = max(actual_radius, pixel_radius);
float fade = actual_radius / radius;

// Compute final position
float3 position = In.Position + radius * normalize(In.Normal);

接下来是第二深度抗锯齿。过滤AA方法包含:

  • 处理AA:MLAA、SMAA、FXAA、DLAA。
  • 分析方法:GPAA、GBAA、DEAA、SDAA。

深度缓冲区和第二深度缓冲区,深度在屏幕空间中是线性的,简化边缘检测,可以预测原始几何。两种类型的边缘:折痕、剪影,剪影需要第二深度缓冲,使用正面剔除进行pre-z通道,或者输出深度以渲染背面几何图形的目标。

先尝试折痕,看深度坡度,计算交点,距离<1像素时有效,如果距离 < 半像素则使用,如果无效,尝试剪影。

尝试作为剪影,邻居深度没用,看第二深度,计算交点,如果距离 < 半像素则使用。

第二深度AA效果对比(左无右有):

Using GPUView to Understand your DirectX 11利用GPU分析工具GPUView阐述了GPU的工作机制、原理及调试过程。图形和WDDM(Windows Display Driver Model)的层级结构关系如下图所示:

发送任务给GPU的过程:

标准的DMA代表了图形系统状态对象、绘制命令、对资源分配的引用(纹理、顶点和索引缓冲区、渲染目标、常量缓冲区)。GPUView可以查看DMA的详情,还可以对上下文、队列进行监控:

CPU软件上下文队列是代表提交给GPU上下文的工作,队列在时间上表示为一个堆栈,堆栈在UMD提交工作时增长,当GPU完成工作时,堆栈收缩。


GPU硬件上下文队列在时间上表示为一个堆栈,通过KMD提交工作时堆栈增长,对象被GPU完成工作时堆栈收缩,间隙表示CPU端瓶颈。还可以选择特定的队列,显示其延迟:

分页缓冲包:作为结果提交分页操作(可能是一个大的纹理),原因通常是准备一个DMA缓冲器,查看分页操作之后的DMA数据包。对于硬件线程,颜色代表空闲,间隙代表工作:

对于线程执行,浅蓝色代表内核模式,暗蓝色代表dxgkrnl(DX内核),红色代表KMD(内核模式驱动):

还可以查看垂直同步情况:

获得正确的遮挡查询,延迟获取结果为N帧,其中N = GPU数,可能需要人为膨胀遮挡体积以避免跳变。避免分页控制显存的使用,特别是在MSAA模式下降低低端硬件的纹理分辨率,避免使用过多的动态数据纹理和顶点缓冲。总之,确保GPU保持工作状态,保持跟踪CPU/GPU交互,保持跟踪线程,监控multi-GPU交互,添加GPUView到工具箱。

Sand Rendering in Journey分享了游戏Journey的沙粒及沙漠的渲染。

沙子的渲染采用了锐化mip、各向异性遮罩、闪光镜面、海洋镜面、漫反射对比度、细节高度图等:

从左到右依次添加了:细节高度图、漫反射对比度、海洋镜面、闪光镜面、各向异性遮罩、锐化mip、最终成像。

沙粒的效果随着相机的距离改变而改变,文中对漫反射也做了修改,而不是使用Lambert。修改后的漫反射着色与Lambert相比,对比度要高得多:

左:修改的漫反射;右:Lambert漫反射。

左:高度图;右:高度细节图,右上两幅是微观细节,右下两幅是较宏观的细节。

Scalable High-Quality Motion Blur and Ambient Occlusion介绍了可扩展的高质量运动模糊和环境光遮蔽。

需要可扩展的原因是能够跨具有不同性能限制的*台共享效果,分享适合不同艺术风格的游戏。好处是视觉一致性、节省开发时间、一致的内容要求、“免费”质量收益。

潜在的可扩展元素有质量旋钮:最大半径、样本数、夹紧输入;分辨率独立性,允许标准化的屏幕单位而不是像素;模块化功能,准确性与速度;附加处理,附加通道,迭代算法。

文中给出的方案是:低端质量设计,向高端延伸,迭代质量和性能,迭代之间有充足的时间。

运动模糊的目标是不依赖资产,与所有几何类型一致,独立于场景复杂性,最小G-buffer编码。这些限制消除了大多数现有技术。*距离观察运动模糊:

核心问题:自然散射效果,需要表示为聚集,对象在其范围之外模糊,产生类似透明的效果。文中的方法:使用tile扩张速度,允许在对象边界之外进行模糊处理,沿扩张速度采样,分散 -> 聚集,根据速度和深度混合样本,背景估计。具体步骤:

  • 渲染速度。计算屏幕空间中的速度,钳制到最大模糊,重新缩放到[0, 1]。

  • 分块最大化速度。NxN下采样,N=最大模糊半径,按幅度记录最大值。

  • 邻域最大化速度。输入分块最大结果,应用3x3盒式过滤器,通过中心和周围分块的大小找到最大值。

  • 重建。中心深度\(Z_C\),颜色\(Color_C\),中心速度\(\overrightarrow{v}_C\),邻域最大值\(\overrightarrow{v}_N\)


  • 采样。沿 \(\overrightarrow{v}_N\)两个方向计算样本,采样\(Z_S\)\(Color_S\)\(\overrightarrow{v}_S\),确定前景或背景,与中心比较。

    • 确定背景还是前景。

  • 对比细节。有趣的案例:背景样本\(Z_C < Z_S\),潜在背景估计,前景样本\(Z_C > Z_S\),可能移过中心,两个样本都在移动(\(||\overrightarrow{v}_S|| \ne 0 , \ ||\overrightarrow{v}_C|| \ne 0\)),也就是想要一些模糊的东西。

  • 最终细节:收集样本,比较权重总和,总和颜色贡献,结果归一化。

运动模糊总体流程。

接下来聊聊可扩展的AO。

AO极大地有利于阴影中的照明,接触和折痕阴影。项目的约束是快速、表面感知、减少锯齿。

核心问题是不想要两种不同的算法,想要的视觉一致性。解决方案是更少但更高质量的样本,基于两件事的贡献:点到中心的距离、样本距离到法线的投影长度,试验衰减函数,直到满意的结果。该AO的步骤如下:

  • 在中心采样深度和法线,重建位置。

  • 采样属性。选择采样位置,采样深度,重建位置。

    ng)

  • 样本贡献。计算𝑣 ⃗,计算‖𝑣 ⃗ ‖,计算𝑣 ⃗⋅𝑛 ̂,根据半径r应用衰减。



    12.png)

  • 总的遮挡。汇总贡献并归一化:

    其中:𝑠𝑐𝑎𝑙𝑒 = 艺术家调整的贡献量表,𝑆 = 样本数。

  • 采样不同的法线对AO也有明显的影响,下图分别是采用GBuffer法线和派生法线:

  • 模糊结果。深度感知双边模糊,软化贡献,宽的滤镜可隐藏图案,结合阴影来摊销成本。最终AO:

可扩展和实现:

  • 设计扩展性。增加半径,更改衰减函数,强调接触阴影,更广泛的影响,变更申请,调制一切,调制环境。
  • 质量扩展性。增加半径,增加样本数,更好的采样模式,更广泛的模糊。

当前的实现:全分辨率4-tap,围绕螺旋采样,围绕随机向量旋转,使用镜像tap,将半径限制为约20像素,转置数学以一次计算,以Blue/Alpha编码深度以进行模糊。

DX11的实现:9 个样本,程序螺旋采样模式,引入一些噪点,无半径夹紧,未夹紧半径的Mip深度,以Blue/Alpha编码深度以进行模糊。

Separable Subsurface Scattering是时任职于暴雪的Jorge Jimenez等人呈现的皮肤渲染和眼睛渲染,详细阐述了皮肤建模、SSS、SSSS及眼球的渲染技术。对于皮肤,扩散曲线如下图:

使用若干项高斯函数拟合皮肤的次表面散射:

任何信号都可以通过若干个有理项来拟合并重建,例如傅里叶、快速傅里叶(FFT)、球谐函数(SH)及此处的扩散曲线等。

可分离参数化配置文件和优化后的公式如下:

当光线在物体的薄部分内部传播时,就会发生半透明,光线行进的距离越远,衰减就越多,意味着下图中第一点的半透明性将低于第二点:

除了距离,另一个因素是到达物体背面的光,但不幸的是,此信息不可用(下图红圈),因为使用的是屏幕空间方法。

观察:背面信息未知,人体皮肤的半透明隐藏了高频细节,在人体皮肤中,反照率不会发生显着变化。假设:可以使用反转的正面法线作为背面的法线,可以使用前面的反照率值,作为背面的反照率值(下图)。

这些假设允许将所有数学简化为下图红圈所示,可以预先计算成一个简单的纹理。

预计算纹理效果如下:

    float scale = 2e4 * (1.0 - translucency) / sssWidth;
    float4 shrinkedPos = float4(worldPosition - 0.005 * worldNormal, 1.0);

    // 通过使用阴影贴图,计算在物体内部行进的距离.
    float4 shadowPosition = mul(shrinkedPos, lightViewProjection);
    float d1 = shadowMap.Sample(LinearSampler, // 'd1' has a range of 0..1 shadowPosition.xy / shadowPosition.w);
    float d2 = shadowPosition.z; // 'd2' has a range of 0..'lightFarPlane'
    d1 *= lightFarPlane; // So we scale 'd1' accordingly:
    float d = scale * abs(d1 - d2);

    // 使用前面显示的预积分方程计算这个距离对应的颜色.
    float dd = -d * d;
    float3 profile = float3(0.233, 0.455, 0.649) * exp(dd / 0.0064) +
                     float3(0.1,   0.336, 0.344) * exp(dd / 0.0484) +
                     float3(0.118, 0.198, 0.0)   * exp(dd / 0.187)  +
                     float3(0.113, 0.007, 0.007) * exp(dd / 0.567)  +
                     float3(0.358, 0.004, 0.0)   * exp(dd / 1.99)   +
                     float3(0.078, 0.0,   0.0)   * exp(dd / 7.41);
    
    // 最后,使用标准环绕照明(wrap lighting), 开始半透明渐变, 比普通的点积要早一点.
    return profile * saturate((0.3 + dot(light, -worldNormal)) / 1.3);

光子映射和上面透射*似的对比。

这种皮肤半透明技术适用于直接光,但不幸的是,无法模拟环境光的半透明度。下图的耳朵里怎么没有光,哦,去到了在鼻子那里......

另外,还会导致下图阴影区域漏光的问题:

在直射光的情况下,我们知道多远,光已经穿过物体,通过使用常规阴影映射。但是在环境光的情况下,我们不知道这些信息,因为环境光不是来自特定方向。

有效的解决方案是:反转法线,向半球的每个方向投射一条光线并取*均值:

\[\text{thickness} \times (1.0 \ – \ \text{ambientOcclusion}) \]

其中,\(\text{thickness}\)是直到命中的光线距离,\(\text{ambientOcclusion}\)是被光线命中的表面上的环境遮挡。但是,耳朵上的环境遮挡太强,并且会过多地衰减透射光,解决方案是渲染环境光遮挡的多次反弹。

左:没有透射;右:使用了透射。

还有阴影映射的瑕疵,可以使用用户引导的透射率。

上:艺术家选择影响范围和方向,在耳朵、鼻子或眼睑等区域。下:用于预先计算每个球体沿特定方向的厚度,特别是计算十度圆锥的*均厚度。

文中还详细阐述了眼球的渲染,涉及诸多细节,可以点击原文或笔者的另外一个系列的文章:

Terrain in Battlefield 3: A Modern, Complete and Scalable System阐述了Frostbite的地形系统,包含可扩展性、工作流程、CPU和GPU性能、程序化虚拟纹理、数据流、稳健性、程序网格生成等。


地形使用了多个栅格资源:高度场、着色器喷溅蒙版、颜色图(用作着色器飞溅顶部的叠加层)、物理材质、破坏深度蒙版、反射光的反照率贴图、额外的遮罩通道。

Frostbite对可扩展性的定义:任意视距(0.06m至30000m),任意细节级别(0.0001m 及以下),任意速度(超级跑车和喷气式飞机)。主要观察:一切都与等级制度有关!层次结构的一致使用提供了“免费”的可扩展性,层次结构对地形渲染并不陌生,Frostbite方法类似于飞行模拟器,用于所有空间表示的四叉树层次结构!

四叉树节点有效载荷比较复杂,可以全部负载或部分负载,也可以LOD负载,抑或是视图相关的负载,

GPU优化:程序化虚拟纹理,使用着色器溅射,艺术家可以创造美丽的地形,渲染非常缓慢(10-20 毫秒)。着色器溅射在视野距离上不可扩展,负担不起多次通道,通过溅射到纹理中,利用帧到帧的一致性(性能),可以多次渲染(可扩展性),使用贴图渲染全屏耗时 2.5-3ms (PS3)。

虚拟纹理键值:每米32个样本,集成两个像素边框的256x256的tile,以图集存储,默认大小为4k x 2k,两个DXT5纹理:

非常大,可以轻松达到1M x 1M (= 1Tpixel)!典型的虚拟纹理为64k x 64k。

间接纹理格式:RGBA8,虚拟纹理tile图集的索引,低分辨率区域的比例因子,其中一个tile覆盖多个间接样本,CLOD渐变因子:用于*滑淡入新合成的tile(淡入tile),以前的LOD(淡入淡出tile)已经在图集中并使用间接mips获取,CLOD因子更新每一帧。

Teratexture的间接纹理可以轻松达到4k x 4k,太大了!使用Clipmap的间接纹理,Clipmap是早期的虚拟纹理实现,用6个64x64的clipmap层替换4k间接纹理。

clipmap间接纹理:每个绘图调用在CPU上解析剪辑图级别,避免额外的像素着色器逻辑,要求每个64x64的图都有自己的mip链,纹理空间必须与世界空间大致组织起来(不是地形问题),使用多个传统的虚拟纹理可能会更好地使用更通用的用例。

Tile合成:Tile在GPU上合成并在GPU或SPU上压缩,好处(与从光盘流式传输相比):磁盘占用空间小 – 数据放大,源栅格数据放大约 1000倍,低延迟:tile已准备好使用下一帧,动态更新:破坏、实时编辑,高效的工作流程:艺术家不必画出数百*方公里的每一块鹅卵石。

地形还启用了数据流,先了解一下流基础。流媒体单元:光栅tile(又名节点有效负载),典型的tile尺寸,包含高度场:133x133x2 字节、掩码:66x66x1字节 x 每个有效负载0-50个tile、颜色:264x264x0.5 字节。固定大小的tile池(图集),典型的图集大小,包含高度场:2048x2048、蒙版:2048x1024、颜色:2048x2048。

流模式:

  • 逐块(又名免费)流式传输,用于较慢的游戏。
  • Tile bundle(又名基于推送)流式传输,用于更快的游戏,与布局关联的tile被捆绑,基于地形分辨率的布局。
  • 混合流(最常见),在选定的生成点和过渡处使用的捆绑包,免费流填充其余部分。

关卡上所有数据的布局,在生成点加载的数据子集,地形分辨率布局定义子集,用户通过关卡时加载(和卸载)的子集。

总之,Frostbite 2拥有强大且称职的地形系统:高度场、阴影、贴花、水、地形装饰,大多数方面都可以很好地扩展:视距、数据分辨率、装饰密度和距离,流畅的工作流程,游戏内编辑,良好的工具范围,良好的性能(CPU、GPU、内存),并行化、流式处理、程序化虚拟纹理。

Loading Based on Imperfect Data介绍Insomniac Games的新引擎中的数据加载基础架构,该引擎为游戏Fuse提供动力,演讲将详细介绍Insomniac Games如何改进其源代码管理,并构建资产文件以在不牺牲工具迭代速度的情况下实现光学媒体的快速加载时间,还介绍一种新颖的磁盘布局方法,用于文件复制,使用蛮力GPU计算能力来计算最终布局。

数据构建在后台自动构建,资产大多以1:1的来源、产出比,没有全局的依赖关系视图,目标是一项资产变更只有 一项文件重建。运行时数据链接,简单构建系统的阴暗面,“先买后付!”,松散的文件加载,I/O和依赖检测同步运行。松散加载流图例如下:

松散加载的好处是随时加载任何资产,非常适合原型制作,资产只在RAM中存储一次,引用计数,易于在运行时重新加载资产。构建加载列表时,后台优化任务,仅在构建系统空闲时运行,运行游戏不需要数据,通过完全依赖扫描产生,需要爬取大量数据。

加载列表结果:完整关卡在DVD上的加载列表:1 分钟,开发时HTTP加载加速:约50%!需要另外2-3倍的DVD加速才能发行,良好:I/O 管道中的停顿消失了,不好:还是搜索受限!

文中还提及了资源去重的技术:


Practical Implementation of Light Scattering Effects Using Epipolar Sampling and 1D Min/Max Binary Trees描述了光散射效应在参与介质中的实际实现,该技术将极线采样与一维最小/最大二叉树相结合,并利用一种新的、简单且有效的半解析解决方案来解决由点光源引起的散射积分。该技术具有许多参数,允许以质量换取性能,这使其适用于各种硬件。内散射积分推导如下:



Rayleigh散射和Mie散射及组合效果如下:

体积阴影计算过程:


径向采样(Epipolar Sampling):


实现概览:

  • 从深度缓冲区重建相机空间z坐标,必需的操作,因为深度是非线性的,而z坐标可以安全地插值。
  • 计算一维纹理,其中包含每个核切片的进入点和退出点。
  • 该纹理以及相机空间z然后用于在极坐标中渲染坐标纹理和相机空间z,在这个阶段,还设置了一个深度模板缓冲区,用于标记有效样本。
  • 检测深度中断并计算插值源纹理。
  • 渲染另一个1D纹理,其中包含按上述计算的切片原点和方向。

  • 原始阴影贴图、方向和原点纹理用于为每个极线切片构建1D最小/最大二叉树。
  • 在模板中标记光线行进样本,并对每个样本执行具有1D最小/最大优化的光线行进算法。

  • 使用插值源纹理对初始内散射进行插值。
  • 内散射从极坐标转换为直角坐标,使用对极和矩形相机空间z纹理来计算双边权重。在此阶段,无法从极坐标插值的这些像素在模板中标记。

  • 最后,对模板中标记的像素执行内散射修复过程。在这个阶段没有使用一维最小/最大优化。

光束效果对比。左上:蛮力的参考图;右上:高质量;左下:均衡;右下:高性能。

Graphics Gems from CryENGINE 3阐述了2013年的CryEngine 3的一些特殊渲染技术,主要是抗锯齿和后处理。

抗锯齿\延迟MSAA的回顾:问题是Multisampled RT中的多次通道 + 读/写,DX10.1引入了SV_SampleIndex / SV_Coverage系统值语义,允许通过多通道解决像素/采样频率通道。SV_SampleIndex强制执行每个子样本的像素着色器并提供当前执行的子样本的索引,索引可用于从Multisampled RT中获取子样本, 例如FooMS.Load(UnnormScreenCoord, nSampleIndex)。SV_Coverage指示像素着色器在光栅阶段覆盖了哪些子样本,还可以修改自定义覆盖掩码的子样本覆盖率。基于DX 11.0 Compute Tiled的延迟着色/光照MSAA更简单,循环遍历带有MSAA标记的子样本。

延迟MSAA的注意事项:简单的理论,麻烦的实践,至少对于复杂的延迟渲染器,非MSAA友好的代码积累很快。打破常规,因为在没有考虑MSAA的情况下添加了新技术,即使仍然有效。很多时候,需要查明并修复非MSAA友好的技术,因为这些技术会引入视觉失真,例如白色/深色轮廓,或根本没有AA。改造渲染器以支持延迟MSAA是具备相当的工作量,而且非常挑剔。

延迟MSAA的自定义解析和每个样本掩码:后处理G-Buffer,执行自定义MSAA解析,预解析样本0,用于像素频率通道,例如照明/其它MSAA相关通道。在同一通道中创建子样本掩码(比较样本相似性,如果不匹配则标记),避免使用默认的SV_COVERAGE,因为它会导致对不需要MSAA的区域进行冗余处理。

延迟MSAA的模板批处理:使用常规模板缓冲区对每个样本模板掩码进行批处理,从模板缓冲区中保留1位,使用子样本掩码更新,标记整个四像素而不是单个像素 -> 提高模板剔除效率,使用模板读/写位掩码来避免每个样本位覆盖,StencilWriteMask = 0x7F,每当模板清除发生时恢复。由于过度使用模板而无法实现?可以使用剪辑/丢弃,额外的开销也来自为每个样本模板读取的额外纹理。

延迟MSAA的像素和采样频率通道:像素频率通道将模板读取掩码设置为每个像素区域的保留位 (0x80),绑定预解析(非多重采样)目标SRV,按常规执行渲染通道。

采样频率通道将模板读取掩码设置为每个样本区域的保留位 (0x80),绑定多重采样目标SRV,通过SV_SAMPLEINDEX索引当前子样本,按常规执行渲染通道。

延迟MSAA的Alpha测试SSAA:Alpha测试需要临时解决方案,默认SV_Coverage仅适用于三角形边缘,创建自己的子样本覆盖掩码,例如检查当前子样本是否使用Alpha测试并设置位。

static const float2 vMSAAOffsets[2] = {float2(0.25, 0.25),float2(-0.25,-0.25)};
const float2 vDDX = ddx(vTexCoord.xy);
const float2 vDDY = ddy(vTexCoord.xy);
[unroll] for(int s = 0; s < nSampleCount; ++s)
{
    float2 vTexOffset = vMSAAOffsets[s].x * vDDX + (vMSAAOffsets[s].y * vDDY);
    float fAlpha = tex2D(DiffuseSmp, vTexCoord + vTexOffset).w;
    uCoverageMask |= ((fAlpha-fAlphaRef) >= 0)? (uint(0x1)<<i) : 0;
}

延迟MSAA的性能优化:延迟级联太阳阴影贴图,像往常一样以像素着色器渲染阴影,延迟着色组合过程中使用双边上采样。访问深度的非不透明技术(例如软粒子),在现实世界场景中,逐样本处理的方法相当慢,在大多数情况下使用Max Depth就可以,而且速度快N倍。许多游戏也在用的方法:跳过Alpha测试超级采样,改用alpha覆盖,甚至不使用alpha测试AA(让形态AA解决这个问题),使用MSAA仅渲染不透明,然后在没有MSAA的情况下渲染透明,假设HDR渲染:注意色调映射是在解析后隐式完成的,结果是高对比度区域的细节丢失。

延迟MSAA的MSAA友好性:注意下图,没有明显的MSAA生效或明显的亮/暗轮廓。

修复后的效果如下:

延迟MSAA的回顾:访问和/或渲染多重采样RT?然后需要关心访问和输出正确的子样本。一般来说,总是应该努力最小化带宽,避免香草(vanilla)延迟照明,优先完全延迟、混合或者完全跳过延迟。如果延迟,优先选用轻量的GBuffer,GBuffer上的每个额外目标都会导致导出数据的开销 ,NV/AMD (GCN):导出成本 = 成本(RT0)+成本(RT1)...,AMD(旧硬件):导出成本 = (RT数) * (最慢的RT),高精度格式是GCN上双线性过滤模式的半速率采样成本,对于照明/某些hdr后期处理:大多数情况下32位R11G11B10F格式就足够了。

抗锯齿 + 4K分辨率下我们是否需要MSAA?下图是高度压缩的4K和原始4K的对比:

抗锯齿/追求更好(和更快)的AA:2011年:替代AA模式(和命名组合)的繁荣年,如FXAA、MLAA、SMAA、SRAA、DEAA、GBAA、DLAA、ETC AA,以及“实时抗锯齿的过滤方法”。着色抗锯齿:Mip映射法线贴图”、LEAN、CLEAN等。

时间SSAA、SMAA 2TX、4X回顾:形态AA + MSAA + 时间SSAA组合,*衡成本/质量权衡,技术相辅相成,时间分量使用2个子像素缓冲区,每帧为2x SSAA添加一个子像素抖动,重新投影前一帧并在当前帧和当前帧之间混合。之前的帧,通过速度长度加权,保持图像清晰度+合理的时间稳定性。


时间AA/常见的鲁棒性缺陷:依赖不透明的几何信息,无法处理信号(颜色)变化或透明度。为了得到正确的结果,所有不透明的几何体都必须输出速度。负面案例:Alpha混合表面(例如粒子)、光照/阴影/反射/uv动画/等,在AA解决之前的任何分散和类似的后期处理。可能导致分散的错误,例如透明度、照明、阴影等方面的重影,可能会出现剪影,来自分散和类似的后期处理(例如Bloom)。多GPU最简单的解决方案:强制资源同步,NVIDIA通过NVAPI公开驱动程序提示以强制同步资源,是NVIDIA的TXAA使用的解决方案。

SMAA 1TX/一个更稳健的时间AA:概念:只跟踪信号变化,不依赖几何信息,为了更高的时间稳定性:在累积缓冲区中累积多个帧,例如TXAA,重新投影累积缓冲区,权重:映射累积缓冲,将颜色缓冲到 当前 的范围内帧邻域颜色范围, 高/低频区域的不同权重(用于保持清晰度)。


float3 cM   = tex2D(tex0, tc.xy); 
float3 cAcc = tex2D(tex0, reproj_tc.xy); 

float3 cTL = tex2D(tex0, tc0.xy); 
float3 cTR = tex2D(tex0, tc0.zw);
float3 cBL = tex2D(tex0, tc1.xy); 
float3 cBR = tex2D(tex0, tc1.zw);

float3 cMax = max(cTL, max(cTR, max(cBL, cBR)));
float3 cMin = min(cTL, min(cTR, min(cBL, cBR)));

float3 wk = abs((cTL+cTR+cBL+cBR)*0.25-cM);

return lerp(cM, clamp(cAcc, cMin, cMax), saturate(rcp(lerp(kl, kh, wk)));

文中还涉及了部分后处理,如Bokeh DOF、运动模糊。

DOF不同权重的效果。

运动模糊的重建滤波器。

总之,文中阐述了实用MSAA详细信息,该做什么和不该做什么。SMAA 1TX是更强大的TAA,只需4个额外的纹理操作和一对ALU。一个合理且性能良好的DOF重建滤波器,可分离的柔性滤镜,任何散景内核形状都可行。第一次通道:0.426 毫秒,第二次通道:0.094 毫秒,总共消耗0.52ms的重构滤波器。一种改进的合理运动模糊重建滤波器,可分离,第一次通道:0.236 毫秒,第二次通道:0.236 毫秒,总共消耗0.472ms的重构滤波器。

Oceans on a Shoestring: Shape Representation, Meshing and Shading介绍海洋渲染的新技术,对现有表示的许多简单改进,允许有效高度查询的新程序表示,以及一种新的可显著减少锯齿的“固定网格”的网格划分技术。当然,还有在表面着色方面的经验。

海洋着色包含形状、网格及着色三大方面的技术。

在形状方面,采用傅立叶合成:几种已知的海浪频率成分模型,可以使用傅里叶变换来合成具有所需光谱的表面,将FFT离线预计算为一组置换贴图,64帧循环动画,64x64空间分辨率,8bit定点值,1.5mb用于位置和法线,对一组波求和。

还有采用波粒子,以模拟在水面上移动的“水滴”,为着色提供表面高度和泡沫值,可以自由移动或拴在浮动物体上,使用smoothstep作为粒子核,可以偏移参数得到波纹:

用于查询的世界轴对齐统一网格,更新后将活动粒子写入网格,将波纹栅格化到网格上的简单循环。

形状还采用了LOD,样品数量有限,尤其是在CPU上网格化时,避免视图依赖的不明显的形状。程序化 – 省略较小的波长,傅立叶合成——斜降(ramp down),波粒子——斜降。

总之,描述形状的多种方式,本文发现的方法是富有表现力且相对快速且易于实现。

网格方面,对于剪辑图(clipmap),将不同分辨率的网格缝合在一起,在预生产期间排除,未来的也行可行。

投影网格简单又高效,视图自适应,顶点随相机移动,插值错误变得生动而明显。在屏幕空间和世界空间对比如下:

如果可以保持顶点静止,我们可以冻结插值错误,可以采取极坐标网格划分(Polar Meshing)

普通投影网格和极坐标网格对比图。

极坐标叠加波浪及各类变换之后的网格形状。

它可以显著减少明显的失真,低开销,相对容易实现。

在着色方面,添加了泡沫、闪光、次表面散射、深/浅颜色,引入抛物线波,将波粒子扩展为“波纹粒子”,呈现在短时间内满足视觉和设计目标的形状描述组合。

Pixel Synchronization: Solving Old Graphics Problems with New Data Structures像素同步的背景、技术、优化等内容。

可编程着色器具有(并将继续具有)巨大的影响,推动了无数新渲染技术的开发,管线后端仍然无法编程,只能从固定菜单订购颜色、z和模板操作,但非常快速且省电。添加新的可编程后端?让它与固定功能硬件并存,发挥各自的优势。可编程后端使得DX11/OGL 4.2 可从像素着色器启用任意读写内存操作,但映射到同一像素的片元可能会导致数据争用。

片元可以乱序着色,不支持顺序依赖算法:

Haswell可以检测片元之间的依赖关系,并且避免数据竞争,保证R/M/W内存操作的原始提交顺序:

像素同步:像素/片段着色器的简单扩展,为R/W内存访问启用排序(即与Alpha混合的顺序相同),只是着色器中的一个函数调用:IntelExt_BeginPixelOrdering()。非常好的性能,在大多数情况下几乎没有性能影响,R/W内存访问由完整的SoC缓存层次结构支持。比从像素着色器读回帧缓冲区更强大,构建和访问任意大小/类型/维度的数据结构(包括体素),与MSAA解耦,可以使用逐像素和/或逐样本数据结构。


一些可编程的混合应用程序:新的混合运算符、非线性色彩空间、奇异编码等,例如RGBE、LogLuv等,延迟着色器的混合,例如通过混合法线和其它材质属性来应用贴花。

K缓冲区:Z-缓冲区的泛化,一次渲染N层图像。无数应用:深度剥离建设性的立体几何、景深和运动模糊、体积绘制...

性能提示:不要清除大缓冲区,清除一个小缓冲区并将其用作透明蒙版。

更小的数据结构可以提高性能,使用更多指令来打包/解包数据,*衡数据结构大小和打包/解包代码的数量,将一维结构化缓冲区寻址为*铺以更好地利用数据的局部性,例如1x2、2x2(2D 纹理)、2x2x2(体素)等,优先在着色器的后半部分插入同步点,增加映射到同一像素的同时着色片段的可能性。推论:尽可能使用硬件z测试以获得更好的性能(Hi-Z很快!)。

总之,可编程着色彻底改变了实时渲染,但不涉及管线的尾部。像素同步是一种新方法,可为3D管线注入新的活力,选择能够更好地解决渲染问题的逐像素数据结构,绘制几何图形以流式方式构建数据,使用数据并享受结果,DX11+扩展现在可用,OpenGL扩展在开发。

REDengine 3 Character Pipeline概述了《巫师2》制作中使用的角色管道,详细说明了使用的问题和解决方案,探讨使用的着色器、为艺术家设定的预算、角色照明的特殊性,尤其是过场动画中的特殊性,最后是使这些角色真正可信的动画和模仿。该文还展示了新系统,该系统利用DX11和Forward+渲染来获得更好的结果,例如头发模拟和渲染、皮肤着色以及完全修改的模拟动画系统,也分享了有关动画如何从动作捕捉工作室进入游戏以及如何为怪物和动物制作动画的详细信息。此外,还讨论了创建具有如此多专为角色创建的功能的一致照明和环境系统的困难。

Advanced Linux Game Programming阐述了Linux系统下的游戏编程技术,包含构建系统改进、信号处理、内存调试及OpenGL调试技巧。Unix信号是异步通知,来源可以是:进程本身、另一个过程、使用者、内核。很像中断,在第一次非原子操作时跳转到处理程序。

系统安装默认处理程序,通常终止和/或转储核心,核心≈Windows术语中的minidump,但会转储整个映射地址范围(截断为RLIMIT_核心字节)。可以指定自定义处理程序,通过sigaction获取/设置处理程序void handler(int, siginfo_t *, void *);,在sigaction()调用中,需要SAU_SIGINFO标志。信号可以嵌套,但不能和主程序共享:

无法调用异步不安全/不可重入函数。

Valgrind是Linux系统下的一款内存调试工具,动态运行时分析框架,动态重新编译,机器码→ IR → 工具→ 机器码,性能通常为未修改代码的25-20%。其内部有用于“远程”调试的gdbserver,每个错误上的SIGTRAP(断点),无限内存检测点!

对于OpenGL调试,古老的方法是每次调用OpenGL后调用glGetError(),获得八分之一的错误代码,需在手册里查一下调用,看看这个特定的错误在这个特定的上下文中意味着什么…然后检查实际情况,GLTEXAGE*()中GL无效值的6个可能原因!通常要附加调试器、重播场景…太糟糕了!

调试回调使得再也不用调用glGetError()了!它可以提供更详细的信息,包括驱动提供的性能提示,看看不同的驱动说了什么,如果没有调试OpenGL上下文(GLX_context_debug_BIT_ARB),可能无法生效。由以下任一方提供(ABI兼容):GL_KHR_debug[OPENGL02]、GL_ARB_debug_输出[OPENGL03]。不同的GPU供应商支持表如下:


可以控制冗长(过滤)(glDebugMessageControl_ARB),向11(GL_DONT_CARE)查询有价值的性能信息,如缓冲区的内存类型、未使用的mip级别…

API调用跟踪可以记录应用程序运行的跟踪,重播并查看跟踪,在特定调用中查找OpenGL状态,检查状态变量、资源和对象:纹理、着色器、缓冲区......使用apitrace或VOGL。

另外,gcc multilib是32/64位交叉编译的先决条件,在Clang和gcc之间来回切换很容易也很有用,使用gold可以大大提高链接时间。缓存gdb索引可以改善调试体验,崩溃处理很容易,但很难正确处理。Valgrind对内存调试有极大的帮助,即使在使用自定义分配器时,通过使用一些扩展,OpenGL调试体验可以大大提高。

2014年,Efficient Usage of Compute Shaders on Xbox One and PS4讲述了主机*台上的计算着色器的特性、应用及优化,如基于GPU的布料模拟、着色器、优化技巧等。尝试将布料从CPU到GPU的主要原因之一是当时的主机的GPU分峰值性能是CPU的15~23倍:

首次方案选用了以下的方式:

但是太多Dispatch,瓶颈还是在CPU。合并多个布料项目以获得更好的性能,所有布料必须具有相同的属性。新方法是一个巨大的计算着色器来模拟整个布料,着色器内的同步点,一次“调度”而不是50次+,使用单个“分派”模拟多个布料条目(最多32个)。其并行化处理如下:

确保连续读取以获得良好性能,合并=1次读取而不是16次读取,即使用阵列结构(SoA)而不是结构阵列(AoS)。

着色器优化方面,普通的规则是瓶颈 = 内存带宽,可以采用数据压缩:

使用局部数据存储(也称为局部共享内存):

在局部数据存储器中存储顶点:

使用更大的线程组,用256或512个线程,以隐藏最多的延时:


The Easy Route To Low Latency Cloud Gaming Solutions阐述了AMD的基于GPU的云渲染框架RapidFire的特性、架构和技术。RapidFire的特点是低延时、高清图像质量、多流、虚拟化支持、高分辨率、可协作、虚拟桌面、自适应网络环境,提供了服务端、网络、客户端、UI等组件。其渲染架构如下:

数据流概览:

客户端数据流:

客户端组件的初始化和循环:

LEAP DIRECT游戏*台——使用RAPIDFIRE的云游戏之弹性编码:

Volumetric Fog: Unified compute shader based solution to atmospheric scattering分享了大气散射导论、现有游戏解决方案、算法概述、实现细节等内容。大气散射包含天空颜色、雾、云、耶稣光、光束、体积阴影等视觉效果,每种效果又对应了不少渲染技术。大气散射涉及了复杂的各类散射,它们的模型和公式说明如下:




游戏中的*似方法:

  • 解析解(简单介质密度函数)。
  • 基于广告牌/粒子。
  • 基于后处理。
  • 射线行进。

文中并没有采用2D射线行进,原因是:通常不是基于物理的,循环使用GPU并行性很差,样本按顺序计算,而不是并行计算,像极线取样这样的解决方案是有限的,无变化的介质密度,没有多个光源,与前向着色不兼容,单层效果,信息存储一个深度,小量边缘失真。

受Light Propagation Volumes启发,采用了体素化的3D网格方案。算法概览:

  • 作为中间存储的体积纹理。
  • 使用计算着色器和UAV高效地进行光线追踪和写入。
  • 解耦典型散射步骤。参与介质密度估计,散射光计算,射线行进,应用效果。

算法细节:

3D纹理布局:

渲染效果:

SCRIPTING PARTICLES阐述了脚本化粒子的特性、实现及优化。脚本化粒子的好处在于更灵活、快速迭代、艺术家可控。

灵活性与性能:脚本编写很棒,但对于高性能任务来说速度太慢,即使使用了JIT。一些高性能领域将受益于更高的灵活性,如粒子模拟、风模拟(和其他矢量场效果)、声音处理等等,如何让脚本为它们工作?为什么脚本转译速度慢?下图有些代码是相同的:相同的机器指令,有些是使用字节码而不是本机代码的开销:

数据范围虚拟机(DATA WIDE VIRTUAL MACHINE):对多个数据项执行每个指令,解码和分支成本摊销,字节码是否与本机代码一样快?无法将数据保存在寄存器中,更多加载和存储,触摸更多的缓存。其循环顺序如下:

处理过程:

  • 构建在Vector4指令抽象之上。
  • 输入/输出数据为通道(SIMD向量阵列)。
  • 字节码包含在通道上操作的指令:pos = ADD pos move
  • 解码指令后,解释器一次将其应用于n个对象。

Vector4 *a = (decode channel ref);
const Vector4 *b = (decode channel ref);
const Vector4 *c = (decode channel ref);
Vector4 *ae = a + n;
// 循环可以被展开成n。
while (a < ae) 
{
    *a = *b + *c;
    ++a; ++b; ++c;
}

此外,在字节码中对常量和临时变量进行特殊处理和优化。概览如下:

  • 离线阶段。数据编译器解析代码:pos = pos + vel * delta_time,生成字节码,必要时引入临时变量:

    r0 = MUL vel (0.0 0.0 0.0 0.0)delta_time
    pos = ADD pos r0
    

    字节码已优化(临时变量消除)。

  • 运行时。打包字节码中的常量,执行指令。

总之,“数据范围解释器”模型是实现高性能的可行解决方案,脚本编写,完全可配置的行为,全动态:可以快速重新加载,无需重新编译引擎,与传统修改器堆栈解决方案相比,开销为18%(与原生相比为34%),支持AVX的脚本化解决方案比本机解决方案更快。未来每个组件一个通道(位置x、位置y、位置z),更多后端:JIT编译器、GPU计算、SPU…

无独有偶,Compute-Based GPU Particle Systems也涉及了GPU粒子,陈述了利用计算着色器对粒子加速的技术,包含碰撞、排序、分块渲染等内容。使用GPU的原因是高度并行的工作负载,释放CPU来做游戏代码,杠杆化计算。粒子的数据结构包含粒子属性(位置、速度、年龄、颜色等)、排序列表(序号、距离)、销毁列表(序号)。发射和模拟计算着色的数据流如下:


碰撞用到了图元、高度场、体素数据、深度缓冲区等。深度缓冲区碰撞时,将粒子投射到屏幕空间,从深度缓冲区读取Z,比较视图空间粒子位置与Z缓冲区值的视图空间位置,使用厚度值。

深度缓冲碰撞响应,使用G缓冲区中的法线,或者多次点击深度缓冲区,注意深度不连续性。为正确的Alpha混合排序,附加混合只是使效果饱和,双调排序在GPU上可以很好地并行。

for( subArraySize=2; subArraySize<ArraySize; subArraySize*=2) // subArraySize == 4
{
    for( compareDist=subArraySize/2; compareDist>0; compareDist/=2) // compareDist == 1
    {
        // Begin: GPU part of the sort
        for each element n
            n = selectBitonic(n, n^compareDist);
        // End: GPU part of the sort
    }
}

光栅化:DrawIndexedIndirectInstanced()或DrawIndirectInstanced(),VertexId=粒子索引(或VertexId/4表示VS公告牌),1个实例。对大粒子的过绘制限制了游戏设计,在纹理周围使用多边形公告板。渲染到一半大小的缓冲区,排序问题,存在失真。

逐tile的双调排序:因为每个线程都会添加一个可见的粒子,粒子以任意顺序添加到LDS,需要分类。仅在tile中排序粒子,而不是全局列表。分块渲染(1个线程=1个像素):

  • 将累加颜色设为float4(0,0,0,0)。
  • 对于tile中的每个粒子(从后到前):
    • 评估粒子贡献。
      • 半径检查。
      • 纹理查找。
      • 可选的法线生产和照明。
    • 手动混合。
      • color = ( srcA x srcCol ) + ( invSrcA x destCol )。
      • alpha = srcA + ( invSrcA x destA )。
  • 写入屏幕大小的UAV。

分块渲染(改进版):

  • 将累加颜色设为float4(0,0,0,0)。

  • 对于tile中的每个粒子(从前到后):

    • 评估粒子贡献。

    • 手动混合。

      • color = ( srcA x srcCol ) + ( invSrcA x destCol )
      • alpha = srcA + ( invSrcA x destA )
    • **if ( accum alpha > threshold ) **

      accum alpha = 1 and bail

  • 写入屏幕大小的UAV。

粗糙剔除:

  • 将粒子放入8x8中。
  • UAV0用于索引,使用偏移将阵列拆分为多个部分。
  • UAV1用于存储每个bin的粒子数量。每个bin1个元素,使用InterlockedAdd()累加计数器。
  • 对于每个活着的粒子:
    • 对于每个bin:
      • 根据bin的截锥*面测试粒子。
      • UAV1中的累加计数器,用于获取要写入的插槽。
      • 将粒子索引添加到UAV0。

性能对比:

结论:利用计算进行粒子模拟,深度缓冲区碰撞,正确混合的双调排序。分块渲染比光栅化更快,非常适合解决严重的过绘制,更可预测的行为。未来的工作是体积跟踪,为OIT添加任意几何体。

Landscape creation and rendering in REDengine 3讲述了巫师系列所用的引擎REDengine的地形系统的创建管线和渲染流程。

REDengine引擎的目标是支持超过16k分辨率的地图,顶点间距小于0.5米,各种景观特征,用于放置洞穴网格的地形孔洞,由一个相对较小的团队进行绘制和填充,地形形状由世界机器生产并导入。在纹理方面有很大的期望,混合材质,基于斜坡的、逐像素,地形必须投射阴影,广泛的复制粘贴功能胜过一般的“景观”。

地形/流:内存中的Clipmap,具有流式区域,使用texture数组。Novigrad的工作设置:46x46的tile,每个512x512(23万+),窗口分辨率=1024x1024,5个clipmap级别,内部顶点间距=~0.37厘米,约74*方公里。

地形/Clipmap:3个流式clipmaps:高度图(16位unorm)、控制图(16位uint)、颜色(32位,分辨率降低,如果是163842高度数据集,则为40962)。3个运行时生成的Clipmap:垂直误差(64x64常见情况)、法线(可选)、地形阴影。

地形/曲面细分:灵感来自Gpu Pro 3的文章,类似的技术应用于clipmap,三角形数量仍然非常好,最大曲面细分因子为8或16时,效果最佳,尤其适用于控制台GPU。

垂直误差图生成:对于每个细分块[x,y](1个控制点=1个曲面细分块):

软件曲面细分:对误差图进行下采样,以便在依赖硬件细分之前在四叉树级别进行简化,避免使用最小细分块的密集网格渲染大区域。

纹理目标:几乎不费吹灰之力就拥有令人信服的前景,从那里开始真正的工作,在特写镜头中融入精美的材质,离线可手动调整纹理大小,只使用UV刷。易于实现:没有材质流,只有一个纹理数组(两个都包含法线贴图)。

地形使用了三*面映射,叠加纹理没有三*面贴图,对于具有三*面贴图的背景纹理样本,选择哪些*面起作用(与纹理获取相比,更喜欢分支),尽可能缩紧混合区域,但不要出现瑕疵。混合区域缩紧如下:


地形阴影clipmap存储阴影中的最大高度,在流式处理clipmap或更改一天中的时间时更新,必须与clipmap计算紧密结合,以避免阴影闪烁,允许几个巨大的网格投射地形阴影。

地形阴影算法:

  • 铺设地形深度-太阳透视图。
  • 渲染到每个纹理的阴影片段clipmap:
    • 对每个纹素:
      • 在对应的高度纹理,计算全世界空间位置(wsPos)
        • For i=0 to n: // n=13有不错的效果
          • 将wsPos转换为太阳空间位置。
          • 比较太阳空间位置的z值和从pt.1纹理获取的z值。
          • 如果位置被遮挡,wsPos.z += step,step减半。
          • 如果位置未被遮挡,则wsPos.z -= step。
        • 重新绘制地形阴影贴图中的最后一个z分量值。

此外,植被还采用了复杂的生成算法和工具链。效果图如下:

Next-Gen Characters From Facial Scans to Facial Animation阐述了高质量的虚拟角色的制作流程,涉及的工具链和技术。文中提出的工作流如下:

1、扫描演员。

扫描阶段使用了摄影矩阵:

2、将原始扫描处理成对齐的混合形状。BlendShape扫描流程概述:

  • 在点处放置定位器。
  • 包裹网格进行扫描。
    • 将关节移动到点。
    • 匹配扫描上最*的点。
    • 放松。
    • 重复。
  • 导出头部。
  • 投影纹理。
    • 迭代应用光流,曲率和高频漫反射。
    • 将光流结果重新应用于网格。
  • 完成。

在以上过程中,需要处理很多额外的细节,例如纹理投影、表情、基础网格等等。

3、压缩blendshape纹理以实现实时回放。

许多漫反射纹理(70),Tiger Woods有数千人。

PCA指南:实际上是偏移量,下图从左到右依次是微笑、中性、微笑偏移(微笑-中性):

重建时,Final = w0*img0 + w1*img1 + w2*img2 + … + w11*img11,只需更改权重(着色器常数)即可设置动画:

计算主成分分析:可以使用SVD(奇异值分解),SVD将解决所有210列,然后砍掉除前12个以外的所有项:

PCA算法见:http://en.wikipedia.org/wiki/Principal_component_analysis。

4、使用mocap驱动混合形状。

查找与关节动画匹配的形状的权重:

5、使用皮肤着色渲染。

渲染使用的技术有Skin SSS、AO with spherical harmonics(SHAO)、Adaptive Tessellation、Eyes、Teeth等。

从左到右:没有阴影、SHAO、阴影、SHAO+阴影。

Hybrid Reconstruction Anti Aliasing讲述了Far Cry 4的混合重建抗锯齿技术HRAA。HRAA的目标是时间稳定性,高质量边缘消除混叠,与4倍RGSS相当的超级采样,1个样本/像素的着色成本,在分辨率为1080p的PS4/X1上的性能约为1ms。HRAA的概览:稳定边缘抗锯齿、时间超采样、时间抗锯齿。稳定边缘抗锯齿包含:

  • 形态:SMAA[Jimenez 11]、FXAA[Lottes 09]

    • 优势:最高的感知质量是静态场景、捕获所有行为、易于集成、使用光栅化数据。
    • 劣势:1080p时1.0-1.5毫秒(PS4/X1),时间不稳定,在运动中摇摆。部分解决:更昂贵的SMAAx4。
  • 分析边缘AA:GBAA[Persson 11]、DEAA[Malan 10]

    • 优势:最高的边缘质量接*基准真相,时间稳定,扩展到Alpha测试(使用SDF获得最佳结果),1080p(PS4/X1)下快速的0.3毫秒。
    • 劣势:复杂集成,每个G缓冲区着色器输出到边缘的距离,几何体着色器/直接顶点访问[Drobot 14],有光栅化问题,光栅化顺序依赖,内容相关,过度曲面细分会明显地关闭AA,不存在相交三角形。

    顶部:到边缘距离的可视化,颜色编码1比特方向(X,Y),符号值编码4位距离。中间:1x质心光栅化的结果。底部:分析解析的结果。请注意,右侧的边缘完全消除了锯齿。由于光栅化错误和亚像素三角形(多个三角形相交处),中间部分显示的边缘不正确。

  • MSAA。

    • 优势:随着样本量的增加,收敛到基本真理,解决亚像素问题。
    • 劣势:内存占用与采样量成线性关系,网格渲染时间随采样量而变化,延迟渲染的复杂集成。

    2.png)

  • EQAA/CSAA。GPU可以将覆盖率样本与颜色/深度片元分离,由廉价覆盖率样本辅助的MSAA=EQAA。

  • 基于覆盖率:覆盖率重建AA(Coverage Reconstruction AA,CRAA)。它将颜色片元与其它覆盖率示例一起使用,最低成本,从覆盖范围重建最终图像,需要能够直接访问样本的硬件,后面是基于AMD GCN架构的演示,其它IHV也支持覆盖率抽样。

FMASK:与颜色缓冲区关联的片段压缩缓冲区,存储样本和颜色片段之间的关联表,对于每个像素存储:对每一个样本,关联片段的位索引。每像素( [1, 2, 4, 8, 16样本] [1, 2, 4位用于颜色索引] + 1位用于UNKNOWN标记):

4-sample/2-fragment = 4 * 2 = 8 bit
8-sample/1-fragment = 8 * 1 = 8 bit
16-sample/8-frag = 16 * 4 = 64 bit

结合下图举个具体的例子:

上图顶部:样本0和1被锚定——有自己的深度片元用于深度测试;上图中部:蓝色三角形命中锚定样本0–蓝色添加到Color Fragments的序号0;上图底部:蓝色三角形覆盖的FMask样本将被关联到Color Fragments的序号0。

CRAA设置:MRT设置:颜色/深度1F xS,管线:Gbuffer渲染、光照、CRAA解析。8x CRAA的样本解析:

  • 对于每一个未知的样本:
    • 获取采样位置。
    • 将样本位置视为向量。
    • 累加在一起。
  • 总和定义了半*面分割像素的*似公式:
    • 计算半*面方向:垂直/水*。
    • 计算半*面坡度。
    • 从方向和坡度推断未知片元:上/下、左/右。
  • 解析像素 = Color Fragment * Coverage + (1-Coverage) * Inferred Fragment。

除了包含多个三角形交点的像素外,8xCRAA结果与8xMSAA相当。在这种复杂的情况下,单个边缘估计无法正确解析边缘。可见的失真与分析方法类似。

8xCRAA LUT:亚像素伪影呢?能消除它们吗?能摆脱ALU而只受限于带宽吗?解决方案:预计算LUT以存储相邻像素权重,使用完整的邻居,穿过像素的多条边/三角形。

8xCRAA LUT效果对比:

时间超采样:基于Killzone: Shadow Fall [Valient14],对数据使用当前帧和上一帧(2个样本),使用N-2框架进行颜色流向测试,N-1样本仅在以下情况下有效:第N帧和第N-1帧之间的运动流是相干的,第N帧和第N-2帧之间的颜色流是一致的(注意N-2和N具有相同的亚像素抖动)。

测试使用3x3邻域,绝对差之和,出于性能原因=>较小的窗口=>更保守。GCN提供硬件加速:SAD、QSAD、MQSAD、打包的插值。

如果N-1个样本不符合几何度量,从N开始插值,如果N-1个样本不符合颜色度量,通过N个颜色边界框限制N-1个样本,提高稳定性,带来了一些新信息。不同采样模式的效果对比如下:

ng)

从左到右:1x、FLIPQUAD、4xRG。

FLIPQUAD采样模式:[AMD 13] AMD_framebuffer_sample_positions,2xMSAA–易于设置,在同等成本下,其质量明显高于梅花形[Laine 06],下图是不同采样模式和误差表:

时间FLIPQUAD模式图如下:

把图案一分为二,帧A(蓝色)部分地渲染,帧B(红色)在随后渲染。需要在quad内逐像素地解析,根据帧在X轴或Y轴上进行方便地混合,Pixel0 = avg(BLUE(0,1), RED(0,2))

TAA:历史指数缓冲器,摊销突然的视觉变化(闪烁),尽可能多地积累新的“重要”数据,使用基于频率的验收指标。操作新数据的邻居(3x3窗口),接**均值的历史样本不会带来新信息,更远的历史样本带来更多信息,历史样本太远可能是一种波动。使用局部最小值/最大值作为软边界。

HRAA的最终实现:

  • 时间稳定的边缘抗锯齿。
    • SMAA(法线+深度+Luma预测阈值)
    • CRAA
    • AEAA(GBAA)
  • 结合TAA的时间FLIPQUAD重建。
    • TFQ+TAA

HRAA在Far Cry 4的最终实现:

  • 时间稳定的边缘抗锯齿。非显而易见的选择。
  • Alpha测试中的SMAA+AEAA。最可靠、最合理的性能。
  • Alpha测试中的CRAA+AEAA。最佳性能,一些内容问题。

它们的效果及性能对比如下:




Reflection System in Thief阐述了游戏《神偷》的引擎的反射系统,包含基本算法、反射系统概览、SSR、IBR、光泽反射、反射管线、局部立方体图反射、艺术管线等内容。

神偷的反射系统的基本算法:*面反射、立方体贴图反射、基于图像的反射、屏幕空间反射,以及局部立方体映射、*面上的图像代理、凹凸*面反射、Hi-Z屏幕空间反射等。

反射系统规范:在下一代*台(PC/PS4/X1)上小于5毫秒,多层反射面,人体高动态对象,准水*表面,应该抓住主要的地标。

解决方案:多层反射系统。每一层负责捕获特定的功能,立方体图(局部+全局)、基于图像的反射(IBR)、屏幕空间反射(SSR)。屏幕空间反射(SSR)的步骤如下图的红色箭头所示:

SSR优化:法线*似为(0,0,1),凹凸是后处理,更好的内存聚合——每次DRAM爆发都有更多有用的数据,使用Early Out,反射低时退出。

// 以下代码有什么【问题】?
...
float4 res = 0;
//Early out
if (reflectionFactor < epsilon)
{
    return res;
}
...
res = tex2D(...);
...

    
// 修复问题版本
...
float4 res = 0;
// Early out
[branch] // X4121:基于梯度的操作必须移动到流控制之外,以防止分支(divergence)
if (reflectionFactor < epsilon)
{
    return res;
}
...
res = tex2Dlod(...); // 使用非梯度指令和强制分支!笔者注:tex2D是梯度指令。
...

在第一次深度缓冲区相交处退出:

如果视线和反射方向的点积 > 0,则当前像素的菲涅尔系数相当低,可以提前退出。根据距离\(d\)而减少样本数量:

\[N_\text{linearSamples} = \max (1,\ k_1 e^{-ik_2 d}) \]

以半分辨率渲染,显著加速,在后处理过程中重建高频数据,最坏情况计时(全屏)约1.0到1.5毫秒,但在真正的地图上真的很难实现!

对于基于图像反射(IBR),UE3做得很好,但神偷每帧需要50个IBR代理,全高清8-10毫秒。依旧好用的半分辨率,IBR房间:将IBR定位到特定级别的位置,向下看时限制IBR反射。分块渲染,使用准水*反射器假设,经验主义,没有提供证据。

对于IBR分块渲染,将屏幕分成垂直的tile,计算代理AABB投影,将垂直边延伸到与屏幕的交点,将代理添加到受影响的tile。

对于光泽反射,在光滑的表面上反射光线发散,取决于反射器和反射对象之间的距离。输出反射距离,以4毫米的精度打包成2个字节,也可用于排序(因此精度高),扩大距离,与DOF方法类似的问题,存在模糊(2通道高斯)。

SSR、IBR混合:SSR的Alpha取决于几个因素:高度、跟踪精度(深度增量)、表面水*度、射线离开屏幕、射线回到摄像机前、“就因为我不想要SSR”,IBR优先合并,然后是立方体贴图,混合在sRGB中完成,但在PS4上使用RGBA16F。通常SSR比较接*,有些物体(火)是例外,使用距离在IBR着色器中排序。

IBR排序,左图未做排序,右图做了排序,故而有正确的火苗反射。

反射凹凸(Reflection Bump):在跟踪过程中,法线是固定的,由于半分辨率,高频信息丢失,类似于折射渲染的算法,所有辅助步骤计时约2毫秒。

艺术管线方面,局部化立方体图管线如下:

所有SKU上使用的默认立方体贴图,体积构造,设置捕获对象的属性,构建立体图的构造过程,立方体贴图被保存和组装,立方体贴图被指定给网格,获得最终结果。

IBR创建管线:确定IBR反射的候选区域(即水坑),为主要地标创建*面,被照亮的场景被烘焙到*面上,*面位置和照明调整(如果需要,增加*面),隐藏并移动到另一个区域。

总之,实时反射并不是一个已解决的问题,SSR需要后备,正在/将要使用多层解决方案,混合分辨率渲染以节省带宽。

Tessellation in Call of Duty: Ghosts详尽地描述了COD的曲面细分技术。曲面细分的实现可以分为3个步骤:离线全局曲面细分、运行时全局曲面细分、特性自适应曲面细分。

细分的过程会产生不规则面,从而导致网格破裂(crack),需要在不规则边缘上插入过渡点:

屏幕空间自适应的算法过程和符号意义如下:

边缘外推和角外推如下:


计算着色器可以当作Hull着色器:

直接使用汇编来代替HLSL,可以获得巨大的性能提升:

此外,还针对Wave使用率(CP/clock、clock/wave)和延迟做了详细的统计:

High Quality Temporal Supersampling由Unreal Engine的Brian Karis演讲,阐述了UE的时间抗锯齿(TAA)的技术、实现、问题及优化。

Brian Karis在文中对比了MSAA、空间过滤(MLAA、FXAA、SMAA等)、镜面波瓣过滤(Toksvig, LEAN, vMF等),发现它们都或多或少存在问题,或者缺失信息,最终选用了时间抗锯齿TAA。

TAA将样本分布到多个帧上,过去Brian Karis在这方面取得了巨大成功,如SSAO、SSR,替换空间滤波,高质量且更便宜。超级采样也是这样吗?

TAA涉及抖动、采样模式、移动*均数等技术点,对于*均的时间点,可以在色调映射之前或之后。在色调映射之前,物理上正确的位置,明亮的值占主导地位,锯齿严重程度受限于样本数量;色调映射之后,所有后处理过滤器都会闪烁,锯齿输入→ 锯齿输出。

直截了当的色调映射解决方案:

  • 混合色调映射之前和之后:在所有后处理之前应用,色调映射输入,累计样本,反转色调映射输出。
  • 与色调映射后的AA质量相同。
  • 向后处理链提供抗锯齿的输入,不再闪烁的泛光。

更好的色调映射解决方案:

  • 色调映射会降低明亮像素的饱和度。

  • 取而代之的是基于亮度的权重采样,保持色度,在知觉上更接*基准真相。

  • 不需要储存权重,重新推导出权重,节省GPR。

    \[\begin{eqnarray} \text{weight} &=& \cfrac{1}{1+\text{luma}} \\ T(\text{color}) &=& \cfrac{\text{color}}{1+\text{luma}} \\ T^{-1}\text{(color)} &=& \cfrac{\text{color}}{1-\text{luma}} \end{eqnarray} \]

重建过滤器有Box过滤(在运动时不稳定)、PRMan抗锯齿指南,以及高斯拟合Blackman Harris 3.3(支持大约2像素宽)。

重投影:当前像素的历史记录可能在屏幕上的其他位置,可能根本不存在,使用与运动模糊相同的速度缓冲区计算,记住移除抖动。

速度精度:一切都需要速度(运动矢量),没有正确速度的运动会模糊。准确度非常重要,微小的不精确会在静态图像上留下条纹,16:16 RG速度缓冲器。很棘手的是程序动画、滚动纹理、几乎不透明的半透明对象。

边缘上的运动:移动轮廓边缘会丢失AA,*滑抗锯齿的边不会随对象移动,实际上是速度缓冲区中的锯齿遮罩,扩大速度,取前最高速。

TAA的其中一个大问题是鬼影(下图),解决方案用深度比较?并非所有样本都具有相同的深度。用速度加权?着色变化和半透明让它失效。

可以采用邻域截取(Neighborhood clamping),将历史限制在当前帧的局部邻域范围内,假设结果是邻居的混合,最小/最大为3x3的邻域截取。

然而,邻域截取存在瑕疵:

基本的邻域截取与红色历史的对比。有很多瑕疵,最明显的是每个边缘都有红色的鬼影,更微妙的是,图像看起来像是低分辨率的。

可以改成使用YCoCg颜色空间的盒子。可以将min和max的基数视作RGB空间的AABB,且可以将Box的朝向定位到亮度方向上(因为亮度有很高的局部对比度,而色度通常不会):

用裁减(clip)而不是截取(clamp):限制为历史和邻域*均的混合,将线段剪裁到长方体,颜色不会像截取一样聚集在盒子的角落里。


基础版和YCoCg版的对比。

对于半透明,半透明不适合时间性的(单一的历史和速度),理想情况下,渲染半透明单独和合成,无法取消深度缓冲区的比较,可能的解决方案:4xMSAA深度预通道,选择要着色的样本。

UE的半透明解决方案:“响应AA”材质标志,在渲染半透明时设置模板,时间AA通过测试模板,并使用最少的反馈,不幸的是,需要>0的反馈来防止可见的抖动。仅适用于火花等小颗粒,其余的由邻居来处理。


TAA就像是一道防火墙,隔离了可见性样本和空间过滤,使得深度无法穿透TAA给空间过滤阶段的DOF使用。

另外在半透明阶段之后特殊路径,给DOF或光束设置数据,接着单独执行TAA,再执行空间过滤阶段的逻辑。

闪烁:相机是静态的,但有些像素会闪烁,丢失的亚像素特征的历史被截取,由于相关抖动,通常是垂直或水*线。截取是瞬间的脉冲,导致锯齿波闪烁。

UE在多次尝试之后,最终的解决方案是:当历史接*截取时,减少混合系数,在截取事件之后发生,特定于事件的内存,不需要额外的存储空间。但还没有完全解决,非常困难!无法解决多个相反的clamp。

模糊的过滤内核:Mipmap偏移所有纹理,超采样的导数不正确。如果对比度低,则减小过滤器内核大小,从技术上讲是锯齿,但看起来不错。可以添加额外的后锐化过滤器,Mitchell 4.0滤波器的负波瓣距离>1像素。

模糊的重投影扩散:可以使用来回误差补偿,没有取得好的结果,可以以更高的分辨率存储历史,但很大开销。当重新投影外部像素时,减少滤波器尺寸和反馈。

噪点过滤:不是它最初的目的,副作用很好,用于SSR和SSAO,随机采样效果很好,不需要额外的成本,几乎完美的镜面反射,仅需16个光线步进。

更多潜在的应用:随机透明度、单样本各向异性镜面反射IBL、软阴影、光线投射的简化步骤、视差遮挡映射、体积光照、路径追踪?虚拟现实?

未来的方向:时空结合、单独半透明、可见性和着色示例、每像素不同的抖动、定制MSAA样本放置、更完整的运动矢量、半透明、运动估计。总之,时间超级采样已准备好生产,高质量、高性能,需要大量的感知调整。更多可参阅:7.4.5 TAA

Realistic Cloud Rendering Using Pixel Sync分享了用像素同步技术来高效地失效云体模拟。模拟云体的常用技术有3种:公告板、射线步进、直接体积渲染,它们各有特点和优缺点。可以尝试将基于粒子的方法的控制与光线行进和切片技术结合起来。关键想法:使用代表实际3D形状的体积粒子,使用基于物理的照明,预先计算照明和其它量,以避免在运行时进行昂贵的计算,执行体积感知混合,而不是alpha混合。算法步骤:

  • 初始步骤。使用球形粒子建模云。

  • 添加预先计算的云密度和透明度。

  • 添加预先计算的光散射。

  • 增加光源遮挡。

  • 添加体积感知混合(通过像素同步启用)。

  • 增加光照散射。

预计算照明:主要的想法是为简单形状预计算基于物理的照明,从这些简单的形状构造云,粒子一词现在指的是这些基本形状(不是单个的微小液滴)。接着预计算光学深度、散射,然后是组合云体,计算光照遮挡,添加体积感知的混合。

左:传统的Alpha混合;右:体积感知的混合。

体积感知的混合算法细节如下:


DirectX不会对像素着色器的执行施加任何顺序,排序发生在输出合并阶段的晚些时候,如果两个线程读取并修改同一内存,结果是不可预测的。像素着色器排序可以确保:读修改写操作受到保护,即在其他线程完成对内存的写入之前,任何线程都不能读取内存;所有内存访问操作都按照提交原语进行渲染的相同顺序进行。

像素同步前(上)后(下)对比。

// Enabling pixel shader ordering
#include "IntelExtensions.hlsl"
...
void YourPixelShader(...)
{
    IntelExt_Init();
    ...
    // 像素同步函数
    IntelExt_BeginPixelShaderOrdering();
    // Access UAV
}

为了提高性能,粒子被渲染到低分辨率缓冲区,然后执行双边滤波,以提高原始分辨率并保留边缘。粒子生成时使用了网格,以摄像机为中心的同心环,下一个环中的粒子大小是内环的两倍,每个细胞包含几层粒子,每个单元中粒子的密度和大小由噪点纹理决定。

粒子渲染:

  • 粒子排序。粒子必须按从后到前的顺序渲染,在GPU上进行排序非常昂贵,可以在CPU上对细胞进行排序,并不是所有的细胞都包含真实的粒子。解决方案:仅为有效单元格输出粒子,使用“流出”来保持顺序,通过一个GS线程处理32个粒子。

  • 粒子处理。DispatchIndirect()用于执行CS计算每个有效粒子的光不透明度,DispatchIndirect()用于执行CS计算每个有效粒子的可见性。

与光照散射技术的集成:云密度纹理是根据光线渲染的,在射线行进的每一步,都要确定一个点是在云层之上还是之下(假设云层具有恒定的高度),如果点位于云下,则对云密度纹理进行采样,以获得云的遮挡,在屏幕空间中,云的透明度和与云的距离用于衰减沿视线的散射。

Adaptive Clothing System in Kingdom Come: Deliverance涉及了自适应的衣服系统。衣服模拟可能的方法:所有项目类型的形状/网格相同,网格之间的大(安全)距离,所有组合的手动调谐,以及混合方法。文中的方法高度现实主义,每件新衣服没有额外的艺术家投入。为了实现衣服的多层材质,使用了光线投射,过程如下:

  • 对每个三角形网格对:
    • 对每个三角形:
      • 挑选N个随机样本(质心坐标)。(下图a)
      • 追踪N条光线。(下图b)
      • 变换交点到顶点权重。(下图c)

效果如下:

Sound Propagation in Hitman分享了Hitman的利用声波建模来实现声音传播系统。

该声音系统涉及传播几何体、遮挡、传播路径、阻挡等方面的内容和模拟。

从左到右:传播几何体、传播计算、传播路径。

Unified Telemetry, Building an Infrastructure for Big Data in Games Development谈到了遥测技术用于游戏的方方面面的性能检测和分析统计,比如性能、尖峰检测、加载时间、启动时间、编译时、日志、内存追踪、缓冲区/池大小跟踪、已用资产/本地化跟踪、网络复制调试、带宽/延迟指标等等。

文中定义的遥测(Telemetry)是一种高度自动化的通信过程,通过该过程,在远程或不可访问的点进行测量和收集其它数据,并传输到接收设备进行监控。应用案例:统计数据收集、事件、状态快照、现场调试。非统一遥测数据和统一遥测数据流程分别如下两图:


好处是更简单的工具,跨域分析,非统计数据的团队范围分析,更容易协作。

Quick and Dirty: 2 Lightweight AI Architectures讲述了一种轻量级的AI架构。AI设计的目标是不需要太多人工智能,需要一种触发事件的方式:关卡成功/失败、声音事件、UI操作、特殊效应,快速开发和迭代至关重要!短时间扩展,实验性游戏设计。

触发器包括:单布尔触发条件,可以是多个子句,并且可以一起使用,行动清单;当布尔子句变为真时,触发器触发,当触发器触发时,执行每个动作(按顺序)。还有基于事件(与大多数架构不同),没有轮询或更新周期(大部分)。

# Play a hint after 20 seconds, but only once
playHint_1_8_moveCamera:
    triggerCondition:
    - and:
        - delay:
            - 20
        - doOnce:
    actions:
    - playSound:
        - ALVO36_Rover

模块化设计,子句是事件处理程序,事件持久性,定时阈值(默认值:667毫秒),如果触发或场景重置(成功或失败),则重置。触发组,小心地设计触发条件,给每个人一个(固定的)优先级,取最高的;全局触发器(例如碰撞失败、程序完成失败),级别初始化=>只是一系列操作。下图是更复杂的触发案例:

Physically-based & Unified Volumetric Rendering in Frostbite讲述了Frostbite引擎的体积渲染技术,以提高视觉质量,让艺术指导更自由,更加物理的体绘制(有意义的材料参数、将材质与照明分离、连贯的结果),统一体积相互作用,结合了照明、常规阴影和体积阴影,可以与不透明、透明和粒子的相互作用。

在渲染体积时,Frostbite限制为单次散射,当光表面与表面相互作用时,可以通过评估例如BRDF来评估反射到相机的光的量,但在参介质面前,事情变得更加复杂,必须考虑透射率。然后,需要通过采集多个样本,沿视图光线对散射光进行积分。还需要考虑每一点到视点的透射率以及在每个位置对散射光进行积分,并考虑相位函数、常规阴影贴图(不透明对象)和体积阴影贴图(参与介质和其它体积实体)。

Frostbite采用了裁减空间的体积:视锥体对齐的3D纹理[Wronski14],世界空间中的视锥体体素=>Froxel。Frostbite是一种基于分块的延迟照明,带有剔除的光源列表的16x16块。在光源分块上对齐体积分块,重复使用每个分块剔除的灯光列表,体积块可以更小(8x8、4x4等)。仔细校正分辨率整数除法,默认值:8x8个体积块,64个深度切片。

数据流如下图,使用剪辑空间体积来存储管线不同阶段的数据,材质属性首先从参与介质的实体中进行体素化。然后使用场景的光源和该材质属性体积,可以生成每个像素的散射光数据,这些数据可以临时上采样以提高质量。最后是集成步骤,为渲染准备数据。

这种体渲染可以结合若干个*行光和点光源,最终渲染出来的效果如下:

还支持粒子体积阴影和不同的采样方式:

The Real-Time Volumetric Cloudscapes of Horizon Zero Dawn阐述了游戏Horizon Zero Dawn中的影视级的体积渲染和云体景观。地*线的世界是开放的,玩家可以穿越很远的距离,支持昼夜循环、动态天气,拥有壮丽的风景山脉、森林、湖泊,天空是风景的一部分。

文中对体积云景观从建模、光照、渲染、优化等方面进行了详细的阐述。建模时考量了各式各样的云体形态和不同的高度:

更详细地,考量了真实世界的诸多变化因素:密度在较低温度下增加,温度随高度降低,高密度沉淀为雨或雪,风向随高度变化,它们随着来自地球的热量而上升,密集区域上升时形成圆形,光线区域像雾一样扩散,大气湍流进一步扭曲了云层。建模的技术要点有分形布朗运动、分层柏林频率、高度位移、程序化云、漂亮的威士忌形状等。尝试了各种各样的噪点生成技术,使用2个3D纹理和1个2D纹理生成动态噪点结果。使用射线步进/采样器、2级细节(低频基云形状、高频细节和扰动),Perlin、Worley和Curl noise制造的定制噪点,由高度信号和覆盖信号调制的密度,由天气模拟/用户输入驱动,带动画。

光照模型考虑了外散射、吸收、内散射等。

使用的光照模型和曲线如下图:


渲染过程中,环境色贡献增加高度,直接照明颜色的贡献来自太阳,大气层挡住了云层深度。采样器做廉价的工作,除非它可能在云中,每次步进用64-128个步进样本、6个光源样本,光源样本在某个深度从完全到低开销。

Sampling Methods for Real-time Volume Rendering也谈及了体积渲染,但关注点在于采样方式,通过控制样本的布局和运动,大幅减少方差,还描述了如何自适应采样深度,以便在保持接*视图的质量的同时渲染大范围的效果。改用下图的采样方式,可以解决旋转或扫射问题,可以动态调整,但更改曲率会移动样本,固定在两种配置之间可能会起作用

区分了水*对流:

采样了自适应采样:

最终的效果可以很好地匹配摄像机的任何运动而不产生跳变等瑕疵:

Stochastic Screen-Space Reflections阐述了Frostbite引擎的随机光栅化的反射效果。Frostbite对反射效果的要求是清晰而模糊的反射、接触硬化、镜面伸长、逐像素的粗糙度和法线:

Frostbite的方法是从表面的重要方向采样开始,但只发射很少的光线,甚至低至每像素一条。

主要区别在于,没有立即返回颜色,而是几乎不存储交点。然后有一个解析过程,在相邻像素上重用这些交点。

在重用过程中,使用*似的锥体拟合,并考虑反射距离,计算每个样本所需的模糊级别。仔细权衡每个像素的BRDF的贡献,这样就避免了法线的涂抹、过度模糊,并保留了清晰的接触点。

SSR的算法流程如下:

射线追踪时使用了层次深度追踪,即最小Z金字塔的无堆栈射线步进:

mip = 0;
while (level > -1)
    step through current cell;
    if (above Z plane) ++level;
    if (below Z plane) --level;

光线重用:相邻像素发射有用的光线,可见性可能会有所不同,可以假设是一样的,以便重用交点结果。

使用局部BRDF作为权重,除以原始的PDF,由射线追踪和生命点返回,邻居可以拥有截然不同的属性,BRDF/PDF比率峰值,比不重复使用更糟糕的结果。

左:1个像素1条光线和1个解析样本(没有重用);右:1个像素1条光线和4个解析样本(1条光线重用到4个像素)。

此外,为了减少方差,对蒙特卡洛进行了改进:

R9.png)

其中:

  • 分子是BRDF加权的图像贡献。
  • 分母是BRDF权重的标准化。
  • FG是预积分的BRDF。

伪代码:

result    = 0.0
weightSum = 0.0
for pixel in neighborhood:
    weight = localBrdf(pixel.hit) / pixel.hitPdf
    result += color(pixel.hit) * weight
    weightSum += weight
result /= weightSum

不同的参数效果如下:

稀疏光线追踪(Sparse raytracing):降低分辨率的光线跟踪,以全分辨率重复使用多条光线,每个像素都有独特的光线混合,自动来源于BRDF,保留每像素法线和粗糙度,权重标准化弥补了差距。

时间重投影:沿G缓冲区深度“涂抹”重新投影,添加反射深度,局部射线*均值,适当反射视差。


14.4.4 渲染技术

14.4.4.1 Inferred Lighting

推断光照(Inferred Lighting,IL)是延迟光照的一个变种,也叫光照预通道渲染(Light Pre-Pass Rendering)。将照明与场景复杂性隔离开来,对于处理破坏至关重要,推断光照 = Light Pre-Pass++,可调照明分辨率、支持MSAA、Alpha照明。推断光照有3个通道:

1、几何通道。准备光照所需的GBuffer。

Red Faction的GBuffer布局如下:

由此可见,GBuffer包含法线(23bit)、镜面反射指数(7bit)、光照类型(2bit)。

2、光照通道。计算光照。

遍历每个可见光源并计算该光源对场景的贡献,累加所有光源的贡献,并将结果存储在称为L-Buffer(光照缓冲区)的纹理中。

计算光照所需的所有信息都来自光源和G-Buffer。

光照信息存储在L-Buffer(64位HDR纹理,见下图)中,漫反射分量存储在RGB通道中,镜面反射分量不存储为全色,而是存储为亮度值,亮度可以像颜色一样累加。

当需要完整的RGB镜面光照颜色时,采用漫反射光照颜色并将其缩放到为规范存储的亮度,为真正的镜面光照分量提供了可接受的*似值。

3、材质通道。使用光照。

遍历可见对象并再次绘制它们,使用材质通道着色器,这些着色器将L-Buffer中的光照信息与为每个对象定义的其余材质(纹理、对象颜色、反射、自发光材质、距离雾、色调映射等)合成在一起,以产生最终的输出颜色,对于每个像素,进入最终场景输出。

以上是推断光照的常规流程,下面开始展示Red Faction的差异化实现。首先从如何在这些材质通道着色器中合成L-Buffer中的光照开始。

合成照明时,简单的普通的light pre-pass系统的做法是像素在G/L/M通道之间完全匹配:

推断光照允许几何体/光照通道使用比材质通道更低的分辨率,允许向上或向下调整照明分辨率以权衡视觉质量与性能。不过,在这种情况下,直接尝试使用L-Buffer值会非常失败。对较低分辨率的L-Buffer进行采样将导致在任何边缘产生混合值,导致光渗色和伪影。

为了理解如何处理这个问题,可以看看一个特定像素到底发生了什么。不妨把每个像素视为一个数学点,而不是一个正方形的颜色区域——这样更容易理解数学。

采样低分辨率光照。

结合下图,被着色的像素属于顶部对象(下图左边黄色),混合这四个光照样本(下图中),四个样本中的三个来自底部对象,几乎是最终颜色的80%(下图右)!如果我们可以检测并纠正这个边缘会怎样?

可以使用不连续敏感过滤(Discontinuity-Sensitive Filtering,DSF),在保留边缘的同时调整L-Buffer的大小,使用第三个G-Buffer:DSF数据,它是一个16位的值(8 位存储对象 ID,由游戏分配;8 位存储普通组ID,保存在网格数据中)。

G-Buffer:DSF数据。

DSF的过程如下:

  • 在材质通道着色器内部,可以知道当前正在绘制@这个像素的哪个对象。(下图左)
  • 比较4个DSF ID中的每一个,DSF与L-Buffer1:1匹配。(下图中)
  • 如果DSF ID不匹配:将混合权重设置为0.0,调整DO匹配的样本加起来为1.0。(下图右)

下图的上排没有DSF,下排有DSF。请注意,第一列中对角线边缘和通风口轮廓周围的“阶梯”伪影减少,第二列和第三列中引擎盖和管道边缘周围的块状明亮像素被消除。

上述的版本运行良好,但可能会很慢,8个纹理查找(4个DSF,4个光照),计算着色器中的加权和。优化:改为调整样本UV,只有一个照明样本,硬件进行混合。

DSF的优化过程。图中顶部的两个样本具有匹配的DSF ID,而底部的两个样本没有,在这种情况下,可以简单地垂直调整UV,从混合中消除底部的两个样本。根据匹配样本的模式,可以以这种方式垂直、水*、两者或都不调整UV。

DSF可以更好地控制照明的质量与速度,还有硬件抗锯齿 (MSAA)。每个像素多个子样本,由硬件管理。LPP(Light Pre-Pass)不擅长处理MSAA,每个像素一个照明样本与多个子样本!使用DSF,IL可以免费处理MSAA!

推断光照可以支持透明度,但存在与MSAA类似的问题:每个像素只有一个L-Buffer样本。具有不同DSF ID的像素可以“隐藏”,在计算光照时将被忽略,可以利用这一事实来发挥优势!

将屏幕分成2x2像素块,每个块中的一个像素(alpha),Alpha像素获得不同的DSF ID,不透明材质会自动剔除这些光照样本。

Alpha使用修改后的DSF,将L-Buffer中的每个2x2块视为一个单元,硬件过滤优化在这里不起作用。

通过使用不同的块像素,最多可能有四层。

IL透明渲染效果如下:

不连续性的总结,支持MSAA、半透明,不支持低分辨率照明。基本思路:检查DSF ID是否与相应像素匹配。如果是,就完成了。如果不匹配,请检查邻居,直到找到匹配的邻居,通常最多检查四个邻居。

IL的限制和问题是:

  • Alpha对象上的低分辨率照明。1/4不透明光照的整体分辨率,尽可能避免高频法线。
  • 头发是IL不擅长的事情,应该避免用于头发!
  • Alpha分层。附加层会降低不透明照明的质量,分配图层以避免碰撞是很困难的!本质上是一个图形着色问题,RF:A 仅使用一层alpha光照。
  • 粒子。粒子不适用于有限的Alpha光照层,可在粒子上伪造光照。
  • DSF增加了标准光预渲染通道的成本。降低照明分辨率有助于*衡这一点,不连续性修补可能是一种成本较低的选择,RF:A使用不同分辨率的补丁:Xbox/PS3:960x540、PC:达到硬件限制,SR3使用完整的 DSF:1280x720场景、800x450照明,还使用了所有4层半透明照明。

1年后,Lighting & Simplifying Saints Row: The Third分享了推断照明的最新迭代,新增了几项优化和功能。

原先的推断照明支持许多完全动态的光源、集成Alpha照明(无前向渲染)、硬件MSAA支持(即使在 DX9上)。而本文在此基础上新增了雨滴照明(需要IL)、更好的树叶支持(仅适用于IL)、屏幕空间贴花(由IL增强)、径向环境光遮蔽 (RAO)(由IL优化)。

雨滴照明:将每个雨滴的单个像素渲染到GBuffer中,照明通道免费地照亮雨滴,DSF自动忽略雨滴采样,雨滴查找它们的照明样本。

植被:推断照明假设场景深度复杂度较低,以保持DSF成本有界,植被打破了这一假设。更快的植被DSF:

在PS3场景完整的DSF,GPU时间:35.7ms,2个样本是33.7ms(节省2ms)。

动态贴花:屏幕空间贴花是应用于屏幕空间的体积贴花,使用DSF ID将贴花限制为特定对象。现有的DSF ID用作贴花鉴别器,以防止贴错对象:

屏幕空间贴花。左边是错误的贴花,右边使用现有的DSF ID修复。

径向环境遮挡(Radial Ambient Occlusion,RAO):大致基于 [Shanmugam & Arikan 2007],遮挡因子基于法线和到盒子或椭圆体的距离,非常像普通光源,用于调制照明的遮挡因子,对于车辆,艺术家放置接*车身的盒子,对于人类,椭圆体自动放置在脚下。

RAO效果对比。左边有,右边无。

14.4.4.2 GPU-Driven Rendering Pipelines

GPU-Driven Rendering Pipelines由Ubisoft Montreal的Ulrich Haar等人呈现,讲述了基于GPU驱动的全新渲染管线,并应用在了刺客信条中,使得刺客信条的场景复杂度成指数增长。

GPU驱动的渲染管线需要用到分簇(cluster),即将网格线拆分成一个个cluster。一个cluster具有固定的拓扑(64个顶点的strip),拆分并重新排列所有网格以适应固定拓扑(插入退化三角形),从共享缓冲区手动获取VS中的顶点,使用实例化非直接绘制接口DrawInstancedIndirect,GPU剔除输出cluster列表和绘制参数。

单个drawcall中任意数量的网格,按cluster边界的GPU剔除,更快的顶点提取,cluster深度排序。

GPU驱动的渲染管线的概览如下:

在CPU方面,仍然执行非常粗糙的截锥体剔除,然后按材质将所有未剔除的对象打包在一起。在GPU上,从逐实例截锥体和遮挡剔除开始,然后,在根据截锥体和遮挡深度剔除cluster之前,执行了一个cluster扩展的步骤。接着再压缩索引缓冲区,压缩过程中剔除一些背面三角形,压缩结果提供给multi-drawcall使用。所有变形的对象都会绕过管线中特定于cluster的部分。

CPU四叉树剔除,每个实例的数据包含变换、LOD因子等,在GPU环形缓冲区中更新,静态实例的持久性。基于非实例数据的Drawcall哈希构建,如材质、渲染状态等,基于哈希的Drawcalls合并。

实例流包含每个实例在GPU缓冲区的偏移量列表,允许GPU访问变换、边界、网格等信息。

然后,GPU使用这些信息的截锥体并剔除实例流。对于通过剔除的所有实例,将发出簇块(cluster chunk)列表。

使用簇块扩展的中间步骤(而不是直接簇扩展),因为每个网格的簇数量变化很大(1-1000),即时簇导出通常在波前内的GPU线程之间非常不*衡,每个簇块最多导出64个簇。

然后,簇剔除步骤使用实例变换和每个簇的边界,根据截锥体和遮挡剔除簇。对于每个簇,还获取一个与视图相关的三角形遮罩,用于预烘焙三角形背面剔除。通过剔除的簇,导出由三角形掩码和索引读写偏移组成的索引压缩作业,这些偏移量是通过对相关实例drawcall图元数量执行原子操作来确定的。

接着,所有未填充簇的索引压缩都会发生在动态索引缓冲区中。压缩索引缓冲区中的空间是在CPU上分配的,因此必须为每个网格实例分配完整(未填充)的索引量。由于压缩索引缓冲区非常小(8mb),意味着一个渲染过程无法完全放入缓冲区,大的处理被分割,以便索引缓冲区压缩和多绘制渲染可以交错进行。

此时,索引压缩将删除被逐簇剔除和三角形背面剔除剔除的三角形。在每个索引压缩计算作业中,一个wavefront处理一个簇,每个线程处理一个三角形,完全独立于其它线程和wavefront。根据簇剔除步骤发出的三角形掩码和输入/输出偏移,每个线程独立地计算输出写入位置到动态索引缓冲区,并复制3个三角形索引。这一步需要5%-10%的几何体渲染。

然后,逐批(batch)使用一个MultiDrawIndexInstancedIndirect调用来渲染在簇剔除期间使用原子操作生成的drawcalls组。

静态三角形背面剔除:以簇为中心的立方体贴图像素截锥体的烘焙三角形可见性,基于摄像头的立方体图查找,获取64位以查看簇中的所有三角形。每个立方体贴图面只有一个像素(每个三角形6位),像素截锥体被远距离切割以提高剔除效率(斜角可能出现误报),10-30%的三角形被剔除。

遮挡深度生成:使用最佳遮挡体进行深度预通道,以全分辨率渲染High-Z和Early-Z,降采样至512x256,结合最后一帧深度的重投影,用于GPU剔除的深度层次结构。

阴影遮挡深度生成:对于每个级联,摄像机深度重投影(约70us),这阴影深度重投影(10us),用于GPU剔除的深度层次结构(30us)。

相机深度重投影的过程如下图:

左上:黄色箭头表示太阳的方向,红线显示一个阴影贴图级联的范围,红色物体是阴影投射者,不能拥有有效的接收者,因为所有可能的接收者都被地面和蓝色物体隐藏。

右上:亮黄色区域标记前景对象创建的地*线,阴影接收者在其下方不可见。每一个比这个地*线更远的阴影投射者都可以被剔除。需要计算黄色区域顶部红线在光照空间中的深度(也就是黄色箭头方向),以用于遮挡剔除。

左下:如果为相机深度中的每个像素渲染一个立方体,从**面拉伸到相机深度的值,可以得到图中所示的黄色立方体。

右下:现在可以看到绿线,即光空间中立方体的最大深度渲染,与前面提到的红线相同。显然,要为相机深度图像的每个像素渲染一个立方体,需要太多立方体。取而代之的是,为相机深度的每个16x16的tile渲染一个立方体,并使用每个tile的最大深度。然后绿线变成了红线的保守*似值。在光源空间中渲染立方体时,重要的是将其填充到一个像素的最小和大小,并考虑最大阴影过滤器大小。请注意,当没有任何透明阴影接收器时,可以不从*面附*的摄影机挤出立方体,而是从每个tile的最小深度挤出立方体。在这些条件下,在最大下采样期间,远*面也可能被拒绝,从而产生更紧密的立方体渲染。渲染立方体时,重要的是直接在像素着色器中输出深度,并使用纹理坐标插值器导出像素深度,而不是固定功能管线提供的深度,因为对于几乎与光源方向*行的三角形来说太不精确。

重投影建筑物的阴影。

相机深度重投影:类似于[Silvennoine12],但由于有雾,遮罩无效:无法使用最小深度,不能排除远*面。64x64像素重投影,可以预处理深度,以消除过绘制。

获得的结果是:在CPU上减少1-2个数量级的绘制调用,约上一代刺客信条的75%,约10倍的物体。在GPU上剔除20-40%的三角形(背面+簇边界),整体增益很小:<几何体渲染的10%,剔除30-80%的阴影三角形。正在进行的工作:更多GPU驱动的静态对象,更批量友好的数据。未来的工作:无绑定纹理、基于DX12和Vulkan。

该文还分享了GPU驱动渲染中的虚拟纹理、虚拟延迟纹理、MSAA技巧、两阶段的遮挡剔除、虚拟阴影映射等技术。

虚拟纹理关键思想:只在内存中保留可见的纹理数据,虚拟256k x 256k纹素图集,128 x 128纹素页,8k x 8k纹理页面缓存,5层纹理数组:反照率、镜面反射、粗糙度、法线等,采用DXT压缩(BC5/BC3)。

视口=单次绘制调用(x2),不同顶点动画类型的动态分支,现代GPU速度快(成本增加2%),簇深度排序提供与深度预处理类似的增益,反向排序的廉价OIT。

额外的VT优势:复杂材质混合和贴花渲染结果存储到VT页面缓存中,数据重用将成本分摊到数百帧上,恒定的内存占用,与纹理分辨率和资源数量无关。

虚拟延迟纹理(Virtual Deferred Texturing):旧想法:将UV存储到G缓冲区,而不是纹素[Auf.07],主要功能:VT页面缓存图集包含所有当前可见的纹理数据,8k x 8k纹理图集的16+16位UV提供了8 x 8亚像素过滤精度。

梯度和切线坐标系:计算屏幕空间中的像素梯度,用于检测邻居的UV距离,没有找到邻居则使用双线性,存储为32位四元数的切线坐标系[Frykholm09],隐式mip和材质id来自VT.Page = UV.xy / 128

回顾和优势:64位,完全填充率,没有MRT。过绘制开销非常低,纹理延迟到光照CS。Quad效率不那么重要,虚拟纹理页面ID通道不再需要。梯度重建的数据相当接*基准真相:

MSAA技巧:关键观察发现UV和切线可以插值,可以使用有序网格4xMSAA模式,以2x2更低分辨率(540p)渲染场景,使用Texture2DMS.Load()在光照计算着色器中分别读取每个样本。

1080P重建:将1080p重建为LDS,边缘像素被完美重建,MSAA为两边运行像素着色器。插值内部像素的UV和切线,质量很好,差异很难发现。

两阶段遮挡剔除:低多边形代理几何体没有额外的遮挡过程,精确的WYSIWYG(所见即所得)遮挡,基于深度缓冲区数据,从HTILE最小/最大缓冲区生成的深度金字塔,O(1)的遮挡测试(gather4)。

第一阶段:使用最后一帧的深度金字塔剔除对象和簇,渲染可见对象。

第二阶段:刷新深度金字塔,测试剔除的对象和簇,渲染误报(false negative)的对象。

如果没有GPU驱动的渲染,这种剔除方法是不可能的,因为它需要对GPU在上一步生成的数据进行低延迟访问。如果CPU在帧中间来回运行,GPU将严重停顿(stall)。下图是Xbox One在1080p的性能数据:

虚拟阴影图(Virtual Shadow Mapping):128k x 128k虚拟阴影贴图,256 x 256纹素页,从Z缓冲区中识别所需的阴影页,使用GPU驱动的管线剔除阴影页面,一次渲染所有页面。

VSM质量和性能:在所有区域接*1:1的阴影到屏幕(shadow-to-screen)的分辨率,测量:在复杂的“稀疏”场景中,速度比SDSM快3.5倍,在简单场景中,VSM略慢于SDSM和CSM。

此外,GPU驱动可以和DX12等新生代图像API更完美地结合:

14.4.4.3 Adaptive Virtual Texture

Adaptive Virtual Texture Rendering in Far Cry 4分享了2015年的Far Cry 4所使用的自适应虚拟纹理的技术,包含虚拟纹理概述、地形、自适应虚拟纹理、渲染挑战等。

虚拟纹理的原理类似于虚拟内存,但是在GPU上实现的。结合下图,虚拟纹理可以表示非常大尺寸的纹理,当需要用到纹理数据时,需要提供虚拟纹理的坐标去查询非直接纹理的信息,从而获取真正的物理页缓存数据。

游戏中的虚拟纹理可以是巨型纹理(Mega Texture)或程序化虚拟纹理(Procedural Virtual Texture)。Mega Texture由Rage的id软件开发(Waveren 2013),纹理数据存储在磁盘上,并根据需要传输到内存中,运行时确定所需的tile(页面)并从磁盘请求它们,tile被加载到tile缓存(物理纹理缓存),页面表(间接纹理)被更新。程序化虚拟纹理用于DICE的Frostbite引擎(Widmark 2012),在运行时将地形渲染分解为虚拟纹理,磁盘中没有高度压缩的虚拟纹理,直接渲染到缺少页面的虚拟纹理中,利用帧到帧的一致性降低地形渲染成本,用于地形渲染的强大GPU优化。

Far Cry 4的程序化虚拟纹理参数见下图,512k x 512k的巨型虚拟纹理、2k x 2k的间接纹理、9k x 9k的物理纹理,其中虚拟纹理和间接纹理有11级mip:

间接纹理的格式如下图,入口(Entry)坐标(x,y):每个入口代表一个虚拟页面,入口坐标 = 虚拟页面坐标 / 虚拟页面大小。入口内容格式是32位整数,其中:PageOffsetX = 物理页面U坐标 / 物理页面大小,PageOffsetY=物理页面V坐标 / 物理页面大小,Mip:此页面的Mip映射级别,调试:仅用于调试(如保存帧数)。

如果用传统的虚拟纹理,512K x 512K虚拟纹理在10 x 10公里的世界上,需要1000万x 1000万虚拟纹理!急需另一种新的技术。

这种新的技术就是自适应虚拟纹理,它基于程序虚拟纹理,10x10KM的世界分为64x64米的区域(sector),*地形地段:在虚拟纹理中分配虚拟图像,更*的区域:更大的虚拟图像,如64K x 64K(64K/64米=10 texel/cm),更远的区域:更小的虚拟图像,如32K x 32K、16K x 16K、…、1K x 1K。

在虚拟纹理中为所有闭合区域分配虚拟纹理,其中下图红色区域是可视化虚拟纹理中紧密区域的虚拟纹理分配,每个彩色方块代表附*每个区域的一个虚拟纹理:

下图的2个红色方块表示离摄像机最*的分区的虚拟纹理,达到64k x64k;黄色离相机稍远,32k x32k;依此类推,直到附*的所有分区都已分配完成。

相机靠*时的提升虚拟纹理大小,下图由32k x 32k提升到64k x 64k:

若相机远离,则反向操作:

在放大过程中,会在虚拟纹理中分配一个较大的虚拟图像,并移除旧的图像。地形材质与附加贴花混合,已经缓存在物理纹理缓存中,偏移并重新使用它们!对于从mip 1到mip 10的所有页面,将间接纹理入口从旧图像复制到新图像,同时向上偏移1个mip。

然后为新虚拟图像更新间接纹理中的所有mip 1入口:

需要特殊处理mip 0页面,因为它们没有在旧图像中渲染。

4个mip 0页有1个对应的mip 1页,临时映射到低一级的mip页面,在这一帧中,图像显得模糊,正确更新后会变得更清晰。在新的虚拟图像中对mip 0中的所有页面执行此操作:

还需要更新间接纹理的mip 0页面,复制间接纹理入口内容:

缩小虚拟图像时,在虚拟纹理中分配一个较小的虚拟图像,并移除旧图像,反向执行放大虚拟图像的步骤。

面临的挑战有:需要减少虚拟页面id缓冲区的内存占用,限制每帧的渲染消耗,生成海量贴花,各向异性过滤,支持三线性过滤。具体的接*方案见原文,此处忽略。最终的性能和效果如下:


14.4.4.4 Signed Distance Field

Dynamic Occlusion With Signed Distance Fields阐述了Unreal Engine使用有向距离场高效高质量地生成环境光遮蔽的效果。

对于动态制作游戏场景中的通用遮挡,没有很好的解决方案,需要柔和、准确且不连贯的可见性查询,用于区域阴影、天空遮挡、反射阴影。解决方案:光线步进的有符号距离场,受Fast Approximations for Global Illumination on Dynamic Scenes启发。早在2011年的Samaritan演示(虚幻引擎3)中使用SDF(有向距离场)光线行进进行反射阴影,单体纹理与静态场景,需要高端硬件。

早期动态天空遮挡质量原型,每像素圆锥体在每个相交的物体上行进,使用27个圆锥体,在一个小场景中的运行速度仅有2FPS!GDC 2015的风筝技术演示中,使用这些方法进行巨大世界的全动态照明,需要980 GTX。

关闭(左)和开启(右)SDF阴影的对比。

当前,Fortnite正在开发中,支持一天中的动态时间,使用这些照明方法,通过大量玩家建筑程序生成关卡,所有都支持PlayStation 4级硬件。SDF存储每个点到最*表面的距离,内部区域存储负距离(有符号),d=0的等值面(level set)是表面。有符号距离在表面上线性插值,精确地表示任何表面方向,尽管是网格。光线相交会根据到表面的距离跳过空白空间,如果光线相交,则光线被遮挡。免费*似圆锥交点,从而获得区域阴影。

SDF与体素的比较:+相等地表示任意方向的表面,+线性插值,+更高的有效分辨率,+减少偏差以避免自相交,+在跟踪中跳过空白,+无预滤波的*似圆锥交点。体素仅表示表面位置和法线,只能从跟踪中获取可见性。从根本上说,SDF是一种表面表示,而不是体积,只能用于入射光照和可见性可以解耦的地方,在任何方向提供柔软、准确的可见性追踪!

场景表达:蛮力执行三角形光线跟踪,离线生成网格SDF,存储在fp16体积纹理中,\(50^3\)足以容纳*均网格(240KB),GPU方法可用于动态更新。SDF需要在任何地方定义,在边界周围添加的边界保证位于曲面之外,有效区域外的样本被夹紧,合成距离给出了*似值。

可以跟踪相机光线并可视化步数:

内部/外部由背面光线命中确定,不需要闭合网格。

半支持部分遮挡体,被视为双面曲面,双面曲面不会导致完全相交,对树叶很有用。然而,射线步进开销大。

实例支持:相同的SDF数据,不同的变换,当前实现中只有均匀的缩放,基于资产缩放的默认分辨率分配。

角根据分辨率进行四舍五入,薄表面只能用内部的负纹理表示,寻找根的必要条件。

在光线行进时,距离场纹理被用于全局场景访问,\(512^3\)的关卡占用约300Mb,场景对象完全在GPU上管理,CPU发送增量更新,移动一个对象只需要更新一个矩阵。剔除对象比CPU快100倍,Kite demo中的200万个树实例 -> 屏幕上的50k@PS4上的.1ms,因为对管道中的对象的所有操作都在GPU上。

地形高度场,根据高度场计算的*似圆锥交点,重用高分辨率高度图,单独通道组合结果。当前表示处理许多游戏场景,非均匀缩放网格除外,以及蒙皮/动态变形网格,和大型有机网格/体积地形,可能的替代图元:解析距离函数、稀疏体积SDF?

足够的分辨率用于直接阴影!具有球形光源形状的放射状灯光(区域阴影),剔除到灯光边界的相交对象,然后是屏幕tile,从包含tile和对象边界的灯光的锥体。球形光源形状的放射状光源的伪代码:

Foreach intersecting object
    Foreach sample step
        DistanceToSurface = Sample SDF
        Track min visibility (DistanceToSurface / ConeRadius)
        Step along ray toward light by abs(DistanceToSurface)
        Terminate if inside or exceeded max step count
Shadow = 1 - MinVisibility

直接阴影:锥遮挡启发式算法,距离表面/圆锥半径=1d交点,发现更智能的启发式算法在视觉上给出了与\(\Big( \cfrac {\text{DistanceToOccluder}}{\text{SphereRadius}}\Big)^X\)相同的结果。

定向光:剔除为最大视图距离的相交对象,然后进入光源空间网格,使用相同的跟踪内核。顶点动画无法表示,要在明显的位置使用级联阴影贴图。

左:CSM;右:CSM+SDF。

三角形LOD(公告牌)与SDF不匹配时,使用保守的深度写入:

使用SDF用作直接阴影的原因:有尖锐接触的区域阴影,可控自阴影失真-世界空间偏差,没有阴影贴图锯齿,与三角形计数无关的性能,取决于对象密度和分辨率(num道),可以相当容易地减少采样,支持大视场范围–无CPU成本,比传统阴影贴图(cubemap/CSM)快30-50%,取决于三角形LOD的效率。

天空遮挡的问题:SSAO仅适用于小规模的屏幕空间的瑕疵,想要中等距离遮挡的通用解决方案(10米),建筑有薄薄的墙。单个SDF锥体追踪可以是软的:

多个锥体(例如9个)覆盖半球(朝向法线的半球),使用min()处理多个对象的遮挡,遮挡距离被限制(默认为10米)。

产生最小遮挡的圆锥体(弯曲法线),其它方向遮挡表示也是可能的,应用于天光的SH漫反射照明,也适用于使用*似圆锥体/圆锥体相交的天空镜面反射。

天空遮挡优化:对象影响剔除到屏幕tile的边界,每个tile有两个深度边界和两个剔除列表,利用对象SDF进一步与tile边界剔除。

在GCN上,使用光栅化器的分块剔除比分块计算着色器更快,当成千上万的物品达到10个时,最大的收益。可能适用于许多其它用例(光源、贴花、反射探针),GCN上的时间是0.4ms->0.2ms。

分块剔除过程:构建绘制参数缓冲区–截锥体剔除的输出,绘制边界几何图形–DrawIndexedInstancedIndirect,像素着色器在UAV中构建分块相交列表。在每个尺寸的1/8处计算圆锥跟踪,严重锯齿,无需进一步过滤,以半分辨率执行过滤,几何感知的双边上采样。

4倍时间超采样(4个抖动位置),重新利用时间消除锯齿的精确像素速度进行重投影,深度拒绝,将拒绝的像素标记为不稳定,时间不稳定区域的空间填充。

采样对象距离场很昂贵,下图的白色=200个对象影响,解决方案是:合成到全局距离场(global distance field)中。

全局距离场:存储在以摄像头为中心的clipmap中,使用了4个clipmap,每个clipmap的尺寸是\(128^3\),Clipmap会随着相机移动而滚动,根据需要从对象SDF合成的新切片,远距离的clipmap更新的频率较低。

全局距离场在地面附*不准确,在圆锥体起点附*采样对象SDF,其余为全局SDF。

全局距离场可以大幅减少对象影响重叠!

14.4.4.5 Destiny's Multi-threaded Renderer Architecture

Destiny's Multi-threaded Renderer Architecture阐述了2015年的Destiny引擎的多线程渲染架构,包含Destiny核心渲染器架构、游戏模拟到GPU数据流、作业和负载*衡考量、延迟减少技术(减少输入和渲染延迟、保持GPU完全饱和)、复杂性封装等内容。更进一步地,涉及粗粒度并行、Destiny渲染器目标、解耦模拟和渲染、核心工作负载作业化、数据驱动的渲染提交及高级优化等技术。

早期的Halo引擎使用了线程上的系统(System on a thread,SOT)的渲染管线:


帧间的时序图如下:

静态每线程负载*衡意味着工作负载分配不理想,所以倾向于在模拟和渲染循环(最大的工作负载)的线程上看到大量的利用率,但其它线程看到了大量的空闲时间。

SOT的缺点是难以跨代/跨*台采用,不适用于异构*台,同步需要游戏状态的完全双缓冲区,序列化的前端高可视性成本,潜在的GPU空闲气泡。它的优点在于方便的数据访问、可扩展、容易、简单线程模型、模拟和渲染的流水线并行执行。

到了Destiny引擎时代,渲染器目标变成:推出一款具有出色视觉效果和响应速度的游戏,复杂、生动、美丽的世界,环境多样的大型目的地形,高质量照明、动态时间、实时阴影、天气因素、高分辨率渲染,许多图形功能。推出一款视觉效果极佳、反应灵敏的游戏,具有可玩性,保持它的可伸缩性,跨代跨*台,在所有*台上都有坚如磐石的性能。保持高效,最大CPU和GPU占用率,让它们有充分的时间,避免GPU空闲,动态负载*衡和智能作业批处理,保持低延迟。保持简单,让人们停止担忧和热爱多线程,在短时间内入门多线程渲染,新功能对现有工作几乎没有改变或没有改变。将模拟与渲染解耦,完全数据驱动的渲染管线。

首先是从渲染解耦出游戏状态遍历。解耦可见性和游戏对象,独立于游戏对象的可见性遍历,根据可见性结果驱动渲染工作负载,将渲染和游戏对象解耦。把那些静态数据收起来,渲染数据不等于游戏对象数据,在对象的生命周期内,渲染数据的大部分是静态的,在渲染器中缓存不可变的渲染数据,注册后只读。提取每帧瞬态渲染特定数据,静态数据已经被缓存,每帧仅提取动态数据,可见性结果定义了要提取的游戏状态,仅提取可见元素的数据。下图是修改前后的对比:

接下来是解耦物体和渲染。在每个系统中单独表示:对象系统(游戏端)、渲染(渲染对象)、可见性(可见性对象),提供跨层通信的接口(使用严格的访问规则,只有在特定的游戏阶段,参加下图详解)。

)

1:游戏对象的每个组件将映射到渲染器侧的特定渲染对象,当一个新的游戏对象被添加到世界中时,它会在渲染器中注册自己,在渲染器中缓存该游戏对象的静态渲染数据,渲染对象可以缓存游戏对象句柄(用于动态对象)以供后面使用。

2:然后,渲染器将渲染对象的句柄返回给游戏对象,游戏对象将其缓存在相应的渲染组件中。

3:使用此渲染对象句柄,游戏对象向可见性系统注册,以创建一个可见性对象,该对象将渲染对象句柄缓存在其中。此渲染对象控制柄指向原始渲染对象。

4:然后将可见性对象句柄返回给游戏对象。现在如果要隐藏游戏对象,只需要注销这个对象的可见性,它就会停止渲染。

5:请注意,渲染对象对可见性一无所知;只有可见性知道渲染对象。因此,将游戏对象系统、可见性和渲染层解耦,现在可以根据可见性结果驱动渲染系统,而无需访问游戏对象。那么动态数据提取呢?。

帧包(frame packet):动态逐帧渲染数据存储,所有特性的渲染器动态数据都存储在那里,完全无状态,每帧用完就扔掉。总占用小,PS3 Xbox360上每帧1MB,占总游戏状态的9%。

一个视图等于视锥体和摄像机的组合,例如玩家视图、阴影视图、开销视图等。

此外,视图还等于渲染作业链单元,如可见性、提取、准备、...、提交等。每个帧环形缓冲区可以包含多个帧包(一般是两个,例如UE),每个帧包可以包含多个视图包。视图包(view packet)包含了视图相关的所有数据,每个视图的作业链见下图右侧:

左:帧环形缓冲、帧包、视图包的结构关系,右:每个视图的作业链和步骤。

视图作业链(View Job Chain)是核心系统作业,总是独立于帧内容运行,数据驱动的视图作业链,动态创建,基于在给定的帧中的内容。

确定视图阶段:保留帧和视图数据包,没有重量级的分配、堆操作等。

提取阶段:为每个视图计算可见渲染列表,存储在可视性环形缓冲区(临时)。它涉及的阶段如下图:

视图链作业在渲染节点上操作,渲染节点效率很高,全能压缩(uber compact),缓存一致性表达,填充渲染节一致性数组。按渲染对象类型对可见列表进行排序,以便后面一致性地执行,在视图包中分配渲染节点。渲染视图节点,存储视图依赖数据,可以分配自定义帧包数据(基于渲染对象类型)。与帧节点共享视图中的数据,帧包内的数据共享,避免冗余数据:

回到前面的图表,在可以解锁下一帧的游戏tick之前,需要完成提取阶段,因此带来了游戏模拟延迟(game simulation latency)。

提取窗口和延迟:从程序中提取块,游戏状态被锁定以供读取,使提取成为最具延迟影响的工作负载。需要优化提取窗口,提取窗口由
所有视图的可见性计算,游戏状态数据提取,可见性计算是CPU工作,能不能把可见性移到提取之外?玩家视图的静态环境可见性是最繁重的工作。

将可见性移出提取后,上一代游戏延迟减少了约4毫秒,而当前一代游戏机延迟减少了约2.5毫秒。

提取:以最少的工作量复制出动态数据,不要运行任何复杂的数据变换,完成并解锁下一个游戏tick的游戏状态/模拟。

准备:仅对可见元素运行准备作业,尽可能基于LOD跳过计算,不要计算无法感知的内容。

准备和模拟的帧序列如下:

现在,当模拟并行运行时,我们将执行准备阶段。与extract类似,prepare也通过迭代视图和帧节点以及缓存的渲染对象数据来操作,为不同的渲染对象执行prepare入口点。Prepare还将每个Prepare作业的结果写入帧数据包。但与extract有一个重要区别:渲染作业不再可以访问游戏对象。在准备过程中,将为下图角色(突袭者)破烂的斗篷运行布料模拟工作,如果突袭者离玩家足够*,还将为他的手指和脚趾骨骼计算非确定性动画(如果他离玩家不够*,则跳过该工作)。还将根据他的垂直计数将他放入一个连续的LOD桶中,以保持战斗帧的一致LOD足迹。

可以对其它视图重复此作业流程(例如如果在一个帧中总共有三个视图),每个视图都有以下作业链:

值得一提的是,UE的场景渲染器的流程和上述的视图作业链高度一致,说明UE也是借鉴了这种并行化渲染架构。

保持简单:简化图形功能开发,渲染工作负载的透明作业,保证缓存一致性和核心工作的潜在同步。解决方案:功能抽象和封装。

渲染特性:渲染特性是图形渲染的一个单元,定义为:缓存一致性数据表示,用于数据管理和呈现的代码路径。可以映射到特性渲染器的实例。Destiny特性渲染器定义图形特性如何映射到:使用缓存数据表示呈现对象,帧包渲染节点表示,每个阶段的作业入口点。(下图)

特性渲染器作业:每个特性渲染器为每个阶段公开一组入口点,约束数据访问:仅读取相应的节点状态和特性渲染对象数据,仅输出到帧数据包,所有输出都自动进行双缓冲。核心渲染器使用这些入口点为每个阶段进行作业,在不影响叶子特性的情况下,当核心架构重构作业依赖关系或加载*衡规则时叶子特性不受影响。

下面是界面提供的功能渲染器的所有入口点集,你可能看不懂它们的名字,但别担心,它们只是想让你了解系统是如何工作的。

性能和内存优化:

  • 按工作频率组织数据和工作负载:跨视图、跨渲染对象,共享数据:保存帧包内存,节省性能:逐频率执行一次代价高昂的计算。
  • 核心架构同步访问和执行,多线程访问共享元素。暴力锁定内部循环会降低性能。为快速共享渲染节点操作开发了自定义无锁原语。
  • 提取或准备每帧操作时,只要对象在任何视图中都可见,每帧一次,每帧节点索引上的无锁同步原语,对于在渲染器中注册的所有对象都是唯一的。
  • 提取或准备每个游戏对象的操作时,多个渲染对象的共享工作负载,蒙皮提取和准备/动态AO计算/前向光探测计算/非确定性动画解算,基于对象骨骼哈希的无锁同步原语。

数据驱动的提交管线之帧提交:高级提交代码执行通道,并行提交,基于游戏状态。建立不频繁但成本高昂的全局状态:渲染目标操作(绑定、清除、解析、解压缩)、全局状态(帧/视图寄存器)。每个通道调用提交视图指令,简单、跨*台的APl,为该视图生成GPU命令。每个提交视图指令都设置提交视图作业,每个*台作业规则各不相同。管线GPU的工作如下:

最终,要做的是从高级提交管线中找出如何启动提交视图作业和功能提交入口点,需要做的是区分哪些可见元素需要提交到贴花阶段,哪些需要提交到透明阶段。

高级提交控制:跨*台代码路径,分阶段组织:1、所需的通道,如着色通道、色调映射/解析等等;2、提交阶段提交指令。

// 高级提交的伪代码
generate_gbuffer_pass()
    set_render_targets(depth_stencil, gbuffer_surfaces)
    setup_viewport_parameters()
    ...
    clear_viewport()
    submit_render_stage_for_view(first_person_view, _render_stage_gbuffer_opaque);

渲染阶段机制:过滤运行时提交过程,提供运行时着色器技术管理,在正确的时间选择正确的技术,允许筛选可见列表,选择正确的网格集,以使用正确的技术提交到正确的过程,专为缓存一致性和快速迭代而构建。

渲染阶段(Render Stage):每个渲染对象可以随时订阅渲染阶段,通过向核心渲染器注册阶段,在注册时或运行时的任何时刻。每个视图都可以在运行时订阅渲染阶段,可见列表将仅过滤到订阅的渲染阶段。

这里的渲染阶段其实是类似于UE的FMeshPassProcessor及其子类,更多可参阅:剖析虚幻渲染体系(03)- 渲染机制

按渲染阶段过滤视图:仅当两个条件都满足时渲染对象才会被提交:该视图支持给定的阶段和该视图包含了订阅该渲染阶段的可见节点。

逐阶段填充提交节点:按渲染阶段筛选可见列表。

逐阶段排序提交节点:某些过程需要在渲染之前进行排序,如透明材质等。在每个阶段的准备过程中进行排序,每个渲染阶段使用可配置回调,每个渲染节点在运行时计算每个阶段的自定义启发式。每个渲染对象可以有一个或多个提交节点,基于小排序键进行内联快速排序。

缓存一致性:所有核心渲染器作业都在渲染节点上操作,轻量化数据结构。所有核心工作负载的快速、缓存一致性排序、分配和遍历,较小的数据重复,在紧凑的迭代循环中实现更快的缓存访问。

逐阶段作业提交视图:分成不同的提交叶作业,如逐阶段、视图、特性、阶段集等,动态负载*衡拆分。对于每个特性提交入口点,仅访问本地和渲染对象数据,如渲染节点、特性渲染对象缓存。

特性渲染器提交内核:每个特性的一致代码路径,可以按特性类型进行排序,只对允许的通道。提交视图作业是经过优化的内核处理器。

渲染抖动和减少延迟:多线程提交,CPU/GPU负载*衡,减少GPU气泡,高效的命令缓冲区刷新,异步交换,通过延迟自动恢复减少抖动。

GPU占用率因素:最终目标是保持GPU完全饱和,永不空闲。重要因素有刷新GPU命令到GPU的时间、绘制调用、执行命令列表指令、渲染目标状态、上一帧在GPU上的工作完成并翻转。

跟踪GPU空闲:计数器不规则,但在某些*台上可用,引擎是用来帮助追踪的,用于所有性能和活动测试,GPU空闲==缺少GPU,有助于精确定位由于GPU不足而导致的延迟更高的位置。

上图显示了很多GPU空闲气泡,表示GPU饥饿状态,会增加整体渲染延迟。

保持GPU的占用:多线程提交,高效生成命令缓冲区刷新,提交作业粒度负载*衡,Vblank同步。

同步到VBlank:基于vblank偏移计算,以便在准备翻转时完成GPU工作,而不会错过一个间隔。一些控制台允许原始vblank事件跟踪,对于完全控制非常有用。其它控制台/PC不需要手动操作,只需旋转一个单独的线程,等待Vblank事件,发布调整大小/全屏事件时,请注意呈现死锁。

多线程提交:复杂的主题,当我们将命令缓冲区刷新到GPU时,无法涵盖所有重要方面。高级提交脚本为每个视图、每个阶段生成命令缓冲区提交。GPU必须按顺序执行命令,主要在将命令缓冲区刷新到GPU时,而不是在生成它们的时候。

高级渲染提交:提交高级别、高成本渲染状态:目标绑定/清除/解决。提交高级提交指令,为渲染阶段渲染视图(可选:进一步的特性过滤),在此层作业化,为该指令生成命令缓冲区。

生成提交作业:作业N一起提交节点,按特性或渲染状态分组。为了提交作业创建而拆分命令缓冲区,来自高级提交脚本作业,用于GPU顺序执行。每个逐特性、逐阶段、逐视图提交作业 == 1个命令缓冲区。

提交作业和GPU占用率:需要CPU和GPU负载的最佳*衡,轻量CPU快速刷新到GPU并启动,接着是携带GPU大量工作的中等CPU等等。按渲染特性和渲染阶段成本对提交作业进行排序,按*台调整,核心渲染器提供了这种机制。

下面涉及动态负载*衡。

负载*衡渲染作业:粗略的方法可能会使初始实现变得过于简单:每个阶段每个特性一个作业(提取/准备/提交),工作量变化很大,例如环境/地形无需准备,角色装备、蒙皮、布料是繁重的工作,开销太大。

动态负载*衡:每个特性都提供了每个阶段的成本函数,需要独立工作吗?基于实体和每个阶段{提取、准备、提交}的特性成本的核心系统批处理,基于帧中的数据自动加载*衡。

异构作业执行:每个特性都指定了它支持的执行单元,核心渲染器会根据可用性自动安排时间,例如:SPU/PPU调度。大多数用于提取,其它具有繁重工作负载的阶段工作于PPU。未来可以扩展到其它系统。

结果:跨多个*台提供低延迟、高效、高度可扩展的执行,如PlayStation 4、PlayStation 3、Xbox One和Xbox 360,特性代码是跨*台的,不受体系架构调整的影响。渲染器的多线程处理是交付Destiny的关键,可扩展性和动态负载*衡,缓存一致性数据访问,异构支持,实现了低延迟,尽管工作量很大。数据驱动的流水线体系结构为创建图形功能提供了极大的灵活性,扎实的封装允许在整个开发过程中进行大量优化。实现了最初设立的目标,成功地将游戏状态遍历与输出解耦,实现了更好的CPU和GPU利用率,通过数据驱动架构将渲染算法分解为任务和数据并行工作负载。

面临的挑战是每个*台有自定义需求:提交作业粒度、命令缓冲区生成,在整个项目中持续优化作业开销成本。

Lessons from the Core Engine Architecture of Destiny讲了Destiny引擎的渲染架构以及如何达成目标。当时的Destiny引擎的模块众多:

引擎支持的特性有:作业图多线程,跨*台,分层代码库实现引擎共享和快速重建,与博弈逻辑解耦的博弈状态和资源生命周期,组件化对象系统,支持高级功能开发,自定义C#编辑器套件,可为游戏性迭代编写深度脚本,对所有内容进行快速迭代,提供了大约60%的这些功能。下图是不同时代和层次的引擎架构演变图例:







14.4.5 开花期总结

总结起来,在2010~2015年间,渲染引擎的总体特点是:

  • 以DirectX 11和OpenGL 4.x的图形API带来了许多新的特性,为游戏引擎和应用程序提供了广阔的发挥空间。
  • GPU硬件结构和驱动层也在适应或推动图形API的发展,计算能力、并行度呈指数级发展,但IO的带宽瓶颈依旧,赶不上ALU的提升。GPU驱动渲染管线的出现为高度复杂的场景渲染提供了坚实的基础。
  • 多线程得到充分利用,如多线程渲染、异步加载、工作线程、线程池、DX11的多线程上下文等等都是其中的具体体现。适应多线程和提升并行的技术和数据结构也得到空前的应用,如SOA、线性数组、DirectCompute、数据驱动等。
  • 渲染技术也得到迅猛的发展,如PBR、延迟光照、推断光照、分块及分簇光照,还有诸多直接光影、间接光影、体积光影等技术。大量的抗锯齿技术被研发出来,典型的代表是FXAA、TAA、SMAA及它们的变种。后处理技术也不甘其后,大量更真实更高效的算法也接踵而至,典型代表是DOF,众多文献都见诸身影。
  • 特殊材质也日新月异,以皮肤、头发、眼睛、布料及自然景观(地形、天空、大气、光束、天气、植被、湖泊、海水、雾气)等为案例的文献遍地开花。
  • 随着移动终端的智能化进程,移动端的渲染也日趋成熟,专用于移动端的GPU、图形API、渲染引擎不断完善,形成完整的生态链。
  • 光线追踪、VR、Web端等新兴技术在悄然发育,为后续的发展奠定了良好的开端和基础。

 

 

  • 本篇未完待续。

 

 

特别说明

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

 

参考文献

posted @ 2022-05-02 20:24  0向往0  阅读(4142)  评论(0编辑  收藏  举报