3D游戏渲染如何工作:纹理和纹理过滤

图片

在有关3D游戏渲染的第三篇文章中,我们将了解在处理顶点处理和栅格化场景之后3D世界会发生什么。尽管纹理处理仅计算和更改多色块的二维网格的颜色,但纹理处理是渲染的最重要阶段之一。

现代游戏中的大多数视觉效果都归结为故意使用纹理-没有它们,游戏将显得无聊且毫无生气。因此,让我们看看它是如何工作的!

第1部分:顶点处理

第2部分:栅格化和光线跟踪

让我们从一个简单的开始


您可以释放在过去一年中的任何三维最畅销的游戏,并自信地说,他们都有一个共同点:它们使用纹理贴图(或只是纹理)。这是一个通用术语,在考虑纹理时,大多数人会呈现相同的图片:包含表面图像(草,石头,金​​属,织物,面部等)的简单平面正方形或矩形。

但是,当使用并结合使用复杂的计算时,这种3D场景中的简单图像可以创建出令人惊奇的逼真的图像。要了解如何做到这一点,让我们将其完全关闭,看看没有纹理的3D世界对象的外观。

正如我们从以前的文章中看到的那样,3D世界由顶点组成-可以移动然后着色的简单形状。然后将它们用于创建图元,然后将它们压缩成二维像素网格。由于我们将不使用纹理,因此需要为这些像素着色。

可以应用的一种方法称为平面着色:采用图元的第一个顶点的颜色,然后将该颜色应用于栅格中图形所覆盖的所有像素。看起来像这样:


显然,水壶看起来不切实际,尤其是因为表面颜色不规则。颜色从一个级别跳到另一个级别,没有平滑的过渡。解决该问题的一种方法是使用Gouraud着色

在此过程中,获取顶点的颜色,然后计算沿三角形表面的颜色变化。为此,使用线性插值。听起来很复杂,但实际上这意味着,例如,如果图元的一侧具有0.2红色的颜色而另一侧具有0.8红色的颜色,则图形的中间将具有介于0.2和0.8之间的颜色(即0.5)。

这个过程足够简单,这是它的主要优点,因为简单意味着速度。许多较旧的3D游戏都使用此技术,因为计算设备的功能受到限制。


巴勒特(Barrett)和乌云(Cloud)在古罗(Gouraud)阴影的所有宏伟中都表现不佳(Final Fantasy VII,1997),

但即使这样的解决方案也有问题-如果光线恰好落在三角形的中间,那么它的拐角(和顶点)可能无法传达此属性。这意味着可以完全消除光造成的眩光。

尽管Gouraud的平面阴影和阴影已在渲染工具中占据了应有的位置,但是上面显示的示例显然是用于纹理增强的候选对象。为了更好地理解将纹理叠加在表面上会发生什么,我们将追溯到1996年。

简短的游戏和GPU历史


大约23年前,id Software发布了Quake,这成为了一个重要的里程碑。尽管这不是第一个使用3D多边形和纹理渲染环境的游戏,但它绝对是最早有效使用它们的游戏之一。

但是她也做了其他事情-她展示了可以使用OpenGL完成的工作(当时该图形API处于第一个版本状态),并且还为Rendition Verite3Dfx Voodoo等第一代图形卡提供了很多帮助


照明峰值和简单纹理。清洁1996,清洁Quake。

按照现代标准,Voodoo非常简单:不支持2D图形,不支持顶点处理,仅进行最简单的像素处理。但是,她很漂亮:


图像:VGA博物馆

她有一个完整的芯片(TMU)可以从纹理中获取像素,另一个芯片(FBI)可以将其与光栅像素混合。该地图可以执行几个附加过程,例如雾或透明效果的实现,但从本质上讲,其功能已结束。

如果我们看一下图形卡结构和操作的基础架构,我们将看到这些过程如何工作。


3Dfx规范。资料来源:Falconfly Central

FBI芯片接收了两个颜色值并将它们混合;其中之一可能是来自纹理的值。混合过程在数学上非常简单,但根据混合的内容和用于执行指令的API的不同而略有不同。

如果您看一下Direct3D在功能和混合操作方面为我们提供了什么,我们将看到每个像素首先被乘以0.0到1.0之间的数字。这确定了像素的颜色多少会影响最终结果。然后,将两个更改的像素颜色相加,相减或相乘;在某些功能中,执行逻辑操作,例如,始终选择最亮的像素。


图片:主动采取措施科技博客

上面显示的图像是如何工作的做法;请注意,像素的alpha值用作左侧像素的系数。该数字表示像素透明度

