实时阴影绘制技术研究

C++博客 首页 新随笔 联系 聚合 管理
  48 Posts :: 20 Stories :: 57 Comments :: 0 Trackbacks

GameRes游戏开发资源网 http://www.gameres.com

 

 

3D游戏程序设计入门(DirectX® 9.0

翁云兵

(2005.5.16更新)

声明:

l         本教程内容绝大部分来自Frank D. Luna所著的《Introduction to 3D Game Programming with DirectX 9.0》。教程内容(特别是语言表达上)大部分是我根据自己理解所写的,因此也不是此书的中文翻译版。

l         由于我的英文水平很差,翻译过来就难免有错了,望读者原谅。当然如你认为我的水平实在是让人无法忍受那么请直接查阅英文教程。

l         由于我的工作太忙且水平有限,计划一周一篇文章。希望读者能够支持我。给我多提意见。

l         此中文教程版权归我所有。

l         非商业应用可免费使用本教程。商业应用请同作者联系,EmailWengYB@126.com

特别感谢:

l         www.GameRes.com是他让我走上了游戏开发的道路。

l         一直关心支持我的同事、同学。

l         我最最亲爱的老婆,没有她我不可能写出这本教程。

第一部分 必备的数学知识

在这最开始的一部分中我们将介绍本书所要用到的数学知识。我们讨论的主题是向量,矩阵和相应的变换,当然还有一些有关面和线的内容。最开始阅读时这部分是可选的。

本教程对这些知识的讨论是很有限的,因此对于不同数学知识背景的读者来说都容易阅读。对于想了解更多更全的这方面信息的读者,请查看有关线性代数的书籍。当然已经学习过线性代数的读者也可将它作为有必要的复习内容来阅读。(这里推荐你看看《线性代数与空间解析几何》)

除此之外,我们还将展示D3DX类中相关的数学模型和执行特殊变换的函数。

目标

学习向量以及它们的3D计算机图形程序

学习矩阵以及学会使用它们来变换3D图形

学习怎样模拟面和线以及它们的3D图形程序

熟悉用于3D数学运算的D3DX库中包含的类和程序的子集

三维空间中的向量

几何学中,我们用有向线段表示向量,如图1。向量的两个属性是他的长度和他的顶点所指的方向。因此,可以用向量来模拟既有大小又有方向的物理模型。例如,以后我们要实现的粒子系统。我们用向量来模拟粒子的速度和加速度。在3D计算机图形学中我们用向量不仅仅模拟方向。例如我们常常想知道光线的照射方向,以及在3D世界中的摄象机。向量为在3维空间中表示方向的提供了方便。

1

向量与位置无关。有同样长度和方向的两个向量是相等的,即使他们在不同的位置。观察彼此平行的两个向量,例如在图1uv是相等的。

我们继续学习左手坐标系。图2显示的是左手坐标系和右手坐标系。两者不同的是Z轴的方向。在左手坐标系Z轴是向书的里面跑的而右手坐标系是向书的外边跑的。

2

因为向量的位置不能改变它的性质,我们能把所有向量平移使他们的尾部和坐标系的原点重合。因此,当一个向量在标准位置我们能通过头点来描述向量。图3显示的是图1中的向量在标准位置的样子。

3

我们通常用小写字母表示一个向量,但有时也用大写字母。如234维向量分别是:u = (ux, uy), N = (Nx, Ny, Nz), c = (cx, cy, cz, cw)。我们现在介绍3D中的4个向量,就4显示的。首先是都由含有0的零向量;它被表示成加粗的0 = (0, 0, 0)。接下来3个特殊的向量标准基向量。它们被叫做i, jk向量,分别沿着坐标系的x,y轴和z轴,并且有1的单位长:i = (1, 0, 0), j = (0, 1, 0), and k = (0, 0, 1)

注意:只有1个单位长度的向量叫做单位向量。

4

D3DX库中,我们能用D3DXVECTOR3类表示3维空间中的向量。它的定义是:

typedef struct D3DXVECTOR3 : public D3DVECTOR

{

public:

    D3DXVECTOR3() {};

    D3DXVECTOR3( CONST FLOAT * );

    D3DXVECTOR3( CONST D3DVECTOR& );

    D3DXVECTOR3( CONST D3DXFLOAT16 * );

    D3DXVECTOR3( FLOAT x, FLOAT y, FLOAT z );

 

    // casting

    operator FLOAT* ();

    operator CONST FLOAT* () const;

 

    // assignment operators

    D3DXVECTOR3& operator += ( CONST D3DXVECTOR3& );

    D3DXVECTOR3& operator -= ( CONST D3DXVECTOR3& );

    D3DXVECTOR3& operator *= ( FLOAT );

    D3DXVECTOR3& operator /= ( FLOAT );

 

    // unary operators

    D3DXVECTOR3 operator + () const;

    D3DXVECTOR3 operator - () const;

 

    // binary operators

    D3DXVECTOR3 operator + ( CONST D3DXVECTOR3& ) const;

    D3DXVECTOR3 operator - ( CONST D3DXVECTOR3& ) const;

    D3DXVECTOR3 operator * ( FLOAT ) const;

    D3DXVECTOR3 operator / ( FLOAT ) const;

 

    friend D3DXVECTOR3 operator * ( FLOAT, CONST struct D3DXVECTOR3& );

 

    BOOL operator == ( CONST D3DXVECTOR3& ) const;

    BOOL operator != ( CONST D3DXVECTOR3& ) const;

 

} D3DXVECTOR3, *LPD3DXVECTOR3;

注意D3DXVECTOR3是从D3DVECTOR继承的。它的定义是:

typedef struct _D3DVECTOR {

    float x, y, z;

} D3DVECTOR;

向量有它们自己的算法,就你在D3DXVECTOR3定义中看到的数学运算。现在你不需要知道它们怎么使用。以后介绍这些向量运算以及一些有用的函数和关于向量它们重要的详细资料。

注意:在3D图形程序中,虽然我们主要关心3D向量,但有时也会用到2D4D向量。在D3DX库中提供了D3DXVECTOR2D3DXVECTOR4类来分别表现2D4D向量。不同维数的向量有着和3D向量一样的性质,也就是它们描述大小和方向,仅仅是在不同的维数中。所有这些向量的数学运算对于不同维数向量都有效只是有一个除外,就是向量积。这些运算我们可通过论述3D向量扩展到2D, 4D甚至n维向量。

向量相等

几何学上,有同样方向和长度的两个向量相等。数学上,我们说有同样维数和分量的向量相等。例如:如果ux = vx, uy = vy, uz = vz.那么(ux, uy, uz) = (vx, vy, vz)。在代码中我们能够用“==”判断两个向量相等。

 

D3DXVECTOR u(1.0f, 0.0f, 1.0f);

D3DXVECTOR v(0.0f, 1.0f, 0.0f);

if( u == v ) return true;

同样的,我们也能用“!=”判断两个向量不相等。

if( u != v ) return true;

注意:当比较浮点数时,必须注意。因为浮点数不是精确的,我们认为相等的两个浮点数是有细微差别的;因此,我们测试它们近似相等。我们定义一个常数EPSILON,把它当作非常小的“buffer”。假如两个数和EPSILON相差很小我们说它们近似相等。换句话说,EPSILON让浮点数有一定的精度。接下来的实例函数是怎样用EPSILON比较两个浮点数相等。

bool Equals(float lhs, float rhs)

{

     // if lhs == rhs their difference should be zero

     return fabs(lhs - rhs) < EPSILON ? true : false;

}

当我们用D3DXVECTOR3类时不必担心,因为它已经帮我们处理了,但是在一般情况下适当注意比较两个浮点数是很重要的。

计算向量大小

几何学上,向量的大小是有向线段的长度。知道向量的分量,利用下面的公式我们就能计算出向量的大小。

u‖表示向量u的长度。例如:计算向量u = (1, 2, 3)v = (1, 1)的大小。

根据公式(1),我们得到:

我们利用D3DX库中下面的函数便能计算向量的大小。

FLOAT D3DXVec3Length( // Returns the magnitude.

CONST D3DXVECTOR3* pV // The vector to compute the length of.

);

D3DXVECTOR3 v(1.0f, 2.0f, 3.0f);

float magnitude = D3DXVec3Length( &v ); // = sqrt(14)

规范化向量

规范化向量是让向量的大小等于1,即被叫作单位向量。我们能利用向量大小以及各个分量把一个向量规范化,就这样:

我们这样表示单位向量û。如:规范化向量u = (1, 2, 3) v = (1, 1)

解答方法:根据(2)(3)我们得到‖u=14 和 ‖v=2,因此:

我们利用D3DX库中下面的函数能规范化向量。

D3DXVECTOR3 *D3DXVec3Normalize(

D3DXVECTOR3* pOut, // Result.

CONST D3DXVECTOR3* pV // The vector to normalize.

);

注意:这个函数返回一个指针因此它能作为一个参数传递给另一个函数。在极大程度上,除非非常特殊的,D3DX数学函数返回一个指针。我们不要轻易的说所有的函数都是这样。

向量相加

我们能够通过分别把两个向量的各个分量相加得到向量之和,注意在相加之前必须保证它们有相同的维数。

5显示的是几何学上的向量相加。

5

我们用重载加法操作符把两个向量相加的代码:

D3DXVECTOR3 u(2.0f, 0.0f, 1.0f);

D3DXVECTOR3 v(0.0f, -1.0f, 5.0f);

// (2.0 + 0.0, 0.0 + (-1.0), 1.0 + 5.0)

D3DXVECTOR3 sum = u + v; // = (2.0f, -1.0f, 6.0f)

向量相减

和加法类似,通过分别把两个向量的各个分量相减得到向量之差。再次重声两个向量必须是相同维数。

6显示的是几何学上的向量相减。

6

我们用重载减法操作符把两个向量相减的代码:

D3DXVECTOR3 u(2.0f, 0.0f, 1.0f);

D3DXVECTOR3 v(0.0f, -1.0f, 5.0f);

D3DXVECTOR3 difference = u - v; // = (2.0f, 1.0f, -4.0f)

6显示,向量减法得到一个从v向量终点到u向量终点的向量。假如我们解释uv的分量,我们能用向量相减找到从一个点到另一个点的向量。这是非常方便的操作因为我们常常想找到从一个点到另一个点的方向的向量。

数与向量的乘积

我们能用个数与向量相乘,就名字暗示的一样,向量按比例变化。这种运算不会改变向量的方向,除非我们用负数去操作,这样也只是方向相反了。

D3DXVECTOR3类提供了一个这种操作的操作符。

D3DXVECTOR3 u(1.0f, 1.0f, -1.0f);

D3DXVECTOR3 scaledVec = u * 10.0f; // = (10.0f, 10.0f, -10.0f)

点积

数学上定义点积是两个向量的乘积。按下面等式计算:

上面的等式不能很明显的体现几何上的意义。利用余弦定律,我们能够发现它们的关系。

u · v =|u||v|cosθ,表示两个向量的点积是它们的摸和夹角的余弦之积。因此,如果u v都是单位向量,那么u · v就是它们夹角的余弦。

一些点积有用的特性

■ 假如u · v = 0,那么uv

■ 假如u · v > 0,那么两个向量的角度θ小于90度。

■ 假如u · v < 0,那么两个向量的角度θ大于90度。

我们使用下面的D3DX函数计算两个向量的点积:

FLOAT D3DXVec3Dot( // Returns the result.

CONST D3DXVECTOR3* pV1, // Left sided operand.

CONST D3DXVECTOR3* pV2 // Right sided operand.

);

D3DXVECTOR3 u(1.0f, -1.0f, 0.0f);

D3DXVECTOR3 v(3.0f, 2.0f, 1.0f);

// 1.0*3.0 + -1.0*2.0 + 0.0*1.0

// = 3.0 + -2.0

float dot = D3DXVec3Dot( &u, &v ); // = 1.0

叉积

第二类乘法是向量的叉积。不象点积,用个数来乘,叉积是用另一个向量相乘。通过把两个向量uv相乘的到另一的向量p.uv两个向量通过十字相乘得到向量p,向量p垂直于uv。也就是说向量p垂直于u并且垂直于u

十字相乘这样计算:

7

如:发现j = k×i = (0, 0, 1)×(1, 0, 0) 并且j同时垂直于ki.

解答:

因此,j = (0, 1, 0).假如u · v = 0,那么uv,这又被称做“点积”。同样的如果j · k = 0并且j · i = 0那么我们便能知道j是既垂直于k又垂直于i的。

我们使用下面的D3DX函数计算两个向量的叉积:

D3DXVECTOR3 *D3DXVec3Cross(

D3DXVECTOR3* pOut, // Result.

CONST D3DXVECTOR3* pV1, // Left sided operand.

CONST D3DXVECTOR3* pV2 // Right sided operand.

);

从图7中我们很明显的得到向量–puv也都相互垂直。我们执行十字相乘返回的是p或者-p的结果。换句话说,u×v = (v×u)。这说明叉积是不可交换的。你能通过左手法则确定叉积返回的向量。按照第一个向量指向第二个向量弯曲你的左手,这时拇指所指的方向就是向量所指的方向。

矩阵

在这一部分我们关注的焦点是数学中的矩阵。它们在3D图形学中应用的程序将在下一部分讲解。

一个m×n的矩阵是由m行和n列的数字组成的矩阵列。行和列的数字就是这个矩阵的维数。我们通过写在下方的数字识别矩阵清单,数字中的第一个表示行第二个表示列。例如下边的M3×3矩阵,B2×4矩阵, C3×2矩阵。

我们使用加粗的大写字母表示矩阵。有时一个矩阵只包含一行或者一列。我们用行矩阵和列矩阵这个特殊的名称来称呼。例如下边就是行和列矩阵:

当使用行或列矩阵时,我们只用一个下标,有时我们还用字母表示。

相等、数乘矩阵以及相加

这部分我们将用到下边4个矩阵:

■假如两个矩阵维数和成员都相同那么它们就相等。例如,A = C 因为AC有同样的维数并且他们的成员都相等。AB同时AD因为他们的成员或者维数是不相同的。

我们能通过数与矩阵的每个成员相乘的到数与矩阵相乘。如矩阵Dk相乘:

假如k = 2,那么:

■当两个矩阵的维数相同时才能把它们相加。和是把两个矩阵相应的成员相加得到。如:

矩阵有加法当然也就有减法,前提是有相同的维数。矩阵减法如图所示:

乘法

矩阵相乘在3D计算机图形学中是非常重要的运算。通过矩阵相乘我们能变换向量把不同向量转换到一起。在下一部分变换将随处可见。

为了得到矩阵之积ABA的列数必须等于B的行数。假如这个条件不满足,就不能相乘。考虑下边两个矩阵,A B,分别是2×3 3×3,如:

我们看乘积AB是可以计算的,因为A的列数等于B的行数。注意乘积BA,它们是不能计算的,因为B的列数不等于A的行数。由此说明一般情况下矩阵乘法不满足乘法交换律(也就是, ABBA)。我们说“一般不可交换”因为一些实例说明有些矩阵乘法还是可以的。

知道了矩阵乘法的计算方法,现在我们就能给出精确的定义:假如A是一个m×n的矩阵B是一个n×p的矩阵,那么它们之积AB可计算并且是一个m×p 的矩阵C, C的成员ij 等于A的第iB的第j相乘:

例如,求解:

我们检查知道乘法是可计算的,因为A的列数等于B的行数。也知道计算的结果是一个2×2的矩阵。根据公式(4),我们得到:

作为练习,检查ABBA

更一般的例子:

单位矩阵

有一种特殊矩阵叫做单位矩阵。单位矩阵是除了对角(左上到右下)以外所有成员都是0,对角都是1的方矩阵。例如,下边是2×2, 3×3, 4×4的单位矩阵:

单位矩阵有如下特性:

MI = IM=M

即,用单位矩阵乘以矩阵不会改变矩阵。此外,这是一个特例:用单位矩阵进行乘法运算满足乘法交换律。单位矩阵可以看作矩阵运算中的数字“1”。

例如:验证2×2矩阵M与单位矩阵相乘得到的结果是M

逆转

下面列举了关于矩阵的重要信息。

只有正方形的矩阵(方阵)才能求逆,因此当我们说矩阵求逆,那么它就是方矩阵。

n×n矩阵M的逆矩阵是一个n×n矩阵表示为M–1

不是每个方矩阵都有矩阵

矩阵和他的矩阵相乘得到一个单位矩阵:M M–1 = M–1M = I注意当我们进行这样的操作时矩阵是可交换的。

矩阵用来解决与其他矩阵相等是非常有用的。例如,考虑等式p= pR 并且假设我们知道pR想求p。首先找到R–1,一旦求得R–1,我们便能求出p,就这样:

矩阵的方法已经超出了本书的范围,但是这在任何一本线性代数书上都有讲解。这部分已经标明了是“基本变换”我们给出它是因为我们要用矩阵细节。在“D3DX 矩阵”部分我们将学习一个为我们求矩阵的D3DX函数。

我们介绍几个有用的推论:(AB) –1 = B–1 A–1。这个性质前提是假定AB都能求并且它们都是有相同维数的方矩阵。

矩阵的转置

矩阵的转置是相互交换矩阵的行和列。因而,m×n的矩阵的转置是一个n×m的矩阵。我们把矩阵M的转置记作MT

例如:求下面两个矩阵的转置:

重声一下,转置是交换矩阵的行和列。

因此:

D3DX 矩阵

当设计Direct3D应用程序时,我们专门特别使用4×4矩阵和1×4行矩阵(向量)。注意使用这两种矩阵意味着可以进行矩阵乘法。

向量-矩阵乘法。即,假如1×4的行矩阵(向量)v4×4的矩阵T,那么积vT可计算并且返回的结果是一个1×4的行矩阵(向量)。

矩阵-矩阵乘法。即,假如4×4的矩阵T4×4的矩阵R,那么积TRRT可计算并且两者返回的结果都是一个4×4的矩阵。注意因为矩阵乘法不满足交换律所以TRRT不一定相等。

D3DX中表示1×4的行矩阵(向量),我们用D3DXVECTOR3D3DXVECTOR4向量类。当然D3DXVECTOR3只有3个成员,不是4个。然而,第4个成员缺省是10(在下一部分有更多信息)。

D3DX中表示4×4的矩阵,我们用D3DXMATRIX类,定义如下:

typedef struct D3DXMATRIX : public D3DMATRIX {

public:

    D3DXMATRIX() {};

    D3DXMATRIX( CONST FLOAT * );

    D3DXMATRIX( CONST D3DMATRIX& );

    D3DXMATRIX( FLOAT _11, FLOAT _12, FLOAT _13, FLOAT _14,

                FLOAT _21, FLOAT _22, FLOAT _23, FLOAT _24,

                FLOAT _31, FLOAT _32, FLOAT _33, FLOAT _34,

                FLOAT _41, FLOAT _42, FLOAT _43, FLOAT _44 );

    // access grants

    FLOAT& operator () ( UINT Row, UINT Col );

    FLOAT  operator () ( UINT Row, UINT Col ) const;

    // casting operators

    operator FLOAT* ();

    operator CONST FLOAT* () const;

    // assignment operators

    D3DXMATRIX& operator *= ( CONST D3DXMATRIX& );

    D3DXMATRIX& operator += ( CONST D3DXMATRIX& );

    D3DXMATRIX& operator -= ( CONST D3DXMATRIX& );

    D3DXMATRIX& operator *= ( FLOAT );

    D3DXMATRIX& operator /= ( FLOAT );

    // unary operators

    D3DXMATRIX operator + () const;

    D3DXMATRIX operator - () const;

    // binary operators

    D3DXMATRIX operator * ( CONST D3DXMATRIX& ) const;

    D3DXMATRIX operator + ( CONST D3DXMATRIX& ) const;

    D3DXMATRIX operator - ( CONST D3DXMATRIX& ) const;

    D3DXMATRIX operator * ( FLOAT ) const;

    D3DXMATRIX operator / ( FLOAT ) const;

    friend D3DXMATRIX operator * ( FLOAT, CONST D3DXMATRIX& );

    BOOL operator == ( CONST D3DXMATRIX& ) const;

    BOOL operator != ( CONST D3DXMATRIX& ) const;

} D3DXMATRIX, *LPD3DXMATRIX;

D3DXMATRIX类是从单数结构D3DMATRIX继承的复数形式。D3DMATRIX的定义是:

typedef struct _D3DMATRIX {

    union {

        struct {

            float        _11, _12, _13, _14;

            float        _21, _22, _23, _24;

            float        _31, _32, _33, _34;

            float        _41, _42, _43, _44;

        };

        float m[4][4];

    };

} D3DMATRIX;

观察D3DXMATRIX类发现有很多有用的运算符,比如对矩阵检测相等,相加和相减,与数相乘,铸造,以及非常重要的两个D3DXMATRIXs彼此相乘。因为矩阵相乘是非常重要的,我们给出一段实例代码:

D3DXMATRIX A(); // initialize A

D3DXMATRIX B(); // initialize B

D3DXMATRIX C = A * B; // C = AB

D3DXMATRIX另一个重要的运算符是圆括号,它允许我们非常方便的为矩阵成员赋值。注意当使用圆括号时我们的下标就C语言数组下标一样是从0开始的。例如,为一个矩阵的ij = 11 赋值,我们写成:

D3DXMATRIX M;

M(0, 0) = 5.0f; // Set entry ij = 11 to 5.0f.

D3DX库也提供下列有用的函数:将D3DXMATRIX转化为单位矩阵,转置D3DXMATRIX矩阵以及求逆矩阵。

D3DXMATRIX *D3DXMatrixIdentity(

     D3DXMATRIX *pout // The matrix to be set to the identity.

);

D3DXMATRIX M;

D3DXMatrixIdentity( &M ); // M = identity matrix

 

 

D3DXMATRIX *D3DXMatrixTranspose(

     D3DXMATRIX *pOut, // The resulting transposed matrix.

     CONST D3DXMATRIX *pM // The matrix to take the transpose of.

);

 

D3DXMATRIX A(...); // initialize A

D3DXMATRIX B;

D3DXMatrixTranspose( &B, &A ); // B = transpose(A)

 

D3DXMATRIX *D3DXMatrixInverse(

     D3DXMATRIX *pOut, // returns inverse of pM

     FLOAT *pDeterminant, // determinant, if required, else pass 0

     CONST D3DXMATRIX *pM // matrix to invert

);

假如我们将不能求逆的矩阵用求逆函数,那么函数将会返回null.同样的,这本书我们忽视第二个参数,并且总是把它设置为0

D3DXMATRIX A(...); // initialize A

D3DXMATRIX B;

D3DXMatrixInverse( &B, 0, &A ); // B = inverse(A)

基本变换

当用Direct3D编程时,我们使用4×4矩阵来进行矩阵变换。用它的原因是:我们设置一个4×4矩阵X是为了更精确的描述矩阵变换。同样我们设置一个相匹配的点或者把向量的分量放置到一个1×4的行矩阵v中。乘积vX返回一个新的向量v。例如:让X沿着x轴平移10个单位同时v = [2, 6, –3, 1],乘积vX = v= [12, 6, –3, 1]

有一些东西需要阐明。我们使用4×4矩阵是因为这样的大小能表现我们需要的所有变换。最初看来一个3×3好象更适合3D。然而这里有很多种我们喜欢用的变换是不能用一个3×3的矩阵来表示的,比如平移、投影、反射。我们使用向量-矩阵相乘来工作,因此我们至少要通过一个矩阵乘法来完成相应的变化。增大到4×4的矩阵,它允许我们用一个矩阵描述更多的变换并且向量-矩阵乘法是可行的。

我们说过把一个相匹配的点或者一个向量的成员放置到一个1×4的行矩阵中。但是点和向量是3D的!为什么我们要用一个1×4的行矩阵呢?我们必须把3D/向量增大为4D的行矩阵是为了让向量-矩阵乘法可定义—1×3的行矩阵和4×4的矩阵相乘是不被定义的。

那么,我们怎么使用第四个成员(我们用w来表示)呢?当我们把一个点放置到一个1×4的行矩阵中时,我们设置w1。允许对点进行适当的平移。因为向量和位置无关,向量的平移是不被定义的,如果试图这样做会返回一个无意义的向量。为了防止对向量进行平移,当在把一个点放置到一个1×4行矩阵中时我们把w设置为0。例如:把点p = (p1, p2, p3)放置到一个行向量中就这样[p1, p2, p3, 1],同样把向量v = (v1, v2, v3) 放置到一个行向量中就这样[v1, v2, v3, 0]

注意:我们设置w = 1是为了让点可以被恰当的移动,同样我们设置w = 0是为了防止向量被平移。当我们检查矩阵实际平移时这是一个非常清晰的模型。

有时一个矩阵变换时我们改变向量成员w的值,即w0 w1。考虑下边例子:

因为p30 p31

我们注意w =p3。当w0 w1时,我们说我们有一个向量在同类空间中,与3维空间中的向量是相对的。我们能通过把向量的每个分量与w相除将同类空间中的向量映射到3维空间中来。例如把同类空间中向量(x, y, z, w) 映射到3维空间中的向量x,我们这样做:

在同类空间中使用然后把它映射到3维空间中来,是被用在3D图形程序设计中作透视图。

矩阵平移

8

       我们能通过与下面的矩阵相乘把向量(x, y, z, 1)沿x轴移动px单位,沿y轴移动py 单位,沿z轴移动pz单位:

将矩阵平移的D3DX函数是:

D3DXMATRIX *D3DXMatrixTranslation(

     D3DXMATRIX* pOut, // Result.

     FLOAT x, // Number of units to translate on x-axis.

     FLOAT y, // Number of units to translate on y-axis.

     FLOAT z // Number of units to translate on z-axis.

);

练习:T(p)做为一个平移变换矩阵,v = [v1, v2, v3, 0]是也任意向量。验证vT(p) = v(即,假如w = 0,验证通过平移不会改变向量)。

平移矩阵求逆只需要简单的将向量p取反即可

矩阵旋转

9

       我们能用下面的矩阵把一个向量围绕x,y z轴旋转δ弧度。注意:当我们俯视绕轴原点时,角度是指顺时针方向的角度。

将矩阵饶着x轴旋转的D3DX函数是:

D3DXMATRIX *D3DXMatrixRotationX(

     D3DXMATRIX* pOut, // Result.

     FLOAT Angle // Angle of rotation measured in radians.

);

将矩阵饶着y轴旋转的D3DX函数是:

D3DXMATRIX *D3DXMatrixRotationY(

     D3DXMATRIX* pOut, // Result.

     FLOAT Angle // Angle of rotation measured in radians.

);

将矩阵饶着z轴旋转的D3DX函数是:

D3DXMATRIX *D3DXMatrixRotationZ(

     D3DXMATRIX* pOut, // Result.

     FLOAT Angle // Angle of rotation measured in radians.

);

旋转矩阵R的逆矩阵等于它的转置矩阵RT= R-1。这样的矩阵我们说它是互相垂直的。

矩阵缩放

10

我们能通过与下面的矩阵相乘把向量沿x轴缩放qx单位,沿y轴缩放qy 单位,沿z轴缩放qz单位:

将矩阵缩放的D3DX函数是:

D3DXMATRIX *D3DXMatrixScaling(

     D3DXMATRIX* pOut, // Result.

     FLOAT sx, // Number of units to scale on the x-axis.

     FLOAT sy, // Number of units to scale on the y-axis.

     FLOAT sz // Number of units to scale on the z-axis.

);

缩放矩阵求逆只需要将每个缩放因子取倒即可:

综合变换

       常常我们要对一个向量进行一系列的变换。比如,我们可能先缩放一个向量,然后旋转它,最后把它平移到指定的位置。

例如:先把向量p = [5, 0, 0, 1] 在所有轴上缩小为原来的1/5,然后沿着y轴旋转π/4,最后把它在x轴上移动1个单位,在y轴上移动2个单位,在z轴上移动3个单位。

解答:注意我们必须完成缩放,沿y轴旋转,以及移动。我们设缩放、旋转、移动的变换矩阵分别是S, Ry, T,如下:

应用缩放,旋转,以及平移一系列变换,我们得到:

我们能用矩阵乘法把几个变换矩阵转换成一个矩阵,它是非常有益的矩阵。比如,重新考虑这部分开始的例子。通过使用矩阵相乘把3个变换矩阵合成一个矩阵。注意我们必须按实际应用的顺序来进行矩阵相乘。

那么 pQ = [1.707, 2, –3.707, 1]

联合变换有提高效率的能力。假如我们需要对一组数量巨大的向量(在3D图形任务中是很普遍的)进行同样的缩放,旋转以及移动变换。替换这一系列的变换,即就等式(5)中对每一个向量的做法,我们能把所有3个变换转换到一个矩阵中,即就在等式(6)中的做法。这样我们只需要对每一个向量进行一次乘法就可以实现3种变换。这就减少了大量的向量-矩阵乘法操作。

一些向量变换函数

D3DX库分别提供了下边两个对点和向量的变换函数。D3DXVec3TransformCoord函数变换点同时设置向量第4个成员为1D3DXVec3TransformNormal函数变换向量并且设置第4个成员为0

D3DXVECTOR3 *D3DXVec3TransformCoord(

         D3DXVECTOR3* pOut, // Result.

         CONST D3DXVECTOR3* pV, // The point to transform.

         CONST D3DXMATRIX* pM // The transformation matrix.

);

 

D3DXMATRIX T(...); // initialize a transformation matrix

D3DXVECTOR3 p(...); // initialize a point

D3DXVec3TransformCoord( &p, &p, &T); // transform the point

 

 

D3DXVECTOR3 *WINAPI D3DXVec3TransformNormal(

         D3DXVECTOR3 *pOut, // Result.

         CONST D3DXVECTOR3 *pV, // The vector to transform.

         CONST D3DXMATRIX *pM // The transformation matrix.

);

 

D3DXMATRIX T(...); // initialize a transformation matrix

D3DXVECTOR3 v(...); // initialize a vector

D3DXVec3TransformNormal( &v, &v, &T); // transform the vector

注意:D3DX库也提供D3DXVec3TransformCoordArrayD3DXVec3TransformNormalArray来分别变换一个点数组和向量数组

平面

一个平面能通过一个向量n和平面上的一个点p0来描述。这个向量n垂直于平面,它被称为此平面的法向量(如图11)。

11

在图12中我们能够发现平面上任意一点p都满足如下等式。即:假如pp0都是平面上一点,那么向量(p - p0)垂直于平面的法向量。

12

当我们通过法向量n和平面上一固定点来描述一个平面时,等式(7)又被写成这样:

这时d = –n·p0

D3DX平面

在代码中描述一个平面:仅仅需要一个法向量n和常数d就可以了。因此我们就使用一个4D向量(我们记录成(n, d))来实现它。D3DX库中用如下的结构来定义一个平面:

typedef struct D3DXPLANE

{

#ifdef __cplusplus

public:

     D3DXPLANE() {}

     D3DXPLANE( CONST FLOAT* );

     D3DXPLANE( CONST D3DXFLOAT16* );

     D3DXPLANE( FLOAT a, FLOAT b, FLOAT c, FLOAT d );

     // casting

     operator FLOAT* ();

     operator CONST FLOAT* () const;

     // unary operators

     D3DXPLANE operator + () const;

     D3DXPLANE operator - () const;

     // binary operators

     BOOL operator == ( CONST D3DXPLANE& ) const;

     BOOL operator != ( CONST D3DXPLANE& ) const;

#endif //__cplusplus

     FLOAT a, b, c, d;

} D3DXPLANE, *LPD3DXPLANE;

对照等式(8)可知:这里a, bc是平面法向量n的成员,d就是那个常数。

点和平面的空间关系

我们判定点和平面的关系主要是利用等式(8)来实现。例如,假设平面(n, d),我们能判定点p和平面的关系

    假如n·p + d = 0,那么点p与平面共面。

    假如n·p + d >0,那么点p平面的前面且在平面的正半空间里。

    假如n·p + d <0,那么点p平面的背面且在平面的负半空间里。

下边的D3DX函数就是利用n·p + d 来判定点和平面的关系的函数:

FLOAT D3DXPlaneDotCoord(

     CONST D3DXPLANE *pP, // plane.

     CONST D3DXVECTOR3 *pV // point.

);

// Test the locality of a point relative to a plane.

D3DXPLANE p(0.0f, 1.0f, 0.0f, 0.0f);

D3DXVECTOR3 v(3.0f, 5.0f, 2.0f);

float x = D3DXPlaneDotCoord( &p, &v );

if( x approximately equals 0.0f ) // v is coplanar to the plane.

if( x > 0 ) // v is in positive half-space.

if( x < 0 ) // v is in negative half-space.

创建平面

我们能通过两种方法创建平面。

第一种方法,直接用指定法线和点创建平面。假设法线n和在平面上的已知点p0,我们就能求出d

D3DX库提供如下函数来完成创建平面的任务:

D3DXPLANE *D3DXPlaneFromPointNormal(

     D3DXPLANE* pOut, // Result.

     CONST D3DXVECTOR3* pPoint, // Point on the plane.

     CONST D3DXVECTOR3* pNormal // The normal of the plane.

);

第二种方法,我们能通过在平面上的3个点创立一个平面。

假如有点p0, p1, p2,那么我们就能得到平面上的两个向量:

因此我们能通过把平面上的两个向量进行十字相乘得到平面的法线。回忆左手坐标系。

那么–(n·p0) = d.

D3DX库提供如下函数来完成通过同一平面上的3个点确定一个平面:

D3DXPLANE *D3DXPlaneFromPoints(

     D3DXPLANE* pOut, // Result.

     CONST D3DXVECTOR3* pV1, // Point 1 on the plane.

     CONST D3DXVECTOR3* pV2, // Point 2 on the plane.

     CONST D3DXVECTOR3* pV3 // Point 3 on the plane.

);

规范化平面

有时我们可能想规范化一个平面的法向量即规范化平面。初一想,好象我们只需规范化其他向量一样规范化平面的法向量就可以了。但是回忆在等式n·p + d = 0中的d = –n·p0。我们明白法向量的长度将影响常数d因此,假如我们规范化法向量,我们必须重新计算d.注意

因此,我们有下边公式来规范化平面(n, d)的法向量:

我们能用下面的D3DX函数来规范化一个平面:

D3DXPLANE *D3DXPlaneNormalize(

     D3DXPLANE *pOut, // Resulting normalized plane.

     CONST D3DXPLANE *pP // Input plane.

);

变换平面

我们能够通过如下处理来变换一个面(n, d),就一个4D向量通过乘以它渴望得到变换的变换矩阵的逆矩阵一样来达到变换目的。(哎,好难说清楚,还是看例子吧。)注意平面的法向量必须首先被规范化。

我们能用下面的D3DX函数来完成操作:

D3DXPLANE *D3DXPlaneTransform(

     D3DXPLANE *pOut, // Result

     CONST D3DXPLANE *pP, // Input plane.

     CONST D3DXMATRIX *pM // Transformation matrix.

);

示例代码:

D3DXMATRIX T(...); // Init. T to a desired transformation.

D3DXMATRIX inverseOfT;

D3DXMATRIX inverseTransposeOfT;

D3DXMatrixInverse( &inverseOfT, 0, &T );

D3DXMatrixTranspose( &inverseTransposeOfT, &inverseOfT );

D3DXPLANE p(...); // Init. Plane.

D3DXPlaneNormalize( &p, &p ); // make sure normal is normalized.

D3DXPlaneTransform( &p, &p, &inverseTransposeOfT );

点到平面上最近的点

假如我们在空间中有一个点p并且想找到在平面( n, d)上的与p最接近一个点q。注意假定平面的法向量是单位长度—这将简化问题。

13

从图13我们能看出q = p + (k_n)k是有符号之分的从点p到平面的距离,也就是点pq之间的有向距离。假如平面的法向量n是单位长度,那么n·p + d 就是从平面到点p有向距离.

射线(可选的)

设想在游戏中的一个玩家,正用他的枪射击敌人。我们怎么判断子弹是否从一个位置击中另一个位置的目标?一个方法是用一条射线模拟子弹,用一个球体模型模拟敌人。(球体模型只是一个球体,它紧紧的围绕一个物体,从而粗略地表示它的大小。球体模型将在第11章中做更详细的介绍。)那么通过计算我们就能够判定是否射中球体。在这部分我们学习射线的数学模型。

射线

一条射线能用一个起点和方向来描述。射线的参数方程是:

14

p0 是射线的起点,u是射线的方向,t是参数。通过赋予不同的t值,我们能计算出在射线上不同的点。要描述一条射线,参数t范围就必须在[0, )之间。实际上,假如我们让t(–, ),那么我们就能得到一条3维空间直线。

线/面相交

假设一条射线p(t) = p0 + tu 和 一个平面n·p + d = 0,我们想知道射线是否与平面相交以及相交的交点信息(如果相交的话)。照这样做,我们把射线代入平面方程并且求满足平面方程的参数t解答出来的参数就是相交的点。

把等式(9)代入平面方程:

假如t 不在[0, )之间,那么射线与平面不相交。

假如t [0, )之间,那么射线与平面相交。且把参数代入射线方程就能找到交点:

摘要(略)

第一部分完

 

第二部分 Direct3D基础

第一章     初始化Direct3D

以前Direct3D的初始化一直是一项单调乏味的工作。幸运的是8.0版本简化了初始化模式并且DX9.0也使用和它相同的模式。然而,在这个过程中仍需要程序员熟知图形学的基础知识和D3D的基本类型,本章的前几节将讲述这方面的内容。在余下的部分里将解释初始化的过程。

目标

学习D3D怎样与图形硬件相互作用

弄懂COMD3D中所扮演的角色

学习基础图形学知识,如2D图片是如何存储的、页面切换和深度缓冲

学习如何初始化D3D

熟悉本书例程中的一些常用的结构

1.1 Direct3D概述

Direct3D是一种低层图形API,它能让我们利用3D硬件加速来渲染3D世界。我们可以把Direct3D看作是应用程序和图形设备之间的中介。例如通知图形设备清空屏幕,应用程序将调用Direct3DIDirect3DDevice9::Clear方法。图1.1显示了应用程序、Direct3D和图形设备之间的关系。

1.1

1.1Direct3D所表示的是Direct3D已定义的供程序员使用的Direct3D接口和函数的集合。这些接口和函数代表了当前版本的Direct3D所支持的全部特性。注意:仅仅因为Direct3D支持某种特性,并不意味着你所使用的图形硬件(显卡)也能支持它。

如图1.1所示,在Direct3D和图形设备之间有一层中介——叫做硬件抽象层(HALHardware Abstraction Layer)。Direct3D不能直接作用于图形设备,因为现在市面上的显卡种类实在是太多了并且每种显卡都有不同的性能和处理事件的方式。例如,两种不同的显卡实现清屏的方式也可能是不同的。因此,Direct3D要求设备制造商实现HALHAL是一组指示设备执行某种操作的特殊设备代码的集合。用这种方法,Direct3D避免了必须去了解某个设备的特殊细节,使它能够独立于硬件设备。

设备制造商在HAL中实现他们的产品所支持的所有特性。HAL将不会实现那些Direct3D支持但硬件产品不支持的特性。调用一个HAL中没有实现的Direct3D的函数将会出错,除非它是顶点处理操作,因为这个功能可以由软件模拟来实现。因此当使用某些仅由市面上少数显卡所支持的高级特性时,必须检测一下设备是否支持。(设备的功能将在1.3.8节中讲解)

1.1.1 REF Device

你也许想把一些你的设备不支持的Direct3D函数写入程序。为了达到这个目的,Direct3D提供了REF Device,它用软件模拟了所有的Direct3D API。这允许你写并测试那些你的显卡不支持的Direct3D特性的代码。例如在本书的第四部分,某些人的显卡可能会不支持顶点和像素着色器。如果你的显卡不支持着色器,你仍然能够使用REF Device测试示例代码。懂得REF Device仅仅是为了发展这是很重要的。它只会和DirectX SDK一起被装载,而不会发布给最终用户。 另外,REF Device实在是太慢了,除了测试以外它没有任何利用价值。

1.1.2 D3DDEVTYPE

在代码中,我们用D3DDEVTYPE_HAL来定义HAL Device,它是D3DDEVTYPE枚举类型的一个成员。同样的,REF Device则由D3DDEVTYPE_REF来定义,它也属于D3DDEVTYPE枚举类型。记住这些类型很重要,因为在创建设备的时候我们需要指定我们将要使用的类型。

1.2 COM

组件对象模型(COM, Component Object Model)是一种能使DirectX独立于编程语言和具有向下兼容性的技术。我们通常把COM对象作为一个接口,你可以把它当作达到某种目的的C++类来使用它。当使用C++DirectX程序的时候,COM的大部分细节对我们来说是透明。但是有一件事,我们必须知道,那就是我们通过某个特殊的COM接口的函数或指针获得了另一个COM接口指针,而不是通过C++的新关键字来创建它。当我们使用完某个接口后,调用它的Release方法比直接Delete它更好。COM对象具有它们自己的内存管理。

COM来说还有很多细节可以了解,但是掌握这些细节对于我们有效的使用DirectX是必须的。

注意:COM接口都具有前缀大写字母“I”,例如表示一个表面的COM接口叫做IDirect3DSurface9

1.3 一些准备工作

Direct3D的初始化过程要求我们对图形学基础知识和Direct3D类型有一定了解。本节将介绍这些知识和类型以确保下一节能把焦点集中在讨论Direct3D初始化上。

1.3.1 表面

表面是一个像素点阵,在Direct3D中主要用来存储2D图形数据。图1.2指明了表面的一些成分。由图可以看出表面数据就像一个矩阵,像素数据实际上存储在线性数组里面。

1.2

表面的WidthHeight是按像素计算的。Pitch以字节为单位。而且Pitch有可能比Width大且依赖于低层硬件,所以不能单纯的认为Pitch = Width * sizeof (pixelFormat)

在代码中,我们可以使用IDirect3DSurface9接口来描述表面。这个接口提供若干方法来直接读写表面数据并且还有一个方法用来返回表面信息。IDirect3DSurface9中最重要的方法是:

l         LockRect—使用这个方法,我们将获得一个指向表面内存的指针,然后,通过一系列指针运算,我们可以对表面上任一个像素点进行读、写操作。

l         UnlockRect——当你调用了LockRect和完成了对表面内存的访问后,你必须调用这个方法给表面解锁。

l         GetDesc——这个方法将通过填充D3DSURFACE_DESC结构来返回表面的描述信息。

最初锁定表面和改写每一像素看来稍微有点迷茫下面的代码表示锁定表面并将每一像素染成红色:

// Assume _surface is a pointer to an IDirect3DSurface9 interface.

// Assumes a 32-bit pixel format for each pixel.

 

// Get the surface description.

D3DSURFACE_DESC surfaceDesc;

_surface->GetDesc(&surfaceDesc);

 

// Get a pointer to the surface pixel data.

D3DLOCKED_RECT lockedRect;

_surface->LockRect(

         &lockedRect,// pointer to receive locked data

         0, // lock entire surface

         0); // no lock flags specified

 

// Iterate through each pixel in the surface and set it to red.

DWORD* imageData = (DWORD*)lockedRect.pBits;

for(int i = 0; i < surfaceDesc.Height; i++)

{

     for(int j = 0; j < surfaceDesc.Width; j++)

     {

         // index into texture, note we use the pitch and divide by

         // four since the pitch is given in bytes and there are

         // 4 bytes per DWORD.

         int index = i * lockedRect.Pitch / 4 + j;

 

         imageData[index] = 0xffff0000; // red

     }

}

 

_surface->UnlockRect();

程序中D3DLOCKED_RECT结构的定义如下:

typedef struct _D3DLOCKED_RECT {

     INT Pitch; // the surface pitch

     void *pBits; // pointer to the start of the surface memory

} D3DLOCKED_RECT;

在这里有一些关于表面锁定代码的一些说明。32-bit像素格式设定这是很重要的,我们把bits转换成DWORDs。这让我们能把每一个DWORD视为表示一个像素。同样我们暂时不用去关心为什么0xffff0000表示红色,关于颜色的说明将在第四章谈到。

1.3.2 Multisampling

由于使用像素矩阵来表示图像,在显示时会出现锯齿状,Multisampling就是使其变得平滑的技术。它的一种最普通的用法即为——全屏抗锯齿(看图1.3)。

1.3

D3DMULTISAMPLE_TYPE枚举类型使我们可以指定全屏抗锯齿的质量等级:

l         D3DMULTISAMPLE_NONE——不使用全屏抗锯齿。

l         D3DMULTISAMPLE_1_SAMPLED3DMULTISAPLE_16_SAMPLE——设定1~16级的等级。

本书的示例程序中没有使用全屏抗锯齿的功能,因为它大大的降低了程序运行速度。如果你实在很想使用它的话,要记住使用IDirect3D9::CheckDeviceMultisampleType来检测你的显卡是否支持。

1.3.3像素格式

当我们创建一个表面或纹理时候,经常需要指定这些Direct3D资源的像素格式。它是由D3DFORMAT枚举类型的一个成员来定义的。这里例举一部分:

l         D3DFMT_R8G8B8——表示一个24位像素,从左开始,8位分配给红色,8位分配给绿色,8位分配给蓝色。

l         D3DFMT_X8R8G8B8——表示一个32位像素,从左开始,8位不用,8位分配给红色,8位分配给绿色,8位分配给蓝色。

l         D3DFMT_A8R8G8B8——表示一个32位像素,从左开始,8位为ALPHA通道,8位分配给红色,8位分配给绿色,8位分配给蓝色。

l         D3DFMT_A16B16G16R16F——表示一个64位浮点像素,从左开始,16位为ALPHA通道,16位分配给蓝色,16位分配给绿色,16位分配给红色。

l         D3DFMT_A32B32G32R32F——表示一个128位浮点像素,从左开始,32位为ALPHA通道,32位分配给蓝色,32位分配给绿色,32位分配给红色。

想了解全部的像素格式请查看SDK文档中的D3DFORMAT部分。

注意:这前三种格式(D3DFMT_R8G8B8D3DFMT_X8R8G8B8D3DFMT_A8R8G8B8)是最常用并为大部分显卡所支持。但浮点像素格式或其它一些类型的支持并不是很广泛,在使用它们前请先检测你的显卡,看是否支持。

1.3.4 内存池

表面和其它一些Direct3D资源被放在多种内存池中。内存池的种类由D3DPOOL枚举类型的一个成员来指定。可用到的内存池有下列几种:

l         D3DPOOL_DEFAULT——表示Direct3D将根据资源的类型和用途把它们放在最合适的地方。这有可能是显存、AGP内存或者系统内存中。值得注意的是,这种内存池中的资源必须要在IDirect3DDevice9::Reset被调用之前消毁掉,并且再次使用时必须重新初始化。

l         D3DPOOL_MANAGED——资源将由Direct3D管理并且按设备的需要来指定放在显存还是放在AGP内存中。当应用程序访问和改变资源时它先把这些资源拷贝到系统内存中,当需要时Direct3D会自动把它们拷贝到显存里

l         D3DPOOL_SYSTEMMEM——指定资源放在系统内存中。

l         D3DPOOL_SCRATCH——指定资源放在系统内存中,它与D3DPOOL_SYSTEMMEM不同之处在于使用这个参数使图形设备不能直接使用本内存池的资源,但资源可以被拷贝出去。

1.3.5 交换链和页面切换

Direct3D通常创建2~3个表面组成一个集合,即为交换链,通常由IDirect3DSwapChain接口来表示。我们不必去了解它更详细的细节。我们也很少去管理它,通常Direct3D会自己去管理。所以我们只要大概的了解一下它就可以了。

交换链以及页面切换技巧被用在使两帧动画之间过度更平滑。图1.4展示的是一个有两个绘制表面的交换链。

1.4

如图1.4,在Front Buffer中的表面将用来在屏幕上显示。显示器不能及时显示Front Buffer中表示的图像。通常情况下,它是每六十分之一秒刷新显示一次,即刷新率为60赫兹。应用程序的帧率经常与监视器的刷新率不同步(比如应用程序的渲染速度可能比显示器的刷新速度快)。然而,我们并不希望在显示器已经显示完成当前之前就更新有下一帧动画的Front Buffer内容,但是我们又不想让程序停止渲染而去等待显示器显示。因此,我们渲染另一个屏幕表面Back Buffer。当监视器将Front Buffer显示出来后,Front Buffer就被放到交换链的末端,即变成图中的Back Buffer,而Back Buffer就会变成交换链中的Front Buffer。这个过程就叫做presenting。图1.5表示了交换的整个过程。

1.5

因此,我们绘图代码的结构就会像下面这样:

1. Render to back buffer

2. Present the back buffer

3. Goto (1)

1.3.6 深度缓冲

深度缓冲也是一个表面,但它不是用来存储图像数据而是用来记录像素的深度信息。它将确定哪一个像素最后被绘制出来。所以,如果要绘制640*480分辨率的图片,那么就会有640*480个深度值。

1.6

1.6展示了一个简单的场景,在这个场景里,一个物体把将另一个物体的一部分遮住了。为了使Direct3D能确定物体的前后关系并正确的绘制出来,我们使用一种深度缓冲,又叫做z-buffering的技术。

深度缓冲为每一个像素计算深度值并进行深度测试。通过深度测试我们可以比较得出哪个像素离摄相机更近并将它画出来。这样就可以只绘制最靠近摄相机的像素,被遮住的像素就不会被画出来。

深度缓冲的格式决定着深度测试的精确性。一个24位的深度缓冲比16位的深度缓冲更精确。通常,应用程序在24位深度缓冲下就能工作的很好,但是Direct3D也同时支持32位的深度缓冲。

l         D3DFMT_D32——表示32位深度缓冲

l         D3DFMT_D24S8——表示24位深度缓冲并保留8位模版缓冲(stencil buffer

l         D3DFMT_D24X8——表示24位深度缓冲

l         D3DFMT_D24X4S4——表示24位深度缓冲并保留4位模版缓冲

l         D3DFMT_D16——表示16位深度缓冲

注意:关于模版缓冲的问题将在第八章详细说明。

1.3.7 顶点处理

顶点是3D图形学的基础,它能够通过两种不同的方法被处理,一种是软件方式(software vertex processing),一种是硬件方式(hardware vertex processing),前者总是被支持且永远可用,后者必须要显卡硬件支持顶点处理才可用。

使用硬件顶点处理总是首选,因为它比软件方式更快,而且不占用CPU资源,这意味CPU至少可以有更多的空闲时间进行别的计算。

注意:如果一块显卡支持硬件顶点处理的话,也就是说它也支持硬件几何转换和光源计算。

1.3.8 设备能力

Direct3D支持的每一项特性都对应于D3DCAPS9结构的一个数据成员。初始化一个D3DCAPS9实例应该以你的设备实际支持特性为基础。因此,在我们的应用程序里,我们能够通过检测D3DCAPS9结构中相对应的某一成员来检测设备是否支持这一特性。

下面将举例说明,假设我们想要检测显卡是否支持硬件顶点处理(换句话说,就是显卡是否支持硬件几何转换和光源计算)。通过查阅SDK中的D3DCAPS9结构,可以得知数据成员D3DCAPS9::DevCaps中的D3DDEVCAPS_HWTRANSFORMANDLIGHT位表示硬件是否支持硬件顶点处理即硬件几何变换和光源计算。程序如下:

bool supportsHardwareVertexProcessing;

 

// If the bit is on then that implies the hardware device

// supports it.

if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT )

{

     // Yes, the bit is on, so it is supported.

     supportsHardwareVertexProcessing = true;

}

else

{

     // No, the bit is off, so it is not supported.

     hardwareSupportsVertexProcessing = false;

}

注意:DevCaps即为“device capabilities

   下一节将学习怎样根据硬件的实际情况来初始化D3DCAPS9

      我们建议你阅读SDK中关于D3DCAPS9的结构,它完整的列出了Direct3D支持的特性。

1.4 初始化Direct3D

下面几点说明怎样初始化Direct3D。根据下边的步骤你能初始化Direct3D

1. 获得一个IDirect3D9接口指针。这个接口用于获得物理设备的信息和创建一个IDirect3DDevice9接口,它是一个代表我们显示3D图形的物理设备的C++对象。

2. 检查设备的技术特性(D3DCAPS9),搞清楚主显卡是否支持硬件顶点处理。我们需要知道假如它能支持,我们就能创建IDirect3DDevice9接口。

3. 初始化一个D3DPRESENT_PARAMETERS结构实例,这个结构包含了许多数据成员允许我们指定将要创建的IDirect3DDevice9接口的特性。

4. 创建一个基于已经初始化好的D3DPRESENT_PARAMETERS结构的IDirect3DDevice9对象。它是一个代表我们显示3D图形的物理设备的C++对象。

请注意,本书使用主显示设备绘制3D图形,如果你的机子只有一块显卡,那它就是主显示设备。如果你有多个显卡,那么你当前使用的显卡将会成为主显示设备(如:用来显示Windows桌面的显卡)。

1.4.1获得IDirect3D9接口

Direct3D初始化是从获得一个IDirect3D9接口指针开始的。使用一个专门的Direct3D函数来完成这个工作是非常容易的,代码如下:

IDirect3D9* _d3d9;

_d3d9 = Direct3DCreate9(D3D_SDK_VERSION);

Direct3DCreate9的唯一一个参数总是D3D_SDK_VERSION,这可以保证应用程序通过正确的头文件被生成。如果函数调用失败,那么它将返回一个空指针。

IDirect3D9对象通常有两个用途:设备列举和创建IDirect3DDevice9对象。设备列举即为查明系统中显示设备的技术特性,显示模式、格式,以及其它每一种显卡各自支持的特性。创建代表物理设备的IDirect3DDevice9对象,我们需要利用这个物理设备的显示模式结构和格式来创建它。为了找到一个工作配置,我们必须使用IDirect3D9的列举方法。

然而,设备列举实在太慢了,为了使Direct3D运行得尽可能快,我们通常不使用这个测试,除了下一节所谈到的一项测试。为了安全跳过它,我们可以选择总是被所有显卡都支持的“安全”配置。

1.4.2 检测硬件顶点处理

当我们创建一个IDirect3DDevice9对象来表示主显示设备时,必须要设定其顶点处理的类型。如果可以的话,当然要选用硬件顶点处理,但是由于并非所有显卡都支持硬件顶点处理,因此我们必须首先检查显卡是否支持。

首先我们要根据主显示设备的技术特性来初始化D3DCAPS9实例。可以使用如下方法:

HRESULT IDirect3D9::GetDeviceCaps(

     UINT Adapter,

     D3DDEVTYPE DeviceType,

     D3DCAPS9 *pCaps

);

l         Adapter——指定要获得哪个显示适配器的特性

l         DeviceType——指定设备类型(硬件设备(D3DDEVTYPE_HAL),软件设备(D3DDEVTYPE_REF))

l         PCaps——返回一个已初始化的D3DCAPS9结构

然后,我们就可以1.3.8部分那样检测显卡的能力了。下面就是代码片段:

// Fill D3DCAPS9 structure with the capabilities of the

// primary display adapter.

 

D3DCAPS9 caps;

d3d9->GetDeviceCaps(

     D3DADAPTER_DEFAULT, // Denotes primary display adapter.

     deviceType, // Specifies the device type, usually D3DDEVTYPE_HAL.

     &caps); // Return filled D3DCAPS9 structure that contains

              // the capabilities of the primary display adapter.

 

// Can we use hardware vertex processing?

int vp = 0;

if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT )

{

     // yes, save in vp the fact that hardware vertex

     // processing is supported.

     vp = D3DCREATE_HARDWARE_VERTEXPROCESSING;

}

else

{

     // no, save in vp the fact that we must use software

     // vertex processing.

     vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING;

}

观察代码,我们使用变量vp来存储顶点处理类型。这是因为在稍后创建IDirect3DDevice9对象时要求指定其顶点处理的类型。

注意:标识符D3DCREATE_HARDWARE_VERTEXPROCESSINGD3DCREATE_SOFTWARE_VERTEXPROCESSING是预定义的值,它们分别代表硬件顶点处理和软件顶点处理。

技巧:若我们开发有一些新的,高级的特性的程序,在使用前我们总是先检查硬件是否支持这些特性。

注意:如果一个应用程序在你的机子上不能运行,说明它用到的一些特性可能你的显卡并不支持,可以试试把设备类型换成REF

1.4.3 填充D3DPRESENT_PARAMETERS结构

初始化过程的下一步是填充一个D3DPRESENT_PARAMETERS结构的实例。这个结构用于设定我们将要创建的IDirect3DDevice9对象的一些特性,它的定义如下:

typedef struct _D3DPRESENT_PARAMETERS_ {

     UINT BackBufferWidth;

     UINT BackBufferHeight;

     D3DFORMAT BackBufferFormat;

     UINT BackBufferCount;

     D3DMULTISAMPLE_TYPE MultiSampleType;

     DWORD MultiSampleQuality;

     D3DSWAPEFFECT SwapEffect;

     HWND hDeviceWindow;

     BOOL Windowed;

     BOOL EnableAutoDepthStencil;

     D3DFORMAT AutoDepthStencilFormat;

     DWORD Flags;

     UINT FullScreen_RefreshRateInHz;

     UINT PresentationInterval;

} D3DPRESENT_PARAMETERS;

