实时模型 — 超越多边形计算

基本了解如何渲染网格后,您可以采用选定的技巧提升渲染性能。

简介

我的多边形预算是多少?美术师在制作实时渲染模型时,经常要问这一问题。这个问题很难回答,因为答案远比一个数字复杂得多。

PlayStation* One 开发期间,一开始我是一名 3D 美术师,后来成为了一名图形编码师。我第一次开始构建 3D 游戏模型时,希望可以借鉴这篇文章。本文中介绍的基础知识应该对许多美术师有用。尽管本文中的大部分信息不会对性能或日常工作产生重大影响,但能够帮助您基本了解图形处理单元 (GPU) 如何绘制您创建的网格。

网格中多边形的数量通常决定了渲染的速度。即使多边形数量通常与每秒帧数 (FPS) 有关,您可能会发现,即使在减少多边形数量之后,网格的渲染速度依然缓慢。基本了解如何渲染网格后,您可以采用选定的技巧提升渲染性能。

表示多边形数据

要了解 GPU 如何绘制多边形,首先需要考虑用于映射多边形的数据结构。多边形由一组点(称为“顶点”)和引用所组成。顶点通常以数值阵列的形式存储,如图 1 所示。

metrics
图 1.一个简单多边形的数值阵列。

在这种情况下,4 个顶点将在三维(x、y、z)中产生 12 个数值。创建多边形时,第 2 个数值阵列描述顶点,如图 2 所示。

triangle polygon data
图 2.一个顶点应用阵列。

这些顶点连接起来将形成两个多边形。请注意,两个三角形(每个都有三个角)可以用四个顶点表示,因为顶点 1 和 顶点 2 可复用于两个三角形。对于由 GPU 处理的这类数据,假设每个多边形是一个三角形。GPU 希望您使用和处理三角形,因为这是它们所要绘制的。如果您想绘制顶点数量不同的多边形,应用需要在 GPU 绘制之前将它们拆分成三角形。例如,如果要用六个包含四条边的多边形创建一个立方体,最高效的方法是用 12 个包含三条边的多边形制作一个立方体;这些三角形将成为 GPU 的绘制对象。通常不计算多边形,而是计算三角形。

上述示例中使用的顶点数据是三维数据,但不一定要这样。您可能只需要两个维度,但通常也会存储其他数据,比如纹理的 UV 坐标和光照法线。

绘制多边形

绘制多边形时,GPU 首先确定绘制多边形的位置。为此,它会计算屏幕上出现三个顶点的位置。这就是所谓的变换。一个被称为“顶点着色器”的小程序在 GPU 上执行计算。

顶点着色器也经常执行其他类型的计算,比如处理动画。计算完多边形的所有三个顶点后,GPU 计算该三角形中有哪些像素,然后继续使用另外一个被称作“片段着色器”的小程序来填充像素。片段着色器通常每像素运行一次。但在极少数情况下,每像素运行一次以上,以改善抗锯齿效果。片段着色器通常被称为像素着色器,因为在大多数情况下,片段直接对应像素(见图 3)。

a polygon
图 3.在屏幕上绘制的一个多边形。

绘制多边形时 GPU 执行操作的时间线如图 4 所示。

rasterization
图 4.GPU 绘制多边形的时间线。

如果将三角形分成两部分,绘制两个三角形(见图 5),则会显示操作的时间线,如图 6 所示。

polygons
图 5.将一个多边形拆分为两个。

rasterization data
图 6.GPU 绘制两个多边形的时间线。

这种情况要求变换和设置两次,但因为像素数量相同,所以这一操作不必光栅化更多的像素。也就是说,多边形翻倍不一定会使渲染时间翻倍。

使用顶点缓存

检查上述示例中的两个多边形,可以看出它们共享两个顶点。这表明这些顶点可能需要计算两次,但被称作“顶点缓存”的机制允许再次使用计算。顶点着色器计算的结果存储在缓存中,这个小内存区用于包含最后几个顶点以供复用。使用顶点缓存绘制两个多边形的时间线如图 7 所示。

rasterization data
图 7.使用顶点缓存绘制两个多边形。

使用顶点缓存时,如果两个多边形共享顶点,那么您绘制两个多边形的速度和绘制一个多边形几乎一样快。

了解顶点参数

为了能够复用顶点,每次投入使用的顶点必须相同。当然,位置必须相同,但其他参数也必须相同。传递给顶点的参数取决于您使用的引擎。两个常见的参数为:

  • 纹理坐标
  • 法线

无论何时在 3D 对象上执行 UP 映射,您创建的任何接缝都意味着接缝沿线的顶点将无法共享。因此,一般来说最好避免出现接缝(见图 8)。

texture maps for 3d objects
图 8.UV 映射纹理接缝。