在其他阶段,将应用雾值(从程序员创建的表中获取雾值,然后执行相同的混合计算);检查和更改可见性和透明度;最后,像素的颜色被写入图形卡的内存。

为什么您需要将这种游览纳入历史?好吧,尽管设计相对简单(特别是与现代怪物相比),但此过程描述了纹理的基本原理:我们采用颜色的值并将其混合在一起,以便模型和环境在特定情况下看起来像它们应该的那样。

现代游戏做同样的事情,唯一的区别是使用的纹理数量和混合计算的复杂性。它们一起模拟了电影中发现的视觉效果,或照明与不同材质和表面的相互作用。

纹理基础


对我们来说,纹理是叠加在构成框架中3D结构的多边形上的平面2D图像。但是,对于计算机来说,这只是二维数组形式的一小块内存。数组的每个元素表示纹理图像中像素之一(通常称为纹理像素 -纹理像素)的颜色值

多边形的每个顶点都有一组两个坐标(通常表示为u,v),告诉计算机纹理的哪个像素与之关联。顶点本身具有一组三个坐标(x,y,z),将纹理像素链接到顶点的过程称为纹理映射

为了了解这种情况的发生,让我们来看一下在本系列文章中已经使用过几次的工具-Real Time Rendering WebGL。现在,我们还丢弃顶点z坐标,并考虑平面上的所有内容。


从左到右:纹理u,v坐标,直接与角顶点x,y坐标绑定。在第二个图像中,y坐标在顶部顶点处增加,但是由于纹理仍附加在顶点上,因此它在垂直方向上拉伸。正确的图像中的纹理已经更改:u增加了,但是结果是纹理被压缩然后重复。

发生这种情况是因为,尽管事实上,由于u的值增加,纹理已经变得更高,但它仍应适合图元-实际上,纹理会部分重复。这是一种实现3D游戏中常见效果的方法:重复纹理。这种效果的例子可以在石质或草地景观以及砖墙中看到。

现在,让我们更改场景,以便有更多基本体,然后再次返回场景的深度。经典的风景视图如下所示,但是现在复制了所有原始图元的框纹理并对其重复了。


原始gif格式的框纹理的大小为66 KB,分辨率为256 x 256像素。框纹理所覆盖的框架部分的初始分辨率为1900 x 680,也就是说,从像素“区域”的角度来看,该区域应仅显示20个框纹理。

但很明显,我们看到比二十箱得多,这意味着,在距离盒子的质地要高于256×256像素。确实,他们经历了一个称为“纹理最小化”的过程(是的,这个词在英语中存在!)。现在,让我们重复一遍,但是这次将相机拉近一个抽屉。


不要忘记纹理只有256 x 256像素的大小,但是在这里我们看到的纹理大于宽度为1900像素的图像的一半。该纹理经过“纹理放大”操作

这两个纹理过程在3D游戏中不断发生,因为当摄像机在场景中移动时,模型会接近或移开,应用于图元的所有纹理都必须与多边形一起缩放。从数学的角度来看,这是一个小问题,实际上,即使是最简单的集成图形芯片也可以轻松地做到这一点。但是,减少和扩大纹理是需要以某种方式解决的新挑战。

场景的迷你副本出现在场景中


解决纹理的第一个问题是距离。如果我们返回第一张带有盒子风景的图像,那么位于地平线附近的盒子实际上只有几个像素的大小。因此,出于两个原因,尝试在如此小的空间中压缩256 x 256像素的图像是没有意义的。

首先,较小的纹理会占用较少的图形卡内存,这很方便,因为您可以尝试将其放入较小的缓存中。这意味着从缓存中删除它的可能性较小,也就是说,重复使用此纹理将提高性能,因为数据将保存在紧密的内存中。由于第二个原因,我们将很快返回,因为它与在靠近相机的纹理中出现的相同问题相关。

需要将大纹理压缩为小图元的问题的标准解决方案是使用mip-textures(mipmaps)。这些是原始纹理的缩小版本;它们可以由引擎本身生成(使用适当的API命令),也可以由游戏设计师预先创建。与上一个相比,每个后续的mip纹理级别都有一半大小。

也就是说,对于盒子纹理,尺寸将为:256 x 256→128 x 128→64 x 64→32 x 32→16 x 16→8 x 8→4 x 4→2 x 2→1 x 1。


