3D游戏渲染的工作原理:照明和阴影

现代游戏中绝大多数视觉效果的实现取决于对照明和阴影的明智使用。没有它们,游戏将变得无聊无聊。在3D游戏渲染分析的第四部分中,我们将重点介绍3D世界中发生的事情以及顶点处理和纹理贴图。我们将再次需要大量的数学知识,并对光学基础知识有扎实的理解。

第1部分:顶点处理

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

第3部分:对纹理进行纹理化和过滤

回顾过去


之前,我们研究了在场景中移动和处理对象的关键方面,它们从三维空间到像素平面网格的转换以及在这些对象上叠加纹理的方法。多年来,此类操作一直是渲染过程中必不可少的一部分,我们可以通过在1993年返回并启动id Software的Doom来看到这一点。


按照现代标准,在此游戏中光和影的使用非常原始:不考虑光源,基于其顶点的每个表面都被赋予一个通用的颜色值或环境光的值。阴影的所有迹象都是由于对纹理的巧妙运用以及对环境颜色的选择而产生的。

没有阴影,因为它们不是程序员的任务:当时的PC是66 MHz处理器(即0.066 GHz!),40 MB硬盘驱动器和具有3D功能最少的512 KB图形卡。快进23:在该系列著名重启中,我们看到了一个完全不同的故事。


许多技术 用于渲染该帧,它具有屏幕空间环境光遮挡,通过前深度映射,散景模糊滤镜,色调校正算子等阶段。动态计算每个表面的阴影和阴影:它们会根据环境条件和玩家的动作不断变化。

由于任何3D渲染操作都需要数学运算(一堆计算!),所以我们最好从任何现代游戏的幕后发生的事情开始。

数学照明


为了正确实现所有功能,我们需要在与各种表面相互作用时准确模拟光的行为。奇怪的是,这个问题在18世纪第一次由一个名叫约翰·海因里希·兰伯特的人解决。

1760年,一位瑞士科学家发行了一本书,名为《Photometria》。在其中,他概述了光的行为的基本规则。其中最显着的是以下情况-表面以某种方式发出光(通过反射或作为光源),使得发出的光的亮度根据法向表面和观察者之间的角度的余弦值而变化。


这个简单的规则为所谓的漫射照明奠定了基础这是一个数学模型,用于根据表面的物理属性(例如,颜色和光反射程度)和光源的位置来计算表面的颜色。

在3D渲染中,这需要大量信息,以这种方案的形式最容易想到:


我们在图像中看到很多箭头,它们是vector,计算颜色需要以下向量:

  • 顶点位置,光源和摄像机在场景中的3个矢量
  • 从顶部的角度来看,两个矢量分别用于光源和摄像机的方向
  • 1个法线向量
  • 1个半向量(总是在照明和相机的方向向量之间的中间)

它们是在处理渲染过程的顶点的阶段计算的,将它们全部组合起来的方程式(称为Lambert模型)具有以下形式:


即,通过将表面的颜色,光源的颜色和顶点的法向矢量标量乘积与光的方向乘以衰减和投影照明系数,来计算漫射照明下顶点的颜色此操作是针对场景中的每个光源执行的,因此在等式的开头是和符号。

等式中的向量(以及我们在下面看到的所有内容)都已标准化(如每个向量上方的图标所示)。归一化矢量保持其原始方向,并且其长度减小到一个单位值(即等于1个测量单位)。

表面和光源的颜色值是标准RGBA数字(红色,绿色,蓝色和alpha透明度)。它们可以是整数(例如,每个颜色通道为INT8),但几乎总是浮点数(例如,FP32)。衰减系数确定从光源移开时照明水平如何降低,并由另一个公式计算得出:


术语A C,A L和A Q是不同的系数(恒定,线性,二次方),它们描述距离如何影响照明水平。所有这些都是由程序员在创建渲染引擎时设置的。在每个图形API中,这都是以自己的方式实现的,但是在对光源类型进行编码时会引入系数。

在我们考虑最后一个系数(泛光)之前,值得注意的是,在3D渲染中,基本上有三种类型的光源:点光源,定向光源和聚光灯。