下面介绍其比较重要的数据成员,至于更详细的信息,请查阅SDK

BackBufferWidth——后备缓冲表面的宽度(以像素为单位)

BackBufferHeight——后备缓冲表面的高度(以像素为单位)

BackBufferFormat——后备缓冲表面的像素格式(如:32位像素格式为D3DFMT——A8R8G8B8

BackBufferCount——后备缓冲表面的数量,通常设为“1”,即只有一个后备表面

MultiSampleType——全屏抗锯齿的类型,详情请看SDK

MultiSampleQuality——全屏抗锯齿的质量等级,详情看SDK

SwapEffect——指定表面在交换链中是如何被交换的,取D3DSWAPEFFECT枚举类型中的一个成员。其中D3DSWAPEFFECT_DISCARD是最有效的

hDeviceWindow——与设备相关的窗口句柄,你想在哪个窗口绘制就写那个窗口的句柄

Windowed——BOOL型,设为true则为窗口模式,false则为全屏模式

EnableAutoDepthStencil——设为trueD3D将自动创建深度/模版缓冲

AutoDepthStencilFormat——深度/模版缓冲的格式

Flags——一些附加特性,设为0D3DPRESENTFLAG类型的一个成员。下列两个最常用的标志

全部的标志请查阅SDK

D3DPRESENTFLAG_LOCKABLE_BACKBUFFER——设定后备表面能够被锁定,这会降低应用程序的性能

D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL——深度/模版缓冲在调用IDirect3DDevice9::present方法后将被删除,这有利于提升程序性能

FullScreen_RefreshRateInHz——刷新率,设定D3DPRESENT_RATE_DEFAULT使用默认刷新率

PresentationInterval——属于D3DPRESENT成员,又有两个常用标志,其余请查SDK

         D3DPRESENT_INTERVAL_IMMEDIATE——立即交换

         D3DPRESENT_INTERVAL_DEFAULT——D3D选择交换速度,通常等于刷新率

填充示例如下:

D3DPRESENT_PARAMETERS d3dpp;

d3dpp.BackBufferWidth = 800;

d3dpp.BackBufferHeight = 600;

d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; //pixel format

d3dpp.BackBufferCount = 1;

d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE;

d3dpp.MultiSampleQuality = 0;

d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;

d3dpp.hDeviceWindow = hwnd;

d3dpp.Windowed = false; // fullscreen

d3dpp.EnableAutoDepthStencil = true;

d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; // depth format

d3dpp.Flags = 0;

d3dpp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;

d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;

1.4.4 创建IDirect3DDevice9对象

在填充完了D3DPRESENT_PARAMETERS结构后,我们就可以用下面的方法创建一个IDirect3DDevice9对象了:

HRESULT IDirect3D9::CreateDevice(

     UINT Adapter,

     D3DDEVTYPE DeviceType,

     HWND hFocusWindow,

     DWORD BehaviorFlags,

     D3DPRESENT_PARAMETERS *pPresentationParameters,

     IDirect3DDevice9** ppReturnedDeviceInterface

);

l         Adapter——指定对象要表示的物理显示设备

l         DeviceType——设备类型,前面说过

l         hFocusWindow——同我们在前面d3dpp.hDeviceWindow的相同

l         BehaviorFlags——设定为D3DCREATE_SOFTWARE_VERTEXPROCESSING或者D3DCREATE_HARDWARE_VERTEXPROCESSING

l         pPresentationParameters——指定一个已经初始化好的D3DPRESENT_PARAMETERS实例

l         ppReturnedDeviceInterface——返回创建的设备

例子:

IDirect3DDevice9* device = 0;

hr = d3d9->CreateDevice(

     D3DADAPTER_DEFAULT, // primary adapter

     D3DDEVTYPE_HAL, // device type

     hwnd, // window associated with device

     D3DCREATE_HARDWARE_VERTEXPROCESSING, // vertex processing type

     &d3dpp, // present parameters

     &device); // returned created device

if( FAILED(hr) )

{

     ::MessageBox(0, "CreateDevice() - FAILED", 0, 0);

     return 0;

}

1.5 初始化Direct3D实例

在本章的例程中,初始化了一个Direct3D应用程序并用黑色填充显示窗口(如图1.7)。

1.7

本书所有的应用程序都包含了d3dUtility.hd3dUtility.cpp这两个文件,它们所包含的函数实现了所有Direct3D应用程序都要去做的一些常见的功能。例如:创建一个窗口、初始化Direct3D、进入程序的消息循环等。将这些功能封装在函数中能使示例程序更加突出该章的主题。另外,在我们学习本书的过程中还会在这两个文件中加上一些通用的代码。

1.5.1 d3dUtility.h/cpp

在开始本章的例程之前,让我们先熟悉一下d3dUtility.h/cpp所提供的函数。d3dUtility.h如下:

// Include the main Direct3DX header file. This will include the

// other Direct3D header files we need.

#include <d3dx9.h>

 

namespace d3d

{

     bool InitD3D(

         HINSTANCE hInstance, // [in] Application instance.

         int width, int height, // [in] Back buffer dimensions.

         bool windowed, // [in] Windowed (true)or

         // full screen (false).

         D3DDEVTYPE deviceType, // [in] HAL or REF

         IDirect3DDevice9** device); // [out] The created device.

 

     int EnterMsgLoop(

         bool (*ptr_display)(float timeDelta));

 

     LRESULT CALLBACK WndProc(

         HWND hwnd,

         UINT msg,

         WPARAM wParam,

         LPARAM lParam);

 

     template<class T> void Release(T t)

     {

         if( t )

         {

              t->Release();

              t = 0;

         }

     }

 

     template<class T> void Delete(T t)

     {

         if( t )

         {

              delete t;

              t = 0;

         }

     }

}

InitD3D——初始化一个应用程序主窗口并进行Direct3D的初始化。如果成功,则输出IDirect3DDevice9接口指针。从它的参数我们可以发现,我们能够设置窗口的大小和以窗口模式运行还是全屏模式运行。要知道它实现的细节,请看示例代码。

EnterMsgLoop——这个函数封装了应用程序的消息循环。它需要输入一个显示函数的函数指针,显示函数为程序中绘制图形的代码块,这样做是为了使显示函数能够在空闲的时候被调用并显示场景,它的实现如下:

int d3d::EnterMsgLoop( bool (*ptr_display)(float timeDelta) )

{

     MSG msg;

     ::ZeroMemory(&msg, sizeof(MSG));

 

     static float lastTime = (float)timeGetTime();

 

     while(msg.message != WM_QUIT)

     {

         if(::PeekMessage(&msg, 0, 0, 0, PM_REMOVE))

         {

              ::TranslateMessage(&msg);

              ::DispatchMessage(&msg);

         }

         else

         {

              float currTime = (float)timeGetTime();

              float timeDelta = (currTime - lastTime)*0.001f;

 

              ptr_display(timeDelta); // call display function

 

              lastTime = currTime;

         }

     }

     return msg.wParam;

}

与“time”有关的代码用于计算每次调用显示函数的时间间隔,即是每帧的时间。

Release——这个模板函数能方便的释放COM接口并将它们的值设为NULL

Delete——这个模板函数能方便的删除一个对象并将指向其的指针设为NULL

WndProc——应用程序主窗口的回调函数

1.5.2 实例框架

通过例框架,我们形成了一种通用的方法去构造本书的示例程序。每一个例程都含有三个函数的实现,当然这不包括回调函数WinMain主函数。这三个函数用特定的代码实现特定的功能。这三个函数是:

l         bool Setup()——在这个函数里,我们将准备一切该程序需要用到的东西,包括资源的分配,检查设备技术特性,设置应用程序的状态

l         void Clearup()——这个函数将释放Setup()中分配的资源,如分配的内存。

l         bool Display(float timeDelta)这个函数包含所有与我们绘图和显示有关的代码。参数timeDelta为每一帧的间隔时间,用来控制每秒的帧数。

1.5.3 D3D Init实例

这个示例程序将创建并初始化一个Direct3D应用程序,并用黑色填充屏幕。注意,我们使用了通用函数简化了初始化过程。

首先,我们要包含d3dUtility.h头文件,并为设备声明一个全局变量:

#include "d3dUtility.h"

 

IDirect3DDevice9* Device = 0;

然后实现我们的框架函数:

bool Setup()

{

     return true;

}

void Cleanup()

{

 

}

在这个程序中,我们不需要使用任何资源或触发任何事件,所以这两个函数都为空。

bool Display(float timeDelta)

{

     if( Device )

     {

         Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000, 1.0f, 0);

         Device->Present(0, 0, 0, 0);// present backbuffer

     }

     return true;

}

