51testing 发表于 2007-10-31 16:28:59

游戏引擎剖析

第1部分: 游戏引擎介绍, 渲染和构造3D世界


介绍
  自Doom游戏时代以来我们已经走了很远。 DOOM不只是一款伟大的游戏,它同时也开创了一种新的游戏编程模式: 游戏 "引擎"。 这种模块化,可伸缩和扩展的设计观念可以让游戏玩家和程序设计者深入到游戏核心,用新的模型,场景和声音创造新的游戏, 或向已有的游戏素材中添加新的东西。大量的新游戏根据已经存在的游戏引擎开发出来,而大多数都以ID公司的Quake引擎为基础, 这些游戏包括Counter Strike, Team Fortress, Tac Ops, Strike Force, 以及Quake Soccer。Tac Ops 和Strike Force 都使用了Unreal Tournament 引擎。事实上, "游戏引擎" 已经成为游戏玩家之间交流的标准用语,但是究竟引擎止于何处,而游戏又从哪里开始呢?像素的渲染,声音的播放,怪物的思考以及游戏事件的触发,游戏中所有这一切的幕后又是什么呢? 如果你曾经思考过这些问题, 而且想要知道更多驱动游戏进行的东西,那么这篇文章正好可以告诉你这些。 本文分多个部分深入剖析了游戏引擎的内核, 特别是Quake引擎,因为我最近工作的公司Raven Software已经在Quake引擎的基础上开发出了多款游戏,其中包括著名的Soldier of Fortune 。


开始
  让我们首先来看看一个游戏引擎和游戏本身之间的主要区别。 许多人们会混淆游戏引擎和整个游戏 。这有点像把一个汽车发动机和整个汽车混淆起来一样 。 你能够从汽车里面取出发动机, 建造另外一个外壳,再使用发动机一次。 游戏也像那。 游戏引擎被定义为所有的非游戏特有的技术。 游戏部份是被称为 '资产' 的所有内容 (模型,动画,声音,人工智能和物理学)和为了使游戏运行或者控制如何运行而特别需要的程序代码, 比如说AI--人工智能。

  对于曾经看过 Quake 游戏结构的人来说, 游戏引擎就是 Quake。exe ,而游戏部分则是 QAGame。dll 和 CGame。dll 。 如果你不知道这是什么意思, 也没有什么关系;在有人向我解释它以前, 我也不知道是什么意思。 但是你将会完全明白它的意思。 这篇游戏引擎指导分为十一个部份。 是的, 从数量上来说,总共是十一个部份! 每个部分大概3000字左右。现在就从第一部分开始我们的探索吧,深入我们所玩游戏的内核,在这里我们将了解一些基本的东西, 为后面的章节作铺垫。。。


渲染器
  让我们从渲染器来开始游戏引擎设计的探讨吧, 我们将从游戏开发者(本文作者的背景)的角度来探讨这些问题。事实上,在本文的各个段落,我们将常常从游戏开发者的角度探讨, 也让您像我们一样思考问题!

  什么是渲染器,为什么它又这么重要呢?好吧,如果没有它,你将什么也看不到。它让游戏场景可视化,让玩家/观众可以看见场景,从而让玩家能够根据屏幕上所看到的东西作出适当的决断。 尽管我们下面的探讨可能让新手感到有些恐惧,先别去理会它。 渲染器做些什么?为什么它是必须的?我们将会解释这些重要问题。

  当构造一个游戏引擎的时候, 你通常想做的第一件事情就是建造渲染器。 因为如果看不见任何东西 – 那么你又如何知道你的程序代码在工作呢? 超过 50% 的 CPU 处理时间花费在渲染器上面; 通常也是在这个部分,游戏开发者将会受到最苛刻的评判。 如果我们在这个部分表现很差,事情将会变得非常糟糕, 我们的程序技术,我们的游戏和我们的公司将在 10 天之内变成业界的笑话。 它也是我们最依赖于外部厂商和力量的地方,在这里他们将处理最大限度的潜在操作目标。 如此说来, 建造一个渲染器确实不象听起来那么吸引人(事实如此), 但如果没有一个好的渲染器, 游戏或许永远不会跻身于排行榜前10 名。

  如今,在屏幕上生成像素,涉及到 3D 加速卡, API ,三维空间数学, 对 3D 硬件如何工作的理解等等。对於主机(游戏机)游戏来说,也需要相同类型的知识,但是至少对于主机, 你不必去尝试击中一个移动中的目标。 因为一台主机的硬件配置是固定的 "时间快照", 和PC(个人计算机)不同, 在一台主机的生命期中,它的硬件配置不会改变。

  在一般意义上,渲染器的工作就是要创造出游戏的视觉闪光点,实际上达到这个目标需要大量的技巧。3D图形本质上是用最少的努力创造出最大效果的一门艺术, 因为额外的 3D 处理在处理器时间和和內存带宽方面都是极为昂贵的。 它也是一种预算, 要弄清楚你想在什么地方花费处理器时间,而你宁愿在什么地方节省一些从而达到最好的整体效果。 接下来我们将会介绍一些这方面的工具,以及怎样更好的用它们让游戏引擎工作。