点光源在所有方向上均匀发光,而方向光源仅在一个方向上发光(从数学的角度来看,这只是一个无限远的点光源)。聚光灯是复杂的定向光源,因为它们以圆锥形状发光。锥体中光线的变化方式决定了锥体内部和外部的大小。

是的,对于探照灯系数,还有另一个等式:


探照灯系数的值为1(即光源不是探照灯)或0(如果顶点在圆锥方向之外)或两者之间的某个计算值。角度φ(phi)和θ(θ)指定聚光灯圆锥的内部/外部的尺寸。

两个向量:L dcs和L dir(与相机方向和聚光灯方向相反)用于确定顶点的圆锥体是否在接触。

现在我们应该记住,所有这些对于计算漫射照明的值都是必需的,并且所有这些操作都必须针对每个场景中的光源,或者至少针对程序员想要考虑的每个光源。这些方程式中的许多是由图形API执行的,但是如果编码器需要对图像进行更多控制,则也可以手动完成这些方程式。

但是,实际上,在现实世界中,有无数个光源:每个表面反射照明,因此它们都影响场景的整体照明。即使在晚上,也有背景照明,无论是恒星和行星还是散落在大气中的光。

为了模拟这一点,计算了另一个照明值:环境照明。


该方程比漫射照明更简单,因为不需要方向。在这里,执行各种系数的简单乘法:

  • C SA-表面照明颜色
  • C GA-突出显示全局3D场景的颜色
  • C LA-场景中所有光源的照明颜色

值得注意的是,再次使用了衰减系数和投影仪系数,以及所有光源的总和。

因此,我们具有背景照明,并且考虑了3D世界各个表面的光源的漫射照明。但是,Lambert模型仅适用于从各个方向反射其表面照明的材质。用玻璃或金属制成的物体产生另一种反射,称为镜面反射 ;当然,他也有一个等式!


这个公式的各个部分应该已经为您所熟悉:我们有两个镜面颜色值(一个是表面C S的值,另一个是光C LS的值),以及通常的衰减和泛光系数。

由于镜面反射非常集中和定向,因此使用两个矢量来确定镜面照明的亮度:顶点法线和半矢量。系数p称为镜面反射,这是一个根据表面材料的属性确定反射亮度的数字。随着p的增加,镜面效果会变亮,但会更加集中且更小。

要考虑的最后一个元素是最简单的,因为它只是一个数字。它称为发射照明,并应用于直接照明的对象,即火焰,手电筒或太阳。

这意味着现在考虑到背景照明(环境)以及不同光源与表面材料的特性(漫反射和镜面反射)之间的相互作用,现在我们有一个方程式和三组方程式用于计算表面顶点的颜色。程序员只能选择一个,也可以通过折叠将所有四个结合起来。


在外观上,组合看起来像这样:


我们考虑的方程式是使用图形API(例如Direct3D和OpenGL)使用它们的标准功能来应用的,但是对于每种照明类型,都有其他算法。例如,可以使用Oren-Nayyar模型实现漫射照明,该模型比Lambert模型更适合于非常粗糙的表面。

可以用考虑了以下事实的模型替换镜面反射方程式:玻璃或金属之类的非常光滑的表面仍然很粗糙,但在微观水平上。这种称为微面算法的模型以数学复杂性为代价,提供了更逼真的图像。

无论使用哪种模型,都可以通过增加将其应用于3D场景的频率来极大地改善它们。

顶点或逐像素计算


当我们检查顶点处理栅格化时,我们发现为每个顶点执行的所有棘手的光照计算结果都应插在顶点之间的表面上。这是因为与表面材料关联的属性存储在顶点内部;当3D世界被压缩为2D像素网格时,像素仅保留在顶点所在的位置。


其余像素需要传输有关顶点颜色的信息,以便颜色在表面上正确混合。 1971年,时任犹他大学研究生的Henri Gouraud提出了一种现在称为Gouraud Shading的方法

他的方法计算速度很快,并且多年来已成为事实上的标准,但是他也有问题。他无法正确插入镜面照明,并且如果对象由少量基本体组成,则这些基本体之间的混合似乎是错误的。

1973年,同样在犹他大学工作的Bui Tyong Fong提出了解决这个问题的方案。在他的研究文章中,Fong演示了一种在栅格化表面上插补顶点法线的技术。这意味着散射和镜面反射模型对于每个像素都可以正常工作,我们可以在David Eck的在线计算机图形学和WebGL 教程中清楚地看到这一点

