详解DX9下3D游戏编程2
详解DX9下3D游戏编程2 [2004-7-2] |
本章的主题就是渲染管道。它是用来创建为3D世界进行几何描述的2D图形并设定一个虚拟摄相机确定这个世界中哪一部分将被透视投影到屏幕上(好难说清呀!还是看图说话吧)。 图2.1:左图表示了多个3D世界的物体与一个摄相机的位置及其方向和可视范围,右图则为最终在屏幕上看见的结果。 学习目的: 1. 要搞清楚我们怎样在D3D中表示3D物体 2. 学习怎样模拟虚拟摄相机 3. 弄明白渲染管道的工作过程. 2.1 Model Representation(模型的显示) 一个场景即为多个物体或模型的集合。一个物体可以由三角形网格(triangle mesh)近似的表示,如图2.2所示。由三角形网格建立一个物体,我们称之为建模。也就是说在3D世界中,我们使用无数的小三角形来构造3D实体对象。3D世界中最基本的图元就是三角形,但是D3D也支持点图元和线图元但我们都不常用到。不过在学到第14章的粒子系统的时候,将会用到点图元。 图2.2:三角形形成的地形 一个多边形的两边相交的点叫做顶点。为了描述一个三角形,我们通常指定三个点的位置来对应三角形的三个顶点,这样我们就能够很明确的表示出这个三角形了。见图2.3。 图2.3:三个顶点定义一个三角形 2.1.1 Vertex Formats(顶点格式) 我们以前定义的点在数学上来说是正确的,但是当我们在D3D环境中使用它的时候就会觉得很不完善。这是因为D3D中的顶点包含了许多附加的属性,而不再单纯的只有空间位置的信息了。例如:一个顶点可以有颜色和法线向量属性(这两个属性分别在第四章和第五章介绍)。D3D让我们可以灵活的构造自己的顶点格式。换句话说,我们可以自己定义顶点的成分。 为了创建一个常规的顶点结构,我们首先要创建一个用来使顶点结构,它能存放我们想要存放的数据。例如,下面我们定放了两种顶点数据类型,一种包含了位置和颜色信息,第二种则包含了位置,法线向量,纹理坐标信息(第六章才会学习纹理)。 struct ColorVertex 一旦我们有了完整的顶点格式,我们就要使用灵活顶点格式(FVF)的组合标志来描述它。例如第一个顶点结构,我们要使用如下的顶点格式:{ float _x, _y, _z; // position DWORD _color; }; struct NormalTexVertex { float _x, _y, _z; // position float _nx, _ny, _nz; // normal vector float _u, _v; // texture coordinates }; #define FVF_COLOR (D3DFVF_XYZ | D3DFVF_DIFFUSE) 这表明它包含位置和颜色属性。 而第二种结构则要使用: #define FVF_NORMAL_TEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1) 这三个标志分别对应了位置,法线向量,纹理坐标的属性。 有一点要谨记,你的标志的顺序必须要和你的顶点结构的顺序一一对应。如果想知道所有的D3DFVF标志,请查阅SDK。 2.1.2 Triangles 三角形是构建3D物体的基本图形。为了构造物体,我们创建了三角形列(triangle list)来描述物体的形状和轮廓。三角形列包含了我们将要画的每一个三角形的数据信息。例如为了构造一个矩形,我们把它分成两个三角形,如图2.4所示,最后指定每个三角形的顶点。 Vertex rect[6] = {v0, v1, v2, // triangle0 v0, v2, v3}; // triangle1 注意:指定三角形顶点的顺序是很重要的,将会按一定顺序环绕排列,这会在2.3.4节学习相关的内容。 图2.4:两个三角形构成一个矩形 2.1.3 Indices 3D物体中的三角形经常会有许多共用顶点。如图2.4所表示的矩形。虽然现在仅有两个点被重复使用,但是当要表现一个更精细更复杂的模型的时候,重复的顶点数将会变得很大。例如2.5所示的立方体,仅有八个顶点,但是当用三角形列表示它的时候,所有的点都被重复使用。 图2.5:三角形定义的立方体 为了解决这个问题,我们引入了索引(indices)的概念。它的工作方式是:我们创建一个顶点列和一个索引列(index list)。顶点列包含所有不重复的顶点,索引列中则用顶点列中定义的值来表示每一个三角形的构造方式。回到那个矩形的示例上来,它的顶点列的构造方式如下: Vertex vertexList[4] = {v0, v1, v2, v3}; 索引列则定义顶点列中的顶点是如何构造三角形的: WORD indexList[6] = {0, 1, 2, // triangle0 0, 2, 3}; // triangle1 2.2 The Virtual Camera(虚拟摄相机) 摄相机确定3D世界中的哪部分是可见的因而需要将哪部分转换为2D图形。在3D世界中摄相机被放置和定向并且定义其视见体,图2.6展示了摄相机模式的简图。视见体是由可视角度和前裁剪面(Near Plane)与后裁剪面(Far Plane)定义一个平截头体。之所以要选择平截头体构造视见体,是因为我们的显示器都是矩形的。在视见体中不能被看见的物体都会被删除,删除这种数据的过程就叫做“裁剪”。 投影窗口(Projection Window)是视见体内的3D几何图形投影生成的用来显示3D图场的2D图像的2D区域。重要的是要知道,我们使用min=(-1,-1)和max=(1,1)来定义投影窗口的大小。 图2.6:用平截头体定义其视见体 为了简化本书接下来的部分绘制,我们使前裁剪面与投影窗口在同一平面上。并且,注意D3D定义投影平面(即投影窗口所在的平面)为Z = 1的平面。 2.3 The Rendering Pipeline 一旦我们描述几何学上的3D场景和设置了虚拟摄相机,我们要把这个场景转换成2D显示在显示器上。这一系列必须完成的操作就叫做渲染管道,又叫渲染流水线。图2.7展示了一个简化的渲染管道,随后将详细解释图中的每一级。 图2.7:一个有删节的渲染管道 渲染管道中的许多级都是从一个坐标系到另一个坐标的几何变换。这些变换都使用矩阵变换,D3D为我们进行变换计算并且如果显卡支持硬件变换的话那就更完美了。使用D3D进行矩阵变换,我们唯一要做的事就是提供变换矩阵。然后使用IDirect3DDevice9::SetTranform方法,它输入一个表示变换类型的参数和一个变换矩阵。如图2.7所示,为了进行一个从本地坐标系到世界坐标系的变换,我们可以这样写: Device->SetTransform(D3DTS_WORLD, &worldMatrix); 在下面的小节我们会了解到这个方法的更多细节。 2.3.1 Local Space(本地坐标系) 本地坐标系又叫做建模空间,这是我们定义物体的三角形列的坐标系。本地坐标系简化了建模的过程。在物体自己的坐标系中建模比在世界坐标系中直接建模更容易。例如,在本地坐标系中建模不像在世界坐标系中要考虑本物体相对于其他物体的位置、大小、方向的关系。 图2.7:一个茶壶在本地坐标系中的定义 2.3.2 World Space(世界坐标系) 一旦我们构造了各种模型,它们都在自己的本地坐标系中,但是我们需要把它们都放到同一个世界坐标系中。物体从本地坐标系到世界坐标系中的换叫做世界变换。世界变换通常是用平移、旋转、缩放操作来设置模型在世界坐标系中的位置、大小、方向。世界变换就是通过各物体在世界坐标系中的位置、大小和方向等相互之间的关系来建立所有物体。 图2.9:一些物体在同一世界坐标系中的相互关系 世界变换由一个矩阵表示,并且在D3D中调用IDirect3DDevice9::SetTranform方法设置它,记住将转换类型设为D3DTS_WORLD。例如我们要在世界坐标系中放置一个立方体定位在(-3,2,6)和一个球体定位在(5,0,-2),我们可以这样写程序: // Build the cube world matrix that only consists of a translation. D3DXMATRIX cubeWorldMatrix; D3DXMatrixTranslation(&cubeWorldMatrix, -3.0f, 2.0f, 6.0f); // Build the sphere world matrix that only consists of a translation. D3DXMATRIX sphereWorldMatrix; D3DXMatrixTranslation(&sphereWorldMatrix, 5.0f, 0.0f, -2.0f); // Set the cube’s transformation Device->SetTransform(D3DTS_WORLD, &cubeWorldMatrix); drawCube(); // draw the cube // Now since the sphere uses a different world transformation, we // must change the world transformation to the sphere’s. If we // don’t change this, the sphere would be drawn using the previously // set world matrix – the cube’s. Device->SetTransform(D3DTS_WORLD, &sphereWorldMatrix); drawSphere(); // draw the sphere 这个例程实在是太简单了,没有用到矩阵的旋转和缩放,这些也是很常用的,但是它至少展示了世界变换是怎样进行的。 2.3.3 View Space(观察坐标系) 世界坐标系中的几何图与摄相机是相对于世界坐标系而定义的,如图2.10所示。然而当摄相机在世界坐标系的任意位置和方向时,投影和其它一些操作会变得困难或低效。为了使事情变得更简单,我们将摄相机平移变换到世界坐标系的源点并把它的方向旋转至朝向Z轴的正方向,当然,世界坐标系中的所有物体都将随着摄相机的变换而做相同的变换,这样就可以使摄相机所看到的区域保持不变。这个变换就叫做观察坐标系变换(view space transformation)。 图2.10:世界坐标系到观察坐标系的变换。 观察坐标的变换矩阵可以通过如下的D3DX函数计算得到: D3DXMATRIX *D3DXMatrixLookAtLH( D3DXMATRIX* pOut , // pointer to receive resulting view matrix CONST D3DXVECTOR3* pEye , // position of camera in world CONST D3DXVECTOR3* pAt , // point camera is looking at in world CONST D3DXVECTOR3* pUp // the world’s up vector – (0, 1, 0) ); pEye参数指定摄相机在世界坐标系中的位置,pAt参数指定摄相机所观察的世界坐标系中的一个目标点,pUp参数指定3D世界中的上方向,通常设Y轴正方向为上方向,即取值为(0,1,0)。 例如:假设我们要把摄相机放在点(5,3,-10),并且目标点为世界坐标系的中点(0,0,0),我们可以这样获得观察坐标系变换矩阵: D3DXVECTOR3 position(5.0f, 3.0f, –10.0f); D3DXVECTOR3 targetPoint(0.0f, 0.0f, 0.0f); D3DXVECTOR3 worldUp(0.0f, 1.0f, 0.0f); D3DXMATRIX V; D3DXMatrixLookAtLH(&V, &position, &targetPoint, &worldUp); 观察坐标系变换也是通过IDirect3DDevice9::SetTranform来实现的,只是要将变换类型设为D3DTS_VIEW,如下所示: Device->SetTransform(D3DTS_VIEW, &V); 2.3.4 Backface Culling(背面拣选) 一个多边形有两个表面,我们将一个标为正面,一个为背面。通常,后表面总是不可见的,这是因为场景中大多数物体是密封的。例如盒子、圆柱体、箱子、字母(characters)等,并且我们也不能把摄相机放入物体的内部。因此摄相机永不可能看到多边形的背面。这就是背面拣选的先决条件,如果我们能看背面,那么背面拣选就不可能工作。 图2.11表示了一个物体在观察坐标系中的正面。一个多边形的边都是面向摄相机叫正面多边形,而一个多边形的边都背对摄相机叫背面多边形。 图2.11:一个物体的正面和背面多边形 由图2.11可知,正面多边形挡住了在它后面的背面多边形,D3D将通过拣选(即删除多余的处理过程)背面多边形来提高效率,这种方法就叫背面拣选。图2.12展示了背面拣选之后的多边形,从摄相机的观察点来看,仍将绘制相同的场景到后备表面,那些被遮住的部分无论如何都永远不会被看见的。 图2.12:经过背面拣选后的多边形 当然,为了完成这项工作,D3D需要知道哪个多边形是正面,哪个是背面。D3D中默认顶点以顺时针方向(在观察坐标系中)形成的三角形为正面,以逆时针方向形成的三角形为背面。 如果我们不想使用默认的拣选状态,我们可以通过改变D3DRS_CULLMODE来改变渲染状态: Device->SetRenderState(D3DRS_CULLMODE, Value); Value可以是如下一个值: 1. D3DCULL_NONE——完全不使用背面拣选 2. D3DCULL_CW——拣选顺时针环绕的三角形 3. D3DCULL_CCW——逆时针方向环绕的三角形会被拣选,这是默认值。 2.3.5 Lighting(光源) 光源定义在世界坐标系中然后被变换到观察坐标系中。观察坐标系中光源给物体施加的光照大大增加了场景中物体的真实性,至于光照的相关函数的细节将会在第五章学习。在本书的第四部分,我们将使用可编程管道实现自己的光照。 2.3.6 Clipping(裁剪) 我们拣选那些超出了视见体范围的几何图形的过程就叫做裁剪。这会出现三种情况: 1. 完全包含——三角形完全在视见体内,这会保持不变,并进入下一级 2. 完全在外——三角形完全在视见体外部,这将被拣选 3. 部分在内(部分在外)——三角形一部分在视见体内,一部分在视见体外,则三角形将被分成两部分,视见体内的部分被保留,视见体之外的则被拣选 图2.13展示了上面三种情况: 图2.13:视见体外的几何图形的裁剪 2.3.7 Projection(投影) 观察坐标系的主要任务就是将3D场景转化为2D表示。这种从n维转换成n-1维的过程就叫做投影。投影的方法有很多种,但是我们只对一种特殊的投影感兴趣,那就是透视投影。因为透视投影可以使离摄相机越远的物体投影到屏幕上后就越小,这可以使我们把3D场景更真实的转化为2D图像。图2.14展示了一个3D空间中的点是如何通过透视投影到投影窗口上去的。 图2.14 投影变换的实质就是定义视见体并将视见体内的几何图形投影到投影窗口上去。投影矩阵的计算太复杂了,这里我们不会给出推导过程,而是使用如下的D3D函数通过给出平截头体的参数来求出投影矩阵。 D3DXMATRIX *D3DXMatrixPerspectiveFovLH( D3DXMATRIX* pOut, // returns projection matrix FLOAT fovY, // vertical field of view angle in radians FLOAT Aspect, // aspect ratio = width / height FLOAT zn, // distance to near plane FLOAT zf // distance to far plane ); 图2.15:平截头体的主要成份 Aspect参数为投影平面的宽高比例值,由于最后都为转换到屏幕上,所以这个比例一般设为屏幕分辨率的宽和高的比值(见2.3.8节)。如果投影窗口是个正方形,而我们的显示屏一般都是长方形的,这样转换后就会引起拉伸变形。 我们还是通过调用IDirect3DDevice9::SetTranform方法来进行投影变换,当然,要把第一个投影类型的参数设为D3DTS_PROJECTION。下面的例子基于一个90度视角、前裁剪面距离为1、后裁剪面距离为1000的平截头体创建投影矩阵: D3DXMATRIX proj; D3DXMatrixPerspectiveFovLH( &proj, PI * 0.5f, (float)width / (float)height, 1.0, 1000.0f); Device->SetTransform(D3DTS_PROJECTION, &proj); 2.3.8 Viewport Transform(视口变换) 视口变换主要是转换投影窗口到显示屏幕上。通常一个游戏的视口就是整个显示屏,但是当我们以窗口模式运行的时候,也有可能只占屏幕的一部分或在客户区内。视口矩形是由它所在窗口的坐标系来描述的,如图2.16。 图2.16:一个视口矩形 在D3D中,视口矩形通过D3DVIEWPORT9结构来表示。它的定义如下: typedef struct _D3DVIEWPORT9 { DWORD X; DWORD Y; DWORD Width; DWORD Height; DWORD MinZ; DWORD MaxZ; } D3DVIEWPORT9; 前四个参数定义了视口矩形与其所在窗口的关系。MinZ成员指定最小深度缓冲值,MaxZ指定最大深度缓冲值。D3D使用的深度缓冲的范围是0~1,所以如果不想做什么特殊效果的话,将它们分别设成相应的值就可以了。 一旦我们填充完D3DVIEWPORT9结构后,就可以如下设视口: D3DVIEWPORT9 vp{ 0, 0, 640, 480, 0, 1 }; Device->SetViewport(&vp); 这样,D3D就会自动为我们处理视口变换。关于视口矩阵的具体信息请参考SDK。 2.3.9 Rasterization(光栅化) 这东西,学过图形学的人都知道,不知道的话随便找本图形学的书看看概念就可以了。 2.4 Summary(摘要) 1.3D物体是由无数的小三角形组成的 2.虚拟摄相机由一个平截头体模拟,并且这个平截头体的体积即为摄相机看见的 3.3D物体都是在本地坐标系定义的,然后被变换到世界坐标系。为了便于投影、拣选和其它一些操作,这个物体又被变换到一个摄相机的观察点为源点且视线为Z轴的观察坐标系。一旦完成变换,物体将被投影到投影窗口。视口变换即为从投影窗口到视口的变换。最后进行光栅化并最终以2D图形输出 附:本章无源码 |