D3D9学习笔记之绘制流水线

什么是绘制流水线?

在给定了3D场景和指定了观察方向的虚拟摄像机的几何描述时,创建一副2D图像。
学习目标:

  • 了解在 Direct3D 中如何表示 3D 物体
  • 学习如何建立虚拟摄像机的模型
  • 理解绘制流水线—由3D场景的几何描述到生成2D图像的过程

模型表示:
物体或模型的集合叫做场景。任何物体都可以用三角网格逼近表示,三角网格是构建物体模型的基本单元。
我们通常不加区分的使用 多边形,图元,网格几何体来描述网格中的三角形。
Direct3D 除了支持三角形图元外也支持线和点图元。

顶点:
一个多边形中相邻两边的交汇点称为顶点。描述三角形单元时,需要指定该三角形单元的3个顶点的位置。而描述物体时,我们需要指定构成该物体的三角形单元列表(三角形单元集合)。

顶点格式:
在 Direnct3D 中,顶点包含上诉的位置信息之外还可以包含其他属性,例如顶点可以有颜色属性,也可以有法线属性,Direct3D 允许我们自行定义顶点的格式,即允许我们定义顶点的各分量。

创建自定义顶点格式:
我们首先需要创建一个包含了我们所希望具有的顶点数据的结构体。
例如:

//包含位置和颜色属性的自定义顶点格式
struct ColorVertex(
	float x,y,z;						//位置
	DWORD color;			
);

//包含位置,法线及纹理坐标的自定义顶点格式
struct NormalTexVertex(
	float x,y,z;						//位置
	float nx,ny,nz;						//法向量
	float u,v;							//纹理坐标
);

顶点定义好后,就需要使用灵活顶点格式标记的组合来描述顶点的组织结构。以上面两种为例,顶点格式可描述为:

//该宏定义的含义是对应于该顶点格式的顶点结构,包含了位置属性和漫反射颜色属性。
#define FVF_COLOR (D3DFVF_XYZ | D3DFVF_DIFFUSE) 

//对应于该顶点格式的顶点结构包含了位置,法线以及纹理坐标属性。
#define FVF_NORMAL_TEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1)

定义灵活顶点格式时必须遵循的一个约定是:灵活顶点格式标记的顺序必须与顶点结构中相应类型数据定义的顺序保持一致。
想了解完整的顶点格式标记列表,请查阅文档中 D3DFVF 相关部分。

三角形单元:
三角形单元是3D物体的基本组成部分。为了构建一个物体,需要创建一个描述物体形状和轮廓的三角形单元列表。三角形单元列表包含了我们所希望绘制的每个独立三角形的数据。例如,为了构建一个正方形,可将其分解为两个三角形单元,并指定每个三角形单元的顶点。
注意:三角形各顶点的指定顺序非常重要,称其为绕序。

索引:
通常,构成一个3D物体的众多三角形单元之间会共享许多公共顶点。模型的细节和复杂度增加时,出现重合顶点的数量会急剧增加。
为了解决该问题,引入了索引概念。其原理如下:我们创建一个顶点列表和一个索引列表,顶点列表包含了全部独立顶点,索引列表包含了指向顶点列表的索引(我理解为顺序或标记),这些索引规定了为构建三角形单元,各顶点应按何种方式来组织。

虚拟摄像机

摄像机指定了场景对观察者的可见部分,即我们将依据哪部分3D场景来创建2D图像。在世界坐标系中,摄像机有一定的位置和方向,定义了可见空间体积。
用几何学中的术语讲,摄像机的拍摄空间体积是一个平截头体。在3D 图形学中也可称为视域体或视截体,采用视域体的主要原因是显示屏为矩形。那些位于视域体之外的物体不可见,在进一步处理时就应将其丢弃。丢弃这类数据的运算过程称为裁剪。
投影窗口是一个2D区域,位于视域体中的3D几何体通过投影映射到该区域中,便创建了3D场景的2D表示。很重要的一点是,矩形投影窗口在X和Y方向坐标的最小值是min = (-1,-1),最大值是 max = (1,1)。
为了简化绘制工作,我们将近裁剪面和投影平面合二为一。注意:Direct3D 将投影平面定义为平面 z = 1。

绘制流水线

一旦建立了3D场景的几何描述,并设置好虚拟摄像机,下面的任务就是在显示器中建立该场景的 2D 表示。为实现这一目标所必须实施的一系列运算统称为绘制流水线。
绘制流水线的大致流程:
局部坐标系 ==>> 世界坐标系 ==>> 观察坐标系 ==>> 背面消隐 ==>> 光照 ==>> 裁剪 ==>> 投影 ==>> 视口坐标系 ==>> 光栅化
绘制流水线中,有几个阶段的任务是将几何体从一个坐标系变换至另一个坐标系。这类坐标变换是借助矩阵来实现的。Direct3D 可帮助我们进行这类运算。这对我们很有利,因为这类运算能得到显卡硬件设备的支持,游戏的速度会快很多。要想用 Direct3D 来完成坐标变换,我们必须做的是仅仅是提供描述坐标变换的变换矩阵。应用一个变换矩阵的方法是使用 IDirect3DDevice9->SetTransform 方法。该方法有两个参数,一个用来描述变换类型,另一个用来描述变换矩阵。
实施局部坐标系到世界坐标系的坐标变换:

Device->SetTransform(D3DTS_WORLD,&worldMatrix);

在绘制流水线中,将多次用到该方法,我们在每个阶段分别进行探究。

局部坐标系:
局部坐标系或建模坐标系,是用于定义构成物体的三角形单元列表的坐标系。
采用局部坐标系的优势体现在它可以简化建模的过程。在物体的局部坐标系中建模要比直接在世界坐标系中容易的多。
例如,局部坐标系允许我们构建物体时无需考虑位置,大小或相对于场景中其他物体的朝向。

世界坐标系:
创建各种物体模型时,每个模型都位于其自身的局部坐标系中。我们还需要将这些物体组织在一起构成世界坐标系中的场景。
位于局部坐标系中的物体通过一个称为世界变换的运算变换到世界坐标系中,该变换通常包括平移,旋转以及比例运算,分别用于设定该物体在世界坐标系中的位置,方向以及模型的大小。
世界变换依据位置,大小和方向的相对关系将所有物体放置在世界坐标系中。

世界变换用一个矩阵来表示,并由 Direct3D 通过 IDirect3DDevice9::SetTransform 方法来加以应用,该方法的第一个参数表示变换的类型,若要进行世界变换,可设为 D3DTS_WOLRD,第二个参数表示所采用的世界变换矩阵。
实施将换物体变换到世界坐标系中的某个点:

//构建只包含转换的立方体世界矩阵
D3DXMATRIX dx;
D3DXMatrixTranslation(&dx, -3.0f, 2.0f, 6.0f);
//构建只包含转换的球体世界矩阵
D3DSMATRIX dx1;
D3DXMatrixTranslation(&dx1, 5.0f, 0.0f, -2.0f);

//设置多维数据集的转换
Device->SetTransform(D3DTS_WORLD, &dx);
drawCube();										//绘制立方体

//既然球体使用了不同的世界变换,我们必须把世界转变成球体。
//不改变这个,球体将使用前面设置的立方体世界矩阵
Device->SetTransform(D3DTS_WORLD, &dx1);
drawSphere();									//绘制球体

实际应用中,物体往往需要同时做平移,方向和比例的运算,这样上面的例子就显得有些简单,但是它清楚的说明了世界变换的原理。

观察坐标系:
在世界空间中,几何体和摄像机都是相对于世界坐标系定义的,但是当摄像机的位置和朝向任意时,投影变换及其他类型的变换就略显困难或效率不高。
为了简化运算,我们将摄像机变换至世界坐标系的原点,并将其旋转,使摄像机的光轴与世界坐标系的 z 轴正方向一致,同时,世界空间中的所有几何体都随着摄像机一同进行变换,以保证摄像机的视场恒定。
这种变换称为取景变换,我们称变换后的几何体位于观察坐标系中。
取景变换矩阵(即观察矩阵)可由如下 D3DX 函数计算得到:

D3DXMATRIX* D3DXMatrixLookAtLH(
	D3DXMATRIX* pOut,					//接收结果视图矩阵的位置
	CONST D3DXVECTOR3* pEye,			//相机在世界上的地位
	CONST D3DXVECTOR3* pAt,				//点相机正在观察世界
	CONST D3DXVECTOR3* pUp				//世界上方向向量-(0,1,0)
);
  • pEye 指定了摄像机在世界坐标系中的位置。
  • pAt 指定了世界坐标系中的被观察点。
  • pUp 世界坐标系中表示“向上”方向的向量。

例,假定射线机位于(5,3,-10),其观察点为世界坐标系的原点(0,0,0),可以这样创建取景变换矩阵:

D3DXVECTOR3 dx1(5.0f, 3.0f, -10.0f);
D3DXVECTOR3 dx2(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 dx3(0.0f, 1.0f, 0.0f);

D3DXMATRIX V;
D3DXMatrixLookAtLH(&V, &dx1, &dx2, &dx3);

取景变换需要用 IDirect3DDevice9::SetTransform 方法来设定,其中对应于变换类型的参数需要指定为 D3DTS_VIEW:

Device->SetTransform(D3DTS_VIEW, &V);

背面消隐:
每个多边形都有两个侧面,将其中一个侧面标记为正面,另外一个侧面标记为背面。
通常,多边形的背面是不可见的,这是由于场景中多数物体都是封闭体,比如盒子,圆柱体,桶,字符等,而且摄像机总是禁止进入物体内部的实体空间的。所以,摄像机是不可能观察到多边形的背面的,这一点很重要,因为如果某一多边形的背面可见,背面消隐就失去了意义。
为了实现背面消隐,Direct3D 需要区分哪些多边形是正面朝向的,哪些是背面朝向的,默认状态下,Direct3D 认为顶点排列顺序为顺时针的三角形单元是正面朝向的,顶点排列顺序为逆时针的三角形单元是背面朝向的。
注意:上面我们强调的是在观察坐标系中,这是因为,如果一个多边形旋转180度,其绕序将完全相反,所以,一个在局部坐标系中定义为顺时针绕序的三角形单元经旋转变换到观察坐标系中时,未必仍保持顺时针绕序。
如果因为某些原因默认的消隐方式不能满足应用的要求,我们可通过修改绘制状态 D3DRS_CULLMODE 来达到目的。

Device->SetRenderState(D3DRS_CULLMODE,Value);

其中 Value 可取以下值:

  • D3DCULL_NONE 完全禁用背面消隐
  • D3DCULL_CW 只对顺时针绕序的三角形单元进行消隐
  • D3DCULL_CCW 默认值,只对逆时针绕序的三角形单元进行消隐

光照:
光源是在世界坐标系中定义的,但必须经取景变换至观察坐标系方可使用。
在观察坐标系中,光源照亮了场景中的物体,从而可以获得较为逼真的显示效果。

裁剪:
现在我们想将那些位于视域体外的几何体剔除掉,这个过程称为裁剪。
一个三角形单元与视域体的相对位置关系有以下3种。

  • 完全在内部 如果三角形单元完全在视域体内,便被保留并转向下一阶段的处理。
  • 完全在外部 如果三角形单元完全在视域体外部,将被剔除。
  • 部分在内(部分在外) 如果三角形单元与视域体的关系是部分在外部分在内,该单元将被分为两部分。位于视域体内的被保留,位于视域体外的将被剔除。

投影:
观察坐标系中,我们的任务是获取3D场景的2D表示,从 n 维变换为 n-1 维的过程称为投影。
实现投影有多种方式,但是我们只对其中的一种感兴趣,即透视投影,透视投影会产生 “视觉缩短” 的视觉效果,即近大远小。这类投影使得我们可以用2D图像表示3D场景。

投影变换定义了视域体,并负责将视域体中的几何体投影到投影窗口中。投影矩阵比较复杂,这里略去其推导过程。我们将使用如下的 D3DX 函数,其功能时依据视域体的描述信息创建一个投影矩阵。

D3DXMATRIX* D3DXMatrixPerspectiveFovLH(
	D3DXMATRIX & pOut,					//返回投影矩阵
	FLOAT fovY,							//弧度垂直视场
	FLOAT Aapect,						//宽高比=宽度/高度
	FLOAT zn,							//距近平面距离
	FLOAT zf							//到远平面的距离
);

上诉函数中值得一提的是纵横比参数,投影窗口中的几何体最终将变换到屏幕显示区,从方形到矩形的显示屏的变换会导致拉伸畸变。所谓纵横比就是显示屏纵横两维尺寸的比率,常用于校正由正方形到矩形的映射而引发的畸变。
纵横比 = 屏幕宽度 / 屏幕高度
投影矩阵的应用需要用方法 IDirect3DDevice::SetTransform 来实现,其中对应变换类型的参数需指定为 D3DTS_PROJECTION。
下面的例子说明了如何依据视域体的描述参数来创建投影矩阵,本例子中视域体的视域度为90度,近裁剪面到坐标原点的距离是1,远裁剪面到原点的距离是1000。

D3DXMATRIX proj;
D3DXMatrixPerspectiveFovLH(&proj, PI*0.5f, (float)width/(float)height, 1.0f, 1000.0f);
Device->SetTransform(D3DTS_PROJECTION, &proj);

注意:想深入了解投影矩阵请参考相关3D图形数学书籍

视口变换:
视口变换的任务是将顶点坐标从投影窗口转换到屏幕的一个矩形区域中,该矩形区域称为视口。
在游戏中,视口通常是整个矩形屏幕区域。但视口也可以是屏幕的一个子区域或称客户区。
矩形的视口是相对于窗口来描述的,因为视口总是处于窗口内部并且其位置要用窗口坐标来指定。
在 Direct3D 中,视口用结构 D3DVIEWPORT9 来表示,其定义如下:

typedef struct D3DVIEWPORT9{
	DWORD X;
	DWORD Y;
	DWORD Width;
	DWORD Height;
	DWORD MinZ;
	DWORD MaxZ;
} D3DVIEWPORT9;


  • 该结构前4个成员定义了视口矩形相对于其父窗口的位置及大小
  • MinZ 指定了深度缓存中的最小深度值
  • MaxZ 指定了深度缓存中的最大深度值
  • Direct3D 将深度缓存的深度范围设在 0-1 区间内,所以除非想要追求某种特性,MinZ 和 MaxZ 应限制在该指定区间内

一旦我们填充好 D3DVIEWPORT8 结构,就可这样来设置视口:

D3DVIEWPORT9 vp = {0, 0, 640, 480, 0, 1};
Device->SetViewport(& vp);

Direct3D 将自动为我们完成视口变换。但为参考起见,我们用下述矩阵来描述视口变换,该矩阵中的变量与 D3DVIEWPORT9 接口中的成员变量一致。

Width/2				0				0				0
   0			-Height/2			0				0
   0				0			MaxZ-MinZ			0
X+(Width/2)	  Y+(Height/2)		  MinZ				1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值