下面显示的碳球使用相同的光照模型着色,但对于左手计算,将垂直进行,然后进行Gouraud阴影处理,以将其插值整个表面。对于右边的球体,计算是逐个像素进行的,差异很明显。


静止图像不能传达Phong阴影带来的所有改进,但是您可以独立运行Ek 在线演示并观看动画。

但是,Fong并没有止步于此,几年后,他发表了另一篇研究文章,他展示了如何使用一个简单的方程式分别进行环境,漫反射和镜面反射的计算:


在这里我们要认真理解!字母k表示的值是环境,漫反射和镜面反射的反射常数。从入射光的大小来看,每个反射光都是相应类型的反射光的一部分。我们在上面的等式中看到的C(每种照明的表面材料的颜色值)。

向量R是“完美反射”向量-如果表面完全光滑,则反射光的移动方向;它是使用表面法线和入射光矢量计算的。向量C是相机方向向量;和- [RC的归一化。

最后,方程中有最后一个常数:α的值确定表面光泽度。材料越光滑(即越类似于玻璃或金属),数量越多。

该方程通常称为Phong反射模型。在他进行研究时,这样的提议是激进的,因为它需要大量的计算资源。吉姆·布林Jim Blinn)创建了模型的简化版本,将公式的一部分从RC替换HN(半距离矢量和表面法线)。必须为每个光源和帧中的每个像素计算R的值,并且对于每个源和整个场景,H足以计算一次。如今,

Blinn-Fong反射模型已成为标准照明系统,默认情况下在Direct3D,OpenGL,Vulkan等中使用。

还有许多其他数学模型,尤其是现在GPU可以处理长而复杂的着色器中的像素;这些公式一起称为双向反射/透射分布函数(BRDF / BTFD);它们是我们玩现代3D游戏时为显示器上的每个像素着色的基础。

但是,到目前为止,我们只考虑了反射光的表面:半透明的材料透射光,而光线则被折射还有一些表面。例如,水以不同程度反射和透射光。

我们将照明提高到一个新的水平


让我们看一下2018年育碧刺客的信条:奥德赛游戏,其中玩家经常在浅河和深海中的水上航行。


涂漆的木材,金属,绳索,织物和水-所有这些都通过大量计算来反射和折射光,

为了最真实地渲染水,同时保持足够的游戏速度,育碧程序员使用了整套技巧。熟悉的三重环境光,散射光和镜面光照亮了水面,但有趣的功能为它们提供了补充。

这些中的第一个通常用于生成水的反射特性-这些是屏幕空间反射(SSR)。此技术可渲染场景,但像素的颜色取决于每个像素深度,即从他到相机的距离。深度存储在所谓的深度缓冲区中。然后,使用所有通常的照明和纹理再次渲染帧,但是将场景另存为渲染纹理,而不是保存为传输到监视器的就绪缓冲区。

之后,进行射线行进。为此,从相机发出光线,并沿光束的方向设置距离。该代码检查光束相对于深度缓冲区中像素的深度。如果它们具有相同的值,则代码将检查正常像素,以查看是否将其定向到相机,如果是,则引擎从渲染纹理中查找相应的像素。然后,另一组指令反转像素的位置,以使其正确反映在场景中。


EA的《冰冻人》引擎中使用的SSR订单。

另外,光在材料内部移动过程中会被散射,对于诸如水或皮革之类的材料,使用了另一个称为次表面散射(SSS)的技巧。我们不会详细解释它,但是您可以在2014 Nvidia演示中阅读如何使用它来创建如此惊人的结果