建造3D世界
  最近,当我和一位从事计算机图形方面工作长达数年之久的人会谈时,她向我吐露道, 当她第一次看到实时操纵计算机 3D 图象时, 她不知道这是怎么实现的, 也不知道计算机如何能够存储 3D 图象。 今天这对于在大街上的普通人来说或许是真实的,即使他们时常玩 PC 游戏, 游戏机游戏, 或街机游戏。

  下面我们将从游戏设计者的角度讨论创造 3D 世界的一些细节,你也应该看一看 Dave Salvator 所写的“3D 管线导论“,以便对3D 图象生成的主要过程有一个整体的了解。

  3D 物体(对象)被储存成 3D 世界中的一系列点(被称为顶点), 彼此之间有相互关系,所以计算机知道如何在世界中的这些点之间画线或者是填充表面。 一个立方体由8个点组成,每个角一个点。立方体有6个表面, 分别代表它的每一个面。 这就是 3D 对象储存的基础。 对于一些比较复杂的 3D 物体, 比如说一个 Quake 的关卡,将有数以千计(有时数以十万计)的顶点, 和数以千计的多边形表面。

  参见上图的线框表示(注:原文在这里有一幅图)。 本质上与上面的立方体例子类似, 它仅仅是由许许多多的小多边形组成的一些复杂场景。模型和世界如何储存是渲染器的一部份功能, 而不属于应用程序/游戏部份。 游戏逻辑不需要知道对象在內存中如何表示, 也不需要知道渲染器将怎样把他们显示出来。 游戏只是需要知道渲染器将使用正确的视野去表示对象, 并将在正确的动画幀中把正确的模型显示出来。

  在一个好的引擎中,渲染器应该是可以完全被一个新的渲染器替换掉, 并且不需要去改动游戏的一行代码。许多跨平台引擎, 而且许多自行开发的游戏机引擎就是这样的,如 Unreal 引擎, --举例来说, 这个游戏 GameCube 版本的渲染器就可以被你任意的替换掉。

  让我们再看看内部的表示方法—除了使用坐标系统,还有其他方法可以在计算机內存里表示空间的点。在数学上,你可以使用一个方程式来描述直线或曲线, 并得到多边形, 而几乎所有的 3D 显示卡都使用多边形来作为它们的最终渲染图元。 一个图元就是你在任何显示卡上面所能使用的最低级的绘制(渲染)单位,几乎所有的硬件都是使用三个顶点的多边形(三角形)。 新一代的 nVidia 和 ATI 显卡可以允许你以数学方式渲染(被称为高次表面), 但因为这不是所有图形卡的标准, 你还不能靠它作为渲染策略。

  从计算的角度来看,这通常有些昂贵,但它时常是新的实验技术的基础,例如,地表的渲染,或者对物件锐利的边缘进行柔化。 我们将会在下面的曲面片小节中更进一步介绍这些高次表面。
剔除概观
  问题来了。 我现在有一个由几十万个顶点/多边形描述的世界。 我以第一人称视角位于我们这个 3D 世界的一边。 在视野中可以看见世界的一些多边形, 而另外一些则不可见, 因为一些物体, 比如一面看得见的墙壁, 遮挡住了它们。 即使是最好的游戏编码人员, 在目前的 3D 显卡上, 在一个视野中也不能处理 300,000个三角形且仍然维持 60fps (一个主要目标)。 显卡不能处理它, 因此我们必须写一些代码,在把它们交给显卡处理之前除去那些看不见的多边形。 这个过程被称为剔除。

  有许多不同的剔除方法。 在深入了解这些之前,让我们探讨一下为什么图形显示卡不能处理超高数量的多边形。 我是说,最新的图形卡每秒钟不能处理几百万个多边形吗?它不应该能够处理吗? 首先,你必须理解市场销售宣称的多边形生成率和真实世界的多边形生成率。 行销上宣称的多边形生成率是图形显示卡理论上能够达到的多边形生成率。

  如果全部多边形都在屏幕上, 相同的纹理,相同的尺寸大小, 正在往显示卡上传送多边形的应用程序除了传送多边形以外什么也不做, 这时显卡能处理多少多边形数量, 就是图形芯片厂商呈现给你的数字。

  然而,在真实的游戏情形中,应用程序时常在后台做着许多其他的事情 -- 多边形的 3D 变换, 光照计算, 拷贝较多的纹理到显卡內存, 等等。 不仅纹理要送到显示卡, 而且还有每个多边形的细节。一些比较新的显卡允许你实际上在显卡內存本身里面储存模型/世界几何细节, 但这可能是昂贵的,将会耗光纹理正常可以使用的空间,所以你最好能确定每一幀都在使用这些模型的顶点, 否则你只是在浪费显示卡上的存储空间。 我们就说到这里了。 重要的是,在实际使用显卡时,并不必然就能达到你在显卡包装盒上所看到的那些指标,如果你有一个比较慢速的CPU , 或没有足够的內存时,这种差异就尤为真实。


基本的剔除方法
  最简单的剔除方式就是把世界分成区域, 每个区域有一个其他可见区域的列表。 那样, 你只需要显示针对任何给定点的可见部分。 如何生成可见视野区域的列表是技巧所在。 再者, 有许多方法可以用来生成可见区域列表, 如 BSP 树, 窥孔等等。

  可以肯定,当谈论 DOOM 或 QUAKE 时,你已经听到过使用 BSP 这个术语了。 它表示二叉空间分割。

  BSP 是一种将世界分成小区域的的方法,通过组织世界的多边形,容易确定哪些区域是可见的而哪些是不可见的 – 从而方便了那些不想做太多绘制工作的基于软件的渲染器。它同时也以一种非常有效的方式让你知道你位于世界中的什么地方。

  在基于窥孔的引擎 ( 最早由 3D Realms 已经取消的 Prey 项目引入游戏世界 )里,每个区域 ( 或房间) 都建造有自己的模型, 通过每个区域的门 ( 或窥孔 )能够看见另外的区段。 渲染器把每个区域作为独立的场景单独绘制。 这就是它的大致原理。 足以说这是任何一个渲染器的必需部份,而且非常重要。

  尽管一些这样的技术归类在 "遮挡剔除"之下,但是他们全部都有同样的目的: 尽早消除不必要的工作。 对於一个FPS游戏(第一人称射击游戏) 来说,视野中时常有许多三角形,而且游戏玩家承担视野的控制,丢弃或者剔除不可见的三角形就是绝对必要的了。 对空间模拟来说也是这样的, 你可以看见很远很远的地方 – 剔除超过视觉范围外面的东西就非常重要。 对于视野受到限制的游戏来说 – 比如 RTS (即时战略类游戏)--通常比较容易实现。 通常渲染器的这个部份还是由软件来完成, 而不是由显卡完成, 由显卡来做这部分工作只是一个时间问题。

51testing 发表于 2007-10-31 16:29:51