Display方法调用了IDirect3DDevice::Clear方法,分别用黑色和1.0填充后备表面和深度/模版缓冲。如果应用程序不停止的话,我们会一直执行这个操作。IDirect3DDevice::Clear声明如下:

HRESULT IDirect3DDevice9::Clear(

     DWORD Count,

     const D3DRECT* pRects,

     DWORD Flags,

     D3DCOLOR Color,

     float Z,

     DWORD Stencil

);

l         Count——pRects组中的矩形的个数

l         pRects——将要清除的屏幕矩形的数组,这使我们可以清除屏幕的某一部分

l         Flags——指定在哪些表面上执行清除表面的操作

         D3DCLEAR_TARGET——目的表面,通常为后备表面

         D3DCLEAR_ZBUFFER——深度缓冲

         D3DCLEAR_STENCIL——模版缓冲

l         Color——使用什么颜色填充清除的表面

l         Z——设置深度缓冲的值

l         Stencil——设置模版缓冲的值

屏幕被填充后,要调用IDirecte3DDevice9::Present方法进行后备表面的交换。

Windows 调函数为一组事件集,即,我们可按ESC键让程序退出。

LRESULT CALLBACK d3d::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)

{

     switch( msg )

     {

         case WM_DESTROY:

         ::PostQuitMessage(0);

         break;

 

         case WM_KEYDOWN:

         if( wParam == VK_ESCAPE )

              ::DestroyWindow(hwnd);

         break;

     }

     return ::DefWindowProc(hwnd, msg, wParam, lParam);

}

