Direct3D几何流水线
大家都知道,一个3D 场景中,我们见到的任何光辉灿烂的物体,
都是由一个一个面片组成的。而装载面片位置信息的就是其各个定点的三维坐标。这是用来在模型中存储的,而要把物体显示在屏幕上,还需要将它们转换成显示器上的二维坐标。这就需要对每个点实施一套 3 to 2 的转换公式,在Direct3D中叫做“几何流水线”(Geometry Pipeline)。
每渲染一桢,我们都要用到这条流水线把所有定点的坐标转化成当前要显示的位置。不过放心,D3D不会改变你原有的顶点坐标,变换出的顶点数据会存放在新的地方用来渲染。想一想物体,也就是面片,也就是顶点要显示在屏幕上,其位置取决于什么呢?首先它一定取决于该点在场景中的位置,然后还在于你从什么角度看,更详细一点就是我的眼睛在哪儿,我注视着哪儿,以及我的视野宽窄等等。
对于每个独立被引入程序的mesh物体,它们的坐标系、坐标原点理论上都应该是不同的,其顶点也都是用局部坐标表示的。那么要做统一的变换,首先应将它们引入到同一个坐标系下,也就是我们称之为“世界坐标系”的坐标。这个变换也因此得名世界变换(World Transform)。对物体所需要做的移动、旋转等工作也是要在此时完成的(这些本质上不就是坐标的更改么)。
经过了以上一些操作后,每个顶点(也就是每个物体)在整个场景中的位置就如你所愿确定下来了。要把它们映射到屏幕上,还要确定观察者(你可以叫他玩家、摄影机都无所谓)的位置和视角。我们是要把所有的点变换到新建立的以观察者为基准的坐标系下。这个步骤就是“视图变换”(View Transform)。实际上和后面要说的射影变换相比,这两种变换并没有什么本质区别。有时候为了效率,可以把世界变换与视图变换合并为一个世界——视图变换。这不就是说你一开始就选择观察者的位置为世界坐标系的原点,并按照视角来确定坐标轴么?
后面一步是“射影变换”(Projection Transform),有必要重点说一下。很多教材(包括MSDN)上都是假装读者已经知道为什么要有射影变换而给读者讲它的。实际上,我们要做的所有坐标转换归根结蒂是要把三维的点投影到二维的屏幕上,如图所示
经过上述两次坐标转换后,我们已经让屏幕平行于坐标轴平面了,也就是说,经过一些比例范围的调整,理论上我们能从点的三维坐标中的某两个直接得到期待已久的屏幕坐标。但是别急,此时得到的坐标绘出的图就像我们小时候画的那些画一样——没有立体感。比如上图那个矩形,因为近大远小,在我们的视野中应该看起来像个梯形。但是如果我们不做任何处理就直接把它的顶点(已经过前两重变换)投影到显示器上(假设平行于图中的XY平面)这样还是一个方方正正的矩形。
想象一下,投影实际上就是把空间中的所有点都压扁,扁到某一个平面上。这样出来的图形自然不会有透视效果。(之所以有近大远小是因为人眼的凸透镜成像,其像高是物距的减函数。这里不多说了)你可能想到让每个点像这样斜着投影,但是仔细想想,如何斜着投影呢?等你想明白了再回答这样做真的方便么?于是另一种办法就是把整个空间范围变成一个棱台(里面的点随之进行放缩)。
相对来说把较远端缩小会造成数据的不准确,因此采用放大较近端。对每个点,我们进行最后一步变换就是根据其远近程度进行一下放缩。
D3D把剪切也纳入此流水线中,尽管它没对顶点作任何变换,只是剔出那些不用的点。
以上就是D3D中的几何流水线。幸运的是,我们并不需要自己去写代码来完成这些转换。实际上我们只需要设计好参数,调用相应的D3D函数设置上面提到的各种决定因素,它会在渲染画面的时候把每个顶点自动转化成所需的屏幕坐标的。正因为这一套流水线操作的通用性和规范性,各种3D渲染引擎都将它封装了,而当代很多先进的显卡都将其固化到硬件线路上,这样大大提高了渲染速度。
下面我们来看看一些具体的实施。在计算机图形学中,坐标的变换通常是通过与一个矩阵(Matrix)相乘来实现的。基本变换包括平移、缩放、旋转都用此方法完成,其他任何的变换,包括不同坐标系之间的互化,也都是通过这三种基本转换完成的。因此说,Matrix无处不在 , 在我们的周围,就在这间屋子里。你能在窗户往外看到它,在电视里看到它。当你上班,去教堂或者缴税你可以感觉到它。你眼前的世界让你看不到真实……(和我们说的Matrix不大一样,不过多少有点这个意思吧)。具体到三维坐标系中,定义某点的坐标为(X,Y,Z)则用(X,Y,Z,W)乘以一个相应的4X4矩阵就可以得到新的坐标(X',Y',Z',W'),这里的W自有用处,一般是1。还有一点很重要,一个矩阵就代表着一重变换,而几个矩阵的乘积就代表着多重变换的合变换。这点用处很大,读者会慢慢体会到。
那么在这条流水线中,按规范我们至少需要三个矩阵来实现以上三步变换,也就是世界矩阵(World Matrix)、视矩阵(View Matrix)以及射影矩阵(Projection Matirx)。
世界矩阵有时候需要我们自己填写,根据我们的各种变换需要来填写一个D3DXMATRIX结构体(其成员就是各行各列的数值),具体方法MSDN上有详细讲解,这里不多做赘述了。之后通过调用IDirect3DDevice9::SetTransform( D3DTRANSFORMSTATETYPE State,CONST D3DMATRIX *pMatrix )设置世界矩阵为你填好的那个。参数意义如下:
D3DTRANSFORMSTATETYPE State
代表你要设置的变换类型。D3DTS_WORLD,D3DTS_VIEW,D3DTS_PROJECTION分别表示要射知识界、视图、射影三种变换
CONST D3DMATRIX *pMatrix
指向一个矩阵结构的指针,就是你所要用到的矩阵。
后面的两个矩阵也要通过此函数设置。D3D中,三个变换矩阵是要存放在固定位置的,每次执行流水线,D3D就依次从这三个位置读取矩阵信息,并乘以所有的点,得到新的点的坐标,这个过程是不用我们操心的。我们调用SetTransform()就是要把填充好的矩阵放进这三个位置中的某一个,第一个参数表示了哪一个。
在设置视矩阵时,我们先要很清楚地(在脑子里或纸上)建立好“视坐标系”。这个坐标系以观察着为原点,沿着视线方向(观察着——注视点方向)为纵深方向(也就是Z轴方向)。仅有两个点还不足以确定一个三维坐标系,我们还需要一个参考点,能与另两个点构成某一个坐标平面。这样的坐标系构件起来后,就可以根据两个坐标系的变换填充视矩阵了。D3D提供了函数
D3DXMATRIX *D3DXMatrixLookAtLH(
D3DXMATRIX *pOut,
CONST D3DXVECTOR3 *pEye,
CONST D3DXVECTOR3 *pAt,
CONST D3DXVECTOR3 *pUp
);
或 D3DXMATRIX *D3DXMatrixLookAtLH( 参数同 ),区别仅在于前者用于左手系而后者用于右手系。该函数自动填充一个矩阵,参数依次是将要填充的矩阵以及上面说到的三个点,这里三个点构成视坐标系的YoZ平面。别忘了调用SetTransform()把这个矩阵交给D3D。经过上一步被统一了坐标的各个顶点将被这个矩阵转到视坐标中。
第三步要将点乘上一个射影矩阵,这个矩阵将越近的点放得越大。填充这个矩阵我们用函数
D3DXMATRIX *D3DXMatrixPerspectiveFovLH(
D3DXMATRIX *pOut,
FLOAT fovY,
FLOAT Aspect,
FLOAT zn,
FLOAT zf
);
或 D3DXMATRIX *D3DXMatrixPerspectiveFovLH( 参数同 ),区别同上面一样。第一个参数仍然是输出矩阵。第二个描述了在Y轴上的视角,弧度制表示,可以想象,视角越大,近端被抻拉的比例就越大。下一个参数是视图区的长宽比。后面两个参数就是最近视平面和最远视平面的位置,用它们的Z坐标(Z坐标的值在射影变换前后是不变的)表示。这两个平面的意义将在下一步说到。
最后说一下这条流水线的倒数第一步——剪切。剪切就是把理论上根本不该看到的点从渲染元中剔除掉(这里不包括因遮挡关系产生的图形的剪切以及隐面消除),用过DirectDraw的朋友很容易想到屏幕范围以外的就是这样的点。在3D世界里,还存在一个最近视平面和一个最远视平面,它们共同组成了一个视图截锥(Viewing Frustum)。对于这个东西,微软有个很好的说法:就好像你在一间黑屋子里向外看,窗户的四个边圈定了视图范围,并且窗户所在平面之前的物体是看不见的(黑屋子里的东西是看不见的),窗户所在的平面就是最近视平面;而且我们并不能看到无限远,总要有个最远视平面。这六个平面视可以根据需要设定的,它们组成了视截锥——下图中的蓝色范围。
可以想象,刚才进行的射影变换也可以说是把视图截锥这个棱台挤压成长方体的过程。读者还能发现,上述D3DXMatrixPerspectiveFovLH( )的参数实际上是描述视截锥的。你会觉得这个蓝色的东西很有用,它与射影变换以及剪切都有着异常紧密的联系。
以上,如图所示,就是一个顶点要被真正用于渲染所经历的四重门。笔者没有介绍多少算法,以及如何推导这几个矩阵。关于这些,网上有大量的文章可供参考,MSDN讲得更加详细,那些才是深入了解的工具,不过笔者相信读者朋友都有这个能力自己推导。本篇旨在阐述一些笔者认为比较重要的概念性问题,希望能给读者一个清晰的思路。欢迎大家来信与我讨论。