基本的图形管线流程
  一个简单的例子,从游戏到多边形绘制的图形管线过程大致是这样:
    · 游戏决定在游戏中有哪些对象, 它们的模型, 使用的纹理, 他们可能在什么动画幀,以及它们在游戏世界里的位置。 游戏也决定照相机的位置和方向。

    · 游戏把这些信息传递给渲染器。以模型为例 ,渲染器首先要查看模型的大小 ,照相机的位置, 然後决定模型在屏幕上是否全部可见, 或者在观察者 (照相机视野) 的左边,在观察者的后面,或距离很远而不可见。它甚至会使用一些世界测定方式来计算出模型是否是可见的。 (参见下面这条)

    · 世界可视化系统决定照相机在世界中的位置,并根据照相机视野决定世界的哪些区域 / 多边形是可见的。有许多方法可以完成这个任务, 包括把世界分割成许多区域的暴力方法,每个区域直接为"我能从区域 D 看见区域 AB & C", 到更精致的 BSP(二叉空间分割)世界。 所有通过这些剔除测试的多边形被传递给多边形渲染器进行绘制。

    · 对於每一个被传递给渲染器的多边形, 渲染器依照局部数学 ( 也就是模型动画) 和世界数学(相对于照相机的位置?)对多边形进行变换,并检查决定多边形是不是背对相机 (也就是远离照相机)。背面的多边形被丢弃。 非背面的多边形由渲染器根据发现的附近灯光照亮。然后渲染器要看多边形使用的纹理,并且确定 API/ 图形卡正在使用那种纹理作为它的渲染基础。 在这里,多边形被送到渲染 API,然后再送给显卡。

  很明显这有些过分简单化了,但你大概理解了。 下面的图表摘自Dave Salvator's 3D 管线一文,可以给你多一些具体细节:

  3D 管线
  - 高层的概观
   1. 应用程序/ 场景
  ·场景/ 几何数据库遍历
  ·对象的运动,观察相机的运动和瞄准
  ·对象模型的动画运动
  ·3D 世界内容的描述
  ·对象的可见性检查,包括可能的遮挡剔除
  ·细节层次的选择 (LOD)

  2. 几何
  ·变换 (旋转,平移, 缩放)
  ·从模型空间到世界空间的变换 (Direct3D)
  ·从世界空间到观察空间变换
  ·观察投影
  ·细节接受/ 拒绝 剔除
  ·背面剔除 (也可以在后面的屏幕空间中做)
  光照
  ·透视分割 - 变换到裁剪空间
  ·裁剪
  ·变换到屏幕空间

  3. 三角形生成
  ·背面剔除 ( 或者在光照计算之前的观察空间中完成)
  ·斜率/ 角度计算
  ·扫瞄线变换

  4. 渲染 / 光栅化
  ·着色
  ·纹理
  ·雾
  ·Alpha 透明测试
  ·深度缓冲
  ·抗锯齿 (可选择的)
  ·显示


通常你会把所有的多边形放到一些列表内, 然後根据纹理对这个列表排序(这样你只需要对显卡传送一次纹理, 而不是每个多边形都传送一次), 等等。在过去,会把多边形按照它们到相机的距离进行排序,首先绘制那些距离相机最远的多边形, 但现在由于 Z 缓冲的出现,这种方法就不是那么重要了。 当然那些透明的多边形要除外,它们要在所有的非半透明多边形绘制之后才能够绘制 ,这样一来,所有在它们后面的多边形就能正确地在场景中显现出来。 当然,象那样,实际上你必须得从后到前地绘制那些多边形。 但时常在任何给定的 FPS 游戏场景中, 通常没有太多透明的多边形。 它可能看起来像有,但实际上与那些不透明的多边形相比,其比率是相当低的。

  一旦应用程序将场景传递到 API, API 就能利用硬件加速的变换和光照处理 (T&L), 这在如今的 3D 显卡中是很平常的事情。 这里不讨论涉及到的矩阵数学(参见Dave的 3D 管线导论),几何变换允许 3D 显卡按照你的尝试,根据相机在任何时间的位置和方向,在世界的正确角度和位置绘制多边形。

  对于每个点或顶点都有大量的计算, 包括裁剪运算,决定任何给定的多边形实际上是否可见,在屏幕上完全不可见或部分可见。 光照运算,计算纹理色彩明亮程度,这取决于世界的灯光从什么角度如何投射到顶点上。 过去,处理器处理这些计算,但现在,当代图形硬件就能为你做这些事情, 这意谓着你的处理器可以去做其他的事情了。很明显这是件好事情 (tm) ,由于不能指望市面上所有的 3D 显卡板上都有T & L, 所以无论如何你自己将必须写所有的这些例程 (再一次从游戏开发者角度来说)。 你将在本文各处的不同段落看到 "好事情 ( tm)" 这个词汇。 我认为,这些特征为使游戏看起来更好作出了非常有用的贡献。 毫不令人吃惊,你也将会看见它的对立面;你猜到了,那就是“坏事情 (tm)”。 我正在尝试争取这些词汇的版权, 你要使用他们就得支付一笔小小的费用哟。


曲面片(高次表面)
  除了三角形,曲面片的使用现在正变得更普遍。 因为他们能用数学表达式来描述几何 ( 通常涉及某种曲线的几何形体) ,而不仅仅只是列出大量的多边形以及在游戏世界中的位置, 所以曲面片 ( 高次表面的另一个名称) 非常好。 这样,你实际上就能够动态地根据方程式来建立( 和变形 )多边形网格, 并决定你实际想要从曲面片上看到的多边形数量。 因此,举例来说,你可以描述一个管道, 然后在世界中就可以有这种管道的许多样例。 在一些房间中, 你已经显示了 10,000个多边形,你可以说,"因为我们已经显示了大量的多边形,而且任何更多的多边形将会使幀速率下降, 所以这个管道应该只有 100 个多边形"。 但在另外一个房间中, 视野中只有 5,000个可见的多边形,你可以说,"因为我们还没有达到预算可以显示的多边形数量 , 所以,现在这个管道能有 500 个多边形"。 非常美妙的东西 --但你必须首先知道所有这些,并建立网格,这不是无足轻重的。 通过 AGP 传送同一个对象的曲面方程确实要比传送其大量顶点节省成本。 SOF2 就使用了这个方法的一种变体来建立它的地表系统。

  事实上现在的 ATI 显卡具有 TruForm, 它能带一个以三角形为基础的模型,并将该模型转换为基于高次表面的模型,使其平滑 — 接着再用十倍三角形数量把模型转换回基于大量三角形的模型 (被称为retesselation)。然后模型送往管线做进一步的处理。 实际上 ATI 仅仅在 T & L 引擎之前增加了一个阶段来处理这个过程。这里的缺点是,要控制哪些模型需要被平滑处理而哪些又不需要。你常常想要一些边缘比较尖锐, 比如鼻子,但它却被不恰当的平滑过了。 这仍然是一种很好的技术,而且我能预见它在将来会被更多的应用。

  这就是第一部份 -- 我们将会在第二部分继续介绍光照和纹理,下面的章节会更加深入。