最后,WinMain按如下步骤运行:

1.         初始化主显示窗口和Direct3D

2.         调用Setup进行程序的准备工作

3.         使用Display函数作为参数进入消息循环

4.         清除应用程序最后释放IDirecte3DDevice9对象

int WINAPI WinMain(HINSTANCE hinstance,

                       HINSTANCE prevInstance,

                       PSTR cmdLine,

                       int showCmd)

{

     if(!d3d::InitD3D(hinstance, 800, 600, true, D3DDEVTYPE_HAL, &Device))

     {

         ::MessageBox(0, "InitD3D() - FAILED", 0, 0);

         return 0;

     }

 

     if(!Setup())

     {

         ::MessageBox(0, "Setup() - FAILED", 0, 0);

         return 0;

     }

 

     d3d::EnterMsgLoop( Display );

 

     Cleanup();

 

     Device->Release();

     return 0;

}

你所看到的,我们用有效的处理Window和初始化Direct3D过程的函数来构建实例是非常简洁的。本书的大部分程序,都是通过执行Setup, Cleanup, Display这三个函数来实现。

注意:不要忘了在你的工程中加入d3d9.libd3dx9.libwinmm.lib 这三个库!

1.6摘要(略)

第二章 渲染管道

本章的主题是渲染管道。它是用来创建为3D世界进行几何描述的2D图形并设定一个虚拟摄相机确定这个世界中哪一部分将被透视投影到屏幕上。