Nvidia的2013 FaceWorks演示(链接

让我们回到刺客的信条:SSS的实现在这里很难被注意到,并且由于速度方面的考虑,它并没有被广泛使用。在AC系列的先前游戏中,Ubisoft 使用了伪造的SSS,但在上一游戏中,它的使用更为复杂,但仍然不如Nvidia演示中所见。

要更改水面上的照明值,请执行其他步骤,以正确模拟由于透明度变化导致的深度影响,该透明度取决于与海岸的距离。当摄像机注视着海岸附近的水时,甚至可以使用更多算法来考虑焦散和折射。

结果令人印象深刻:


刺客信条:奥德赛-在所有荣耀中渲染水。

我们看着水,但是空气中的光运动如何?灰尘,水分和其他元素也会导致光散射。其结果是,光射线接收音量,不要只停留了一组直接照射。

体积照明的主题可以扩展到十几篇文章,因此我们将讨论游戏《古墓丽影:崛起》是如何处理的。在下面的视频中,只有一种主要的照明光源-阳光穿过建筑物的开口。


为了产生大量的光,游戏引擎会拍摄摄像机的可见性金字塔(请参见下文),然后将其深度按指数方式分成64个部分。然后,将每个切片栅格化为大小为160 x 94元素的网格,并将所有这些数据保存为FP32格式的三维渲染纹理。由于纹理通常是二维的,因此金字塔体积的“像素”称为体素


对于4 x 4 x 4体素块,计算着色器确定哪些活动光源会影响该体积,然后将该信息写入另一个三维渲染纹理。然后,为了估算体素块内部的光的总“密度”,使用了称为Hengy-Greenstein散射函数的复杂公式

然后,引擎将执行更多的着色器以精炼数据,然后沿着金字塔的切片执行光线行进,并累积光密度值。Eidos-Montréal声称,在Xbox One上,所有这些操作大约需要0.8毫秒!

尽管并非在所有游戏中都使用此技术,但玩家希望在今天发布的几乎所有流行的3D游戏中都能看到体积覆盖,尤其是在第一人称射击游戏和动作冒险游戏中。


在2018年《古墓丽影》的续集中使用的体积照明。

最初,这种照明技术被称为“神圣射线”,或者用科学术语称为“暮光”。最早使用该游戏的游戏之一是Crytek于2007年发布的第一款Crysis

但是,这并不是真正的体积照明-该过程包括以深度缓冲区(用作蒙版)形式对场景进行初始渲染-另一个缓冲区,其中像素颜色越接近相机,颜色就越暗。

对该遮罩缓冲区进行了几次采样,着色器获取了样本,并通过模糊将它们混合在一起。此操作的结果与完成的场景混合在一起:


在过去的12年中,图形卡取得了巨大的进步。孤岛危机发布时功能最强大的GPU是Nvidia GeForce 8800 Ultra最快的现代GPU- GeForce RTX 2080 Ti具有30倍以上的计算能力,14倍的内存和6倍的带宽。

尽管渲染复杂度增加,但借助所有这些计算能力,现代游戏仍可以提供更高的图形准确性和总体速度。


Ubisoft的《 The Division 2》中的“神圣之光”,

但实际上,这种效果表明,尽管正确照明对于视觉准确性至关重要,但实际上没有照明更为重要

阴影的本质


让我们使用游戏Shadow of the Tomb Raider开始新的篇章在下图中,与阴影有关的所有图形选项均被禁用;在右边它们被包括在内。差别很大,对吧?


由于阴影在现实世界中自然形成,因此错误地实施阴影的游戏将永远看起来不正确。我们的大脑习惯于使用阴影作为视觉支持来创建相对深度,位置和运动的感觉。但是在3D游戏中做到这一点非常困难,或者至少很难做到正确。

让我们从鸭子开始。她在这里到处走动,阳光直射到她并被正确遮挡。


在场景中实现阴影的第一种方法是在模型下添加阴影“点”。这是完全不现实的,因为阴影的形状与投射阴影的对象的形状不匹配。但是,这种方法很容易创建。

最早的3D游戏(例如1996年的《古墓丽影》)使用了这种方法,因为当时的硬件(例如Sega Saturn和Sony PlayStation)无法提供更好的功能。这种方法在模型移动的表面上方绘制了一组简单的图元,然后对其进行了着色。还使用了简单纹理底部的绘图。


第一种方法是投射阴影在这种情况下,发出阴影的图元被投影到包含地板的平面上。为此,吉姆·布林(Jim Blinn)在80年代后期创建了一些必要的数学计算方法。按照现代标准,这是一个简单的过程,并且最适合简单的静态对象。


但是由于优化,阴影投影提供了动态阴影的第一个有价值的示例,例如在1999年的《Kingpin: Interplay 的犯罪生活》中如下图所示,只有动画角色(甚至是老鼠!)也有阴影,但这比简单的斑点要好。


这种方法最严重的问题是:(a)阴影的完全不透明性,以及(b)投影方法将阴影发射到一个平面上(例如,在地面上)。

这些问题可以通过在为投影的图元着色和为每个字符执行几次投影时应用一小部分透明度来解决,但90年代后期的PC硬件功能无法应付额外的渲染。

用于创建阴影的现代技术


早在1977年,就已经提出了一种更准确的实现阴影的方法。在德克萨斯州奥斯汀大学工作期间,富兰克林·克劳(Franklin Crowe)发表了一篇研究文章,其中他提出了几种使用阴影体积的技术

一般而言,它们可以描述如下:该过程确定哪些图元指向光源;他们的肋骨伸展到一个平面。虽然这与投影阴影非常相似,但重要的区别在于,然后使用创建的阴影体积检查像素是否在该体积之内/之外。由于有了这些信息,阴影可以在所有表​​面上发出,而不仅仅是地面。

1991年,蒂姆·海德曼(Tim Heidmann)对这项技术进行了改进,硅显卡Mark Kilgard于1999年进行了进一步的开发,我们将考虑的方法是id软件公司John Carmack于2000年创建的(尽管Carmack的方法是两年前由Creative Labs的Bilodo和Songa独立打开的; (为了避免法律问题卡马克被迫更改了代码)。

这种方法需要多帧渲染(称为多通道渲染- 这是90年代初期非常昂贵的过程,如今已广泛使用)和称为模板缓冲的概念

与帧缓冲区和深度不同,它不是由3D场景本身创建的-此缓冲区是以栅格形式在所有维度(即xy中的分辨率相等的值数组存储在其中的值用于告诉渲染引擎如何处理帧缓冲区中的每个像素。

使用此缓冲区的最简单示例是用作掩码:


具有阴影量的方法大致如下执行:

  • 我们将场景渲染到帧缓冲区,但仅使用环境照明(如果像素包含光源,我们还将其中包括所有发射值)
  • , , ( (back-face culling)). (, ) . (.. «») - .
  • , (front-face culling) -, .
  • , , -.

这些模板缓冲区和阴影体积(通常称为模板阴影)在2004年《毁灭战士3》 id软件游戏中使用


注意角色走过的表面仍然可以通过阴影看到吗?这是相对于阴影投影的第一个优势。此外,这种方法还允许您考虑到光源的距离(结果是获得较弱的阴影)并将阴影投射到任何表面(包括角色本身)上。

但是此技术具有严重的缺点,最明显的缺点是阴影的边缘完全取决于用于创建投射阴影的对象的图元的数量。此外,多次传递与对本地内存的许多读/写操作相关联,这就是为什么使用模版阴影在性能方面非常昂贵的原因。

此外,由于所有图形API在其上分配的位数很少(通常只有8位),因此可以使用模板缓冲区检查阴影卷的数量。但是,由于模版阴影的计算成本,通常不会出现此问题。

还有另一个问题-阴影本身远非现实。为什么?因为所有光源-灯,明火,灯笼和太阳-都不是空间中的单个点,即它们会发出某些区域的光。即使在下面显示的最简单的情况下,真实阴影也很少具有清晰定义的边缘。


阴影的最暗区域称为全阴影(本影);阴影称为阴影。半影总是较亮的阴影,并且两者之间的边界通常很模糊(因为通常有很多光源)。使用模具缓冲区和体积很难对此建模,因为创建的阴影以错误的形式存储,因此可以对其进行处理。阴影贴图可以解救

基本程序兰斯·威廉姆斯Lance Williams)在1978年开发这很简单:

  • 对于每个光源,我们从该光源的角度渲染场景,从而创建深度的特殊纹理(即没有颜色,照明,纹理等)。该缓冲区的分辨率不必等于完成的帧的大小,但是越高越好。
  • , ( x,y z) , .
  • : , .

显然,这是另一种多遍过程,但是可以使用像素着色器执行最后一步,以便将深度检查和后续照明计算合并为一遍。而且,由于创建阴影的整个过程并不取决于所使用的图元数量,因此它比使用模板缓冲区和阴影量要快得多。

不幸的是,上述基本技术会生成各种视觉伪像(例如,透视混叠“暗疮”,“平移”),其中大部分与深度纹理的分辨率和位大小有关。所有GPU和图形API都具有类似于纹理的局限性,因此已创建了一系列附加技术来解决这些问题。

使用纹理获取深度信息的好处之一是,GPU可以非常快速且以许多不同方式对其进行采样和过滤。在2005年,Nvidia展示了一种纹理采样方法,该方法可以解决由标准阴影引起的某些视觉问题。另外,他为阴影的边缘提供了一定程度的平滑度。这项技术称为百分比紧密过滤


大约在同一时间,Futuremark演示了3DMark06级联阴影贴图(CSM)的使用。这是一种为每个光源创建具有不同分辨率的多个深度纹理的技术。高分辨率纹理在源附近使用,而较低的纹理则在距源一定距离处使用。结果是场景中的阴影过渡更加平滑而没有失真。 Donnelly和Loritzen在2006年的方差阴影映射(VSM)程序中改进了此技术,在2010年的Intel在其样本分发算法(SDSM)中对其进行了改进。




在古墓丽影

暗影中使用SDSM为了改善画面,游戏开发人员经常使用一整套阴影技术,但主要方法仍然是阴影贴图。但是,它只能应用于少量的有源光源,因为如果您尝试为每个反射或发光的表面建模,则帧速率将灾难性地下降。

幸运的是,有一种适用于任何对象的便捷技术。它给人的印象是到达物体的照明亮度降低(由于他或其他物体稍微挡住了光线)。此功能称为环境光遮挡。她有很多版本。其中一些是由硬件制造商专门设计的,例如,AMD创建了HDAO(高清环境光遮挡),而Nvidia具有HBAO +基于水平的环境光遮挡)。

无论使用哪种版本,在场景完全渲染后都将应用它,因此将其归类为后处理效果。事实上,对于每个像素被计算多少,我们看到它的场景(更多有关这这里这里)通过比较在深度缓冲(同样,存储为一个纹理)对应的点周围的像素的像素深度的价值。

对深度缓冲区进行采样,然后计算最终像素的颜色,对于确保环境光遮挡的质量起着重要的作用。与阴影一样,所有版本的环境光遮蔽功能都需要程序员根据情况谨慎配置和调整代码,以使其正常运行。


没有AO(左)和HBAO +(右)的古墓丽影的阴影。

但是,如果实施得当,这种视觉效果会给人留下深刻的印象。在上图中,请注意人的手,菠萝和香蕉以及周围的草木和植被。HBAO +对像素颜色所做的更改很小,但是现在所有对象看起来都可以更好地内置到环境中(在左侧,似乎有人悬在地面上)。

如果选择本文讨论的任何最后一款游戏,那么在处理光照和阴影时将在其中使用的渲染技术的列表将为文章本身的长度。尽管并不是每个新的3D游戏都拥有所有这些技术,但是诸如Unreal的通用游戏引擎允许您有选择地启用它们,而工具包(例如Nvidia公司)提供了可以插入游戏的代码。这证明它们不是高度专业化的超现代方法-曾经是最好的程序员的财产,现在它们对任何人都可用。

如果不提及光线追踪,就无法完成有关照明和阴影的这篇文章。我们已经在本系列文章中讨论了此过程,但是当前的技术开发水平需要忍受较低的帧频和大量的现金支出。

但是,该技术受到下一代游戏机Microsoft和Sony的支持,这意味着在未来几年中,它的使用将成为全世界寻求改善游戏视觉质量的开发人员的另一种标准工​​具。看看Remedy在她最新的Control游戏中成功实现了什么


我们已经从假纹理阴影和简单的环境照明中走了很长一段路!

那不是全部


在本文中,我们尝试讨论了3D游戏中使用的基本数学计算和技术,这些方法和技巧使它们尽可能逼真。我们还研究了光与物体和材料相互作用建模所基于的技术。但这一切只是冰山一角。

例如,我们跳过了诸如节能照明,镜头光晕,光晕,高动态渲染,辐射转移,色调校正,雾,色差,光子贴图,焦散,光能传递等主题,此列表不胜枚举。简短的研究将需要增加3-4篇文章。

All Articles