世界的灯光
  在变换过程中, 通常是在称为观察空间的坐标空间中, 我们遇到了最重要的运算之一: 光照计算。 它是一种这样的事情, 当它工作时,你不关注它,但当它不工作时, 你就非常关注它了。有很多不同的光照方法,从简单的计算多边形对于灯光的朝向,并根据灯光到多边形的方向和距离加上灯光颜色的百分比值,一直到产生边缘平滑的灯光贴图叠加基本纹理。而且一些 API 实际上提供预先建造的光照方法。举例来说,OpenGL 提供了每多边形,每顶点,和每像素的光照计算。

  在顶点光照中,你要决定一个顶点被多少个多边形共享,并计算出共享该顶点的所有多边形法向量的均值(称为法向量),并将该法向量赋顶点。一个给定多边形的每个顶点会有不同的法向量,所以你需要渐变或插值多边形顶点的光照颜色以便得到平滑的光照效果。 你没有必要用这种光照方式查看每个单独的多边形。 这种方式的优点是时常可以使用硬件转换与光照(T & L)来帮助快速完成。 不足之处是它不能产生阴影。 举例来说,即使灯光是在模型的右侧,左手臂应该在被身体投影的阴影中,而实际上模型的双臂却以同样的方式被照明了。

  这些简单的方法使用着色来达到它们的目标。 当用平面光照绘制一个多边形时, 你让渲染(绘制)引擎把整个多边形都着上一种指定的颜色。这叫做平面着色光照。 (该方法中,多边形均对应一个光强度,表面上所有点都用相同的强度值显示,渲染绘制时得到一种平面效果,多边形的边缘不能精确的显示出来) 。

  对于顶点着色 ( Gouraud 着色) ,你让渲染引擎给每个顶点赋予特定的颜色。 在绘制多边形上各点投影所对应的像素时,根据它们与各顶点的距离,对这些顶点的颜色进行插值计算。 (实际上Quake III 模型使用的就是这种方法, 效果好的令人惊奇)。

  还有就是 Phong 着色。如同 Gouraud 着色,通过纹理工作,但不对每个顶点颜色进行插值决定像素颜色值, 它对每个顶点的法向量进行插值,会为每个顶点投影的像素做相同的工作。对于 Gouraud 着色,你需要知道哪些光投射在每个顶点上。对于 Phong 着色,你对每个像素也要知道这么多。

  一点也不令人惊讶, Phong 着色可以得到更加平滑的效果,因为每个像素都需要进行光照计算,其绘制非常耗费时间。平面光照处理方法很快速, 但比较粗糙。Phong 着色比 Gouraud 着色计算更昂贵,但效果最好,可以达到镜面高光效果("高亮")。 这些都需要你在游戏开发中折衷权衡。


不同的灯光
  接着是生成照明映射,你用第二个纹理映射(照明映射)与已有的纹理混合来产生照明效果。这样工作得很好, 但这本质上是在渲染之前预先生成的一种罐装效果。如果你使用动态照明 (即,灯光移动, 或者没有程序的干预而打开和关闭),你得必须在每一幀重新生成照明映射,按照动态灯光的运动方式修改这些照明映射。灯光映射能够快速的渲染,但对存储这些灯光纹理所需的内存消耗非常昂贵。你可以使用一些压缩技巧使它们占用较少的的内存空间,或减少其尺寸大小, 甚至使它们是单色的 (这样做就不会有彩色灯光了),等等。 如果你确实在场景中有多个动态灯光, 重新生成照明映射将以昂贵的CPU周期而告终。

  许多游戏通常使用某种混合照明方式。 以Quake III为例,场景使用照明映射, 动画模型使用顶点照明。 预先处理的灯光不会对动画模型产生正确的效果 -- 整个多边形模型得到灯光的全部光照值 -- 而动态照明将被用来产生正确的效果。 使用混合照明方式是多数的人们没有注意到的一个折衷,它通常让效果看起来"正确"。 这就是游戏的全部 – 做一切必要的工作让效果看起来"正确",但不必真的是正确的。

  当然,所有这些在新的Doom引擎里面都不复存在了,但要看到所有的效果,至少需要 1GHZ CPU 和 GeForce 2 显卡。是进步了,但一切都是有代价的。

  一旦场景经过转换和照明, 我们就进行裁剪运算。 不进入血淋淋的细节而,剪断运算决定哪些三角形完全在场景 (被称为观察平截头体) 之内或部份地在场景之内。完全在场景之内的三角形被称为细节接受,它们被处理。对于只是部分在场景之内的三角形, 位于平截头体外面的部分将被裁剪掉,余下位于平截头体内部的多边形部分将需要重新闭合,以便其完全位于可见场景之内。 (更多的细节请参考我们的 3D 流水线指导一文)。

  场景经过裁剪以后,流水线中的下一个阶段就是三角形生成阶段(也叫做扫描 线转换),场景被映射到2D 屏幕坐标。到这里,就是渲染(绘制)运算了。


纹理与MIP映射
  纹理在使3D场景看起来真实方面异常重要,它们是你应用到场景区域或对象的一些分解成多边形的小图片。多重纹理耗费大量的内存,有不同的技术来帮助管理它们的尺寸大小。纹理压缩是在保持图片信息的情况下,让纹理数据更小的一种方法。纹理压缩占用较少的游戏CD空间,更重要的是,占用较少内存和3D 显卡存储空间。另外,在你第一次要求显卡显示纹理的时候,压缩的(较小的) 版本经过 AGP 接口从 PC 主存送到3D 显卡, 会更快一些。纹理压缩是件好事情。 在下面我们将会更多的讨论纹理压缩。


