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 非商业应用可免费使用本教程。商业应用请同作者联系,Email:WengYB@126.com。
特别感谢:
l www.GameRes.com是他让我走上了游戏开发的道路。
l 一直关心支持我的同事、同学。
l 我最最亲爱的老婆,没有她我不可能写出这本教程。
第一部分 必备的数学知识
在这最开始的一部分中我们将介绍本书所要用到的数学知识。我们讨论的主题是向量,矩阵和相应的变换,当然还有一些有关面和线的内容。最开始阅读时这部分是可选的。
本教程对这些知识的讨论是很有限的,因此对于不同数学知识背景的读者来说都容易阅读。对于想了解更多更全的这方面信息的读者,请查看有关线性代数的书籍。当然已经学习过线性代数的读者也可将它作为有必要的复习内容来阅读。(这里推荐你看看《线性代数与空间解析几何》)
除此之外,我们还将展示D3DX类中相关的数学模型和执行特殊变换的函数。
目标
学习向量以及它们的3D计算机图形程序
学习矩阵以及学会使用它们来变换3D图形
学习怎样模拟面和线以及它们的3D图形程序
熟悉用于3D数学运算的D3DX库中包含的类和程序的子集
三维空间中的向量
几何学中,我们用有向线段表示向量,如图1。向量的两个属性是他的长度和他的顶点所指的方向。因此,可以用向量来模拟既有大小又有方向的物理模型。例如,以后我们要实现的粒子系统。我们用向量来模拟粒子的速度和加速度。在3D计算机图形学中我们用向量不仅仅模拟方向。例如我们常常想知道光线的照射方向,以及在3D世界中的摄象机。向量为在3维空间中表示方向的提供了方便。
图1
向量与位置无关。有同样长度和方向的两个向量是相等的,即使他们在不同的位置。观察彼此平行的两个向量,例如在图1中u和v是相等的。
我们继续学习左手坐标系。图2显示的是左手坐标系和右手坐标系。两者不同的是Z轴的方向。在左手坐标系Z轴是向书的里面跑的而右手坐标系是向书的外边跑的。
图2
因为向量的位置不能改变它的性质,我们能把所有向量平移使他们的尾部和坐标系的原点重合。因此,当一个向量在标准位置我们能通过头点来描述向量。图3显示的是图1中的向量在标准位置的样子。
图3
我们通常用小写字母表示一个向量,但有时也用大写字母。如2、3和4维向量分别是:u = (ux, uy), N = (Nx, Ny, Nz), c = (cx, cy, cz, cw)。我们现在介绍3D中的4个向量,就象图4显示的。首先是都由含有0的零向量;它被表示成加粗的0 = (0, 0, 0)。接下来3个特殊的向量标准基向量。它们被叫做i, j和k向量,分别沿着坐标系的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向量,但有时也会用到2D和4D向量。在D3DX库中提供了D3DXVECTOR2和D3DXVECTOR4类来分别表现2D和4D向量。不同维数的向量有着和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向量终点的向量。假如我们解释u和v的分量,我们能用向量相减找到从一个点到另一个点的向量。这是非常方便的操作因为我们常常想找到从一个点到另一个点的方向的向量。
数与向量的乘积
我们能用一个数与向量相乘,就象名字暗示的一样,向量按比例变化。这种运算不会改变向量的方向,除非我们用负数去操作,这样也只是方向相反了。
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,那么u⊥v。
■ 假如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 |
叉积
第二类乘法是向量的叉积。不象点积,用一个数来乘,叉积是用另一个向量相乘。通过把两个向量u和v相乘的到另一的向量p.把u和v两个向量通过十字相乘得到向量p,向量p垂直于u和v。也就是说向量p垂直于u并且垂直于u。
十字相乘就象这样计算:
图7
如:发现j = k×i = (0, 0, 1)×(1, 0, 0) 并且j同时垂直于k和i.
解答:
因此,j = (0, 1, 0).假如u · v = 0,那么u⊥v,这又被称做“点积”。同样的如果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中我们很明显的得到向量–p与u和v也都相互垂直。我们执行十字相乘返回的是p或者-p的结果。换句话说,u×v = -(v×u)。这说明叉积是不可交换的。你能通过左手法则确定叉积返回的向量。按照第一个向量指向第二个向量弯曲你的左手,这时拇指所指的方向就是向量所指的方向。
矩阵
在这一部分我们关注的焦点是数学中的矩阵。它们在3D图形学中应用的程序将在下一部分讲解。
一个m×n的矩阵是由m行和n列的数字组成的矩阵列。行和列的数字就是这个矩阵的维数。我们通过写在下方的数字识别矩阵清单,数字中的第一个表示行第二个表示列。例如下边的M是3×3矩阵,B是2×4矩阵, C是3×2矩阵。
我们使用加粗的大写字母表示矩阵。有时一个矩阵只包含一行或者一列。我们用行矩阵和列矩阵这个特殊的名称来称呼。例如下边就是行和列矩阵:
当使用行或列矩阵时,我们只用一个下标,有时我们还用字母表示。
相等、数乘矩阵以及相加
这部分我们将用到下边4个矩阵:
■假如两个矩阵维数和成员都相同那么它们就相等。例如,A = C 因为A和C有同样的维数并且他们的成员都相等。A≠B同时A≠D因为他们的成员或者维数是不相同的。
■我们能通过数与矩阵的每个成员相乘的到数与矩阵相乘。如矩阵D与k相乘:
假如k = 2,那么:
■当两个矩阵的维数相同时才能把它们相加。和是把两个矩阵相应的成员相加得到。如:
■矩阵有加法当然也就有减法,前提是有相同的维数。矩阵减法如图所示:
乘法
矩阵相乘在3D计算机图形学中是非常重要的运算。通过矩阵相乘我们能变换向量把不同向量转换到一起。在下一部分变换将随处可见。
为了得到矩阵之积AB,A的列数必须等于B的行数。假如这个条件不满足,就不能相乘。考虑下边两个矩阵,A 和 B,分别是2×3 和 3×3,如:
我们看乘积AB是可以计算的,因为A的列数等于B的行数。注意乘积BA,它们是不能计算的,因为B的列数不等于A的行数。由此说明一般情况下矩阵乘法不满足乘法交换律(也就是, AB≠BA)。我们说“一般不可交换”因为一些实例说明有些矩阵乘法还是可以的。
知道了矩阵乘法的计算方法,现在我们就能给出精确的定义:假如A是一个m×n的矩阵B是一个n×p的矩阵,那么它们之积AB可计算并且是一个m×p 的矩阵C, C的成员ij 等于A的第i个与B的第j个相乘:
例如,求解:
我们检查知道乘法是可计算的,因为A的列数等于B的行数。也知道计算的结果是一个2×2的矩阵。根据公式(4),我们得到:
作为练习,检查AB≠BA。
更一般的例子:
单位矩阵
有一种特殊矩阵叫做单位矩阵。单位矩阵是除了对角(左上到右下)以外所有成员都是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 并且假设我们知道p’和R想求p。首先找到R–1,一旦求得R–1,我们便能求出p,就象这样:
求逆矩阵的方法已经超出了本书的范围,但是这在任何一本线性代数书上都有讲解。这部分已经标明了是“基本变换”我们给出它是因为我们要用矩阵细节。在“D3DX 矩阵”部分我们将学习一个为我们求逆矩阵的D3DX函数。
我们介绍几个有用的推论:(AB) –1 = B–1 A–1。这个性质前提是假定A和B都能求逆并且它们都是有相同维数的方矩阵。
矩阵的转置
矩阵的转置是相互交换矩阵的行和列。因而,m×n的矩阵的转置是一个n×m的矩阵。我们把矩阵M的转置记作MT。
例如:求下面两个矩阵的转置:
重声一下,转置是交换矩阵的行和列。
因此:
D3DX 矩阵
当设计Direct3D应用程序时,我们专门特别使用4×4矩阵和1×4行矩阵(向量)。注意使用这两种矩阵意味着可以进行矩阵乘法。
■ 向量-矩阵乘法。即,假如1×4的行矩阵(向量)v和4×4的矩阵T,那么积vT可计算并且返回的结果是一个1×4的行矩阵(向量)。
■ 矩阵-矩阵乘法。即,假如4×4的矩阵T和4×4的矩阵R,那么积TR和RT可计算并且两者返回的结果都是一个4×4的矩阵。注意因为矩阵乘法不满足交换律所以TR和RT不一定相等。
在D3DX中表示1×4的行矩阵(向量),我们用D3DXVECTOR3和D3DXVECTOR4向量类。当然D3DXVECTOR3只有3个成员,不是4个。然而,第4个成员缺省是1或0(在下一部分有更多信息)。
在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的行矩阵中时,我们设置w为1。允许对点进行适当的平移。因为向量和位置无关,向量的平移是不被定义的,如果试图这样做会返回一个无意义的向量。为了防止对向量进行平移,当在把一个点放置到一个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的值,即w≠0 且 w≠1。考虑下边例子:
因为p3≠0 且 p3≠1。
我们注意w =p3。当w≠0 且 w≠1时,我们说我们有一个向量在同类空间中,与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个成员为1。D3DXVec3TransformNormal函数变换向量并且设置第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库也提供D3DXVec3TransformCoordArray和D3DXVec3TransformNormalArray来分别变换一个点数组和向量数组
平面
一个平面能通过一个向量n和平面上的一个点p0来描述。这个向量n垂直于平面,它被称为此平面的法向量(如图11)。
图11
在图12中我们能够发现平面上任意一点p都满足如下等式。即:假如p、p0都是平面上一点,那么向量(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, b和c是平面法向量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到平面的距离,也就是点p和q之间的有向距离。假如平面的法向量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怎样与图形硬件相互作用
弄懂COM在D3D中所扮演的角色
学习基础图形学知识,如2D图片是如何存储的、页面切换和深度缓冲
学习如何初始化D3D
熟悉本书例程中的一些常用的结构
1.1 Direct3D概述
Direct3D是一种低层图形API,它能让我们利用3D硬件加速来渲染3D世界。我们可以把Direct3D看作是应用程序和图形设备之间的中介。例如通知图形设备清空屏幕,应用程序将调用Direct3D的IDirect3DDevice9::Clear方法。图1.1显示了应用程序、Direct3D和图形设备之间的关系。
图1.1
图1.1中Direct3D所表示的是Direct3D中已定义的供程序员使用的Direct3D接口和函数的集合。这些接口和函数代表了当前版本的Direct3D所支持的全部特性。注意:仅仅因为Direct3D支持某种特性,并不意味着你所使用的图形硬件(显卡)也能支持它。
如图1.1所示,在Direct3D和图形设备之间有一层中介——叫做硬件抽象层(HAL,Hardware Abstraction Layer)。Direct3D不能直接作用于图形设备,因为现在市面上的显卡种类实在是太多了并且每种显卡都有不同的性能和处理事件的方式。例如,两种不同的显卡实现清屏的方式也可能是不同的。因此,Direct3D要求设备制造商实现HAL。HAL是一组指示设备执行某种操作的特殊设备代码的集合。用这种方法,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
表面的Width和Height是按像素计算的。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_SAMPLE…D3DMULTISAPLE_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_R8G8B8、D3DFMT_X8R8G8B8、D3DFMT_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_VERTEXPROCESSING和D3DCREATE_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——设为true,D3D将自动创建深度/模版缓冲
AutoDepthStencilFormat——深度/模版缓冲的格式
Flags——一些附加特性,设为0或D3DPRESENTFLAG类型的一个成员。下列两个最常用的标志
全部的标志请查阅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.h和d3dUtility.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.lib、d3dx9.lib、winmm.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 |
也就是说,用顶点列表中的0(vertexList[0])、1(vertexList[1])和2(vertexList[2])顶点构成三角形0;用顶点列表中的0(vertexList[0])、2(vertexList[2])和3(vertexList[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。例如我们要在世界坐标系中放置一个立方体定位在(-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可知,正面多边形挡住了在它后面的背面多边形,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_DISCARD和D3DLOCK_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 you’re 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_DESC和D3DINDEXBUFFER_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::BeginScene和IDirect3DDevice9::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; |
接下来定义我们的顶点结构以及结构中顶点的格式。在这个例子中顶点结构只保存顶点的位置信息:
struct Vertex
{
Vertex(){}
Vertex(float x, float y, float z)
{
_x = x; _y = y; _z = z;
}
float _x, _y, _z;
static const DWORD FVF;
};
const DWORD Vertex::FVF = D3DFVF_XYZ; |
让我们把它迁移到框架程序(见1.53节)上。Setup函数创建顶点和索引缓存,锁定它们,把构成立方体的顶点写入顶点缓存,以及把定义立方体的三角形的索引写入索引缓存。然后把摄象机向后移动几个单位以便我们能够看见在世界坐标系中原点处被渲染的立方体。
bool Setup()
{
// create vertex and index buffers
Device->CreateVertexBuffer(
8 * sizeof(Vertex),
D3DUSAGE_WRITEONLY,
Vertex::FVF,
D3DPOOL_MANAGED,
&VB,
0);
Device->CreateIndexBuffer(
36 * sizeof(WORD),
D3DUSAGE_WRITEONLY,
D3DFMT_INDEX16,
D3DPOOL_MANAGED,
&IB,
0);
// fill the buffers with the cube data
Vertex* vertices;
VB->Lock(0, 0, (void**)&vertices, 0);
// vertices of a unit cube
vertices[0] = Vertex(-1.0f, -1.0f, -1.0f);
vertices[1] = Vertex(-1.0f, 1.0f, -1.0f);
vertices[2] = Vertex( 1.0f, 1.0f, -1.0f);
vertices[3] = Vertex( 1.0f, -1.0f, -1.0f);
vertices[4] = Vertex(-1.0f, -1.0f, 1.0f);
vertices[5] = Vertex(-1.0f, 1.0f, 1.0f);
vertices[6] = Vertex( 1.0f, 1.0f, 1.0f);
vertices[7] = Vertex( 1.0f, -1.0f, 1.0f);
VB->Unlock();
// define the triangles of the cube:
WORD* indices = 0;
IB->Lock(0, 0, (void**)&indices, 0);
// front side
indices[0] = 0; indices[1] = 1; indices[2] = 2;
indices[3] = 0; indices[4] = 2; indices[5] = 3;
// back side
indices[6] = 4; indices[7] = 6; indices[8] = 5;
indices[9] = 4; indices[10] = 7; indices[11] = 6;
// left side
indices[12] = 4; indices[13] = 5; indices[14] = 1;
indices[15] = 4; indices[16] = 1; indices[17] = 0;
// right side
indices[18] = 3; indices[19] = 2; indices[20] = 6;
indices[21] = 3; indices[22] = 6; indices[23] = 7;
// top
indices[24] = 1; indices[25] = 5; indices[26] = 6;
indices[27] = 1; indices[28] = 6; indices[29] = 2;
// bottom
indices[30] = 4; indices[31] = 0; indices[32] = 3;
indices[33] = 4; indices[34] = 3; indices[35] = 7;
IB->Unlock();
// position and aim the camera
D3DXVECTOR3 position(0.0f, 0.0f, -5.0f);
D3DXVECTOR3 target(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
D3DXMATRIX V;
D3DXMatrixLookAtLH(&V, &position, &target, &up);
Device->SetTransform(D3DTS_VIEW, &V);
// set projection matrix
D3DXMATRIX proj;
D3DXMatrixPerspectiveFovLH(
&proj,
D3DX_PI * 0.5f, // 90 - degree
(float)Width / (float)Height,
1.0f,
1000.0f);
Device->SetTransform(D3DTS_PROJECTION, &proj);
// set the render states
Device->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
return true;
} |
Display方法有两个任务;它必须更新场景并且紧接着渲染它。既然想旋转立方体,那么我们将对每一帧增加一个角度使立方体能在这一帧旋转。对于这每一帧,立方体将被旋转一个很小的角度,这样我们看起来旋转就会更平滑。接着我们使用IDirect3DDevice9::DrawIndexedPrimitive方法来绘制立方体。
bool Display(float timeDelta)
{
if( Device )
{
//
// spin the cube:
//
D3DXMATRIX Rx, Ry;
// rotate 45 degrees on x-axis
D3DXMatrixRotationX(&Rx, 3.14f / 4.0f);
// incremement y-rotation angle each frame
static float y = 0.0f;
D3DXMatrixRotationY(&Ry, y);
y += timeDelta;
// reset angle to zero when angle reaches 2*PI
if( y >= 6.28f )
y = 0.0f;
// combine rotations
D3DXMATRIX p = Rx * Ry;
Device->SetTransform(D3DTS_WORLD, &p);
//
// draw the scene:
//
Device->Clear(0, 0,
D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
0xffffffff, 1.0f, 0);
Device->BeginScene();
Device->SetStreamSource(0, VB, 0, sizeof(Vertex));
Device->SetIndices(IB);
Device->SetFVF(Vertex::FVF);
Device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,
0, 0, 8, 0, 12);
Device->EndScene();
Device->Present(0, 0, 0, 0);
}
return true;
} |
最后,我们释放使用过的所有内存。这意味着释放顶点和索引缓存接口:
void Cleanup()
{
d3d::Release<IDirect3DVertexBuffer9*>(VB);
d3d::Release<IDirect3DIndexBuffer9*>(IB);
} |
3.7 摘要(略)
第四章 色彩
在上一章中我们用线框模式渲染了场景中的物体。这一章我们将学习怎样渲染有颜色的物体。
目标
l 学习在Direct3D中怎样描述颜色
l 弄懂怎样给三角形赋予颜色
4.1 颜色表示法
在Direct3D中,颜色是使用RGB三部分来描述的。也就是说,我们要分别指定红、绿和蓝三种颜色的值。混合这三个颜色决定最终的颜色。利用这三种颜色我们能够表现数万种颜色。
我们使用两种不同的结构来存储RGB数据。这第一种是D3DCOLOR,它实际上一个DWORD即32位。在D3DCOLOR类型中的这些位按照8-bit被分为4个部分,每一部分存储的是该色的亮度值。如图4.1所示。
图4.1
每种颜色占用内存的一个字节,各颜色亮度值的取值范围是0-255。这个值越接近0就越暗,越接近255就越亮。
注意:现在不要管alpha部分;它被用在alpha混合中——在第7章中会讲解。
指定其中的每一部分并且把它放到D3DCOLOR中适当的位置需要使用到一些位操作。Direct3D为我们提供了一个完成这个任务的宏D3DCOLOR_ARGB.它使用包含每种颜色以及alpha位一共4个参数。每一个参数的取值必须在0-255之间,如:
D3DCOLOR brightRed = D3DCOLOR_ARGB(255, 255, 0, 0);
D3DCOLOR someColor = D3DCOLOR_ARGB(255, 144, 87, 201); |
另外,我们也能使用D3DCOLOR_XRGB宏,它与刚才的宏类似只不过不必指定alpha部分;不过我们最好还是把alpha指定为0xff(255)。
#define D3DCOLOR_XRGB(r,g,b) D3DCOLOR_ARGB(0xff,r,g,b) |
在Direct3D中另外一种存储颜色的结构是D3DCOLORVALUE。在这个结构中,我们分别使用一个浮点数来表示每一部分的亮度值。其取值范围是0-1,0表示没有亮度,1表示最大亮度。
typedef struct _D3DCOLORVALUE {
float r; // the red component, range 0.0-1.0
float g; // the green component, range 0.0-1.0
float b; // the blue component, range 0.0-1.0
float a; // the alpha component, range 0.0-1.0
} D3DCOLORVALUE; |
另外,我们能够使用D3DXCOLOR结构,就象D3DCOLORVALUE包含同样的数据成员一样。同时提供有用的构造函数和重载操作符,这将让颜色处理更容易。D3DXCOLOR的定义如下:
typedef struct D3DXCOLOR
{
#ifdef __cplusplus
public:
D3DXCOLOR() {}
D3DXCOLOR( DWORD argb );
D3DXCOLOR( CONST FLOAT * );
D3DXCOLOR( CONST D3DXFLOAT16 * );
D3DXCOLOR( CONST D3DCOLORVALUE& );
D3DXCOLOR( FLOAT r, FLOAT g, FLOAT b, FLOAT a );
// casting
operator DWORD () const;
operator FLOAT* ();
operator CONST FLOAT* () const;
operator D3DCOLORVALUE* ();
operator CONST D3DCOLORVALUE* () const;
operator D3DCOLORVALUE& ();
operator CONST D3DCOLORVALUE& () const;
// assignment operators
D3DXCOLOR& operator += ( CONST D3DXCOLOR& );
D3DXCOLOR& operator -= ( CONST D3DXCOLOR& );
D3DXCOLOR& operator *= ( FLOAT );
D3DXCOLOR& operator /= ( FLOAT );
// unary operators
D3DXCOLOR operator + () const;
D3DXCOLOR operator - () const;
// binary operators
D3DXCOLOR operator + ( CONST D3DXCOLOR& ) const;
D3DXCOLOR operator - ( CONST D3DXCOLOR& ) const;
D3DXCOLOR operator * ( FLOAT ) const;
D3DXCOLOR operator / ( FLOAT ) const;
friend D3DXCOLOR operator * (FLOAT, CONST D3DXCOLOR& );
BOOL operator == ( CONST D3DXCOLOR& ) const;
BOOL operator != ( CONST D3DXCOLOR& ) const;
#endif //__cplusplus
FLOAT r, g, b, a;
} D3DXCOLOR, *LPD3DXCOLOR; |
注意:D3DCOLORVALUE和D3DXCOLOR结构都有4个浮点数成员。这使我们的颜色处理符号能象4D向量一样。颜色向量能被加,减以及缩放。另一方面点积和叉积不能用于颜色向量,但是颜色成员相乘是可以的。因此在D3DXCOLOR类中执行的乘法就是成员相乘。它的定义如下:
现在使用下面全局颜色常量更新我们的d3dUtility.h文件:
namespace d3d
{
.
.
.
const D3DXCOLOR WHITE( D3DCOLOR_XRGB(255, 255, 255) );
const D3DXCOLOR BLACK( D3DCOLOR_XRGB( 0, 0, 0) );
const D3DXCOLOR RED( D3DCOLOR_XRGB(255, 0, 0) );
const D3DXCOLOR GREEN( D3DCOLOR_XRGB( 0, 255, 0) );
const D3DXCOLOR BLUE( D3DCOLOR_XRGB( 0, 0, 255) );
const D3DXCOLOR YELLOW( D3DCOLOR_XRGB(255, 255, 0) );
const D3DXCOLOR CYAN( D3DCOLOR_XRGB( 0, 255, 255) );
const D3DXCOLOR MAGENTA( D3DCOLOR_XRGB(255, 0, 255) );
} |
4.2 顶点颜色
图元的颜色是由构成它的顶点的颜色决定的。因此,我们必须把一个颜色成员加入到我们的顶点数据结构中。注意D3DCOLORVALUE类型不能用在这里,因为Direct3D希望用一个32位的值来描述顶点的颜色。(通过使用顶点着色器我们能为顶点颜色使用4D颜色向量,它能提供一个128位的颜色,但是对于我们现在的水平来说那太超前了。顶点着色器将在17章中介绍。)
struct ColorVertex
{
float _x, _y, _z;
D3DCOLOR _color;
static const DWORD FVF;
}
const DWORD ColorVertex::FVF = D3DFVF_XYZ | D3DFVF_DIFFUSE; |
4.3 着色处理
着色处理发生在光栅化和指定图元上的顶点颜色怎样被计算成像素颜色之间。目前这里有2种着色处理模式可用:平面着色(flat shading)和高洛德着色(Gouraud shading)。
平面着色,图元像素的颜色是均匀的,且就是指定图元第一个顶点的颜色。因此一旦三角形的第一个顶点被指定成红色,那么它的其他三个顶点也将会是红色。通过使用平面着色来为第二和第三个顶点着色。
ColorVertex t[3];
t[0]._color = D3DCOLOR_XRGB(255, 0, 0);
t[1]._color = D3DCOLOR_XRGB(0, 255, 0);
t[2]._color = D3DCOLOR_XRGB(0, 0, 255); |
平面着色使物体呈现是斑驳的,因为没有从一个颜色到另一个颜色的平滑过渡。一个更好的着色模式叫做高洛德着色(也被叫做平滑着色)。高洛德着色,图元表面的颜色是由每个顶点通过线性插值来赋予。图4.2显示了分别使用平面着色和高洛德着色处理的红色三角形。
图4.2
就象Direct3D中很多东西一样,着色处理模式是受Direct3D设置状态决定的。
// set flat shading
Device->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_FLAT);
// set Gouraud shading
Device->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD); |
4.4 实例程序:彩色三角形
这个实例程序展示了分别使用本章中的平面着色和高洛德着色处理的三角形。渲染出的图片如图4.2所示。首先我们定义如下的全局变量:
D3DXMATRIX World;
IDirect3DVertexBuffer9* Triangle = 0; |
我们包含一个D3DXMATRIX,它将存储我们将要绘制的三角形在世界坐标中的变换信息。Triangle变量是存储三角形顶点数据的顶点缓存。注意,我们只需要存储一个三角形,因为我们能用它在世界坐标系中不同位置绘制若干次。
Setup方法创建顶点缓存同时填充上带颜色信息的三角形顶点数据。三角形的第一个顶点填充为全亮度红色(255)第二个填充全亮度绿色(255),第三个填充全亮度蓝色(255)。最后,在这个例子中我们屏蔽掉灯光。值得注意的是该例子使用的是一个新的ColorVertex结构,就象在4.2节中说明的一样。
bool Setup()
{
// create vertex buffer
Device->CreateVertexBuffer(
3 * sizeof(ColorVertex),
D3DUSAGE_WRITEONLY,
ColorVertex::FVF,
D3DPOOL_MANAGED,
&Triangle,
0);
// fill the buffers with the triangle data
ColorVertex* v;
Triangle->Lock(0, 0, (void**)&v, 0);
v[0] = ColorVertex(-1.0f, 0.0f, 2.0f, D3DCOLOR_XRGB(255, 0, 0));
v[1] = ColorVertex( 0.0f, 1.0f, 2.0f, D3DCOLOR_XRGB( 0, 255, 0));
v[2] = ColorVertex( 1.0f, 0.0f, 2.0f, D3DCOLOR_XRGB( 0, 0, 255));
Triangle->Unlock();
// set projection matrix
D3DXMATRIX proj;
D3DXMatrixPerspectiveFovLH(
&proj,
D3DX_PI * 0.5f, // 90 - degree
(float)Width / (float)Height,
1.0f,
1000.0f);
Device->SetTransform(D3DTS_PROJECTION, &proj);
// set the render states
Device->SetRenderState(D3DRS_LIGHTING, false);
return true;
} |
Display函数使用不同的着色模式在两个不同的地方分别绘制2个Triangle。每个三角形的位置由世界矩阵World来决定。
bool Display(float timeDelta)
{
if( Device )
{
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0);
Device->BeginScene();
Device->SetFVF(ColorVertex::FVF);
Device->SetStreamSource(0, Triangle, 0, sizeof(ColorVertex));
// draw the triangle to the left with flat shading
D3DXMatrixTranslation(&World, -1.25f, 0.0f, 0.0f);
Device->SetTransform(D3DTS_WORLD, &World);
Device->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_FLAT);
Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
// draw the triangle to the right with gouraud shading
D3DXMatrixTranslation(&World, 1.25f, 0.0f, 0.0f);
Device->SetTransform(D3DTS_WORLD, &World);
Device->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_GOURAUD);
Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
Device->EndScene();
Device->Present(0, 0, 0, 0);
}
return true;
} |
4.5 摘要(略)
第五章 灯光
为了提高场景的真实性,我们可以为其加入灯光。灯光也能帮助表现物体的立体感以及物体的体积。当使用灯光时,我们不再自己指定顶点的颜色;Direct3D中每个顶点都通过灯光引擎来计算顶点颜色,该计算是基于定义的灯光资源,材质以及灯光资源关心的表面方向。通过灯光模型计算顶点颜色会得到更真实的场景。
目标
l 学习Direct3D支持的灯光资源,以及它们照射出的灯光类型。
l 弄懂怎样定义灯光去影响其照射的表面。
l 找出怎样算术描述三角形的方向以便我们能够确定灯光照射到三角形的角度。
5.1灯光的组成
在Direct3D灯光模型中,灯光是通过灯光资源的三个成员之一来照射的,即有三种灯光。
l 环境光(Ambient Light)——这种类型的灯光将被其他所有表面反射且被用在照亮整个场景。例如,物体的各部分都被照亮,对于一个角度,甚至穿过不在光源直接照射的地方他们都能被照亮。环境光的使用是粗略的,便宜的,它模仿反射光。
l 漫反射(Diffuse Reflection)——这种灯光按照特殊方向传播。当它照射到一个表面,它将在所有方向上均匀的反射。因为漫射光在所有方向上都均匀的反射,被反射的光线将到达眼睛而与观察点无关,因此我们不必为观察者考虑。因而,漫射光仅仅需要考虑灯光方向和表面的姿态。这种灯光将成为你的资源中照射的普通灯光。
l 镜面反射(Specular Reflection)——这种灯光按照特殊方向传播。当它照射到一个表面时,它严格地按照一个方向反射。这将产生一个明亮的光泽,它能在某角度被看见。因为这种灯光在一个方向反射。明显的观察点,必须考虑灯光的方向和表面姿态,且必须按照镜面灯光等式来考虑。镜面灯光被用在物体上产生高光的地方,这种光泽只有在灯光照射在磨光的表面上才会产生。
镜面光比其他灯光类型要求更多的计算;因此,Direct3D提供了一个开关选择。实际上,它默认是被关闭的;要使用镜面光你必须设置D3DRS_SPECULARENABLE渲染状态。
Device->SetRenderState(D3DRS_SPECULARENABLE, true); |
每一种灯光都是通过D3DCOLORVALUE结构或者描述灯光颜色的D3DXCOLOR来描绘的。这里有几个灯光颜色的例子:
D3DXCOLOR redAmbient(1.0f, 0.0f, 0.0f, 1.0f);
D3DXCOLOR blueDiffuse(0.0f, 0.0f, 1.0f, 1.0f);
D3DXCOLOR whiteSpecular(1.0f, 1.0f, 1.0f, 1.0f); |
注意:在D3DXCOLOR类中的alpha值用在描述灯光颜色时是被忽略的。
5.2材质
在现实世界中我们看到的物体颜色将由物体反射回来的灯光颜色来决定。比如,一个红色的球是红色的,因为它吸收所有的灯光颜色除了红色光。红色光是被球反射回来进入我们眼睛的,因此我们看到的球是红色的。Direct3D通过我们定义的物体材质来模拟这些所有的现象。材质允许我们定义表面反射灯光的百分比。在代码中通过D3DMATERIAL9结构描述一个材质。
typedef struct _D3DMATERIAL9 {
D3DCOLORVALUE Diffuse, Ambient, Specular, Emissive;
float Power;
} D3DMATERIAL9; |
l Diffuse——指定此表面反射的漫射光数量。
l Ambient——指定此表面反射的环境光数量。
l Specular——指定此表面反射的镜面光数量
l Emissive——这个是被用来给表面添加颜色,它使得物体看起来就象是它自己发出的光一样。
l Power——指定锐利的镜面高光;它的值是高光的锐利值。
举例,想得到一个红色的球。我们将定义球的材质来只反射红光吸收其他颜色的所有光:
D3DMATERIAL9 red;
::ZeroMemory(&red, sizeof(red));
red.Diffuse = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f); // red
red.Ambient = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f); // red
red.Specular = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f); // red
red.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f); // no emission
red.Power = 5.0f; |
这里我们设置绿色和蓝色的值为0,这表明材质反射0%此颜色的光。我们设置红色为1,表示材质反射100%的红光。注意,我们能够控制每种灯光反射的颜色(环境、漫射和镜面光)。
同样假如我们定义一个只发出蓝色光的光源,对球的光照将失败因为蓝色光将被全部吸收而没有红光被反射。当物体吸收了所有光以后,物体看起来就为黑色。同样的,当物体反射100%的红、绿和蓝光,物体就将呈现为白色。
因为手工填充一个材质结构将是乏味的工作,我们添加下列有用的函数和全局材质常数到d3dUtility.h/cpp文件中:
D3DMATERIAL9 d3d::InitMtrl(D3DXCOLOR a, D3DXCOLOR d,
D3DXCOLOR s, D3DXCOLOR e, float p)
{
D3DMATERIAL9 mtrl;
mtrl.Ambient = a;
mtrl.Diffuse = d;
mtrl.Specular = s;
mtrl.Emissive = e;
mtrl.Power = p;
return mtrl;
}
namespace d3d
{
.
.
.
D3DMATERIAL9 InitMtrl(D3DXCOLOR a, D3DXCOLOR d, D3DXCOLOR s, D3DXCOLOR e, float p);
const D3DMATERIAL9 WHITE_MTRL = InitMtrl(WHITE, WHITE, WHITE, BLACK, 8.0f);
const D3DMATERIAL9 RED_MTRL = InitMtrl(RED, RED, RED, BLACK, 8.0f);
const D3DMATERIAL9 GREEN_MTRL = InitMtrl(GREEN, GREEN, GREEN, BLACK, 8.0f);
const D3DMATERIAL9 BLUE_MTRL = InitMtrl(BLUE, BLUE, BLUE, BLACK, 8.0f);
const D3DMATERIAL9 YELLOW_MTRL = InitMtrl(YELLOW, YELLOW, YELLOW, BLACK, 8.0f);
} |
顶点结构没有材质属性;一个通用的材质必须被设置。设置它我们使用IDirect3DDevice9::SetMaterial(CONST D3DMATERIAL9*pMaterial)方法。
假设我们想渲染几个不同材质的物体;我们将按照如下的写法去做:
D3DMATERIAL9 blueMaterial, redMaterial;
...// set up material structures
Device->SetMaterial(&blueMaterial);
drawSphere(); // blue sphere
Device->SetMaterial(&redMaterial);
drawSphere(); // red sphere |
5.3顶点法线
面法线(face normal)是描述多边形表面方向的一个向量(如图5.1)。
图5.1
顶点法线(Vertex normals)也是基于同样的概念,但是我们与其指定每个多边形的法线,还不如为每个顶点指定(如图5.2)。
图5.2
Direct3D需要知道顶点法线以便它能够确定灯光照射到物体表面的角度,并且一旦计算了每个顶点的灯光,Direct3D需要知道每个顶点的表面方向。注意顶点法线不一定和面法线相同。球体/环形物就是很好的实物例子,它们的顶点法线和三角形法线是不相同的(如图5.3)。
图5.3
为了描述顶点的顶点法线,我们必须更新原来的顶点结构::
struct Vertex
{
float _x, _y, _z;
float _nx, _ny, _nz;
static const DWORD FVF;
}
const DWORD Vertex::FVF = D3DFVF_XYZ | D3DFVF_NORMAL; |
注意,我们已经将上一章中使用的颜色成分去除了。这是因为我们将使用灯光来计算顶点的颜色。
作为一个简单的物体比如立方体和球体,我们能够通过观察看见顶点法线。对于更多复杂的网格,我们需要一个更多的机械方法。假设一个由p0,p1,p2构成的三角形,我们需要计算每个顶点的法线n0,n1,n2。
简单的步骤,我们列举它是为了找到由三个点构成的三角形的面法线,同时使用面法线作为顶点法线。首先计算三角形上的两个向量:
那么面法线是:
每个顶点的法线和面法线是相等的:
下面是一个C函数,它通过三角形的三个顶点计算三角形的面法线。注意这个函数的三个顶点是按照顺时针方向指定的。假如不是这样,那么法线方向将是相反的。
void ComputeNormal(D3DXVECTOR3* p0,
D3DXVECTOR3* p1,
D3DXVECTOR3* p2,
D3DXVECTOR3* out)
{
D3DXVECTOR3 u = *p1 - *p0;
D3DXVECTOR3 v = *p2 - *p0;
D3DXVec3Cross(out, &u, &v);
D3DXVec3Normalize(out, out);
} |
当用三角形近似表示曲面时,使用面法线作为顶点法线不能表现一个平滑的结果。一个更好的方法是找到顶点法线的平均法线。为了找到顶点v的顶点法线vn,我们找到网格模型中所有三角形的面法线记为顶点v。vn是通过计算他们的平均面法线得到的。这里有一个例子,假设有3个三角形它们的面法线分别是n0,n1,n2,指定为顶点v。那么vn的平均法线就是:
通过改变“舞台”,把顶点法线变为non-normal,这是有可能的。因此这样最好是安全的且在通过D3DRS_NORMALIZENORMALS设置渲染状态来改变“舞台”后,Direct3D从新规格化所有法线。
Device->SetRenderState(D3DRS_NORMALIZENORMALS, true); |
5.4光源
Direct3D支持三种类型的光源。
l 点光源——这种光源在世界坐标中有一个位置且向所有方向上都照射光线。
图5.4
l 方向光源——这种光源没有位置但是向指定方向发出平行光线。
图5.5
l 聚光灯——这种类型的光源和手电筒的光类似;它有位置并且发出的光在指定方向上按照圆锥形照射。这个圆锥形有两个角度,θ和φ。角度θ描述内圆锥,φ描述外圆锥。
图5.6
在代码中一个灯光资源是通过D3DLIGHT9结构来表现的。
typedef struct _D3DLIGHT9 {
D3DLIGHTTYPE Type;
D3DCOLORVALUE Diffuse;
D3DCOLORVALUE Specular;
D3DCOLORVALUE Ambient;
D3DVECTOR Position;
D3DVECTOR Direction;
float Range;
float Falloff;
float Attenuation0;
float Attenuation1;
float Attenuation2;
float Theta;
float Phi;
} D3DLIGHT9; |
l Type——定义灯光类型,我们能够使用下面三种类型之一:D3DLIGHT_POINT, D3DLIGHT_SPOT, D3DLIGHT_DIRECTIONAL
l Diffuse——此光源发出的漫射光颜色。
l Specular——此光源发出的镜面光颜色。
l Ambient——此光源发出的环境光颜色。
l Position——用一个向量来描述的光源世界坐标位置。这个值对于灯光的方向是无意义的。
l Direction——用一个向量来描述的光源世界坐标照射方向。这个值不能用在点光源上。
l Range——灯光能够传播的最大范围。这个值不能比 大。且不能用于方向光源。
l Falloff——这个值只能用在聚光灯上。它定义灯光在从内圆锥到外圆锥之间的强度衰减。它的值通常设置为1.0f。
l Attenuation0, Attenuation1, Attenuation2——这些衰减变量被用来定义灯光强度的传播距离衰减。它们只被用于点光源和聚光灯上。Attenuation0定义恒定衰减,Attenuation1定义线性衰减,Attenuation2定义二次衰减。适当的使用这个公式,D是代表到光源的距离,A0,A1,A2与Attenuation0,1,2相匹配。
l Theta——只用于聚光灯;指定内圆锥的角度,单位是弧度。
l Phi——只用于聚光灯;指定外圆锥的角度,单位是弧度。
就象初始化D3DMATERIAL9结构一样,初始化D3DLIGHT9结构是一件单调乏味的工作。我们添加下面的函数到d3dUtility.h/cpp文件中用于初始化简单灯光。
namespace d3d
{
.
.
.
D3DLIGHT9 InitDirectionalLight(D3DXVECTOR3* direction, D3DXCOLOR* color);
D3DLIGHT9 InitPointLight(D3DXVECTOR3* position, D3DXCOLOR* color);
D3DLIGHT9 InitSpotLight(D3DXVECTOR3* position, D3DXVECTOR3* direction, D3DXCOLOR* color);
} |
使用这些函数是非常简单的。我们现在只是演示怎样使用InitDirectionalLight。其他的也很类似:
D3DLIGHT9 d3d::InitDirectionalLight(D3DXVECTOR3* direction, D3DXCOLOR* color)
{
D3DLIGHT9 light;
::ZeroMemory(&light, sizeof(light));
light.Type = D3DLIGHT_DIRECTIONAL;
light.Ambient = *color * 0.4f;
light.Diffuse = *color;
light.Specular = *color * 0.6f;
light.Direction = *direction;
return light;
} |
然后创建一个方向光源,它沿着x轴正方向照射白色灯光。我们按照下面的方法来做:
D3DXVECTOR3 dir(1.0f, 0.0f, 0.0f);
D3DXCOLOR c = d3d::WHITE;
D3DLIGHT9 dirLight = d3d::InitDirectionalLight(&dir, &c); |
在把D3DLIGHT9初始化好以后,我们需要用Direct3D内在支持的灯光来注册。就象这样做:
Device->SetLight(
0, // element in the light list to set, range is 0-maxlights
&light);// address of the D3DLIGHT9 structure to set |
一旦灯光注册了,我们就能使用下面的列举的例子来开或关灯光了:
Device->LightEnable(
0, // the element in the light list to enable/disable
true); // true = enable, false = disable |
5.5实例程序:灯光
这一章的例子是创建如图5.7所显示的场景。它示范了怎样指定顶点法线,怎样创建材质,以及怎样创建和使用一个方向灯光。注意在这个示例程序中我们不会使用在文件d3dUtility.h/cpp中的材质和灯光函数。因为我们想展示怎样手动来做这些设置。
图5.7
给场景增加灯光的步骤是:
1、 允许使用灯光。
2、 为每个物体创建材质并且在渲染相应物体前应将材质附予物体。
3、 创建一个或多个光源,设置它们,把它们设为可用。
4、 将其他附加光源设为可用,比如镜面高光。
首先我们初始化一个全局顶点缓存用他来存储“金字塔”的顶点:
IDirect3DVertexBuffer9* Pyramid = 0; |
Setup函数包含本章的所有代码,因此我们忽略其他函数。它执行刚才讨论的步骤来给场景加入灯光。Setup方法首先允许使用灯光,当然这不是必须的因为默认设置就是允许使用灯光的。
bool Setup()
{
Device->SetRenderState(D3DRS_LIGHTING, true); |
下一步,我们创建顶点缓存,锁定,并且把“金字塔”的三角形顶点放入其中。顶点法线是利用5.3节中的运算法则预先计算好的。注意三角形共享顶点,但它们的法线不能共享;因此对这个物体使用索引列表并不是最有利的。例如,所有三角形都共享顶点(0,1,0);然而,对每个三角形,它们的顶点法线是不相同的。
Device->CreateVertexBuffer(
12 * sizeof(Vertex),
D3DUSAGE_WRITEONLY,
Vertex::FVF,
D3DPOOL_MANAGED,
&Pyramid,
0);
// fill the vertex buffer with pyramid data
Vertex* v;
Pyramid->Lock(0, 0, (void**)&v, 0);
// front face
v[0] = Vertex(-1.0f, 0.0f, -1.0f, 0.0f, 0.707f, -0.707f);
v[1] = Vertex( 0.0f, 1.0f, 0.0f, 0.0f, 0.707f, -0.707f);
v[2] = Vertex( 1.0f, 0.0f, -1.0f, 0.0f, 0.707f, -0.707f);
// left face
v[3] = Vertex(-1.0f, 0.0f, 1.0f, -0.707f, 0.707f, 0.0f);
v[4] = Vertex( 0.0f, 1.0f, 0.0f, -0.707f, 0.707f, 0.0f);
v[5] = Vertex(-1.0f, 0.0f, -1.0f, -0.707f, 0.707f, 0.0f);
// right face
v[6] = Vertex( 1.0f, 0.0f, -1.0f, 0.707f, 0.707f, 0.0f);
v[7] = Vertex( 0.0f, 1.0f, 0.0f, 0.707f, 0.707f, 0.0f);
v[8] = Vertex( 1.0f, 0.0f, 1.0f, 0.707f, 0.707f, 0.0f);
// back face
v[9] = Vertex( 1.0f, 0.0f, 1.0f, 0.0f, 0.707f, 0.707f);
v[10] = Vertex( 0.0f, 1.0f, 0.0f, 0.0f, 0.707f, 0.707f);
v[11] = Vertex(-1.0f, 0.0f, 1.0f, 0.0f, 0.707f, 0.707f);
Pyramid->Unlock(); |
为物体产生了顶点数据以后,我们描述利用灯光表现各自材质的物体间是怎样相互影响的。在这个例子中,“金字塔”反射出白光,自身不发光,且会产生一些高光。
D3DMATERIAL9 mtrl;
mtrl.Ambient = d3d::WHITE;
mtrl.Diffuse = d3d::WHITE;
mtrl.Specular = d3d::WHITE;
mtrl.Emissive = d3d::BLACK;
mtrl.Power = 5.0f;
Device->SetMaterial(&mtrl); |
接着,我们创建一个方向光并将其设为可用。方向光是沿着x轴的正方向照射的。灯光照射最强的白色漫射光(dir.Diffuse = WHITE),较弱的白色镜面光(dir.Specular = WHITE * 0.3f)以及一个中等强度的白色环境光(dir.Ambient = WHITE *0.6f)。
D3DLIGHT9 dir;
::ZeroMemory(&dir, sizeof(dir));
dir.Type = D3DLIGHT_DIRECTIONAL;
dir.Diffuse = d3d::WHITE;
dir.Specular = d3d::WHITE * 0.3f;
dir.Ambient = d3d::WHITE * 0.6f;
dir.Direction = D3DXVECTOR3(1.0f, 0.0f, 0.0f);
Device->SetLight(0, &dir);
Device->LightEnable(0, true); |
最后,我们设置状态使法线从新规格化且把镜面高光设置为可用。
Device->SetRenderState(D3DRS_NORMALIZENORMALS, true);
Device->SetRenderState(D3DRS_SPECULARENABLE, true);
// ... code to set up the view matrix and projection matrix
// omitted
return true;
} |
5.6附加实例
这一章中还有三个附加的例子。它们使用D3DXCreate*函数来创建组成场景的3D物体。D3DXCreate*函数创建的顶点数据是D3DFVF_XYZ | D3DFVF_NORMAL格式。在增加的函数中为我们的网格模型的每个顶点计算了顶点法线。这些实例演示了怎样使用方向光,点光源,以及聚光灯。图5.8显示的是方向光实例中的一个场景图。
图5.8
5.7摘要(略)
第六章 纹理
纹理映射是一种允许我们为三角形赋予图象数据的技术;这让我们能够更细腻更真实地表现我们的场景。例如,我们能够创建一个立方体并且通过对它的每个面创建一个纹理来把它变成一个木箱(如图6.1)。
图6.1
在Direct3D中一个纹理是通过IDirect3DTexture9接口来表现的。一个纹理是一个类似像素矩阵的表面它能够被映射到三角形上。
目标
l 学习怎样指定纹理到三角形上。
l 弄懂怎样创建一纹理。
l 学习怎样通过过滤纹理来创建一个更光滑的图象。
6.1 纹理坐标
Direct3D使用一个纹理坐标系统,它是由用水平方向的u轴和竖直方向v轴构成。由u,v坐标决定纹理上的元素,它被叫做texel。注意v轴是向下的(如图6.2)。
图6.2
同样,注意规格化的坐标间隔,[0,1],它被使用是因为它给Direct3D一个固定的范围用于在不同尺寸的纹理上工作。
对每一个3D三角形,我们都希望在给它贴图的纹理上定义一个用相应的三角形。(如图6.3)。
图6.3
我们再一次修改原来的顶点结构,添加一个用于在纹理上定位的纹理坐标。
struct Vertex
{
float _x, _y, _z;
float _nx, _ny, _nz;
float _u, _v; // texture coordinates
static const DWORD FVF;
};
const DWORD Vertex::FVF = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1; |
我们在顶点格式中添加了一个D3DFVF_TEX1,它是说我们的顶点结构中包含了一个纹理坐标。
现在每个三角形都通过顶点的三个对象来建立,同时也通过纹理坐标定义了一个相应的纹理三角形。
6.2创建并赋予材质
纹理数据通常是从存储在磁盘中的图片文件中读取的,且被读进IDirect3DTexture9对象中。我们能够使用下面的D3DX函数完成这项工作:
HRESULT D3DXCreateTextureFromFile(
LPDIRECT3DDEVICE9 pDevice, // device to create the texture
LPCSTR pSrcFile, // filename of image to load
LPDIRECT3DTEXTURE9* ppTexture // ptr to receive the created texture
); |
这个函数能够读取下面图片格式中的任意一种:BMP,DDS,DIB,JPG,PNG,TGA。
例如,用一个名为stonewall.bmp的图片创建一个纹理,我们将按照下面的例子来写:
IDirect3Dtexture9* _stonewall;
D3DXCreateTextureFromFile(_device, "stonewall.bmp", &_stonewall); |
设置当前纹理,我们使用下面的方法:
HRESULT IDirect3DDevice9::SetTexture(
DWORD Stage, // A value in the range 0-7 identifying the texture
// stage – see note on Texture Stages
IDirect3DBaseTexture9* pTexture // ptr to the texture to set
); |
例子:
Device->SetTexture(0, _stonewall); |
注意:在Direct3D中,你能够设置八个纹理,它们能够组合起来创建更多细节的图象。这又被叫做多重纹理。在本书的第四部分以前我们不会使用多重纹理;因此现在我们总是设置stage为0。
为了销毁一个纹理,我们设置pTexture为0。例如,假如不想用一个纹理来渲染物体,那么我们就这样写:
Device->SetTexture(0, 0);
renderObjectWithoutTexture(); |
假如场景中有使用不同纹理的三角形,我们就必须添加与下面类似的一些代码:
Device->SetTexture(0, _tex0);
drawTrisUsingTex0();
Device->SetTexture(0, _tex1);
drawTrisUsingTex1(); |
6.3过滤器
就象以前提及的,纹理被映射到屏幕中的三角形上。通常纹理三角形和屏幕三角形是不一样大的。当纹理三角形比屏幕三角形小时,纹理三角形会被适当放大。当纹理三角形比屏幕三角形大时,纹理三角形会被适当缩小。这两种情况,变形都将会出现。过滤(Filtering)是一种Direct3D用它来帮助这些变形变的平滑的技术。
Direct3D提供了三种不同的过滤器;每种都提供了一个不同的品质级别。越好的品质越慢,因此你必须在品质与速度之间取得一个平衡。纹理过滤器是用IDirect3DDevice9::SetSamplerState方法来设置的。
l Nearest point sampling——这是默认的过滤方法且返回最差的效果,但是它的计算是最快的。下面的代码就是设置Nearest point sampling作为缩小放大的过滤器:
Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_POINT);
Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_POINT); |
l Linear filtering——这种过滤产生还算比较好的效果,在今天的硬件上处理它还是非常快的。它是被推荐使用的。下面的代码就是设置Linear filtering作为缩小放大的过滤器。
Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); |
l Anisotropic filtering——这种过滤产生最好的效果,但是处理时间也是最长的。下面的代码就是设置Anisotropic filtering作为缩小放大的过滤器。
Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_ANISOTROPIC);
Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC); |
当使用Anisotropic filtering时,我们必须设置D3DSAMP_MAXANISOTROPY等级,它决定处理的质量。该值越高处理的效果越好。检查D3DCAPS9结构确认你的显卡是否支持此功能。下面的代码设置该值为4:
Device->SetSamplerState(0, D3DSAMP_MAXANISOTROPY, 4); |
6.4 Mipmaps
就象6.3节所说的,在屏幕上的三角形和纹理三角形通常是不一样大的。为了使这个大小差异变小,我们为纹理创建mipmaps链。也就是说将一个纹理创建成连续的变小的纹理,但是对它们等级进行定制过滤,因此对我们来说保存细节是很重要的(如图6.4)。
图6.4
6.4.1 Mipmaps过滤器
mipmap过滤器是被用来控制Direct3D使用mipmaps的。设置mipmap过滤器,你可以这样写:
Device->SetSamplerState(0, D3DSAMP_MIPFILTER, Filter); |
在Filter处你能用下面三个选项中的一个:
l D3DTEXF_NONE——不使用mipmap。
l D3DTEXF_POINT——通过使用这个过滤器,Direct3D将选择与屏幕三角形大小最接近的mipmap等级。一旦等级选定了,Direct3D就将按照指定的过滤器进行缩小和放大过滤。
l D3DTEXF_LINEAR——通过使用这个过滤器,Direct3D将选择两个最接近的mipmap等级,缩小和放大过滤每个等级,然后线性联合计算它们两个等级来得到最终的颜色值。
6.4.2 Direct3D中使用Mipmaps
在Direct3D中使用Mipmaps是很简单的。假如你的显卡支持Mipmaps,那么使用D3DXCreateTextureFromFile将为你产生一个Mipmap链。Direct3D自动选择与屏幕三角形最匹配的Mipmap。因此Mipmap有非常广泛的应用,且它能被自动设置。
6.5 寻址模式
以前,我们规定纹理坐标必须指定在[0,1]之间。从技术上来说这是不正确的;他们能够超出这个范围。纹理坐标也可以在[0,1]的范围之外,它通过Direct3D的寻址模式来定义。这里有四种寻址模式:环绕纹理寻址模式、边框颜色纹理寻址模式、截取纹理寻址模式、镜像纹理寻址模式,这里分别给出了它们的示意图6.5,6.6,6.7,6.8。
图6.5(环绕) 图6.6(边框)
图6.7(截取) 图6.8(镜像)
在这些图片中,纹理坐标通过(0,0)(0,3)(3,0)(3,3)顶点来定义。在u轴和v轴上方块又被分成子块放进3×3的矩阵中。假如,你想让纹理按5×5的方格来平铺,你就应该指定环绕纹理寻址模式并且纹理坐标因该设置为(0,0)(0,5)(5,0)(5,5)。
下面的代码片段列举的是怎样设置这四种寻址模式:
// set wrap address mode
if( ::GetAsyncKeyState('W') & 0x8000f )
{
Device->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
Device->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
}
// set border color address mode
if( ::GetAsyncKeyState('B') & 0x8000f )
{
Device->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_BORDER);
Device->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_BORDER);
Device->SetSamplerState(0, D3DSAMP_BORDERCOLOR, 0x000000ff);
}
// set clamp address mode
if( ::GetAsyncKeyState('C') & 0x8000f )
{
Device->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
Device->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
}
// set mirror address mode
if( ::GetAsyncKeyState('M') & 0x8000f )
{
Device->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_MIRROR);
Device->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_MIRROR);
} |
6.6实例程序:有纹理的方块
这个例子是怎样为方块加上纹理以及设置一个纹理过滤器(如图6.9)。假如你的显卡支持,通过D3DXCreateTextureFromFile函数一个mipmap链将被自动创建。
图6.9
注意:还提供了其他两个例子大家就自己看看了。
为一个场景增加纹理的必要步骤是:
1. 用纹理坐标指定的,创建物体的顶点。
2. 用D3DXCreateTextureFromFile函数读取一个纹理到IDirect3DTexture9接口中。
3. 设置缩小倍数,放大倍数以及mipmap过滤器。
4. 在你绘制一个物体前,用IDirect3DDevice9::SetTexture设置与物体关联的纹理。
我们先定义几个全局变量;一个是顶点缓存,它存储方块的顶点。另外一个是我们为方块映射的纹理:
IDirect3DVertexBuffer9* Quad = 0;
IDirect3DTexture9* Tex = 0; |
Setup程序是很容易读懂的;我们用已经定义了纹理坐标的两个三角形创建一个方块。然后把文件dx5_logo.bmp读进IDirect3DTexture9接口中。接着使用SetTexture方法赋予纹理。最后设置缩小和放大过滤器进行线性过滤,我们也可以设置mipmap过滤器来进行D3DTEXF_POINT:
bool Setup()
{
//
// Create the quad vertex buffer and fill it with the
// quad geoemtry.
//
Device->CreateVertexBuffer(
6 * sizeof(Vertex),
D3DUSAGE_WRITEONLY,
Vertex::FVF,
D3DPOOL_MANAGED,
&Quad,
0);
Vertex* v;
Quad->Lock(0, 0, (void**)&v, 0);
// quad built from two triangles, note texture coordinates:
v[0] = Vertex(-1.0f, -1.0f, 1.25f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f);
v[1] = Vertex(-1.0f, 1.0f, 1.25f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f);
v[2] = Vertex( 1.0f, 1.0f, 1.25f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f);
v[3] = Vertex(-1.0f, -1.0f, 1.25f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f);
v[4] = Vertex( 1.0f, 1.0f, 1.25f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f);
v[5] = Vertex( 1.0f, -1.0f, 1.25f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f);
Quad->Unlock();
//
// Create the texture and set filters.
//
D3DXCreateTextureFromFile(
Device,
"dx5_logo.bmp",
&Tex);
Device->SetTexture(0, Tex);
Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
Device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_POINT);
//
// Don't use lighting for this sample.
//
Device->SetRenderState(D3DRS_LIGHTING, false);
//
// Set the projection matrix.
//
D3DXMATRIX proj;
D3DXMatrixPerspectiveFovLH(
&proj,
D3DX_PI * 0.5f, // 90 - degree
(float)Width / (float)Height,
1.0f,
1000.0f);
Device->SetTransform(D3DTS_PROJECTION, &proj);
return true;
} |
我们现在可以渲染方块了,且通常已经为它赋予了纹理:
bool Display(float timeDelta)
{
if( Device )
{
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0);
Device->BeginScene();
Device->SetStreamSource(0, Quad, 0, sizeof(Vertex));
Device->SetFVF(Vertex::FVF);
Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
Device->EndScene();
Device->Present(0, 0, 0, 0);
}
return true;
} |
6.7摘要(略)
第七章 混合
在这一章里我们介绍一种叫做混合(blending)的技术,它允许我们混合像素,我们通常用已经光栅化的像素光栅化同一位置的像素。换句话说就是我们在图元上混合图元。这种技术允许我们完成多种特效。
目标
l 弄懂怎样混合以及怎样使用它。
l 学习Direct3D支持的不同类型的混合方式。
l 弄懂alpha混合能够被用来控制图元的透明度。
7.1混合因素
观察图7.1,我们将一个红色的茶壶绘制在一个木质背景上。
图7.1
假设想让茶壶有一个透明度,以便我们能够透过茶壶看见背景(如图7.2)。
图7.2
我们怎样才能实现这个效果呢?我们只需要在木箱子上光栅化茶壶三角形,我们需要结合像素颜色,就象通过茶壶显示木箱那样来计算茶壶的像素颜色。结合像素值的意思就是用以前写过的目标像素值去估算源像素值这被叫做混合。注意混合的效果不仅仅象是玻璃透明一样。我们有很多选项来指定颜色是怎样被混合的,就象7.2部分中看到的一样。
这是很重要的,认识三角形普遍利用以前写入后缓存中的像素来与之混合来光栅化。在示例图片中,木箱图片首先被画出来且它的像素在后缓存中。我们然后绘制茶壶,以便用木箱的像素来混合茶壶的像素。因此,当使用混合时,下面的规则将被遵循:
规则:首先不使用混合绘制物体。然后根据物体离摄象机的距离使用混合对物体拣选;这是非常有效的处理,假如物体是在视图坐标中,那么你能够利用z分量简单地拣选。最后使用从后到前的顺序混合绘制物体。
下面的公式是用来混合两个像素值的:
上面的所有变量都是一个4D颜色向量(r,g,b,a),并且 符号是表示分量相乘。
l OutputPixel——混合后的像素结果。
l SourcePixel——通常被计算的像素,它是利用在后缓存中的像素来被混合的。
l SourceBlendFactor——在[0,1]范围内的一个值。它指定源像素在混合中的百分比。
l DestPixel——在后缓存中的像素。
l DestBlendFactor——在[0,1]范围内的一个值。它指定目的像素在混合中的百分比。
源和目的混合要素使我们能够按照多种途径改变原始源和目的像素,允许实现不同的效果。7.2节列举了能够被使用的预先确定的值。
混合默认是被关闭的;你能够通过设置D3DRS_ALPHABLENDENABLE渲染状态为true来开启它:
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true); |
7.2混合要素
通过设置不同的源和目的要素,我们能够创造很多不同的混合效果。通过实验,使用不同的组合看看它们到底能实现什么效果。你能够通过设置D3DRS_SRCBLEND和D3DRS_DESTBLEND渲染状态来分别设置源混合要素和目的混合要素。例如我们可以这样写:
Device->SetRenderState(D3DRS_SRCBLEND, Source);
Device->SetRenderState(D3DRS_DESTBLEND, Destination); |
这里Source和Destination能够使用下面混合要素中的一个:
l D3DBLEND_ZERO——blendFactor=(0, 0, 0, 0)
l D3DBLEND_ONE——blendFactor=(1, 1, 1, 1)
l D3DBLEND_SRCCOLOR——blendFactor=(rs, gs, bs, as)
l D3DBLEND_INVSRCCOLOR——blendFactor=(1-rs, 1-gs, 1-bs, 1-as)
l D3DBLEND_SRCALPHA——blendFactor=(as, as, as, as)
l D3DBLEND_INVSRCALPHA——blendFactor=(1-as, 1-as, 1-as, 1-as)
l D3DBLEND_DESTALPHA——blendFactor=(ad, ad, ad, ad)
l D3DBLEND_INVDESTALPHA——blendFactor=(1-ad, 1-ad, 1-ad, 1-ad)
l D3DBLEND_DESTCOLOR——blendFactor=(rd, gd, bd, ad)
l D3DBLEND_INVDESTCOLOR——blendFactor=(1-rd, 1-gd, 1-bd, 1-ad)
l D3DBLEND_SRCALPHASAT——blendFactor=(f, f, f, 1) , f=min(as, 1 – ad)
l D3DBLEND_BOTHINVSRCALPHA——这种混合模式设置源混合要素为(1-as, 1-as, 1-as, 1-as,)以及目的混合要素为(as,as,as,as)。这种混合模式仅对D3DRS_SRCBLEND有效。
源和目的混合要素的默认值分别是D3DBLEND_SRCALPHA和D3DBLEND_INVSRCALPHA。
7.3透明度
在以前的章节中我们忽略了颜色顶点和材质中的alpha部分,那是因为当时它并不是必须的。现在它首先被用在混合中。
Alpha部分主要是用来指定像素的透明等级。我们为每个像素的alpha部分保留8位,alpha的有效值在[0,255]范围内,[0,255]代表不透明度[0%,100%]。因此,像素的alpha为0时,表示完全透明,像素的alpha为128时,表示50%透明,像素的alpha为255时,表示完全不透明。
为了让alpha部分描述像素的透明等级,我们必须设置源混合要素为D3DBLEND_SRCALPHA以及目的混合要素为D3DBLEND_INVSRCALPHA。这些值碰巧也是被默认设置的。
7.3.1Alpha通道
代替使用Alpha部分来计算遮影,我们能够从纹理的alpha通道中得到alpha信息。Alpha通道是额外的设置位,用它来保存每一个点的alpha值。当一个纹理被映射到一个图元上时,在alpha通道中的alpha信息也被映射,并且它们利用alpha信息为每个像素赋予纹理。图7.3显示了一个带8位alpha通道的图片。
图7.3
图7.4显示的是一个利用alpha通道指定透明度来渲染的一个纹理方块。
图7.4
7.3.2指定Alpha资源
默认情况下,假如设置一个有alpha通道的纹理,alpha值从在alpha通道中获得。假如没有alpha通道,那么alpha值是通过顶点颜色获得。然而,你能够通过下面的渲染状态来指定使用哪一个资源:
// compute alpha from diffuse colors during shading
Device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
Device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
// take alpha from alpha channel
Device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
Device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); |
7.4使用DirectX纹理工具创建Alpha通道
绝大多数普通图象文件格式没有存储alpha信息。在这一部分我们给你演示怎样使用DirectX纹理工具来创建一个带alpha通道的DDS文件。DDS文件是一个为DirectX应用程序和纹理设置的图象格式。DDS文件能够利用D3DXCreateTextureFromFile被读进纹理中,就象bmp和jpg文件一样。DirectX纹理工具被放在你的DXSDK目录下的\Bin\DXUtils文件夹下(我是放在C:\Program Files\Microsoft DirectX 9.0 SDK (February 2005)\Utilities\Bin\x86下的,文件名是DxTex.exe)。
打开DirectX纹理工具,并且把本章中示例文件夹下的crate.jpg文件用工具打开。木箱被自动的按照24位RGB纹理被读取。它包含8位红色,8位绿色,以及8位蓝色。我们需要将该纹理增加为32位ARGB纹理,增加的是额外的8位alpha通道。从菜单中选择Format,选择Change Surface Format。一个象图7.5的对话框将被弹出。选择A8R8G8B8格式点击OK。
图7.5
它创建了一个32位颜色深度的图象,它的每个象素都有8位alpha通道,8位红色,8位绿色,8位蓝色。我们下一步是向alpha通道中写入数据。我们将图7.3中的8位灰色图片信息写进alpha通道中。选择菜单中的File,选择Open Onto Alpha Channel Of This Texture。一个对话框将弹出让你选择包含你想要写入alpha通道中数据信息的图片。选择alphachannel.bmp文件。图7.6显示的是程序已经插入了alpha通道数据。
图7.6
现在用你选择的文件名存储纹理;我们使用cratewalpha.dds文件名。
7.5实例程序:透明度
这个实例程序是在一个木箱背景上绘制一个透明的茶壶,就象图7.2所显示的一样。在这个例子中alpha值是从材质中得到。应用程序允许我们通过按A或S键来增加/减少alpha的值。
使用混合的必要步骤是:
1. 设置混合要素D3DRS_SRCBLEND 和 D3DRS_DESTBLEND。
2. 假如你使用alpha部分,指定资源(材质或alpha通道)。
3. 允许alpha混合渲染状态。
对于这个例子,我们定义下面的全局变量:
ID3DXMesh* Teapot = 0; // the teapot
D3DMATERIAL9 TeapotMtrl; // the teapot’s material
IDirect3DVertexBuffer9* BkGndQuad = 0; // background quad - crate
IDirect3DTexture9* BkGndTex = 0; // crate texture
D3DMATERIAL9 BkGndMtrl; // background material |
Setup方法设置很多东西;我们省略了很多与本章无关的代码。关心混合,Setup方法指定alpha值的获取资源。在这个例子中,我们通过材质指定alpha值。注意我们设置茶壶的材质alpha部分为0.5,也就是说茶壶将按照50%的透明度被渲染。我们在这里也要设置混合要素。要注意的是在这个方法中我们不能将alpha混合设置为启用。理由是alpha混合要进行额外的处理并且应该仅在需要用时才被使用。举例,在这个例子中只有茶壶需要用允许alpha混合来被渲染——而方块不需要。因此,我们在Display函数中启用alpha混合。
bool Setup()
{
TeapotMtrl = d3d::RED_MTRL;
TeapotMtrl.Diffuse.a = 0.5f; // set alpha to 50% opacity
BkGndMtrl = d3d::WHITE_MTRL;
D3DXCreateTeapot(Device, &Teapot, 0);
...// Create background quad snipped
...// Light and texture setup snipped
// use alpha in material's diffuse component for alpha
Device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
Device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
// set blending factors so that alpha
// component determines transparency
Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
...// view/projection matrix setup snipped
return true;
} |
在Display函数中,我们检测假如A或S键被按下那么就通过增加或减少材质的alpha值来反馈。注意这个方法要保证alpha值不会超出[0,1]的范围。我们然后渲染背景。最后,我们启用alpha混合,利用alpha混合来渲染茶壶,关闭alpha混合。
bool Display(float timeDelta)
{
if( Device )
{
//
// Update
//
// increase/decrease alpha via keyboard input
if( ::GetAsyncKeyState('A') & 0x8000f )
TeapotMtrl.Diffuse.a += 0.01f;
if( ::GetAsyncKeyState('S') & 0x8000f )
TeapotMtrl.Diffuse.a -= 0.01f;
// force alpha to [0, 1] interval
if(TeapotMtrl.Diffuse.a > 1.0f)
TeapotMtrl.Diffuse.a = 1.0f;
if(TeapotMtrl.Diffuse.a < 0.0f)
TeapotMtrl.Diffuse.a = 0.0f;
//
// Render
//
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
0xffffffff, 1.0f, 0);
Device->BeginScene();
// Draw the background
D3DXMATRIX W;
D3DXMatrixIdentity(&W);
Device->SetTransform(D3DTS_WORLD, &W);
Device->SetFVF(Vertex::FVF);
Device->SetStreamSource(0, BkGndQuad, 0, sizeof(Vertex));
Device->SetMaterial(&BkGndMtrl);
Device->SetTexture(0, BkGndTex);
Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
// Draw the teapot
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
D3DXMatrixScaling(&W, 1.5f, 1.5f, 1.5f);
Device->SetTransform(D3DTS_WORLD, &W);
Device->SetMaterial(&TeapotMtrl);
Device->SetTexture(0, 0);
Teapot->DrawSubset(0);
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
Device->EndScene();
Device->Present(0, 0, 0, 0);
}
return true;
} |
注意:在本章中有另一个使用纹理通道来演示alpha混合的例子texAlpha。与上边的代码不同之处仅仅在于得到alpha值是从纹理而不是从材质。
// use alpha channel in texture for alpha
Device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
Device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); |
这个应用程序读取的是一个在7.4节中用DX Tex Tool工具创建的带有alpha通道的DDS文件。
7.6摘要(略)
实例一 三角形 http://dev.gameres.com/Program/Visual/3D/3DGameExample1.rar
实例二 立方体 http://dev.gameres.com/Program/Visual/3D/3DGameExample2.rar
实例三 茶壶 http://dev.gameres.com/Program/Visual/3D/3DGameExample3.rar
实例四 D3DXCreate http://dev.gameres.com/Program/Visual/3D/3DGameExample4.rar
实例程序:彩色三角形 http://dev.gameres.com/Program/Visual/3D/3DGameExample5.rar
实例程序http://dev.gameres.com/Program/Visual/3D/3DGameExample6.rar (05-04-17)
实例程序http://dev.gameres.com/Program/Visual/3D/3DGameExample7.rar (05-04-25)
实例程序http://dev.gameres.com/Program/Visual/3D/3DGameExample8.rar (05-05-16)
翁云兵 Email :WengYB@126.com
GameRes游戏开发资源网 http://www.gameres.com