为了准确照亮表面,通常每个顶点都存储一条法线,即从表面向外指出的一个矢量。让所有多边形共享一个由同一条法线定义的顶点,可以使形状看起来非常平滑。这便是所谓的平滑着色。如果每个三角形都有自己的法线,那么多边形之间的边缘将凹凸不平,而表面光滑平坦,这就是它为什么被称作平面着色。图 9 显示了两个相同的网格,一个采用平滑着色,另一个采用平面着色。

flat vs smooth shading on 3d planes
图 9.平滑着色与平面着色对比。

平滑着色的几何体由使用 16 个着色顶点的 18 个三角形组成。对 18 个三角形进行平面着色需要 54(18×3)个顶点,因为没有共享顶点。即使两个网格的多边形数量相同,但它们的性能仍然不同。

评估形状

GPU 的速度很快,主要因为它能并行执行许多操作。GPU 营销材料通常强调它们拥有的管道数量,这决定了 GPU 所能同时执行的操作量。GPU 绘制多边形时,它会分配许多管道来填充一方块像素。通常是维度大约为 8 × 8 像素的一个四方形。GPU 持续执行直至填满所有像素。显然,三角形不是四方形,因此四方形中的部分像素位于三角形内部,部分像素位于三角形外部。硬件将分配给四方形中的所有像素,甚至是哪些落在多边形外部的像素。计算完四方形中的所有顶点后,硬件就会舍弃三角形外部的像素。

图 10 显示了一个需要三个图块来进行绘制的三角形。使用大多数经过计算的蓝色像素,但红色区域中的像素落在三角形外部,将被舍弃。

tiles example for triangel 3d object
图 10.用于绘制三角形的三个图块。

图 11 中的多边形 — 像素数量完全相同,但向外延伸 — 需要更多图块来进行填充;每个图块(红色区域)中的大部分操作都将被舍弃。

tiles example for triangel 3d object
图 11.填充超出图像中的图块。

绘制中的像素数量只是渲染中的一小部分,但多边形的形状也很重要。为了提高效率,并避免出现长而细的多边形,尽量使用每条边长度大致相同,且每个角接近 60 度的三角形。在图 12 中,两个平面已通过两种不同方式绘制出三角形,但渲染时看起来相同。

example of different triangulation on same surface
图 12.用两种不同的方式绘制出三角形的平面。

它们拥有数量完全相同的多边形和像素,但由于左侧平面中长而细的多边形比右侧平面多,因此它的渲染速度较慢。

过度绘制

要绘制一个六角星,您可以构建一个包含 10 个多边形的网格,也可以仅使用两个多边形绘制相同的形状,如图 13 所示。

two different menthods for triangulations
图 13.绘制六角星的两种不同方法。

您也许认为绘制两个多边形(而非 t)更快,但在这种情况下不然,因为六角星中心的像素要绘制两次。这便是所谓的过度绘制,基本上像素的绘制不止一次。在渲染过程中,过度绘制是一件非常自然的事情。例如,如果一个角色站在柱子后面,身体一部分被遮住,那么即使柱子遮住了一部分,也要绘制整个角色。一些引擎采用高级算法以避免绘制最终图像中所没有的对象,但非常困难。相比 GPU 进行绘制,CPU 通常需要更长的时间来确定不需要绘制的内容。

作为一名美术师,您必须承认有时候需要过度绘制,但最好尽可能删除无法看到的表面。如果与设计师团队合作,可以让他们为您的游戏引擎添加调试模式,这样一切能够变得透明。这样有利于更轻松地发现可以删除的隐藏多边形。

实施地板上的盒子

图 14 显示了一个简单场景:放在地板上的一个盒子。地板由 2 个三角形组成,盒子由 10 个三角形组成。红色区域是该场景中的过度绘制部分。

triangulation of box on floor
图 14.地板上的盒子。

在这种情况下,GPU 将绘制盒子下方的地板,尽管我们永远看不到这块区域。如果在盒子下方的地板上创建一个孔,那么您的多边形增多,过度绘制减少,如图 15 所示。

triangulation of box on floor
图 15.盒子下方的孔,用于避免过度绘制。

在这类场景中,您需要执行判断调用。有时候值得通过减少多边形来换取部分过度绘制。有时候值得添加多边形以避免过度绘制。又比如,下列两幅图显示了两个外观相同,有一些尖刺伸出表面的网格。在第一个网格中,如图 16,尖刺放在表面上方。

spikes on plane - triangulation
图 16.表面上的尖刺。

在第二个网格中,如图 17,尖刺下方的表面上切割了一些孔,以减少过度绘制。

spikes on plane - more extensive triangulation
图 17.尖刺下方切割的孔。

这种情况下,为了切割孔,增加了许多细长的多边形。此外,避免过度绘制的表面并不是很大,因此在该场景中这种技巧没有效果。

