在本章中,您将学习:
- 如何通过显卡成为最佳伙伴
- 为什么您不(可能)成为图形驱动程序的最佳伙伴
- 如何克服不可避免的瓶颈
- 什么是抽奖电话以及如何减少抽奖次数
框架的解剖
实时引擎渲染的每一帧都是CPU和GPU(也称为图形卡)完成工作的结果。为简化起见,CPU准备数据并发出命令(绘制调用)供GPU处理。
引擎代码无法直接控制GPU。相反,它必须使用API:应用程序编程接口发送命令。OpenGL,DirectX,Vulkan或Metal是最受欢迎的API的示例,尽管它也可能是游戏机的专有接口,例如Playstation 4的GNM。对于PC和移动设备,必须通过图形驱动程序进一步转换这些API函数调用。
您应该记住的一种重要的API函数是draw调用。整本书中无数次提到它。甲绘图调用是绘制给定网孔的请求:在屏幕上(或进入缓冲液)上,(准确地说三角形的批次)使用指定的着色器。我将在本章稍后详细解释该想法。
至于数据,在大多数情况下,GPU上的内存是物理上独立的硬件-视频RAM,也称为VRAM。因此,还需要在主系统RAM和VRAM之间移动数据。
这意味着图形单元必须等待。它只能在以下情况下开始绘制:
- 游戏引擎的渲染代码已完成(在CPU上)
- 生成的绘图调用被转换为直接GPU代码(由在CPU上运行的驱动程序)
- 必要的数据已从RAM推送到VRAM(如果那里尚未存在)
准备就绪后,图形卡即可开始执行其工作。
抽签
图形代码的目标是绘制所需的内容。在理想情况下,多边形将被变换,着色并显示在屏幕上。但是,事情再简单不过了,尤其是在PC和移动设备上。虽然GPU确实可以处理大多数与图形相关的计算,但仍需要由CPU控制。绘制内容的命令(也称为绘制调用)由引擎代码提交。即使是最强大的图形卡,也可能由于过度工作的中央单元而成为瓶颈。更糟的是,绘图调用(在Vulkan和DX12之前)必须通过图形卡驱动程序提交,图形卡驱动程序本身是一个软件,增加了另一层沉重的间接寻址。
对于具有面向对象代码经验的人来说,经典的图形API仅拥有呈现内容的单个全局状态可能会令人惊讶。渲染另一个网格或切换材质意味着发出绘画调用,更改全局绑定的资源。它包括如下情况:
- 要渲染不同的网格
- 将应用其他材质球(材质)
- 着色器更改的参数
- 一个开关到另一个渲染通道发生
- 应用了全屏(后处理)着色器
- 渲染目标的内容将由CPU读取(例如,粒子模拟)
所有这些通常都需要抽奖。中端DX11时代的PC在16 ms内可以处理的逼真的抽奖数量在成百上千之间。游戏通常在可见场景中具有成千上万个网格。在基于物理的管道中,这些网格中的每个网格可能至少使用3个纹理。分派这么多命令会对性能造成致命影响。
为了缓解此问题,多年来,程序员提出了许多聪明的解决方案。最重要的是对请求进行批处理,或至少对请求进行排序。关键是在网格及其所有参数相同的情况下组合调用。建筑物可能包含数千个零件,但其中许多可能只是同一网格的实例。如果将这些网格物体合并到单个对象中,则仅需要1个绘制调用。
对于场景的静态部分,对于保证不发生改变或移动的对象,此功能特别有效。这种扫描场景以寻找候选对象的过程称为静态批处理。Unity是采用此方法的引擎之一。但是,当推得太远时,它可能会对像剔除(将对象隐藏在视线之外)之类的系统产生负面影响,因为现在引擎正在处理巨大的整体对象,而不是较小的对象。
虚幻引擎开发人员更喜欢轻量级的变体-只需按其网格物体和着色器参数对要绘制的对象列表进行排序即可。这样可以在通过另一个绘制调用切换参数之前渲染多个对象。尽管在减少命令数量方面不如批处理有效,但这种方法保留了更大的灵活性。
两种技术的有效性取决于场景中唯一的网格和材质的数量。一栋建筑物可能包含数千个零件。但是,如果其中大多数只是相同可重用片段的实例,则引擎将能够大大减少必要的绘制调用次数。在3D建模工作流程中,这种方法称为模块化环境技术。
您可以通过~按键(代字号)并输入:来检查正在运行的游戏中的抽奖次数stat SceneRendering。
我是说实例吗?称为GPU实例化的技术是批处理的最终形式。它允许加载单个网格,然后仅以几乎零的CPU相关开销标记副本。
虽然有很多限制。除了位置,旋转和比例(每个实例所显示的内容取决于引擎)之外,实例之间几乎没有任何改变。所有副本必须使用相同的材料和特性。
它已作为UE4中的手动解决方案提供,但是从4.23版开始,虚幻引擎还会尝试自动查找确切的副本。然后,如果可能,它将它们转换为实例。
并行和流水线
GPU是大规模并行的。这意味着他们使用数百或数千个内核,同时完成同一任务。与此相比,x86 CPU领域是标准的4到16核。在GPU上分配给相同波前(任务)的所有内核都将遵循相同的代码行,并进行同步。他们将只处理不同的顶点/像素。这就是为什么分支比在CPU上困难的原因,从而使某些算法效率低下或无法实现。但是,对于处理图形而言,如此小巧而丰富的核心是完美的选择-考虑使用相同着色器和纹理的所有相邻像素。
但是,GPU并不是一个庞大的研讨会。它是由几个专用工厂组成的网络–流水线阶段。这些设施可以合作,但是其中一个必须完成生产,然后第二个可以进一步处理有效载荷。顶点着色器需要首先转换多边形,以便片段着色器可以对这些三角形进行着色,以计算像素的最终颜色。这意味着像素着色器必须等待,直到被喂入数据为止。
以简化的方式,DirectX 11 / OpenGL 4管道可以如下所示:
顶点着色器→细分→几何着色器→栅格化→片段着色器
当GPU收到绘制调用时,投影矩阵会变换三角形,以遵循当前视图的正确透视图。这是顶点着色器的工作。然后,GPU 通过执行片段着色器开始对其栅格化。不同种类的着色器之间的区别是整个GPU 管道的重要骨干。
还有一个硬件负责在屏幕外剪切三角形。在Unreal的标准工具中只能使用顶点着色,片段着色和细分,因此我会好奇地寻找其他步骤的定义。
这些步骤的顺序是在硬件级别上预先确定的。早在PlayStation 1和T&L级PC图形加速器的时代,管道的整个中间部分都是固定的。程序员唯一可以控制的就是输入,例如网格,纹理和常规曲面参数。至少从GeForce 3开始,可编程管线就大受欢迎。现在,硬件在着色方面非常灵活,大部分工作由自定义着色器程序进行详细控制。
栅格化
栅格化是当前流行的实时着色方法。为了使说明更容易,让我将三角形栅格化的步骤及其片段的阴影(即屏幕像素)描述为一个单独的“栅格化”概念。
按照其想法,光栅化更接近早期CG电影的渲染算法,而不是自2010年代以来在好莱坞和建筑可视化中占主导地位的基于路径跟踪的解决方案。光线跟踪和路径跟踪模拟了相机发出的光并与表面碰撞的路径时,光栅化仅处理绘制直接可见的表面。如果多个三角形占据一个像素,则将它们一个接一个地绘制。(但是,在完全不透明的网格的情况下,游戏引擎会使用深度缓冲区来削减不必要的工作)。
屏幕空间中属于给定三角形的区域逐像素填充。生成的颜色(尽管请把它看作一个值:一个向量)是片段着色器程序的直接输出。它对场景中的其他三角形一无所知。它所能使用的全部是来自顶点着色器的数据-和纹理(作为输入提供)。
着色器是否可能不了解三角形?那它如何执行令人信服的照明呢?实际上,除了直接照明外,实时渲染中的每个现象都仅由于附加数据而存在。其中包括环境的反射,二次光的反射(又称全局照明)以及看起来如此基础的东西-阴影。这个数据通常产生或多个渲染处理经过:光栅化的额外的步骤,用不同的着色器来执行。这是引擎自己的管道的工作。
引擎的渲染管道
引擎的管道包括基于CPU和GPU的操作。它包括前面提到的多次通过的整个设置。关于传递的章节列出了虚幻引擎每帧完成的20多个传递。提供的结果信息将作为后续通过的着色器的输入。
不过,虚幻引擎的管道还有很多。例如,遮挡负责丢弃从相机的角度不可见的网格。它已经完成了流水线的早期阶段,甚至没有被着色器发送来进行处理。光源的位置和属性将提供给适当的路径-取决于延迟阴影和前向阴影之间的选择。
仅在最后一次扫描完成后,图像才能显示在屏幕上。如果启用了垂直同步(VSync),则也可能会延迟或丢弃图像以达到所需的帧速率(例如每秒60帧)。