2.1

目标

l         要弄清楚我们怎样在Direct3D中表示3D物体

l         学习怎样模拟虚拟摄相机

l         弄懂渲染管道——这个过程是用几何学来表现3D场景和用它来产生2D图象

2.1表现模型

一个场景是多个物体或模型的集合。一个物体可以用三角形网格(triangle mesh)来近似表示,如图2.2所示。由三角形网格建立一个物体,我们称之为建模。3D世界中最基本的图元就是三角形,但是Direct3D也支持点图元和线图元但我们都不常用到。不过在学到第14章的粒子系统的时候,将会用到点图元。

2.2

一个多边形的两边相交的点叫做顶点。为了描述一个三角形,我们通常指定三个点的位置来对应三角形的三个顶点(如图2.3),这样我们就能够很明确的表示出这个三角形了。

2.3

2.1.1 顶点格式

我们以前定义的点在数学上来说是正确的,但是当我们在Direct3D环境中使用它的时候就会觉得很不完善。这是因为在Direct3D中的顶点包含了许多附加的属性,而不再单纯的只有空间位置的信息了。例如:一个顶点可以有颜色和法线向量属性(这两个属性分别在第四章和第五章介绍)。Direct3D让我们可以灵活的构造自己的顶点格式。换句话说,我们可以自己定义顶点的成员。

为了创建一个自定义的顶点结构,我们首先要创建一个包含能存放我们选择的顶点数据的结构。例如,下面我们定放了两种顶点数据类型,一种包含了位置和颜色信息,第二种则包含了位置,法线向量,纹理坐标信息(“纹理”见第六章)。

struct ColorVertex

{

     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

};

一旦我们有了完整的顶点格式,我们就要使用灵活顶点格式(FVF)的组合标志来描述它。例如第一个顶点结构,我们要使用如下的顶点格式:

#define FVF_COLOR (D3DFVF_XYZ | D3DFVF_DIFFUSE)

上面的顶点结构表明它包含位置和颜色属性。

而第二种结构则要使用:

#define FVF_NORMAL_TEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1)

上面的顶点结构表明它包含了位置,法线向量,纹理坐标的属性。

有一点要注意,你的标志的顺序必须要和你的顶点结构的顺序一一对应。如果想知道所有的D3DFVF标志,请查阅SDK文档。

2.1.2 三角形

三角形是构建3D物体的基本图形。为了构造物体,我们创建了三角形列表(triangle list)来描述物体的形状和轮廓。三角形列包含了我们将要画的每一个三角形的数据信息。例如为了构造一个矩形,我们把它分成两个三角形,如图2.4所示,最后指定每个三角形的顶点。

2.4

Vertex rect[6] = {v0, v1, v2, // triangle0

                   v0, v2, v3}; // triangle1

注意:指定三角形顶点的顺序是很重要的,将会按一定顺序环绕排列,这会在2.3.4节学习相关的内容。

2.1.3 索引

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

