这就是第一部份 -- 我们将会在第二部分继续介绍光照和纹理,下面的章节会更加深入。
第2部份: 3D环境的光照和纹理
世界的灯光
在变换过程中, 通常是在称为观察空间的坐标空间中, 我们遇到了最重要的运算之一: 光照计算。 它是一种这样的事情, 当它工作时,你不关注它,但当它不工作时, 你就非常关注它了。有很多不同的光照方法,从简单的计算多边形对于灯光的朝向,并根据灯光到多边形的方向和距离加上灯光颜色的百分比值,一直到产生边缘平滑的灯光贴图叠加基本纹理。而且一些 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 显卡, 会更快一些。纹理压缩是件好事情。 在下面我们将会更多的讨论纹理压缩。
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 流水线一文。