所有的mip纹理都打包在一起,因此该纹理具有相同的文件名,但尺寸变大。纹理的填充方式使得u,v坐标不仅确定哪个纹理元素叠加在帧中的像素上,而且还确定哪个纹理纹理。然后,程序员根据帧的像素深度的值编写渲染器,该渲染器确定要使用的mip纹理。例如,如果该值非常高,则像素距离较远,这意味着您可以使用较小的mip纹理。

细心的读者可能会注意到缺乏mip纹理-他们必须通过增加纹理的大小来为此付出代价。盒子的原始纹理为256 x 256像素,但是如上图所示,带有mip纹理的纹理现在的大小为384 x256。是的,它有很多空白空间,但是无论我们如何包装较小的纹理,通常一侧的纹理大小将增加至少50%。

但这仅适用于先前创建的mip纹理。如果将游戏引擎编程为正确生成它们,则增加量不超过原始纹理大小的33%。因此,由于用于存储Mip纹理的内存量略有增加,因此我们获得了性能和视觉质量的提升。

以下是禁用/启用mip纹理的图像的比较:


在图像的左侧,“原样”使用了盒子的纹理,这导致了颗粒感的出现和远方的莫尔条纹在右侧,使用mip纹理可以实现更平滑的过渡,在地平线上,盒子的纹理被模糊成统一的颜色。

但是,谁想要模糊的纹理破坏他们喜欢的游戏的背景呢?

双线性,三线性,各向异性-对我来说所有这些都是中文字母


从纹理中选择像素以将其覆盖在帧中的像素上的过程称为采样纹理,并且在理想世界中,无论大小,位置,方向等如何,都会有一个理想地匹配为其设计图元的纹理。换句话说,对纹理进行采样将是简单的一对一纹理像素映射。

但是,由于事实并非如此,因此在采样纹理时需要考虑几个因素:

  • 是否缩小或放大了纹理?
  • 纹理是源纹理还是Mip纹理?
  • 以什么角度显示纹理?

让我们按顺序分析它们。第一个因素很明显:如果增加了纹理,则在图元中将覆盖比图元更多的像素。当减少时,情况正好相反-每个纹理像素现在应该覆盖几个像素。这是一个问题。

第二个因素不会造成问题,因为使用了mip纹理来绕过对遥远基元的纹理进行采样的问题,因此唯一的任务是以一定角度显示纹理。是的,这也是一个问题。为什么?因为所有纹理都是为“严格在前面”查看而生成的图像。用数学语言来讲,正常的表面纹理与当前在其上显示纹理的名义表面相匹配。

因此,如果纹理像素太少或太多,或者它们成一定角度放置,则需要一个称为“纹理过滤”的附加过程如果不使用此过程,则得到以下信息:


在这里,我们用字母R的纹理替换了盒子的纹理,以更清楚地显示图像变成什么混乱而无需过滤纹理!

诸如Direct3D,OpenGL和Vulkan之类的图形API提供相同的过滤类型集,但为其使用不同的名称。实际上,它们全都归结为以下几点:

  • 近点采样
  • 线性纹理过滤
  • 各向异性纹理过滤

实际上,采样最近的点采样并不是一个滤镜,因为只有它采样了所需纹理像素的最近像素(例如,从内存复制),然后将其与像素的原始颜色混合。

在这里,线性滤波对我们有帮助。所需的纹素坐标u,v传输到采样设备,但是采样器采用四个纹素,而不是获取最接近这些坐标的纹素。这些是通过对最近点进行采样而选择的像素的上方,下方,左侧和右侧的像素。

然后使用带有权重的公式将这四个纹理像素混合。例如,在Vulkan中,公式如下所示:


T表示纹理像素的颜色,其中f是过滤的结果,而1-4是四个采样纹理像素的颜色。根据具有坐标u,v的点距纹理中心的距离来获取alphabeta 幸运的是,对于那些涉及3D图形的人,这是自动在图形芯片中发生的。实际上,这正是3dfx Voodoo卡TMU芯片所做的:它采样了四个纹理像素,然后将它们混合在一起。在Direct3D中,此过程的双线性过滤名称很奇怪

但是自Quake和TMU芯片问世以来,图形卡已经学会了如何在一个时钟周期内执行双线性滤波(当然,如果纹理已经位于最近的内存中)。

线性过滤可以与mip纹理一起使用,如果要使过滤复杂化,可以从纹理中获取四个纹理像素,然后从下一层次的mip纹理中获取四个纹理像素,将它们混合在一起。在Direct3D中它叫什么?三线性过滤。这个过程中“三个”从何而来?因此,我们不知道...