也就是说,用顶点列表中0vertexList[0])、1vertexList[1])和2vertexList[2])顶点构成三角形0用顶点列表中0vertexList[0])、2vertexList[2])和3vertexList[3])顶点构成三角形1

2.2虚拟摄相机

摄相机确定3D世界中的哪部分是可见的因而需要将哪部分转换为2D图形。在3D世界中摄相机被放置和定向并且定义其可视体,图2.6展示了我们的摄相机模型。

2.6

可视体是由可视角度和前裁剪面(Near Plane)与后裁剪面(Far Plane)定义一个平截头体。之所以要选择平截头体构造可视体,是因为我们的显示器都是矩形的。在可视体中不能被看见的物体都会被删除,删除这种数据的过程就叫做“裁剪”。

投影窗口(Projection Window)是可视体内的3D几何图形投影生成的用来显示3D场景的2D图像的2D区域。重要的是要知道,我们使用min=(-1,-1)max=(1,1)来定义投影窗口的大小。

为了简化本书接下来的部分绘制,我们使前裁剪面与投影窗口在同一平面上。并且,注意Direct3D定义的投影平面(即投影窗口所在的平面)是Z = 1的平面

2.3 渲染管道

一旦我们描述几何学上的3D场景和设置了虚拟摄相机,我们要把这个场景转换成2D图象显示在显示器上。这一系列必须完成的操作就叫做渲染管道。图2.7展示了一个简化的渲染管道,随后将详细解释图中的每一部分。

2.7

渲染管道中的许多级都是从一个坐标系到另一个坐标的几何变换。这些变换都通过矩阵变换来实现。Direct3D为我们进行变换计算并且如果显卡支持硬件变换的话那就更有利了。使用Direct3D进行矩阵变换,我们唯一要做的事就是提供从一个系统变换到另一个系统的变换矩阵就可以了。我们使用IDirect3DDevice9::SetTranform方法提供变换矩阵。它输入一个表示变换类型的参数和一个变换矩阵。如图2.7所示,为了进行一个从自身坐标系到世界坐标系的变换,我们可以这样写:

Device->SetTransform(D3DTS_WORLD, &worldMatrix);

在下面的小节我们会了解到这个方法的更多细节。

