原文:ExtremeTech 3D
Pipeline Tutoria
June 13, 2001 - By Dave
Salvator
转载请注明出处。
介绍
从令人着迷的电影特效,到医学成像、电子游戏和更多的领域,3D图形所带来的冲击不亚于一场革命。这项从五年前(1996年)开始,带来了个人电脑消费风暴的技术,根源于学术界和军事。其实从某种程度上来说,我们今天在个人电脑上所享受的3D图形算是一种“和平红利”,许多最初设计军事模拟器的专家如今在从事3D图形芯片,电影和游戏开发产业的工作。
除娱乐之外,3D图形技术在计算机辅助设计(CAD)上为工业设计带来了巨大的飞跃。制造商不但可以不使用任何材料设计并“构造”出他们的产品,还可以通过连接拥有电脑辅助建造(CAM)系统的制造机械来使用这些3D模型。但对我们大多数人来说,3D图形技术给娱乐产业带来的冲击最大,包括电影和游戏,正因为如此,我们许多人都对3D图形相关的技术和术语有所了解。
记住不是所有的渲染系统都有着相同的目标。对于离线渲染系统,像CAD程序中所使用的,精确度比帧速要重要得多。例如,这种模型有可能用来制造飞机部件。实时渲染器,像游戏引擎和模拟器,趋向于强调固定的帧速来得到平滑流畅的动画效果,为了这一目的宁愿牺牲掉图形和纹理的细节。
有些渲染器是混合型的,比如电影《玩具总动员》所使用的。艺术家和程序员们使用Pixar的Renderman技术来创造眩目的视觉场景,但动画的每一帧在服务器场——一个计算机集群,渲染工作可以在其中分布式进行——上要耗费数小时进行渲染。这些离线渲染的帧之后被排成标准电影的每秒24帧的序列,来制作电影的最终剪辑。
随着急速增长的对3D芯片渲染能力的需求,“消费级3D”和“工作站级3D”之间的界线已经非常模糊了。然而,实时游戏引擎仍然做了许多折衷来保持帧速,一些甚至设计了“油门”来控制当帧速降低到规定的水平之下时自动打开和关闭某些特性。相比之下,工作站级3D需要一些当今游戏不需要的高级特性。
3D图形领域广大而复杂。我们的目标是写出一系列相当技术性的,但通俗易懂的,有关3D图形技术的文章。我们从用来创建3D图形的,被称作3D管线的多步处理流程开始。我们会遍历整个3D管线,从场景中一个三角形开始,到最终一个像素被画出。我们会提到一些与渲染3D场景相关的数学,尽管对3D算法只是做简单的介绍(如果你想深入学习,在本文最后我们列出了相当数量的参考)。
在管线中,准备就绪
因为3D图形渲染本身的有序特性,因为有如此多的计算要做,大量的数据要处理,整个处理过程被拆分成不同的步骤,有时被称作阶段(Stage)。这些阶段被依次送入前面提到的3D图形管线中。
创建场景所需的巨大工作量使得3D渲染系统的设计者们(包括硬件和软件)寻找一切可能的方法来去除不必要的工作。有设计师这么讽刺过,“3D图形学是欺骗而不被抓住的艺术”解释一下,这句话的意思是说,3D图形学中的一种艺术形式就是优雅地通过降低场景中的显示细节来获得更好的性能,但却不让观察者注意到显示质量上的损失。处理器和内存带宽是宝贵的资源,因此设计师为节约它们所做的任何努力都会对性能非常有利。一个重要的例子就是拣选,它用来告诉渲染器,“如果观察摄像机(观察者的眼睛)看不到它,别自找麻烦去处理它,只操心摄像机能看到的东西吧。”
伴随着相关处理步骤的数量和复杂度,这些阶段(Stage)在管线中的次序可以在执行时改变。之后我们很快会更加详细地检阅这些阶段中所做的操作,概括地说,一个常规的3D管线顺序拆分为四个片段:应用/场景,几何图元,三角形设置和光栅化/渲染。虽然下面的片段草图可能看起来比较吓人,当你读完这篇文章时,你会成为极少数真正理解3D图形工作原理的人中的一员,我们认为你将会想要理解得更加深入。
操作在哪里完成
3D世界的大多数高层次方面操作由管线中应用阶段部分(有争论认为它技术上不属于3D管线的一部分)的应用软件负责管理,余下的三个主要阶段通常由一套应用程序接口(API)负责管理,例如SGI的OpenGL,微软的Direct3D,或Pixar的Renderman。API通过调用图形驱动程序和硬件来在硬件中执行绝大多数的图形操作。
图形API实际上是为应用提供了硬件抽象,反过来说,为应用提供了真正的设备无关性。因而,这些API通常被称作硬件抽象层(HALs)。它们的设计目标非常简明——应用开发者一旦为某个API写出了程序,这个程序就可以(而且应该)运行在任何支持这个API的硬件上。相反的,硬件制造者为这些API编写驱动程序,这样为这些API写的应用就可以在它们的硬件上运行了(图1)。说明一下,添加“应该”这个词是因为有些时候因为兼容性问题会导致API的不正确使用(被称作违规),这会导致应用依赖于某个特定的硬件特性,或者导致在某个硬件驱动下API特性被错误地执行,最终产生了错误的或不可预知的结果。
空间入侵者
深入管线细节之前,我们首先要从高层了解3D物体和3D世界是如何定义,物体如何被定义,放置,定位,如何在更大的3D空间内,或者就在它们自己的边界内进行控制。在一个3D渲染系统中,多个笛卡尔坐标系(x-(左/右),y-(上/下),z-(远/近))被应用在管线的不同阶段。被运用在互相关联但各不相同的目的的同时,每个坐标系都提供了精确的数学方法来在空间中定位和描述物体。毫无意外的,每个这样的坐标系都被称作“空间”。
3D场景中的物体和场景本身在经过3D管线时被连续地进行转换或者变换,一共要通过五个空间。下面是这些空间的简单概述:
模型空间(Model
Space):模型在它们自己的坐标系中,原点是模型上的某个点,比如在一个英式足球运动员模型的右脚上。而且,模型有一个控制点或者“把手”。用来移动模型,3D渲染器只需要移动控制点,因为模型空间中模型的坐标与它的控制点保持固定的关联关系。此外,同样的“把手”也可以用来旋转物体。
世界空间(World
Space):在这里模型被放置在实际的3D世界中,使用一个统一的世界坐标系。事实上许多3D程序跳过了世界空间而直接转到了裁剪空间或视图空间。OpenGL中并不真正有世界空间这个概念。
视图空间(View Space - 也被称作摄像机空间 Camera Space):这个空间中,当前观察摄像机被程序定位在3D世界坐标的某个点上(通过图形API)。世界坐标系被执行变换(使用矩阵数学,我们稍后会探讨),从而使摄像机(你的眼睛)处在坐标系的原点,沿着z轴朝向场景。如果世界空间的变换被跳过了,场景就被直接变换到视图空间,摄像机同样被放在原点沿着z轴方向。沿着z轴远离摄像机深入场景时z坐标的值是增加还是减少由程序员决定的,但我们现假设你沿着z轴看过去的坐标是增加的。注意拣选,背面剔除和光照操作是在视图空间进行的。
视图体实际上由投影(Projection)来创建的,就像名字所暗示的那样,“投影场景”到摄像机的前面。从这个意义上来说,这是种角色倒转,摄像机现在变成了放映机,场景的视图体通过关联摄像机进行定义。把摄像机想象成某种全息放映机,但它不是把3D图像放映到空气中,而是把场景投影“进”你的显示器。这个视图体的形状可以是矩形(称作平行投影)也可以是棱锥形(称作透视投影),后者被称作平截视图体(View Frustum)(通常也叫做视图体(Frustum),虽然视图体这个名字更常用)。
视图体定义了摄像机可以看到的部分,同样重要的是,它也定义了摄像机看不到的部分,这样一来,许多物体模型和世界的某些部分都可以被丢弃,以节约3D芯片的运算周期和内存带宽。
视图体看起来像是个顶部被切掉的棱锥。棱锥的顶部紧贴摄像机的视点并向外发散。视图体的顶部被称作近(前)截面,另一面被称作远(后)截面。整个被渲染的3D场景必须处在近截面和远截面之间,同时被视图体的侧面和顶部限制。如果模型中的三角形(或者世界的一部分)超出了视图体,它们就不会被处理。同样的,如果一个三角形有一部分在视图体里面一部分在外面,超出的部分会被视图体截掉,因此这里才会使用“裁剪”(Clipping)这个术语。尽管视图空间中的视图体有裁剪面,裁剪的实际操作是在视图体变换到裁剪空间时执行的。
深入空间
裁剪空间(Clip
Space):与视图空间相似,但是视图体被“压扁”成了一个单位立方体,x和y坐标被规格化到了-1到1之间,z坐标在0到1之间,这可以简化裁剪计算。“透视分割”执行规格化操作,通过用一个特殊的“w”值除所有的x,y和z顶点坐标,这个值就是我们后面将会详细说明的缩放因子。透视分割使近处的物体变大,远处的物体变小,跟你所期望看到的真实场景一样。
屏幕空间(Screen Space):在这里3D图像被转换为x和y的二维屏幕坐标来做二维显示。注意z和w坐标依然被图形系统保留,用于最终渲染之前的深度/Z缓存(参见下面的Z缓存一节)和背面剔除中。注意场景到像素的转换,也就是光栅化的操作,还没有开始。
因为这些不同空间之间的变换绝大多数实质上都是改变参照系,很容易混淆。3D管线容易把人搞晕的一部分原因就是没有一个“确定的”路线来执行所有这些操作,因为研究人员和程序员们各自发现了一些有用的技巧和优化方法,而且通常都会有多个方法可以解决一个给定的3D/数学问题。但是,通常空间变换的处理步骤都按照我们前面介绍的顺序来进行。
要对这些不同空间之间如何相互影响有个概念,考虑一下下面这个例子:
拿几块乐高玩具,把它们拼在一起做个模型。想象一下把单独的块认作物体的边缘,块与块的连接部分看作顶点(虽然乐高建筑并不使用3D模型常用的三角形图元,但我们的例子用四边形也很合适)。把这个物体放在你面前,模型空间的原点坐标应该是左下方离你最近的那个角,模型的所有坐标都由这个点来衡量。原点实际上可以是模型的任意一部分,但左下方最近的角是最常用的。当你在房间里(3D世界空间或者视图空间,由3D系统决定)移动这个物体,块与块之间的相对位置是固定不变的(模型空间),虽然它们相对房间的坐标在变化(世界空间或视图空间)。
3D管线数据流
某种意义上来说,3D芯片就是物理具体化的3D管线,数据在这里从一个阶段(Stage)“顺流”到另一个阶段。有一处需要注意,管线中的大多数应用/场景阶段和早期的图元阶段的操作是对每顶点进行的,然而拣选和裁剪是对每三角形进行的,渲染操作是对每像素进行的。为提高性能,管线中不同阶段的计算可以重叠。例如,顶点和像素操作在Direct3D和OpenGL中都相互独立,所以可以做到一个三角形在图元阶段时,另一个在光栅化阶段。而且,图元阶段对两个或多个顶点的计算和光栅化阶段对两个或多个顶点的计算(来自相同的三角形)可以同时执行。
管线的另一个优势是,因为在图元阶段不会有数据在顶点间传递,渲染阶段像素间也没有数据传递,芯片制造者可以实现多个像素管线,通过并行处理这些独立实体来获得可观的性能提升。还有需要注意的一处是实时渲染中管线的使用,尽管有许多好处,也不是没有缺点。举个例子,一旦一个三角形被送入管线,程序员几乎就跟它挥手道别了。要获取管线中顶点的状态或者颜色/透明度信息要付出非常高的性能代价,还可能导致管道阻塞,这是绝对不该做的事。
管线阶段——深入探讨
1.
应用/场景
3D应用程序自身可以认作是3D管线的开始,尽管它并不真的是图形子系统的一部分,但却是由它来启动图形生成处理以生成最终的场景或者动画帧。应用程序还设置观察摄像机的位置,也就是你在3D世界中的“眼睛”。物体,包括动的和不动的首先在应用程序中以几何图元或者基本的构造块来描述。其中三角形是最常用的图元。它们简单易用因为三个顶点恰好描述一个平面,但四个或更多顶点的多边形不一定位于同一个平面上。更复杂的系统支持被称作高次表面,它们是不同类型的曲面图元,这些我们很快会讲到。
3D世界和其中的物体是在程序中创建的,像3D Studio
Max,Maya,AutoDesk 3D
Studio,Lightwave和Softimage我们仅举这几个例子。3D艺术家们通过这些程序不但可以创建模型,还可以让它们动起来。模型首先用大量的三角形构建,然后进行着色和贴纹理。接下来,依赖于渲染引擎的约束——离线的或者实时的——艺术家可以降低这些高精度模型的三角形数量来适合给定的性能限制。
应用程序控制物体从一帧移动到另一帧,无论是离线渲染器还是游戏引擎。应用程序遍历几何数据库收集必要的物体信息(几何数据库包含了构成物体的所有几何图元),移动所有下一帧动画中要变化的物体。以一个游戏引擎为例,渲染器不会完成所有的工作。游戏引擎同样必须处理AI(人工智能),碰撞检测和物理特性,声音,以及网络(如果游戏在网络多人模式下进行)。
所有的模型都有一个默认的“姿势”,比如人物模型,默认姿势是所谓的“达芬奇姿势”,因为这个姿势很像达芬奇著名的“维特鲁威人”。一旦应用程序指定了模型的新“姿势”,模型就准备好了下一步处理。
一些应用程序会在这里执行一个操作,叫做“遮挡剔除”,可见测试测定物体是部分还是全部被它前方的其他物体遮挡(盖住)。如果被挡住了,被挡住的物体,或者物体被挡住的部分会被丢弃。节省下的计算代价值得考虑,否则这些计算要在管线中执行,尤其是有着复杂深度的场景,也就是处在场景后面的物体有多“层”物体在它们前面,在观察摄像机前挡住了它们。
如果这些被遮挡的物体可以提早被剔除,它们就不必在管道中继续前进,这就节省了不必要的光照、着色和纹理计算。例如,在一个你跟哥斯拉对抗的游戏中,那个大家伙藏在你正走向的一幢大楼后面,你看不到他(卑鄙的家伙)。这时游戏引擎不必绘制哥斯拉的模型,因为大楼的模型挡在了他的前面,这就可以节约下硬件在这一帧动画中渲染哥斯拉模型的消耗。
一个更重要的步骤是对每个物体的一个简单的可见检查。这可以通过检查对象是否在视图体中(全部或者部分)来完成。一些引擎还尝试测量视图体中的一个物体是否完全被另一个物体挡住了。这些操作典型地使用简单的概念如入口(Portal)和可见集(Visibility
Sets)来完成,特别是室内场景。这是在3D游戏引擎中实现的两种简单的技术,使引擎不必再绘制3D世界在摄像机中看不到的部分。[Eberly, p.
413]最初Quake中使用的是称作潜在可视集(PVS)的技术,它把世界划分为小块。本质上就是,如果玩家处在世界的一个特定部分,看不到其他区域时,游戏引擎就不必处理其他部分的数据。
另一个程序员们喜欢的降低工作量的技巧是使用绑定盒(Bounding
Box)。例如你有个10000个三角面的杀手兔模型,与其测试模型的每个三角形,程序员可以把模型用一个绑定盒包住,绑定盒由12个三角形组成(六面体的每个面两个三角形)。然后就可以测试剔除条件(基于绑定盒的顶点而不是兔子模型的顶点)来确定杀手兔在场景中是否可见。在你通过指定杀手兔模型中的共享顶点(相邻三角形可以共享的顶点,我们后面会详细探究的一个概念)来进一步减少顶点数之前,针对于这个测试你已经把总顶点数从30000(杀手兔模型)个减少到了36个(绑定盒)。如果检测表明绑定盒在场景中不可见,杀手兔模型就可以简单地被丢弃,你就为自己省了一大堆工作。
你LOD了没有?
另一个消除额外工作的方法就是所谓的物体细节等级(Level of
Detail),被称为LOD。这项技术是有损的,但对于它的一般应用来说,损失的模型细节通常是觉察不到的。物体模型由数个不同的细节等级构造。很恰当的例子是一个最高细节等级的战斗机模型使用10000个三角形,附带的低精度细节等级分别由5000,2500,1000和500个三角形组成。具体使用哪一个细节等级由战斗机距离观察摄像机的距离决定。如果很近,就使用最高的细节等级,如果是刚刚能在摄像机中看到而已,就使用最低的细节等级,在这两种情况之间,就使用其它的细节等级。
细节等级的选择总是由应用程序在将物体送入管线做进一步处理之前完成。为了决定使用哪一个细节等级,应用程序把物体的一个简化版本(通常只是中心点)映射到视图空间,以测定到物体的距离。这项操作独立于管线执行。必须确定具体的细节等级以决定将哪个三角形的集合(不同细节等级)送入管线。
几何专用技巧
一般来说,三角形多的模型看起来更加真实。这些三角形的信息——它们在3D世界中的位置,颜色等——保存在每个三角形的顶点描述中。这些3D世界中的顶点集合被称作场景数据库,跟上面提到的把动物模型做为几何数据库非常相像。模型中的弯曲部分,比如汽车上的轮子,需要许多三角形来近似形成一个光滑曲面。在一个圆形中努力减少顶点/三角形数量会造成不利影响,例如,使用低细节等级将造成一个“崎岖不平”的圆,构成圆的三角形顶点都可以看到。如果用更多的三角形来表现这个圆,它的边缘看起来会平滑得多。我们可以进行优化来减少实际送入管线的三角形数量却不必降低模型细节,因为相连的三角形顶点是共享的。程序员可以使用连接的三角形绘制模式来降低顶点数,即三角带和扇形。例如:
在三角带模式下,最简单的例子就是由两个三角形拼成的矩形,它拥有一条共享的斜边。通常,单独绘制两个这样的三角形会产生六个顶点。但是,由于两个三角形连在一起,它们形成了一个简单的,由四个顶点描述的三角带,把平均每个三角形需要的顶点数减少到了两个,优于原先的三个。看起来减少得并不多,随着三角形数目的增多(顶点数也同样变化)优势也会愈加明显,平均每个三角形单独拥有的顶点数降低到了一个。[RTR, p. 234]以下是计算平均顶点数的公式,给定m个三角形:
1+2m
因此,在一个有100个三角形的三角带中,平均每个三角形顶点数是1.02个,或者说总共有102个顶点,相比独立三角形的300个顶点是相当可观的节约。在这个例子中,我们通过把m个三角形改用三角带达到最大限度节约了成本,一共是m+2个顶点。这些节约可以积少成多,当考虑Direct3D中使用32个字节的数据来描述一个顶点的属性(如位置,颜色,透明度等)。当然,整个场景不可能只由三角带和扇形组成,但为了相应节约的成本,开发人员会尽量使用它们。
在扇形情况下,程序员可能使用20个三角形排列在一个扇形区来描述一个半圆。正常情况下由60个顶点组成,但通过使用扇形来描述,顶点数量可以降低到22个。第一个三角形有三个顶点,但接下来每个新增的三角形只需要增加一个顶点,扇形的中心有一个所有三角形共享的顶点。同样达到了使用三角带/扇形最大限度的节约。
使用三角带和扇形的另一大优势是,它们是“无损”的数据压缩,这意味着并没有任何信息和图形精度为了数据压缩和速度提升而损失掉。此外,以三角带和扇形向硬件传送三角形可以提高顶点缓存的效率,这可以加速几何处理的性能。另一个程序员可以使用的工具是索引过的三角形列表,可以用来描述巨量的三角形,m个三角形,使用m/2个顶点,大约是使用三角带和扇形压缩量的两倍。这个代表性的方法是大多数硬件架构的首选方法。
曲面简介
与其使用大量三角形来表现一个曲面,3D艺术家和程序员还可以使用另一个工具:高次曲面。这些曲面图元有着更为复杂的数学描述,但在一些情况下,这些额外的复杂性仍然比用大量三角形来描述物体更加廉价。这些图元有一些相当古怪的名字:参数多项式(称为样条),非均匀有理B样条(NURBs),贝塞尔曲线(Bezier),参数双三次曲面和N-Patches。因为3D硬件对三角形理解得最好,这些在应用层定义的曲面会被栅格化,或者被转换为三角形,转换由API运行时,显卡驱动或者硬件执行,以通过3D管线做进一步处理。如果曲面从CPU送入3D图形卡做变换和光照处理时由硬件执行栅格化,性能还有可能进一步提升,但这会给AGP接口带来少量负载,是一个潜在的瓶颈。
2.
几何处理
继续前进:四种主要变换
物体在帧与帧之间变化位置来产生运动的假象,在3D世界中,物体通过四种操作被移动和操作,统称为变换。变换实际上是把不同类型的“变换矩阵”以矩阵数学作用在物体顶点上。这些变换操作都属于仿射几何(Affine),意味着它们发生在仿射空间中——一个包含点和向量的数学空间。仿射变换保留了线的平行关系,但点与点之间的距离可以变化。(看下面的例子,显示了一个正方体变成了一个长方体,边与边的平行关系被保留下来,但物体内的角度发生了变化?)。当物体在一个特定的坐标系或空间中移动,或者从一个空间转到另一个空间时使用这些变换操作。
平移:物体沿着三个坐标轴的任意一个到另一个位置的移动。数学操作:通常是加法或减法(加一个负数),不过为了提高效率做了一些转换,所以这项操作通过矩阵乘法来完成,跟旋转和缩放操作一样。
旋转:就像名字暗示的那样,一个物体可以沿任意的轴旋转。数学操作:最简单的旋转例子就是物体在坐标系的原点,顶点的每个坐标值乘上θ角(物体旋转的角度)的sin或cos值就得到了旋转后的坐标。如果一个物体要同时绕几个坐标轴旋转,旋转的计算顺序就非常重要,不同的顺序会得到不同的结果。绕任意轴旋转需要一些额外的工作。可能首先需要进行一个变换移动到原点,接着做一些旋转操作使旋转轴对齐坐标系的z轴。然后做想要的旋转,然后对齐z轴的操作要被撤销,然后把物体移回原位。
缩放:模型的大小变化,在透视投影(我们很快会详细讨论的一种变换)中用来产生场景深度效果。数学操作:乘一个缩放因子,如每个坐标乘2,物体会大一倍。缩放可以被归一化,这样三个轴向的缩放比例相同,或者每个顶点做不同程度的缩放。负的缩放因子会产生物体的镜像。
斜切:(又称错位变换)沿着一个或多个轴向操作使模型的形状产生变化。例如一盒方形果冻盛在一个盘子里放在桌上,把盘子的一边提起成45度,果冻因为重力会变成偏菱形。果冻模型的顶部会向着3D世界的地面歪斜。数学操作:把一个坐标值的函数加在另一个上,比如把x的值加在y上而x不变。
请看下图:
就像前面所讲,3D变换计算大量使用矩阵数学来描述。矩阵为变换条件提供了简便的表示,3D矩阵数学通常是些简单的乘法和加法。
变换处理的效率来自于以下事实:多次矩阵操作可以连接组成一个单独的矩阵,把这个矩阵应用在顶点上作为一次单独的矩阵操作。这就把矩阵操作的设置消耗分散到了整个场景中。有人可能会认为要给出使用笛卡尔坐标系的各种空间的三个坐标值,应该是个3x3的矩阵。然而这里用的是“齐次坐标”,是个4x4矩阵。使用齐次坐标可以把需要加法的变换用乘法来处理,比如平移。多个不同变换也可以通过齐次坐标连接为乘法运算。第四项实际上就是缩放因子“w”。最初设置为1,用来在裁剪空间中做投影变换时计算深度值,并且在光栅化阶段作为像素值的透视修正插值。
从空间到空间
继续进入到几何图元部分,我们很可能把物体从模型空间变换到世界空间,然后从世界空间到视图空间,或者有些时候直接变换到视图空间,我们前面提到过。从一个坐标空间转换到另一个时,要用到许多基本的变换。有些可能像一个翻转变换一样简单,也有复杂得多的,比如包括两个平移和一个旋转的组合。举个例子,世界空间变换到视图空间通常包括一个平移和一个旋转。转换到视图空间之后,许多有趣的事情就开始发生了。
第一部分就讲到这里。在下一部分我们将概括几何图元部分,并且涉及光照,裁剪,三角形设置以及渲染相关的许多方面。