51testing 发表于 2007-10-31 16:30:55

MIP 映射(多纹理映射)
  游戏引擎用来减少纹理内存和带宽需求的另外一个技术就是 MIP 映射。 MIP 映射技术通过预先处理纹理,产生它的多个拷贝纹理,每个相继的拷贝是上一个拷贝的一半大小。为什么要这样做?要回答这个问题,你需要了解 3D 显卡是如何显示纹理的。最坏情况,你选择一个纹理,贴到一个多边形上,然后输出到屏幕。我们说这是一对一的关系,最初纹理映射图的一个纹素 (纹理元素) 对应到纹理映射对象多边形的一个像素。如果你显示的多边形被缩小一半,纹理的纹素就每间隔一个被显示。这样通常没有什么问题 -- 但在某些情况下会导致一些视觉上的怪异现象。让我们看看砖块墙壁。 假设最初的纹理是一面砖墙,有许多砖块,砖块之间的泥浆宽度只有一个像素。如果你把多边形缩小一半, 纹素只是每间隔一个被应用,这时候,所有的泥浆会突然消失,因为它们被缩掉了。你只会看到一些奇怪的图像。

  使用 MIP 映射,你可以在显示卡应用纹理之前,自己缩放图像,因为可以预先处理纹理,你做得更好一些,让泥浆不被缩掉。当 3D 显卡用纹理绘制多边形时,它检测到缩放因子,说,"你知道,我要使用小一些的纹理,而不是缩小最大的纹理,这样看起来会更好一些。" 在这里, MIP 映射为了一切,一切也为了 MIP 映射。


多重纹理与凹凸映射
  单一纹理映射给整个3D 真实感图形带来很大的不同, 但使用多重纹理甚至可以达到一些更加令人难忘的效果。过去这一直需要多遍渲染(绘制),严重影响了像素填充率。 但许多具有多流水线的3D 加速卡,如ATI's Radeon 和 nVidia's GeForce 2及更高级的显卡,多重纹理可以在一遍渲染(绘制)过程中完成。 产生多重纹理效果时, 你先用一个纹理绘制多边形,然后再用另外一个纹理透明地绘制在多边形上面。这让你可以使纹理看上去在移动,或脉动, 甚至产生阴影效果 (我们在照明一节中描述过)。绘制第一个纹理映射,然后在上面绘制带透明的全黑纹理,引起一种是所有的织法黑色的但是有一个透明分层堆积过它的顶端 , 这就是 -- 即时阴影。 该技术被称为照明映射 ( 有时也称为 暗映射),直至新的Doom ,一直是Id引擎里关卡照明的传统方法。

  凹凸贴图是最近涌现出来的一种古老技术。几年以前 Matrox 第一个在流行的 3D 游戏中发起使用各种不同形式的凹凸贴图。就是生成纹理来表现灯光在表面的投射,表现表面的凹凸或表面的裂缝。 凹凸贴图并不随着灯光一起移动 -- 它被设计用来表现一个表面上的细小瑕疵,而不是大的凹凸。 比如说,在飞行模拟器中,你可以使用凹凸贴图来产生像是随机的地表细节,而不是重复地使用相同的纹理,看上去一点趣味也没有。

  凹凸贴图产生相当明显的表面细节,尽管是很高明的戏法,但严格意义上讲,凹凸贴图并不随着你的观察角度而变化。比较新的 ATI 和 nVidia 显卡片能执行每像素运算,这种缺省观察角度的不足就真的不再是有力而快速的法则了。 无论是哪一种方法, 到目前为止,没有游戏开发者太多的使用; 更多的游戏能够且应该使用凹凸贴图。


高速缓存抖动 = 糟糕的事物
  纹理高速缓存的管理游戏引擎的速度至关重要。 和任何高速缓存一样,缓存命中很好,而不命中将很糟糕。如果遇到纹理在图形显示卡内存被频繁地换入换出的情况,这就是纹理高速缓存抖动。发生这种情况时,通常API将会废弃每个纹理,结果是所有的纹理在下一幀将被重新加载,这非常耗时和浪费。对游戏玩家来说,当API重新加载纹理高速缓存时,会导致幀速率迟钝。

  在纹理高速缓存管理中,有各种不同的技术将纹理高速缓存抖动减到最少 – 这是确保任何 3D 游戏引擎速度的一个决定性因素。 纹理管理是件好事情 – 这意味着只要求显卡使用纹理一次,而不是重复使用。这听起来有点自相矛盾,但效果是它意谓着对显卡说,"看, 所有这些多边形全部使用这一个纹理,我们能够仅仅加载这个纹理一次而不是许多次吗?" 这阻止API ( 或图形驱动软件) 上传多次向显卡加载纹理。象OpenGL这样的API实际上通常处理纹理高速缓存管理,意谓着,根据一些规则,比如纹理存取的频率,API决定哪些纹理储存在显卡上,哪些纹理存储在主存。 真正的问题来了:a) 你时常无法知道API正在使用的准确规则。 b)你时常要求在一幀中绘制更多的纹理,以致超出了显卡内存空间所能容纳的纹理。

  另外一种纹理高速缓存管理技术是我们早先讨论的纹理压缩。很象声音波形文件被压缩成 MP3 文件,尽管无法达到那样的压缩比率,但纹理可以被压缩。 从声音波形文件到MP3的压缩可以达到 11:1的压缩比率,而绝大多数硬件支持的纹理压缩运算法则只有 4:1 的压缩比率,尽管如此,这样能产生很大的差别。 除此之外,在渲染(绘制)过程中,只有在需要时,硬件才动态地对纹理进行解压缩。这一点非常棒,我们仅仅擦除即将可能用到的表面。

  如上所述,另外一种技术确保渲染器要求显卡对每个纹理只绘制一次。确定你想要渲染(绘制)的使用相同纹理的所有多边形同时送到显卡,而不是一个模型在这里,另一个模型在那里,然后又回到最初的纹理论。仅仅绘制一次,你也就通过AGP接口传送一次。Quake III 在其阴影系统就是这么做的。处理多边形时,把它们加入到一个内部的阴影列表,一旦所有的多边形处理完毕,渲染器遍历纹理列表,就将纹理及所有使用这些纹理的多边形同时传送出去。

  上述过程在使用显卡的硬件 T & L(如果支持的话)时,并不怎么有效。你面临的结局是,满屏幕都是使用相同纹理的大量的多边形小群组,所有多边形都使用不同的变换矩阵。这意谓着更多的时间花在建立显卡的硬件 T & L 引擎 ,更多的时间被浪费了。 无论如何,因为他们有助于对整个模型使用统一的纹理,所以它对实际屏幕上的模型可以有效地工作。但是因为许多多边形倾向使用相同的墙壁纹理,所以对于世界场景的渲染,它常常就是地狱。通常它没有这么严重,因为大体而言,世界的纹理不会有那么大,这样一来API的纹理缓存系统将会替你处理这些,并把纹理保留在显卡以备再次使用。

  在游戏机上,通常没有纹理高速缓存系统(除非你写一个)。在 PS2 上面,你最好是远离"一次纹理" 的方法。在 Xbox 上面, 这是不重要的,因为它本身没有图形内存(它是 UMA 体系结构),且所有的纹理无论如何始终保留在主存之中。

  事实上,在今天的现代PC FPS 游戏中,试图通过AGP接口传送大量纹理是第二个最通常的瓶颈。最大的瓶颈是实际几何处理,它要使东西出现在它应该出现的地方。在如今的3D FPS 游戏中,最耗费时间的工作,显然是那些计算模型中每个顶点正确的世界位置的数学运算。如果你不把场景的纹理保持在预算之内,仅居其次的就是通过AGP接口传送大量的纹理了。然而,你确实有能力影响这些。 通过降低顶层的 MIP 级别(还记得系统在哪里不断地为你细分纹理吗?), 你就能够把系统正在尝试送到显卡的纹理大小减少一半。你的视觉质量会有所下降-- 尤其是在引人注目的电影片断中--但是你的幀速率上升了。这种方式对网络游戏尤其有帮助。实际上,Soldier of Fortune II和Jedi Knight II: Outcast这两款游戏在设计时针对的显卡还不是市场上的大众主流显卡。为了以最大大小观看他们的纹理,你的3D 显卡至少需要有128MB的内存。这两种产品在思想上都是给未来设计的。

  上面就是第 2 部份。在下面章节中,我们将介绍许多主题,包括内存管理,雾效果,深度测试, 抗锯齿,顶点着色,API等