2.3.1自身坐标系(Local Space

自身坐标系又叫做建模空间,这是我们定义物体的三角形列的坐标系。自身坐标系简化了建模的过程。在物体自己的坐标系中建模比在世界坐标系中直接建模更容易。例如,在自身坐标系中建模不像在世界坐标系中要考虑本物体相对于其他物体的位置、大小、方向关系。

2.8

2.3.2世界坐标系(World Space

一旦我们构造了各种模型,它们都在自己的自身坐标系中,但是我们需要把它们都放到同一个世界坐标系中。物体从自身坐标系到世界坐标系中的换叫做世界变换。世界变换通常是用平移、旋转、缩放操作来设置模型在世界坐标系中的位置、大小、方向。世界变换就是通过各物体在世界坐标系中的位置、大小和方向等相互之间的关系来建立所有物体。

2.9

世界变换由一个矩阵表示,并且在Direct3D中调用IDirect3DDevice9::SetTranform方法设置它,记住将转换类型设为D3DTS_WORLD。例如我们要在世界坐标系中放置一个立方体定位在(-326)和一个球体定位在(50-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 cubes 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 spheres. If we

// dont change this, the sphere would be drawn using the previously

// set world matrix the cubes.

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 worlds up vector (0, 1, 0)

);

pEye参数指定摄相机在世界坐标系中的位置,pAt参数指定摄相机所观察的世界坐标系中的一个目标点,pUp参数指定3D世界中的上方向,通常设Y轴正方向为上方向,即取值为(010)。

例如:假设我们要把摄相机放在点(53-10),并且目标点为世界坐标系的中点(000),我们可以这样获得视图坐标系变换矩阵:

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可知,正面多边形挡住了在它后面的背面多边形,Direct3D将通过拣选(即删除多余的处理过程)背面多边形来提高效率,这种方法就叫背面拣选。图2.12展示了背面拣选之后的多边形,从摄相机的观察点来看,仍将绘制相同的场景到后备表面,那些被遮住的部分无论如何都永远不会被看见的。

2.12

当然,为了完成这项工作,Direct3D需要知道哪个多边形是正面,哪个是背面。Direct3D中默认顶点以顺时针方向(在观察坐标系中)形成的三角形为正面,以逆时针方向形成的三角形为背面。

如果我们不想使用默认的拣选状态,我们可以通过改变D3DRS_CULLMODE来改变渲染状态:

Device->SetRenderState(D3DRS_CULLMODE, Value);

Value可以是如下一个值:

l         D3DCULL_NONE——完全不使用背面拣选

l         D3DCULL_CW——拣选顺时针环绕的三角形

l         D3DCULL_CCW——逆时针方向环绕的三角形会被拣选,这是默认值。

2.3.5光源(Lighting

光源定义在世界坐标系中然后被变换到视图坐标系中。视图坐标系中光源给物体施加的光照大大增加了场景中物体的真实性,至于光照的相关函数的细节将会在第五章学习。在本书的第四部分,我们将使用可编程管道实现自己的光照。

2.3.6裁剪(Clipping

我们拣选那些超出了可视体范围的几何图形的过程就叫做裁剪。这会出现三种情况:

l         完全包含——三角形完全在可视体内,这会保持不变,并进入下一级

l         完全在外——三角形完全在可视体外部,这将被拣选

l         部分在内(部分在外)——三角形一部分在可视体内,一部分在可视体外,则三角形将被分成两部分,可视体内的部分被保留,可视体之外的则被拣选

2.13展示了上面三种情况:

2.13

2.3.7投影(Projection

视图坐标系的主要任务就是将3D场景转化为2D图像表示。这种从n维转换成n-1维的过程就叫做投影。投影的方法有很多种,但是我们只对一种特殊的投影感兴趣,那就是透视投影。因为透视投影可以使离摄相机越远的物体投影到屏幕上后就越小,这可以使我们把3D场景更真实的转化为2D图像。图2.14展示了一个3D空间中的点是如何通过透视投影到投影窗口上去的。

2.14

投影变换的实质就是定义可视体并将可视体内的几何图形投影到投影窗口上去。投影矩阵的计算太复杂了,这里我们不会给出推导过程,而是使用如下的Direct3D函数通过给出平截头体的参数来求出投影矩阵。

2.15

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

);

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

Direct3D中,视口矩形通过D3DVIEWPORT9结构来表示。它的定义如下:

typedef struct _D3DVIEWPORT9 {

     DWORD X;

     DWORD Y;

     DWORD Width;

     DWORD Height;

     DWORD MinZ;

     DWORD MaxZ;

} D3DVIEWPORT9;

前四个参数定义了视口矩形与其所在窗口的关系。MinZ成员指定最小深度缓冲值,MaxZ指定最大深度缓冲值。Direct3D使用的深度缓冲的范围是0~1,所以如果不想做什么特殊效果的话,将它们分别设成相应的值就可以了。

一旦我们填充完D3DVIEWPORT9结构后,就可以如下设视口:

D3DVIEWPORT9 vp{ 0, 0, 640, 480, 0, 1 };

Device->SetViewport(&vp);

这样,Direct3D就会自动为我们处理视口变换。现在还是给出视口变换矩阵作为参考:

2.3.9光栅化(Rasterization

在把三角形每个顶点转换到屏幕上以后,我们就画了一个2D三角形。光栅化是计算需要显示的每个三角形中每个点颜色值(如图2.17)。

2.17

光栅化过程是非常繁重的计算,它应该通过硬件图形处理来完成。它的处理结果就是把2D图象显示在显示器上。

2.4 摘要()

第三章 Direct3D中画画

       在上一章中我们学习了创建和渲染场景的概念。这一章中我们将这些东西用于实践,同时学习怎样在Direct3D中画一些几何物体。本章中所讲的有些Direct3D接口和方法很重要,因为它们的使用会贯穿全书。

目标

l         要弄清楚Direct3D中怎样存储顶点和索引。

l         怎样使用渲染状态来改变渲染结果

l         学习怎样渲染场景

l         学习怎样用D3DXCreate*函数创建更多的复杂的3D形体

3.1顶点/索引缓存

       顶点和索引缓存有相似的接口并且共享相似的方法;因此我们把它们合在一起讲解。一个顶点缓存是一块连续的存储了顶点数据的内存。同样的,一个索引缓存是一块连续的存储了索引数据的内存。我们使用顶点和索引缓存保存我们的数据是因为它们能被放置在显存中。渲染显存中的数据要比渲染系统内存中的数据快的多。

       在代码中,一个顶点缓存是通过IDirect3DVertexBuffer9接口来定义的。类似的,一个索引缓存是通过IDirect3DIndexBuffer9接口来定义。

3.1.1创建一个顶点和索引缓存

我们能使用下面两个方法创建一个顶点缓存和索引缓存:

HRESULT IDirect3DDevice9::CreateVertexBuffer(

     UINT Length,

     DWORD Usage,

     DWORD FVF,

     D3DPOOL Pool

     IDirect3DVertexBuffer9** ppVertexBuffer,

     HANDLE* pSharedHandle

);

 

HRESULT IDirect3DDevice9::CreateIndexBuffer(

     UINT Length,

     DWORD Usage,

     D3DFORMAT Format,

     D3DPOOL Pool,

     IDirect3DIndexBuffer9** ppIndexBuffer,

     HANDLE* pSharedHandle

);

这两个方法大部分参数是相同的,因此我们一起介绍它们。

l         Length —— 分配给缓存的字节大小。假如想得到一个能存储8个顶点的顶点缓存,那么我们就要在顶点结构中设置这个参数为 8 * sizeof ( Vertex )

l         Usage —— 指定关于怎样使用缓存的额外信息。这个值可以是0,没有标记,或者是下面标记的一个或多个的组合:

D3DUSAGE_DYNAMIC——设置这个参数可以使缓存是动态的。在下一页说明静态和动态缓存。

D3DUSAGE_POINTS——这个参数指定缓存存储原始点。原始点将在第14章粒子系统中介绍。这个参数仅仅用在顶点缓冲中。

D3DUSAGE_SOFTWAREPROCESSING——使用软件顶点处理

D3DUSAGE_WRITEONLY——指定应用程序只能写缓存。它允许驱动程序分配最适合的内存地址作为写缓存。注意如果从创建好的这种缓存中读数据,将会返回错误信息。

l         FVF —— 存储在缓存中的顶点格式

l         Pool —— 缓存放置在哪一个内存池中

l         ppVertexBuffer ——返回创建好的顶点缓存的指针。

l         pSharedHandle ——没有使用;设置为0

l         Format ——指定索引的大小;使用D3DFMT_INDEX16设置16位索引,使用D3DFMT_INDEX32设置32位索引。注意并非所有设备都支持32位索引;请检查设备能力。

l         ppIndexBuffer ——返回创建好的索引缓存的指针。

注意:不使用D3DUSAGE_DYNAMIC参数创建的缓存被叫做静态缓存。静态缓存通常被放置在显存中,在其中的数据能被很有效的处理。然而,对于静态缓存,从中读取和写入数据是很慢的,因为访问显存是很慢的。因为这个原因我们用静态缓存存储静态数据(不需要被经常改变的数据)。对于静态缓存地形和建筑物是很好的后选例子,因为在应用程序中他们通常不需要被改变。静态缓存应该在应用程序初始话的时候就被填充好,而不是在运行时才做。

注意:使用D3DUSAGE_DYNAMIC参数创建的缓存被叫做动态缓存。动态缓存通常被放在AGP内存中,这种内存中的数据能被很快的更新。处理动态缓存中的数据不会比处理静态缓存中的数据快,因为这些数据必须在渲染前被转移到显存中,动态缓存的好处是它们能够被稍微快点地被更新(比CPU写快)。因此,假如你需要经常更新缓存中的数据,那么你就应该使用动态缓存。对于动态缓存粒子系统是很好的一个应用,因为它们是动态的,并且他们通常每一帧都会被更新。

注意:在程序中读取显存和AGP内存都是非常慢的。因此,假如你在运行时需要读取你的几何物体,最好的方案是指定一块系统内存,都在其中拷贝并且读取数据。

下边是创建一个静态顶点缓存的例子,该缓存能存储8个顶点。

IDirect3DVertexBuffer9* vb;

device->CreateVertexBuffer(

     8 * sizeof( Vertex ),

     0,

     D3DFVF_XYZ,

     D3DPOOL_MANAGED,

     &vb,

     0);

3.1.2 访问缓冲内存

为了访问一个顶点/索引缓存,我们需要得到一个指针。我们通过一个指针获得缓存数据必须使用Lock方法。当我们访问完缓存后必须对它解锁。一旦有一个指向内存的指针,我们就能对它进行读写。

HRESULT IDirect3DVertexBuffer9::Lock(

     UINT OffsetToLock,

     UINT SizeToLock,

     BYTE** ppbData,

     DWORD Flags

);

HRESULT IDirect3DIndexBuffer9::Lock(

     UINT OffsetToLock,

     UINT SizeToLock,

     BYTE** ppbData,

     DWORD Flags

);

3.1

这两个方法的参数都是完全相同的。

l         OffsetToLock —— 偏移量,以字节为单位,从缓存开始位置到锁定开始位置的距离。如图3.1

l         SizeToLock 锁定的字节数。

l         ppbData 一个指向锁定内存开始位置的指针。

l         Flags 标记描述怎样锁定内存。它可能是0或者是下面参数中的1个或多个的组合:

D3DLOCK_DISCARD——这个参数仅仅会在动态缓存时被使用。它指示硬件丢弃缓存并返回一个指向新分配的缓存的指针。这是很有用的因为当我们存取一个新分配的缓存时它允许硬件继续从丢弃的缓存渲染。这防止了硬件延迟。

D3DLOCK_NOOVERWRITE——这个参数仅仅会在动态缓存时被使用。它声明你将向缓存中添加数据。即,你不能向已经渲染的内存中写数据。这是有好处的因为他允许你在添加新数据到缓存的同时让硬件继续渲染。

D3DLOCK_READONLY——这个参数声明你锁定的缓存只能从中读取数据而不能写数据。这允许一些内在的优化。

         用参数D3DLOCK_DISCARDD3DLOCK_NOOVERWRITE的地址实际上就是缓存的一部分被使用(正在渲染)时它被锁定。假如情况允许这些标记被使用,当在锁定时他们防止渲染停止。

         下边的例子展示了通常怎样使用Lock方法。注意当我们使用完以后要调用Unlock方法。

Vertex* vertices;

_vb->Lock(0, 0, (void**)&vertices, 0); // lock the entire buffer

vertices[0] = Vertex(-1.0f, 0.0f, 2.0f); // write vertices to

vertices[1] = Vertex( 0.0f, 1.0f, 2.0f); // the buffer

vertices[2] = Vertex( 1.0f, 0.0f, 2.0f);

_vb->Unlock(); // unlock when youre done accessing the buffer

3.1.3 找回顶点和索引缓存信息

有时我们需要得到顶点/索引缓存信息。下面的例子示范了用于获得这些信息的方法:

D3DVERTEXBUFFER_DESC vbDescription;

_vertexBuffer->GetDesc(&vbDescription); // get vb info

 

D3DINDEXBUFFER_DESC ibDescription;

_indexBuffer->GetDesc(&ibDescription); // get ib info

D3DVERTEXBUFFER_DESCD3DINDEXBUFFER_DESC结构的定义如下:

typedef struct _D3DVERTEXBUFFER_DESC {

     D3DFORMAT Format;

     D3DRESOURCETYPE Type;

     DWORD Usage;

     D3DPOOL Pool;

     UINT Size;

     DWORD FVF;

} D3DVERTEXBUFFER_DESC;

 

typedef struct _D3DINDEXBUFFER_DESC {

     D3DFORMAT Format;

     D3DRESOURCETYPE Type;

     DWORD Usage;

     D3DPOOL Pool;

     UINT Size;

} D3DINDEXBUFFER_DESC;

3.2 渲染状态

Direct3D提供了多种渲染状态,它影响几何物体怎样被渲染。渲染状态有默认值,因此假如你的应用程序需要不同于默认设置的渲染时,你仅仅改变它即可。一种渲染效果会一直起作用,直到你下一次改变渲染状态为止。为了设置一个渲染状态,我们使用下面的方法:

HRESULT IDirect3DDevice9::SetRenderState(

     D3DRENDERSTATETYPE State, // the state to change

     DWORD Value // value of the new state

);

例如,在这一章的例子中我们将使用线框模式渲染我们的物体。因此,我们设置如下的渲染状态:

_device->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);

注意:查看DirectX SDK中关于D3DRENDERSTATETYPE的信息。其中详细介绍了所有的渲染状态。

3.3 绘制准备

一旦我们创建好一个顶点缓存以及一个索引缓存(可选的)后,我们就为渲染其中的内容准备得差不多了,但是在渲染前我们还有3个步骤必须先做。

1、  设置资源流。设置资源流与一个顶点缓存挂钩,此流就是一个流入渲染管线的几何信息的流。

下面的方法是用于设置一个资源流:

HRESULT IDirect3DDevice9::SetStreamSource(

     UINT StreamNumber,

     IDirect3DVertexBuffer9* pStreamData,

     UINT OffsetInBytes,

     UINT Stride

);

l         StreamNumber——确定我们的顶点缓存与哪一个资源流挂钩。在这本书中我们不使用多重流;因此我们总是使用0号流。

l         pStreamData——一个指向我们想与流挂钩的那个顶点缓存的指针。

l         OffsetInBytes——相对流开始处的偏移量。以字节为单位,它指定被填入渲染管道的顶点数据的开始位置。通过检查D3DCAPS9结构中的D3DDEVCAPS2_STREAMOFFSET标志,假如你的设备支持,那么这个参数就有一些非0值。

l         Stride——我们在顶点缓存中操作的每个部分的流的字节大小。

例如,假设vb是一个已经填充了顶点信息的顶点缓存:

_device->SetStreamSource( 0, vb, 0, sizeof( Vertex ) );

2、设置顶点格式。在这里我们指定后面用来绘图调用的顶点的顶点格式。

_device->SetFVF( D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 );

3、设置索引缓存。假如我们使用了索引缓存,我们必须设置后面要用于绘制操作的索引缓存。每次我们只能使用一个索引缓存;因此假如你需要用一个不同的索引缓存绘制一个物体时,你必须转换到另一个上。下面的代码设置一个索引缓存:

_device->SetIndices( _ib ); // pass copy of index buffer pointer

3.4用顶点/索引缓存绘制

在我们创建好顶点/索引缓存以及做好准备工作以后,我们就能绘制我们的几何物体了。这是通过使用DrawPrimitive或者DrawIndexedPrimitive传送几何信息到达渲染管线的。这些方法从顶点流中获得顶点信息以及从索引缓存中获得索引信息。

3.4.1 IDirect3DDevice9::DrawPrimitive

这个方法被用在不使用索引信息来绘制图元。

HRESULT IDirect3DDevice9::DrawPrimitive(

     D3DPRIMITIVETYPE PrimitiveType,

     UINT StartVertex,

     UINT PrimitiveCount

);

l         PrimitiveType——我们绘制的图元类型。比如,我们能绘制点和线以及三角形。以后我们使用三角形,用D3DPT_TRIANGLELIST参数。

l         StartVertex——索引到在顶点流中的一个元素。设置渲染顶点中的开始点。这个参数给予我们一定的机动性去绘制一个顶点缓存中的某部分。

l         PrimitiveCount——绘制图元的个数。

例子:

// draw four triangles.

_device->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 4);

3.4.2 IDirect3DDevice9::DrawIndexedPrimitive

这个方法被用在使用索引信息来绘制图元。

HRESULT IDirect3DDevice9::DrawIndexedPrimitive(

     D3DPRIMITIVETYPE Type,

     INT BaseVertexIndex,

     UINT MinIndex,

     UINT NumVertices,

     UINT StartIndex,

     UINT PrimitiveCount

);

l         Type——我们绘制的图元类型。比如,我们能绘制点和线以及三角形。以后我们使用三角形,用D3DPT_TRIANGLELIST参数。

l         BaseVertexIndex——一个基本数字,在调用中用它去加上索引。参看下面的说明。

l         MinIndex——将被引用的最小索引值。

l         NumVertices——在此调用中将被引用的顶点数。

l         StartIndex——索引到索引缓存中的某个位置,它标记开始渲染的开始索引点。

l         PrimitiveCount——绘制图元的个数。

例子:

_device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 8, 0, 12);

注意:BaseVertexIndex参数需要一些特别的解释。在解释过程中将会用到的图3.2

3.2

在索引缓存中定位顶点相应的也就在顶点缓存中定位了。然而,假设我们想将球,盒子,圆柱体的顶点放置到一个公共的顶点缓存中。对于每一个物体,我们将不得不再计算在公共顶点缓存中的索引。这个新的索引值是通过与一个偏移量相加得到。注意这个偏移量是标准的顶点,而不是字节。

       我们需要计算物体在公共顶点缓存中的索引值。Direct3D允许我们通过设置BaseVertexIndex参数的到一个顶点偏移量,随后Direct3D就能利用顶点自身的索引重新计算新的索引。

3.4.3 开始/结束场景

最后一点就是所有绘制方法都必须在IDirect3DDevice9::BeginSceneIDirect3DDevice9::EndScene方法之间被调用。例如我们将这样写:

_device->BeginScene();

     _device->DrawPrimitive(...);

_device->EndScene();

3.5 D3DX几何物体

通过在代码中建造每个三角形来建造3D物体是一件非常枯燥的事。幸运的是,D3DX库已经为我们提供了一些方法来产生简单3D物体的网格数据。

D3DX库提供如下6种网格生成函数。

l         D3DXCreateBox

l         D3DXCreateSphere

l         D3DXCreateCylinder

l         D3DXCreateTeapot

l         D3DXCreatePolygon

l         D3DXCreateTorus

3.3

6种函数的使用都很类似,并且使用D3DX网格数据结构ID3DXMesh使用ID3DXBuffer接口一样。这些接口回在第10章和11章中讲解。现在,我们忽视它们的详细信息,只需简单使用它们即可。

HRESULT D3DXCreateTeapot(

     LPDIRECT3DDEVICE9 pDevice, // device associated with the mesh

     LPD3DXMESH* ppMesh, // pointer to receive mesh

     LPD3DXBUFFER* ppAdjacency // set to zero for now

);

一个使用D3DXCreateTeapot函数的例子:

ID3DXMesh* mesh = 0;

D3DXCreateTeapot(_device, &mesh, 0);

一旦生成了网格数据,我们就能使用ID3DXMesh::DrawSubset方法绘制图形了。这个方法有一个参数,它用来识别网格的一个子集。这个网格是通过上面的D3DXCreate*函数中的一个子集创建的,因此可以给这个参数指定0值。一个渲染网格的例子:

_device->BeginScene();

     mesh->DrawSubset(0);

_device->EndScene();

当你使用了网格以后,你必须释放(release)它:

mesh->Release();

_mesh = 0;

3.6 实例程序:三角形、立方体、茶壶、D3DXCreate*

这里有4个例子。

l         三角形——这是非常简单的应用程序,它示范了在线框模式下怎样创建并渲染一个三角形。

l         立方体——只比三角形稍微复杂一点,这个程序渲染一个线框立方体。

l         茶壶——这个程序使用D3DXCreateTeapot函数创建并渲染一个纺纱茶壶。

l         D3DXCreate——这个程序创建并渲染几种不同的能够使用D3DXCreate*函数创建的3D物体。

让我们简单讨论一下创建立方体的例子。通过对它的学习你自己就能很快地理解其他例子。

       这个简单的绘制和渲染立方体的程序的运行结果如图3.4

3.4

首先我们定义下边两个全局变量来保存立方体的顶点和索引数据:

IDirect3DVertexBuffer9* VB = 0;

IDirect3DIndexBuffer9* IB = 0;

下一步,我们定义两个全局常量,由它们来指定我们的屏幕大小:

const int Width = 800;

const int Height = 600;