这好比为地上的一座房子建模。您可以保持地面原样不动,也可以在地上挖一个洞,将房子放在这个洞里。对于地下没有洞的房子,需要的过度绘制较多。但这取决于几何体以及观察这座房子的角度。如果绘制房子下方的地面,从房子内部向下观察时,这种方法会产生许多过度绘制。但如果从飞机上俯瞰这座房子,那么情况将大不相同。在这些情况下,最好是在游戏引擎中添加调试模式,以使表面透明,这样你可以看到表面后方经过了绘制,但最终用户看不到的内容。

当 Z 缓冲区变成 Z 竞争

GPU 绘制两个重叠的多边形时,如何确定哪个多边形在前,哪个在后?早期的计算机显卡创新者花费了大量时间来解决这一问题。Ed Catmull(后来担任 Pixar 和 Walt Disney 动画工作室的总裁)写了一篇论文,介绍了十种不同的问题解决方法。他指出,如果计算机有充足的内存来存储每像素一个深度值,这一问题就很容易解决。在 20 世纪 70 年代和 80 年代,这是容量巨大的内存。但在今天,大多数 GPU 都采取这种运行方式:称为 Z 缓冲区。

Z 缓冲区(也称深度缓冲区)的工作原理:每个像素都有一个与之相关的深度值。每当硬件绘制一个对象,它会计算绘制像素与摄像头的距离。然后检查像素现有的深度值。如果与摄像头的距离比与新像素远,那么绘制新像素。如果现有像素与摄像头的距离比新像素近,那么将永远不会绘制新像素。这种方法可以解决许多问题,即使您有相交的多边形,也能很好地解决问题。

example of intersecting polygon
图 18.深度缓冲区处理的相交多边形。

然而,Z 缓冲区没有无限精度。如果两个表面与摄像头的距离几乎完全相同,那么 GPU 会混淆,随机选择一个表面,如图 19 所示。

Conflict in overlapping objects
图 19.处于同一深度的表面产生显示问题。

这就是所谓的 Z 竞争,它看起来是一种异常和问题。通常情况下,表面离摄像头越远,Z 竞争现象越严重。引擎设计师可以结合修复程序来缓解这一问题,但如果美术师构建非常接近的重叠多边形,仍然会出现这一问题。另外一个示例是张贴海报的墙壁。海报与摄像头的距离和它后面墙壁与摄像头的距离几乎一样,因此出现 Z 竞争的风险非常高。解决方法是在海报后面的墙上切割一个孔。这样也能减少过度绘制。

Example of overlapping polygons
图 20.重叠多边形的 Z 竞争示例。

在极端情况下,对象相互接触时也会发生 Z 竞争现象。在图 20 中,您可以看到地板上的一个盒子,由于我们不能在盒子所在的地板上切割一个洞,Z 缓冲区可能会混淆地板与盒子接触的边缘。

使用绘制调用

GPU 的速度非常快,CPU 很难跟上它们。由于 GPU 基本上只执行一项操作,因此它们更容易加快执行速度。显卡,顾名思义,就是计算多个像素,因此能够构建并行计算许多像素的硬件。但 GPU 仅调用 CPU 让其绘制的内容。如果 CPU 不能快速反馈 GPU,GPU 就会闲置。每次 CPU 让 GPU 绘制某一内容就称为绘制调用。基本绘制调用包括绘制一个网格,包含一个着色器和一组纹理。

假设一颗慢速 CPU和一颗快速 GPU,前者每帧反馈 100 次绘制调用,后者每帧绘制 100 万个多边形。那么在这种情况下,理想的绘制调用可绘制 10,000 个多边形。如果网格只有 100 个多边形,GPU 只能每帧绘制 10,000 个多边形。那么 GPU 99% 的时间都将处于闲置状态。在这种情况下,您可以轻松地免费为网格增加多边形数量。

绘制调用的构成及其成本可能因引擎和架构的不同而有很大差异。有些引擎能够以单次绘制调用的形式批处理许多网格,但所有网格都需要有相同的着色器或满足其他限制条件。Vulkan*、DirectX* 12 等新 KPI 经过专门设计,可通过优化程序与显卡驱动程序之间的通信来解决这一问题,从而增加每帧发出的绘制调用数量。

如果您的团队正在编写自己的引擎,请告诉引擎开发人员绘制调用的局限性。如果您使用的是 Unreal*、Unity* 软件等现成的引擎,请考虑进行一些性能基准测试来确定引擎的局限性。您会发现自己能够在不降低性能的情况下增加多边形数量。

结论

我希望本篇概述文章可帮助大家从不同的方面了解渲染性能。每家 GPU 厂商的风格不同。每种引擎和硬件平台都有许多特别的注意事项。必须随时与渲染工程师沟通,了解应用的最佳实践。

关于作者

Eskil Steenberg 是一名独立的游戏和工具开发人员。他担任顾问,并负责开发独立项目。所有截图都来源于正使用 Eskil Steenberg 开发的工具进行的项目。您可以更多地了解他的 Quel Solaar 项目,并关注他的 Twitter @quelsolaar。

Para obtener información más completa sobre las optimizaciones del compilador, consulte nuestro Aviso de optimización.