值得一提的最后一种过滤方法是各向异性。实际上,这是对通过双线性或三线性滤波执行的过程的改进。最初,它计算图元表面的各向异性程度(这是一个令人惊讶的复杂过程)-该值由于其方向而增加了图元长宽比的变化:


上图显示了相同的正方形图元,边长相等;但逐渐转为矩形,其宽度变化大于高度变化。因此,右侧的图元的各向异性程度要大于左侧的图元(并且在正方形的情况下,该程度为零)。

许多现代3D游戏允许您打开各向异性过滤,然后将其级别更改(从1倍更改为16倍),但是它真正改变了什么?此参数控制在每个初始线性样本中获取的附加纹理像素样本的最大数量。假设在游戏中启用了8x的各向异性双线性过滤。这意味着它将获得32个值而不是四个texel值。

使用各向异性过滤时的区别显而易见:


只需转到上图,然后将最大点的采样与最大16倍各向异性三线性滤波进行比较即可。令人惊讶的顺利!

但是要获得这种平滑的纹理美感,就必须在性能上付出代价:在最大设置下,各向异性三线性过滤将为渲染的每个像素从纹理中接收128个样本。即使使用最好的现代GPU,也无法在一个时钟周期内实现。

例如,如果您使用AMD Radeon RX 5700 XT,那么处理器内部的每个纹理块可以在一个时钟周期内使用多达32个texel地址,然后在下一个时钟周期从内存中加载32 texel值(每个都有32位的大小),然后将其中四个混合在一起机智。也就是说,将128个纹理像素样本混合为一个样本至少需要16个时钟周期。


具有7纳米处理技术的GPU AMD RDNA Radeon RX 5700

如果5700 XT的时钟速度为1605 MHz,则十六个周期仅需10 纳秒。仅使用一个纹理单元对4K帧中的每个像素执行这些周期将仅花费70毫秒。太好了,看起来性能并不重要!

即使在1996年,3Dfx Voodoo和类似的卡片也迅速地处理了纹理。他们最多可以给出每个周期1个具有双线性滤波的texel,并且TMU芯片频率为50 MHz,这意味着每秒可以处理5000万个texel。以800 x 600和30 fps的速度运行的游戏仅需要1400万像素,每秒可进行双线性过滤。

但是,只有在所有纹理都在最近的内存中且每个像素只有一个纹理像素的情况下,这才是正确的。二十年前,需要在原始图元上覆盖多个纹理的想法是完全陌生的,但如今已成为标准。让我们看看为什么这一切都会改变。

添加照明


要了解为什么纹理化变得如此重要,请看一下Quake中的以下场景:


这是一张黑暗的图像,因为黑暗是游戏的气氛,但我们看到黑暗并非到处都是-墙壁和地板的某些碎片比其他碎片更轻,从而在这些区域营造出明亮的感觉。

组成墙壁和地板的图元叠加有相同的纹理,但是在将它们应用于帧像素之前,还有另一种称为`` 光照贴图''的纹理与纹理像素的值混合在一起。在雷神之锤中,灯光地图是预先计算的,并由游戏引擎创建。它们用于生成静态和动态照明级别。

使用它们的优点是,复杂的光照计算是使用纹理而不是顶点进行的,从而以低速成本为代价大大改善了场景的外观。显然,图像并不完美:在地板上,照明区域和阴影之间的边界非常清晰。

在许多方面,光照贴图只是另一种纹理(别忘了它们都是常规的2D数据集),因此此场景是使用多重纹理的第一个示例之一。顾名思义,这是将两个或多个纹理叠加在图元上的过程。在Quake中使用光照贴图已成为克服Gouraud阴影限制的一种方法,但是在增加图形卡功能范围的过程中,应用多重纹理的方法也得到了扩展。

像那个时代的许多其他卡一样,3Dfx Voodoo在一次渲染过程中可以执行的操作数量也受到限制。实际上,通过是一个完整的渲染周期:从处理顶点到栅格化帧,然后更改像素并将其写入完成的帧缓冲区。二十年前,游戏几乎总是使用单遍渲染。


Nvidia GeForce 2 Ultra,大约在2000年底。图片:Wikimedia

发生这种情况是因为仅用于应用附加纹理的第二个顶点处理在性能方面过于昂贵。在Voodoo之后,当ATI Radeon和Nvidia GeForce 2图形卡出现时,我们不得不等待几年,这些图形卡能够一次通过多纹理处理。