第3部份: 内存使用,特效和API


关于内存使用的思考
  让我们想一想,在今天实际上是如何使用3D 显卡内存的以及在将来又会如何使用。 如今绝大多数3D显卡处理32位像素颜色,8位红色, 8位蓝色,8 位绿色,和 8 位透明度。这些组合的红,蓝和绿256个色度,可以组成 16。7 百万种颜色-- 那是你我可以在一个监视器上看见的所有颜色。

  那么,游戏设计大师John Carmack 为什么要求 64 位颜色分辨率呢? 如果我们看不出区别,又有什么意义呢? 意义是: 比如说, 有十几个灯光照射模型上的点,颜色颜色各不相同。 我们取模型的最初颜色,然后计算一个灯光的照射,模型颜色值将改变。 然后我们计算另外的一个灯光, 模型颜色值进一步改变。 这里的问题是,因为颜色值只有8位,在计算了4个灯光之后,8位的颜色值将不足以给我们最后的颜色较好的分辨率和表现。分辨率的不足是由量化误差导致的,本质原因是由于位数不足引起的舍入误差。

  你能很快地用尽位数,而且同样地,所有的颜色被清掉。每颜色16 或 32 位,你有一个更高分辨率,因此你能够反复着色以适当地表现最后的颜色。这样的颜色深度很快就能消耗大量的存储空间。我们也应提到整个显卡内存与纹理内存。这里所要说的是,每个3D 显卡实际只有有限的内存,而这些内存要存储前端和后端缓冲区,Z 缓冲区,还有所有的令人惊奇的纹理。最初的 Voodoo1 显卡只有2MB显存,后来 Riva TNT提高到16MB显存。然后 GeForce 和 ATI Rage有32MB显存, 现在一些 GeForce 2 到 4的显卡和 Radeons 带有 64MB 到128MB 的显存。 这为什么重要? 好吧,让我们看一些数字…

  比如你想让你的游戏看起来最好,所以你想要让它以32位屏幕, 1280x1024分辨率和32位 Z- 缓冲跑起来。 好,屏幕上每个像素4个字节,外加每个像素4字节的Z-缓冲,因为都是每像素32位。我们有1280x1024 个像素 – 也就是 1,310,720个像素。基于前端缓冲区和Z-缓冲区的字节数,这个数字乘以8,是 10,485,760字节。包括一个后端缓冲区,这样是 1280x1024x12, 也就是 15,728,640 字节, 或 15MB。 在一个 16MB 显存的显卡上,就只给我们剩下1MB 来存储所有的纹理。 现在如果最初的纹理是真32 位或 4字节宽,那么我们每幀能在显卡上存储 1MB/4字节每像素 = 262,144个像素。这大约是4 个 256x256 的纹理页面。

  很清楚,上述例子表明,旧的16MB 显卡没有现代游戏表现其绚丽画面所需要的足够内存。很明显,在它绘制画面的时候,我们每幀都必须重新把纹理装载到显卡。实际上,设计AGP总线的目的就是完成这个任务,不过, AGP 还是要比 3D 掀卡的幀缓冲区慢,所以你会受到性能上的一些损失。很明显,如果纹理由32位降低到16位,你就能够通过AGP以较低的分辨率传送两倍数量的纹理。如果你的游戏以每个像素比较低的色彩分辨率跑, 那么就可以有更多的显示内存用来保存常用的纹理 (称为高速缓存纹理) 。 但实际上你永远不可能预知使用者将如何设置他们的系统。如果他们有一个在高分辨率和颜色深度跑的显卡,那么他们将会更可能那样设定他们的显卡。



  我们现在开始讲雾,它是某种视觉上的效果。如今绝大多数的引擎都能处理雾, 因为雾非常方便地让远处的世界淡出视野,所以当模型和场景地理越过观察体后平面进入视觉范围内时,你就不会看见它们突然从远处跳出来了。 也有一种称为体雾的技术。这种雾不是随物体离照相机的距离而定,它实际上是一个你能看见的真实对象,并且可以穿越它,从另外一侧出去 -- 当你在穿越对象的时候,视觉上雾的可见程度随着变化。想象一下穿过云团 -- 这是体雾的一个完美例子。体雾的一些好的实现例子是Quake III一些关卡中的红色雾,或新的Rogue Squadron II 之 Lucas Arts的 GameCube 版本。其中有一些是我曾经见过的最好的云--大约与你能看见的一样真实。

  在我们讨论雾化的时候,可能是简短介绍一下 Alpha 测试和纹理Alpha混合的好时机。当渲染器往屏幕上画一个特定像素时,假定它已经通过 Z- 缓冲测试 (在下面定义),我们可能最后做一些Alpha测试。我们可能发现为了显示像素后面的某些东西,像素需要透明绘制。这意味着我们必须取得像素的已有值,和我们新的像素值进行混和,并把混合结果的像素值放回原处。这称为读-修改-写操作,远比正常的像素写操作费时。

  你可以用不同类型的混合,这些不同的效果被称为混合模式。直接Alpha混合只是把背景像素的一些百分比值加到新像素的相反百分比值上面。还有加法混合,将旧像素的一些百分比,和特定数量(而不是百分比)的新像素相加。 这样效果会更加鲜明。 (Kyle's Lightsaber在 Jedi Knight II 中的效果)。

  每当厂商提供新的显卡时,我们可以得到硬件支持的更新更复杂的混合模式,从而制作出更多更眩目的效果。GF3+4和最近的Radeon显卡提供的像素操作,已经到了极限。
模板阴影与深度测试
  用模板产生阴影效果,事情就变得复杂而昂贵了。这里不讨论太多细节(可以写成一篇单独的文章了),其思想是,从光源视角绘制模型视图,然后用这个把多边形纹理形状产生或投射到受影响的物体表面。

  实际上你是在视野中投射将会“落”在其他多边形上面的光体。最后你得到看似真实的光照,甚至带有视角在里面。因为要动态创建纹理,并对同一场景进行多遍绘制,所以这很昂贵。

  你能用众多不同方法产生阴影,情形时常是这样一来,渲染质量与产生效果所需要的渲染工作成比例。有所谓的硬阴影或软阴影之分,而后者较好,因为它们更加准确地模仿阴影通常在真实世界的行为。 通常有一些被游戏开发者偏爱的“足够好”的方法。如要更多的了解阴影,请参考 Dave Salvator的 3D 流水线一文。


深度测试
  现在我们开始讨论深度测试, 深度测试丢弃隐藏的像素,过度绘制开始起作用。过度绘制非常简单 – 在一幀中,你数次绘制一个像素位置。它以3D场景中Z(深度)方向上存在的元素数量为基础,也被称为深度复杂度。如果你常常太多的过度绘制, -- 举例来说, 符咒的眩目视觉特效,就象Heretic II,能让你的幀速率变得很糟糕。当屏幕上的一些人们彼此施放符咒时,Heretic II设计的一些最初效果造成的情形是,他们在一幀中对屏幕上每个相同的像素画了40次! 不用说,这必须调整,尤其是软件渲染器,除了将游戏降低到象是滑雪表演外,它根本不能处理这样的负荷。深度测试是一种用来决定在相同的像素位置上哪些对象在其它对象前面的技术,这样我们就能够避免绘制那些隐藏的对象。

  看着场景并想想你所看不见的。 换句话说,是什么在其他场景对象前面,或者隐藏了其他场景对象? 是深度测试作出的这个决定。

  我将进一步解释深度深度如何帮助提高幀速率。想像一个很琐细的场景,大量的多边形 (或像素)位于彼此的后面,在渲染器获得他们之间没有一个快速的方法丢弃他们。对非Alpha混合的多边形分类排序( 在Z- 方向上),首先渲染离你最近的那些多边形,优先使用距离最近的像素填充屏幕。所以当你要渲染它们后面的像素(由Z或者深度测试决定)时,这些像素很快被丢弃,从而避免了混合步骤并节省了时间。如果你从后到前绘制,所有隐藏的对象将被完全绘制,然后又被其他对象完全重写覆盖。场景越复杂,这种情况就越糟糕,所以深度测试是个好东西。


抗锯齿
  让我们快速的看一下抗锯齿。当渲染单个多边形时,3D 显卡仔细检查已经渲染的,并对新的多边形的边缘进行柔化,这样你就不会得到明显可见的锯齿形的像素边缘。两种技术方法之一通常被用来处理。 第一种方法是单个多边形层次,需要你从视野后面到前面渲染多边形,这样每个多边形都能和它后面的进行适当的混合。如果不按序进行渲染,最后你会看见各种奇怪的效果。在第二种方法中,使用比实际显示更大的分辩率来渲染整幅幀画面,然后在你缩小图像时,尖锐的锯齿形边缘就混合消失了。这第二种方法的结果不错,但因为显卡需要渲染比实际结果幀更多的像素,所以需要大量的内存资源和很高的内存带宽。

  多数新的显卡能很好地处理这些,但仍然有多种抗锯齿模式可以供你选择,因此你可以在性能和质量之间作出折衷。对於当今流行的各种不同抗锯齿技术的更详细讨论请参见Dave Salvator 的3D 流水线一文。


顶点与像素着色
  在结束讨论渲染技术之前,我们快速的说一下顶点和像素着色,最近它们正引起很多关注。顶点着色是一种直接使用显卡硬件特征的方式,不使用API。举例来说,如果显卡支持硬件 T & L ,你可以用DirectX或OpenGL编程,并希望你的顶点通过 T & L 单元 (因为这完全由驱动程序处理,所以没有办法确信),或者你直接利用显卡硬件使用顶点着色。它们允许你根据显卡自身特征进行特别编码,你自己特殊的编码使用T & L 引擎,以及为了发挥你的最大优势,显卡必须提供的其他别的特征。 事实上,现在nVidia 和ATI 在他们大量的显卡上都提供了这个特征。

  不幸的是,显卡之间表示顶点着色的方法并不一致。你不能象使用DirectX或者OpenGL 那样,为顶点着色编写一次代码就可以在任何显卡上运行,这可是个坏消息。然而,因为你直接和显卡硬件交流,它为快速渲染顶点着色可能生成的效果提供最大的承诺。( 如同创造很不错的特效 -- 你能够使用顶点着色以API没有提供的方式影响事物)。事实上,顶点着色正在真的将3D 图形显示卡带回到游戏机的编码方式,直接存取硬件,最大限度利用系统的必须知识,而不是依靠API来为你做一切。对一些程序员来说,会对这种编码方式感到吃惊,但这是进步代价。

  进一步阐述,顶点着色是一些在顶点被送到显卡渲染之前计算和运行顶点效果程序或者例程。你可以在主CPU上面用软件来做这些事情,或者使用显卡上的顶点着色。 为动画模型变换网格是顶点程序的主选。

  像素着色是那些你写的例程,当绘制纹理时,这些例程就逐个像素被执行。你有效地用这些新的例程推翻了显卡硬件正常情况做的混合模式运算。这允许你做一些很不错的像素效果, 比如,使远处的纹理模糊,添加炮火烟雾, 产生水中的反射效果等。一旦 ATI 和 nVidia 能实际上就像素着色版本达成一致( DX9's 新的高级阴影语言将会帮助促进这一目标), 我一点不惊讶DirectX 和OpenGL采用Glide的方式-- 有帮助开始, 但最终不是把任何显卡发挥到极限的最好方法。我认为我会有兴趣观望将来。
最后(In Closing...)
  最终,渲染器是游戏程序员最受评判的地方。在这个行业,视觉上的华丽非常重要,因此它为知道你正在做的买单。对于渲染器程序员,最坏的因素之一就是3D 显卡工业界变化的速度。一天,你正在尝试使透明图像正确地工作;第二天 nVidia 正在做顶点着色编程的展示。而且发展非常快,大致上,四年以前为那个时代的 3D 显卡写的代码现在已经过时了,需要全部重写。 甚至John Carmack 这样描述过,他知道四年以前为充分发挥那个时期显卡的性能所写的不错的代码,如今很平凡 -- 因此他产生了为每个新的id项目完全重写渲染器的欲望。Epic 的Tim Sweeney赞同 -- 这里是去年他给我的评论:

  我们已经足足花费了9个月时间来更换所有的渲染代码。最初的 Unreal 被设计为软件渲染和后来扩展为硬件渲染。下一代引擎被设计为 GeForce 及更好的图形显示卡,且多边形吞吐量是Unreal Tournament的100倍。

  这需要全部替换渲染器。很幸运,该引擎模块化程度足够好,我们可以保持引擎的其余部分—编辑器,物理学,人工智能,网络--不改动,尽管我们一直在以许多方式改进这些部分。

  搭配长篇文章的短篇报导(Sidebar):API -- 祝福和诅咒
  那么什么是API? 它是应用程序编程接口,将不一致的后端用一致的前端呈现出来。举例来说,很大程度上每种3D显示卡的3D实现方式都有所差别。然而,他们全部都呈现一个一致的前端给最终使用者或者程序员,所以他们知道他们为X 3D显示卡写的代码将会在Y 3D显示卡上面有相同的结果。好吧,不管怎样理论上是那样。 大约在三年以前这可能是相当真实的陈述,但自那以后,在nVidia 公司的引领下,3D显卡行业的事情发生了变化。

  如今在PC领域,除非你正计划建造自己的软件光栅引擎,使用CPU来绘制你所有的精灵,多边形和粒子 -- 而且人们仍然在这样做。跟Unreal一样,Age of Empires II: Age of Kings有一个优秀的软件渲染器 – 否则你将使用两种可能的图形API,OpenGL或者 DirectX 之一。OpenGL是一种真正的跨平台API (使用这种API写的软件可以在Linux,Windows和MacOS上运行。), 而且有多年的历史了,为人所熟知,但也开始慢慢地显示出它的古老。 大约在四年以前,定义OpenGL驱动特征集一直是所有显示卡厂商工作的方向。

  然而,一旦在目标达成以后,没有预先制定特征工作方向的路线图,这时候,所有的显卡开发商开始在特征集上分道扬镳,使用OpenGL扩展。

  3dfx 创造了T- 缓冲。 nVidia 努力寻求硬件变换和光照计算。Matrox努力获取凹凸贴图。等等。 我以前说过的一句话,"过去几年以来,3D显示卡领域的事情发生了变化。"委婉地说明了这一切。

  无论如何,另一个可以选择的API是 DirectX。这受Microsoft公司控制,且在PC 和 Xbox 上被完美地支持。由于明显的原因,DirectX 没有Apple或者 Linux 版本。因为Microsoft控制着 DirectX,大体上它容易更好地集成在Windows里面。

  OpenGL和DirectX之间的基本差别是前者由‘社区’拥有,而后者由Microsoft拥有。如果你想要 DirectX 为你的 3D 显示卡支持一个新的特征,那么你需要游说微软,希望采纳你的愿望,并等待新的 DirectX发行版本。对于OpenGL,由于显示卡制造商为3D显示卡提供驱动程序,你能够通过OpenGL扩展立即获得显示卡的新特征。这是好,但作为游戏开发者,当你为游戏编码的时候,你不能指望它们很普遍。它们可能让你的游戏速度提升50%,但你不能要求别人有一块GeForce 3 来跑你的游戏。好吧,你可以这么做,但如果你想来年还在这个行业的话,这是个相当愚蠢的主意。

  这是对这个问题极大的简单化,对我所有描述的也有各种例外情况,但这里一般的思想是很确实的。对于DirectX ,在任何既定时间你容易确切地知道你能从显示卡获得的特征,如果一个特征不能获得,DirectX 将会用软件模拟它(也不总是一件好事情,因为这样有时侯非常的慢,但那是另外一回事)。对于OpenGL,你可以更加贴近显示卡的特征,但代价是不能确定将会获得的准确特征。
页: [1]
查看完整版本: 游戏引擎剖析