这些GPU在像素处理区域(即管道中具有多个纹理单元,因此从两个单独的纹理中获取具有双线性过滤的texel成为最简单的任务。这进一步增加了光照贴图的普及度,并允许游戏使其完全动态化,并根据游戏环境的条件更改光照值。

但是只要有几个纹理,就可以做更多的事情,因此让我们探索它们的功能。

改变高度是正常的


在本系列有关3D渲染的系列文章中,我们没有讨论GPU的角色如何影响整个过程(我们将讨论这一点,但现在不讨论!)。但是,如果您回到第1部分并了解处理顶点的整个复杂过程,您可能会认为这是GPU必须完成的所有工作中最困难的部分。

长期以来,游戏程序员竭尽所能减少这种负担。他们必须采取各种技巧来确保与使用多个顶点时相同的图像质量,但不对其进行处理。

这些技巧大多数都使用称为高度图法线图的纹理可以从前者创建后者的事实将这两个概念联系在一起,但是现在,让我们仅看一下称为“凹凸贴图”的技术


由Emil Persson演示渲染中创建的图像压纹纹理被禁用/启用压纹纹理

使用称为“高度图”的2D数组,看起来像原始纹理的奇怪版本。例如,上图显示了逼真的砖纹理覆盖在两个平面上。纹理及其高度图如下所示:


高度图的颜色表示砖块表面的法线(我们在一系列文章的第1部分中介绍了法线)。当渲染过程达到将砖纹理应用于表面的阶段时,将执行一系列计算以基于其法线更改砖纹理的颜色。

结果,尽管砖块继续保持完全平坦,但砖块本身看起来更三维。如果仔细观察,尤其是在砖的边缘,您会发现此技术的局限性:纹理看起来有些变形。但这是一个快速技巧,可让您添加更多表面细节,因此压纹纹理非常流行。

法线贴图与高度图相似,仅纹理颜色是法线本身。换句话说,不需要将高度图转换为法线的计算。您可以提出一个问题:颜色如何描述空间中的向量?答案很简单:每个纹理元素都有一组r,g,b(红色,绿色,蓝色),这些值直接对应于法线向量x,y,z


左图显示了在不平坦表面上法线方向的变化。为了描述具有平坦纹理(中间轮廓)的相同法线,我们为其指定了颜色。在这种情况下,我们将值r,g,b(0.255.0)用于笔直向上的矢量,然后增加红色的值(向左倾斜)和蓝色的值(向右倾斜)。

请记住,此颜色不会与原始像素混合,它只是告诉处理器法线指示的方向,以便可以正确计算相机,光源和纹理表面之间的角度。

当在场景中使用动态照明时,以及在渲染过程计算照明效果逐像素(而不是针对每个顶点)变化时,浮雕纹理和法线贴图的优势显而易见。今天,现代游戏使用了一组纹理来改善此技巧的质量。


图片:来自Twitter的Ryan Benno

令人惊讶的是,这面逼真的墙只是一个平坦的表面,砖头和砖石水泥的细节并不是使用数百万个多边形制成的。相反,仅五个纹理和周到的计算使用就足够了。

高度图用于生成砖的阴影投射,法线图用于模拟所有较小的表面变化。粗糙纹理用于改变光从墙的各种元素反射的方式(例如,光滑的砖比粗糙的水泥更均匀地反射光)。

在AO图像中命名的最后一张卡片创建了过程的一部分,称为环境光遮挡:我们将在以下文章中更详细地研究此技术,但现在我们可以说它有助于提高阴影的真实感。

纹理映射是一个关键过程。


开发游戏时,纹理化绝对必要。以2019年的游戏《王国来:拯救》Kingdom Come:Deliverance)为例,这是15世纪在波西米亚成立的第一人称角色扮演游戏。设计师试图创造那个时期最现实的世界。为了使玩家沉浸在数百年前的生活中,最好实现历史上准确的景观,建筑物,衣服,发型,日常用品等等。

游戏中这张图片中的每个纹理都是由艺术家手动创建的,这还要归功于程序员控制的渲染引擎。其中一些很小,细节很简单,因此经过轻微过滤或用其他纹理处理(例如鸡翅)。


其他则具有高分辨率和许多小细节。他们经过各向异性过滤并与法线贴图和其他纹理混合-只是看着前景中人的脸。程序员考虑了每个场景对象的纹理要求的差异。

今天,所有这一切都发生在许多游戏中,因为玩家期望更高的细节和逼真度。纹理变得越来越大,并且越来越多的纹理叠加在表面上,但是纹理像素采样并将它们叠加在像素上的过程基本上与Quake时代相同。最好的技术永远不会消失,无论它们有多古老!

All Articles