14.1 引言
光乃万物之源。我们无法想象,这个美丽怕人的世界,如果没有光的陪伴,会是怎样的一副满目苍夷。
计算机3D 世界作为现实世界的高度逼真的模仿,必然也少不了光照的陪伴。回到我们的Direct3D应用程序中来,在Direct3D 中运用光照,能有效地增强3D 场景的真实感。在3D 场景中使用光照其实非常简单,我们不需要为物体的每个顶点都指定颜色值,只要告诉Direct3D 我们使用的是什么类型的光照,物体的材质的具体参数以及物体表面相对于光源的朝向, Direct3D 就会根据其内置的算法,计算出每个顶点的颜.色值,产生出逼真的光照效果。
当然,随着学习的深入,功力的加深,可以不依赖于Direct3D 中内置的光照算法,直接运用各种功能的着色器,自己编写出更加优化更加逼真的光照效果来。对于刚刚接触到Direct3D 中的光照和材质,我们还是先掌握固定功能流水线中的这一套易学易懂的这一块内容,先把基础打牢,先学会走,这样才能为自己编写更优的光照效果做铺垫。一套完整的光照体系, 有两个组成方面: 光照和材质。这两者天生就是一对好搭档,我们可以把它们看做光照计算的两要素,想要绘制出具有光照的真实三维世界,两者缺一不可。下面开始正式讲解, 首先是四大光照类型。
14.2 四大光照类型
1 . 环境光( Ambient Light)
一个物体即使没有直接被光源照射,但是只要有光线通过其他物体的折射、反射到达这个物体,它也可能被看见。这种基于整个自然界环境的整体亮度,称为环境光( Ambient Light ) 或者背景光。环境光没有位置或者方向上的特征,只有一个颜色亮度值,而且不会衰减,所以在所有方向和所有物体表面上投射的环境光的数量是国定不变的。想要以较低的代价和开销来近似模拟光照的话, 直接开启环境光是一个不错的选择。
在Direct3D 中环境光的设置非常简单,可以直接使用SetRenderState 方法,代码如下:
- pd3dDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(36,36,36)) ;
其中第一个参数填D3DRS_AMBIENT,代表环境光的设置,而第二个参数填一个颜色值就可以了。
2 . 漫反射光(Diffuse Light)
漫反射光在我们的生活中最为普遍,太阳的直射、日光灯的照射都可以看成漫反射的近似。这种类型的光沿着特定的方向传播。当它到达某一表面时,将沿着各个方向均匀反射,所以无论从哪个方向观察,物体表面的亮度都是相同的。采用漫反射这种光照模型时,无需考虑观察者的位置,但是需要考虑漫反射光的空间位置和方向。从一个光源发出的光一般都是这种类型的。漫反射光并没有简洁的设置方法, 具体下文会讲到的,请大家继续往后看。
3. 镜面反射光(Specular Light)
镜面反射光,顾名思义, 沿着特定的方向传播,当此类光到达一个表面时, 将严格地沿着另一个方向反射,从而形成只能在一个角度范围内才能观察到的高亮度照射。这种光照模型模拟了从光滑发光面如镜子、一块金属或者一块发光塑料等材料来进行光线反射的情形。如果我们移动一下光源的话,就会发现镜面亮光区所发生的变化,这意味着镜面反射取决于观察者的角度。我们可以这
么说,漫反射与视觉无关,而镜面反射与视觉相关。
需要注意的是, 镜面光与其他类型的光相比,计算量要大得多, Direct3D 默认情况是关闭镜面反射的。如果我们想启用镜面反射的话,可以使用下面的代码,即把渲染状态D3DRS_SPECULARENABLE 设为true;
- pd3dDevice ->SetRenderState (D3DRS_SPECULARENABLE, true);
4. 自发光(Emissive Light)自发光就是对象自己发出的光,它是根据通过对象的自发光材质实现的。下面我们在讲解材质时讲到的D3DMATERIAL9 结构体, 这个结构体的成员Emissive 描述自发光的颜色和透明度。自发光影响着一个对象的颜色,比如我们可以通过设置自发光的颜色属性, 把一些灰暗的材质变得明亮一些。
需要提出来的是, 我们可以使用材质的自发光属性来“照亮”这个对象,而不用在场景内部添加灯光,从而缩小了计算量。自发光属性创建的材质并不发射出能被场景内其他对象反射的光,也就是说,它发出的光并不参与光运算,而为了实现反射光, 需要在场景中添加额外的灯光。
讲解完四大光照类型,接下来当然少不了三大光源类型。
14.3 三大光源类型
在Direct3D 中的光源类型和光照类型是两个完全不同的概念,光照模型描述的是光线的反射特征,而光源类型主要强调的是能够产生这些光照模型的方式以及光线的位置、方向、强度等特征。
D3D 中主要有3 种类型的光源,合称为三大光源: 点光源( Point Light )、方向光(Directional Light )和聚光灯(SpotLight)。
在Direct3D 9.0c 中,讲到光源,涉及到一个结构体,那就是D3DLIGHT9 结构体, 在展开讲解各种光源类型之前, 先来看看这个结构体的具体内容,我们可以在MSDN 中查到它的原型:
- 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, *LPD3DLIGHT;
- 第一个参数, D3DLIGHTTYPE 类型的Type , 表示光源的类型, 在D3DLIGHTTYPE 这个枚举体中取值,而D3DLIGHTTYPE 枚举体有如下定义:
- typedef enum D3DLIGHTTYPE {
- D3DLIGHT_POINT = 1,
- D3DLIGHT_SPOT = 2,
- D3DLIGHT_DIRECTIONAL = 3,
- D3DLIGHT_FORCE_DWORD = 0x7fffffff
- } D3DLIGHTTYPE, *LPD3DLIGHTTYPE;
由它的命名可以很容记住,D3DLIGHT_POINT 为点光源类型,D3DLIGHT_SPOT 为聚光灯类型, D3DLIGHT_DIRECTIONAL 为方向光源类型, 而最后一个参数依然是前面我们几次提到的没有存在感的那个,我们不用去纠结它。
- 第二个参数到第四个参数是一个类型的,我们结合起来讲解。这三个参数都是D3DCOLORVALUE 类型的颜色值, 参数名分别为Diffuse 、Specular 、Ambient , 分别表示这个光源的漫反射、镜面反射和环境光的颜色值。
- 第五个参数, D3DVECTOR 类型的Position , 表示光源的位置。
- 第六个参数, D3DVECTOR 类型的Direction , 表示光源的光照方向。
- 第七个参数, float 类型的Range , 表示光源的光照范围,只在某些光源类型中有意义。
- 第八个参数以及第十二、第十三个参数又是一个类型的, 我们一起来介绍。这3 个参数就是同为float 类型的Falloff、Theta 以及Phi ,都是用在聚光灯光源类型中的,也就是说只有我们把D3DLIGHT9 的第一个参数Type 设为D3DLIGHT_SPOT 聚光灯类型的时候, 这3 个参数才有意义。
- 第九、第十、第十一这3 个参数显然也是同一阵营的, 我们也一起介绍, Attenuation0~Attenuation2 都为衰减系数, 定义了光强随着距离衰减的方式, 衰减公式如下:
其中, D 为光源到顶点的距离, Ao-A2 分别对应衰减系数Attenuation0~ Attenuation2 。接下来,我们看看这个结构体的用法。在Direct3D 中使用光照, 就是用我们这个D3DLIGHT9结构体实例化一个具体的光源类型,然后无脑地进行喜闻乐见的填空题操作, 对这个结构体的参数进行赋值, 赋值完成后调用IDirect3DDevice9 接口的SetLight 方法设置光源, 最后调用IDirect3DDevice9 接口的LightEnable 方法启用光照就可以了。下面我们分别来看看这两个方法。首先是SetLight 方法,用于设置光源:- HRESULT SetLight(
- [in] DWORD Index,
- [in] const D3DLight9 *pLight
- );
- 第一个参数, DWORD 类型的Index ,取值于0~7 之间, 表示选择第1 ~8 个光源。
- 第二个参数, const D3DLIGHT9 类型的*pLight,显然就是指向D3DLIGHT9 结构体的指针,包含设置好的灯光类型。在使用的时候, 这里填写我们之前实例化的D3DLIGHT9 结构体的名称,并在名称之前加一个“ &” 取地址符号就可以了。
然后是LightEnable 方法, LightEnable 方法用于启用光照:- HRESULT LightEnable(
- [in] DWORD LightIndex,
- [in] BOOL bEnable
- );
- 第一个参数,DWORD 类型的Index ,取值于0~7 之间,表示选择第1~8 个光源。
- 第二个参数,BOOL 类型的bEnable , 填true 或者false 表示启用或者禁用第一个参数里面指定的光照。
讲解完需要用到的光源结构体和两个方法,下面我们继续三大光源的讲解。1 . 点光源
点光源( Point Light )具有颜色和位置,但没有方向,它向所有方向发射的光都一样。
点光源是一个从中心向空间中各个方向发射相等强度光线的光源,且光的亮度会随着距离而衰减。要定义一个点光源,只要实例化一个D3DLIGHT9 结构体,将第一个参数设为D3DLIGHT_POINT,然后进行其余参数的设置就可以了,这样实例化出的这个结构体就是一个点光源了。下面看一个点光源设置的实例:
- D3DLIGHT9 light;
- ::ZeroMemory(&light, sizeof(light);
- light.Type = D3DLIGHT_POINT;
- light.Ambient = D3DXCOLOR(0.6f, 0.6f, 0.6f, 1.0f);
- light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
- light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f);
- light.Position = D3DXVECTOR3(0.0f, 200.0f, 0.0f);
- light.Attenuation0 = 1.0f;
- light.Attenuation1 = 0.0f;
- light.Attenuation2 = 0.0f;
- light.Range = 300.0f;
- pd3dDevice->SetLight(0, &light);
- pd3dDevice->LightEnable(0, true);
上面的这段代码非常容易理解,首先实例化一个D3DLIGHT9 结构体,然后把这个结构体用ZeroMemory 置零, 接着开始做填空题填充这个D3DLIGHT9 结构体,因为是点光源,所以Light.Type这个参数设为D3DLIGHT_POINT ,表示点光源。设置完之后,接着用SetLight 设置光源,用LightEnable 启用光照。
2 . 方向光源
方向光源是从无穷远处发出的一组平行、均匀的光线,在场景中以相同的方向传播,只具有颜色和方向,不受到衰减和范围的影响。同样地,要定义一个方向光源的话,实例化一个D3DLIGHT9结构体,将第一个参数设为D3DLIGHT_DIRECTIONAL ,然后进行其余参数的设置即可,这样实例化出的这个结构体就是一个方向光源了。下面我们看一个方向光源设置的实例:
- D3DLIGHT9 light;
- ::ZeroMemory(&light, sizeof(light);
- light.Type = D3DLIGHT_DIRECTIONAL;
- light.Ambient = D3DXCOLOR(0.6f, 0.6f, 0.6f, 1.0f);
- light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
- light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f);
- light.Direction = D3DXVECTOR3(1.0f, 0.0f, 0.0f);
- pd3dDevice->SetLight(0, &light);
- pd3dDevice->LightEnable(0, true);
这里的聚光灯光源,类似我们在演唱会时看到的那样的聚光灯,也如我们生活中的探照灯。聚光灯发出的光由一个明亮的内椎体( inner cone ) 和大一点的外椎体( outer cone )组成。内锥体中的光最亮,内锥体到外椎体外围的光强逐渐衰减,到了外椎体以外,已经衰减得没有光了。
上面我们讲到,光线强度从内锥体到外锥体逐渐衰减,通过聚光灯的Falloff、Theta 和Phi 这3个属性共同来控制其衰减规律。下图显示了Phi 和Theta 参数之间的关系,以及它们是如何影响着一个聚光灯的内外锥体的。
而Falloff用于控制光强如何从内锥体的外侧向外锥体的内侧减弱的,通常我们将其设为1.0f,使光线在两个圆锥间平滑地减弱。下图清晰地显示了Falloff 参数是如何影响内锥体和外锥体之间的光强变化的。
因为聚光灯受到衰减规律和光照范围的影响,场景中的每个顶点在计算光照时, 都要考虑这些因素, 这使得聚光灯成为在Direct3D 中首屈一指的高开销光源,因此我们要谨慎使用聚光灯。
同样地,要定义一个聚光灯光源,只要实例化一个D3DLIGHT9 结构体,将第一个参数设为D3DLIGHT_SPOT 然后进行其余参数的设置即可,这样实例化出的这个结构体就是一个聚光灯光源了。
讲完聚光灯的概念,下面是一个设置聚光灯光源的实例:
- D3DLIGHT9 light;
- ::ZeroMemory(&light, sizeof(light);
- light.Type = D3DLIGHT_SPOT;
- light.Position = D3DXVECTOR3(100.0f, 100.0f, 100.0f);
- light.Direction = D3DXVECTOR3(-1.0f, -1.0f, -1.0f);
- light.Ambient = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f);
- light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
- light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 0.3f);
- light.Attenuation0 = 1.0f;
- light.Attenuation1 = 0.0f;
- light.Attenuation2 = 0.0f;
- light.Range = 300.0f;
- light.Falloff = 0.1f;
- light.Phi = D3DX_PI / 3.0f;
- light.Theta = D3DX_PI / 6.0f;
- pd3dDevice->SetLight(0, &light);
- pd3dDevice->LightEnable(0, true);
14.4 材质
对于光照计算来说, 光照和材质两者缺一不可。物体表面的材质属性决定了它能反射什么颜色的光线以及能反射多少。在Direct3D 中,关于物体表面的材质属性,由一个结构体D3DMATERIAL9来负责管理。我们可以在MSDN 中查到这个结构体有如下原型:
- typedef struct D3DMATERIAL9 {
- D3DCOLORVALUE Diffuse;
- D3DCOLORVALUE Ambient;
- D3DCOLORVALUE Specular;
- D3DCOLORVALUE Emissive;
- float Power;
- } D3DMATERIAL9, *LPD3DMATERIAL9;
第一个参数, D3DCOLORVALUE 类型的Diffuse,表示物体表面对漫反射光的反射率, 我们可以发现这个参数类型为D3DCOLORVALUE ,也就是Direct3D 中的颜色类型。我们可以用像D3DXCOLOR (A, R , G, B )这样的句式来表示某种颜色,其中A、R、G、B 分别表示0. 0f 到1.0f 之间的透明色、红色、绿色、蓝色的分量值,比如这样写: D3DXCOLOR(0.5f, 0.5f, 0.7f, 1.0f)。
第二个参数, D3DCOLORVALUE 类型的Ambient , 表示物体表面对环境光的反射率,填一个颜色值。
第三个参数, D3DCOLORVALUE 类型的Specular,表示物体表面对镜面反射光的反射率,同样是填一个颜色值
第四个参数, D3DCOLORVALUE 类型的Emissive,表示物体的自发光颜色值,同样是填一个颜色值。
第五个参数, float 类型的Power,表示镜面反射指数,它的值越大,高光强度和周围亮度相差就越大。
我们还需要知道,物体的顶点颜色的亮度总和有一个公式:物体的颜色总和=物体反射环境光+物体反射漫反射光+物体反射镜面反射光+自发光。
也就是说,物体的最终颜色值由D3DMATERIAL9 结构体中设置的4 种颜色值共同决定。
在做完填空题,设置好我们的材质属性后,就需要调用一个SetMaterial 方法来设置当前使用的材质属性,我们可以在MSDN 中查到这个方法有如下原型:
- HRESULT SetMaterial(
- [in] const D3DMATERIAL9 *pMaterial
- );
这个方法唯一的一个参数就是指向D3DMATERIAL9 结构体的指针, 使用它时要记得在前面加上一个“ & ”取地址符号。
讲解完相关概念,下面看一个设置材质的实例:-
- D3DMATERIAL9 mtrl;
- ::ZeroMemory(&mtrl, sizeof(mtrl));
- mtrl.Ambient = D3DXCOLOR(0.5f, 0.5f, 0.7f, 1.0f);
- mtrl.Diffuse = D3DXCOLOR(0.6f, 0.6f, 0.6f, 1.0f);
- mtrl.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 0.3f);
- mtrl.Emissive = D3DXCOLOR(0.3f, 0.0f, 0.1f, 1.0f);
- g_pd3dDevice->SetMaterial(&mtrl);
另外需要注意,如果没有在程序中用代码来指定材质属性的话, Direct3D 有自己的一套默认材质, 默认材质反射所有的漫反射光,但不反射环境光和镜面反射光,也没有自发光颜色。我们可以理解为Direct3D 在内部为我们默认写了如下代码:-
- D3DMATERIAL9 mtrl;
- ::ZeroMemory(&mtrl, sizeof(mtrl));
- mtrl.Ambient = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);
- mtrl.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
- mtrl.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);
- mtrl.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);
- mtrl.Power = 0.Of ;
- g_pd3dDevice->SetMaterial(&mtrl);
另外再提一个与SetMaterial 非常相似的GetMaterial 方法, 用于获取Direct3D 使用的当前材质,原型如下:- HRESULT GetMaterial(
- [out] D3DMATERIAL9 *pMaterial
- );
14.5 关于顶点法线
在使用光照所绘制的3D 场景中,计算物体顶点的颜色值除了需要光源和物体的材质信息外,还需要知道每个顶点的法向量, 以便根据光线的入射方向与法向量的夹角,计算发射光线的最终颜色值。
这里提出了顶点法线和面法线两个概念,我们首先需要清楚地理解并区分这两个概念。
面法线很容易理解,即垂直于三角形平面的一条法线。那顶点法线又从何而来呢,严格地从法线的定义上来说, 其实顶点是不存在法线的, 那为何又有顶点法线这个概念呢? 让顶点也拥有法线,是为了在光照计算时,能够知晓光线到达表面时的入射角,以便在多面体的表面获得一种平滑的效果。
一般情况下,顶点法线与面法线的方向是相同的。但是在某些特殊的情况下,顶点法线并不与面法线相同。比如一个近似的球体或圆的顶点法线和面法线就不一致。
顶点法线可以在定义的顶点结构中进行描述。我们需要在前面已经拥有的顶点结构体中添加一组用于描述顶点法向量的数据成员。当然修改了顶点结构体,对应的FVF 灵活顶点格式的宏需要和结构体对应,也就是需要添加一句D3DFVF_NORMAL。
下面我们用第12 章里关于顶点格式设计的代码来做演示:
-
-
-
- struct CUSTOMVERTEX
- {
- FLOAT x, y, z;
- DWORD color;
- };
- #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE) //FVF灵活顶点格式
然后,与之对应的是我们添加顶点法线坐标后的代码:- struct CUSTOMVERTEX
- {
- FLOAT x, y, z;
- FLOAT nx , ny, nz ;
- DWORD color;
- };
- #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ| D3DFVF_NORMAL | D3DFVF_DIFFUSE) //FVF灵活顶点格式
既然在设计顶点格式的时候我们添加了顶点法线这个顶点属性,在后面的“顶点缓存、索引缓存绘图四步曲之三: 访问顶点缓存和索引缓存”这一步里面除了需要顶点坐标和顶点颜色外, 当然也需要填写顶点法向量的坐标了。对于简单的物体而言,比如立方体和球体,我们完全可以通过观察来得到这些顶点的法向量。然而,对于不规则或者复杂的物体,则需要另寻高招。对于复杂的物体,我们可以认为每个顶点的法向量与该顶点构成的三角形面的法向量相同。假设一个三角形由顶点P0, P1, P2 这三个顶点构成的,现在我们要计算每个顶点的法向量,就是求这个三角形面的法向量。其求法如下图:
也就是首先计算位于三角形平面内代表两条边的向量, 然后对这两条向量做叉乘运算就可以了,即:
P0 - P1=U
P2 - P1 = V
U × V=n
当然,当我们使用一组三角形渐进来表示曲面时,使用上述方法计算出的顶点法向量将会产生不光滑的效果。因此,另一种计算顶点法向量的方式应运而生一一计算法向量的均值(normal averaging ):首先我们求出共享该顶点的3 个三角形的面法向量,然后取它们的平均值作为该顶点的顶点法向量,如下图:
另外, 在变换过程中, 我们的顶点法线有可能不再是规范化的了。所以,最好的方法是在变换完成之后,通过在SetRenderState 方法把D3DRS_NORMALIZENORMALS 这个参数设为true来使所有的法向量规范化,也就是这样写:
- pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true );
14.6 总结与升华
本章到此为止讲解了许多内容,我们有必要做一下总结。关于Direct3D 中的光照的知识可以归纳如下:
- 光照计算两要素:光照和材质。
- 四大光照:环境光、镜面反射光,漫反射光和自发光。
- 三大光源:点光源、平行光源和聚光灯。
- 光源属性:一个结构体D3DLIGHT9 、两个方法SetLight 和LightEnable 。
- 材质属性:一个结构体D3DMATERIAL9 、一个方法SetMaterial 和法线向量的计算。
14.7 几何体的快捷绘制
14.7.1 D3D中内置的几何体概述
通过前面的讲解大家应该会发现, 使用顶点缓存和索引缓存通过绘制三角形单元来绘制图形还是有一点复杂的, 需要一定的想象力和大量顶点和索引的设置,我们一般很少通过逐个设置顶点缓存和索引缓存来进行几何体的绘制, 而比较人性化的做法是, Direct3D 为我们提供了几种特殊的生成简单3D 几何体的网格数据的方法。
下面我们就来看看这几个快捷的几何体绘制函数,它们分别是
立方体( Cube )、圆环体( Torus )、多边形( Polygon )、球面体( Sphere )、茶壶( Teapot )和圆柱体( Cylinder )。
我们可以在DirectX SDK( 以目前最新的Microsoft DirectX SDK (June 2010))为例找到这些函数的说明文档, 这是这些函数最官方最原始的参考资料了。
先给大家看看这几个函数的全家福, 后面我们会详细介绍使用方法:
可以看到,这些函数的名字取的都非常地憨厚老实, 一开始是D3DX , 即它们都来自我们熟知的D3DX 库,然后紧跟其后一个Create,表示是在创建某东西,最后跟上对应的几何体名,比如Teapot 茶壶,这样连起来就非常好记忆了,D3DXCreateTeapot。
14.7.2 D3D中几种内置的几何体绘制四步曲
首先需要说明的是下面提到的ID3DXMesh 接口类型是后面我们在讲解网格相关知识的时候重点讲解的内容,这里大家先对其有一个大概的印象就可以了。
想要通过这几个函数快捷绘制出一个几何体,需要以下几个步骤:
- 定义一个ID3DXMesh接口类型的对象。
- 调用这六个函数的其中一个对我们在第一步里面定义的这个对象进行初始化,也就是把创建好的网格存储在我们定义好的ID3DXMesh 类型的对象中。
- 在Direct3D 渲染五步曲的第二步,也就是在BeginScene 之后调用DrawSubset 方法进行网格图形的绘制,即是拿着第二步里面初始化好的ID3DXMesh 接口类型的对象指一下DrawSubset(0)方法就好了。
- 绘制完成之后,调用ID3DMesh 接口的Release 方法,或者我们自定义的COM 接口释放宏进行资源的释放。
总结起来也就简明扼要八个字: 定义, 创建, 绘制, 释放。
方便大家理解各个过程,下面给出一个注释好的代码例子:
-
- ID3DXMesh* meshBox;
-
- D3DXCreateBox(g_pd3dDevice, 2.0f, 2.0f, 2.0f, &meshBox, 0);
-
- g_pd3dDevice->BeginScene();
- meshBox- >DrawSubset (0);
-
- meshBox->Release();
14.7.3 D3D中几种内置几何体的创建
这一小节里面讲解的都是上文我们提到的几何体绘制四步曲的第二步里面的具体创建方法,但是配套的调用实例都包含了四步曲里面的前三步,大家需要注意一下。
(1) 立方体的创建——D3DXCreateBox 方法
D3DXCreateBox 方法用于创建一个立方体,或者说是一个盒子。
- HRESULT D3DXCreateBox(
- __in LPDIRECT3DDEVICE9 pDevice,
- __in FLOAT Width,
- __in FLOAT Height,
- __in FLOAT Depth,
- __out LPD3DXMESH *ppMesh,
- __out LPD3DXBUFFER *ppAdjacency
- ;
下面是一个调用实例,用于创建一个长方体:
-
- ID3DXMesh* meshBox;
- D3DXCreateBox (
- g_pd3dDevice,
- 2.0,
- 2.0f,
- 2.0f,
- &meshBox,
- 0
- ) ;
- g_pd3dDevice->BeginScene();
- meshBox->DrawSubset(O);
( 2 )柱体的创建——D3DXCreateCylinder 方法
D3DXCreateCylinder 方法用于创建柱体。- HRESULT D3DXCreateCylinder(
- __in LPDIRECT3DDEVICE9 pDevice,
- __in FLOAT Radius1,
- __in FLOAT Radius2,
- __in FLOAT Length,
- __in UINT Slices,
- __in UINT Stacks,
- __out LPD3DXMESH *ppMesh,
- __out LPD3DXBUFFER *ppAdjacency
- ;
- 第五个参数,UINT 类型的Slices ,表示柱体的外围有几个面, 举个例子,如果设为8 的话,我们创建的就是八角柱了。
- 第六个参数,UINT 类型的Stacks ,表示柱体的两端间共有几段,这个后面光照效果的时候需要用到的。
依然是一个调用实例, 方便大家快速掌握这个函数的用法:
-
- ID3DXMesh* meshCylinder;
- D3DXCreateCylinder (
- g_pd3dDevice,
- 280.f, // Z 轴负方向的半径大小
- 10.0f,
- 3000.0f,
- 60,
- 60,
- &meshBox,
- 0
- ) ;
- g_pd3dDevice->BeginScene();
- meshBox->DrawSubset(O);
( 3 ) 2D 多边形的创建——D3DXCreatePolygon 方法
D3DXCreatePolygon 方法用于快速创建一个2D 多边形。
- HRESULT D3DXCreatePolygon(
- __in LPDIRECT3DDEVICE9 pDevice,
- __in FLOAT Length,
- __in UINT Sides,
- __out LPD3DXMESH *ppMesh,
- __out LPD3DXBUFFER *ppAdjacency
- );
下面看一个调用实例,方便大家快速掌握这个函数的用法:
-
- ID3DXMesh* meshPolygon;
- D3DXCreatePolygon(
- g_pd3dDevice,
- 2.0f,
- 6,
- &meshBox,
- 0
- ) ;
- g_pd3dDevice->BeginScene();
- meshBox->DrawSubset(O);
(4 )球面体创建一一D3DXCreateSphere 方法
D3DXCreateSphere 方法用于创建一个球面体。
- HRESULT D3DXCreateSphere(
- __in LPDIRECT3DDEVICE9 pDevice,
- __in FLOAT Radius,
- __in UINT Slices,
- __in UINT Stacks,
- __out LPD3DXMESH *ppMesh,
- __out LPD3DXBUFFER *ppAdjacency
- ;
依然是一个调用实例:
-
- ID3DXMesh* meshSphere;
- D3DXCreateSphere(
- g_pd3dDevice,
- l.Of,
- 10,
- 10,
- &meshBox,
- 0
- ) ;
- g_pd3dDevice->BeginScene();
- meshBox->DrawSubset(O);
( 5 )茶壶的创建——D3DXCreateTeapot 方法
D3DXCreateTeapot 方法用于创建一个茶壶。
- HRESULT D3DXCreateTeapot(
- __in LPDIRECT3DDEVICE9 pDevice,
- __out LPD3DXMESH *ppMesh,
- __out LPD3DXBUFFER *ppAdjacency
- );
这个函数的调用实例如下:
-
- ID3DXMesh* meshTeapot;
- D3DXCreateTeapot(
- g_pd3dDevice,
- &meshBox,
- 0
- ) ;
- g_pd3dDevice->BeginScene();
- meshBox->DrawSubset(O);
(6 〕圆环的创建一一D3DXCreateTorus 方法
D3DXCreateTorus 方法用于创建一个圆环体。- HRESULT D3DXCreateTorus(
- __in LPDIRECT3DDEVICE9 pDevice,
- __in FLOAT InnerRadius,
- __in FLOAT OuterRadius,
- __in UINT Sides,
- __in UINT Rings,
- __out LPD3DXMESH *ppMesh,
- __out LPD3DXBUFFER *ppAdjacency
- );
- 第四个参数,UINT 类型的Sides, 表示我们创建的圆环的外圈有几个面, 也就是大圆的轮廓是几边形,这个值显然要大于等于3.
依然是一个调用实例:
-
- ID3DXMesh* meshTorus;
- D3DXCreateTorus(
- g_pd3dDevice,
- l.Of,
- 3.0f,
- 10,
- 10,
- &meshBox,
- 0
- ) ;
- g_pd3dDevice->BeginScene();
- meshBox->DrawSubset(O);
最后做一下总结, 首先,我们这些方法的第一个参数和最后两个参数都是相同的。然后,这些函数的使用方法不用去强行记忆,用到的时候按D3DX Create+几何体名字的命名方法去查一下SDK 文档或者稍微查看一下本小节的知识就可以了。
(7) 详细注释的示例程序D3Ddemo6
首先,我们定义了几个全局的ID3DXMesh 对象,以及几个D3DXMATRIX 矩阵类型。这里定义ID3DXMesh 对象就是本节介绍的几何体绘制四步曲的第一步, 而矩阵类型用来确定绘制的每个几何体不同的世界坐标位置以及做出旋转的镜头效果。相关代码如下:
- LPD3DXMESH g_teapot = NULL;
- LPD3DXMESH g_cube = NULL;
- LPD3DXMESH g_sphere = NULL;
- LPD3DXMESH g_torus = NULL;
- D3DXMATRIX g_WorldMatrix[4],R;
然后,我们在Objects_Init()方法中做了几何体绘制四步曲的第二步,创建几何体(我们准备绘制立方体、茶壶、球面体、圆这四个几何体, 就在这一步当中进行了创建),调用了一些SetRenderState 关闭了光照, 开启了背面消隐,以及将填充模式默认设置为线框填充。因为Objects_lnit()方法在整个程序运行过程中只会运行一次,所以默认的渲染模式的设置都可以在这里进行。
关掉光照的代码g_pd3dDevice-> SetRenderState(D3DRS_LIGHTING,FALSE )应该这样理解:关掉这种需要通过光的反射才能看到物体的自然界现象。
如果不关掉这种自然现象的话,那么还是需要对应的光照分量+对应的材质,才能显示出物体来。相关代码如下:-
-
-
- HRESULT Objects_Init(HWND hwnd)
- {
-
- if(FAILED(D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1, false, DEFAULT_CHARSET,
- OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("微软雅黑"), &g_pFont)))
- return E_FAIL;
- srand(timeGetTime());
-
-
- if(FAILED(D3DXCreateBox(g_pd3dDevice, 2, 2, 2, &g_cube, NULL)))
- return false;
- if(FAILED(D3DXCreateTeapot(g_pd3dDevice, &g_teapot, NULL)))
- return false;
- if(FAILED(D3DXCreateSphere(g_pd3dDevice, 1.5, 25, 25,
- &g_sphere, NULL))) return false;
- if(FAILED(D3DXCreateTorus(g_pd3dDevice, 0.5f, 1.2f, 25, 25,
- &g_torus, NULL))) return false;
-
-
- g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
- g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
- g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
-
- return S_OK;
- }
接着,就是在Direct3D_Render()函数中进行绘制了, 需要注意, 四大矩阵变换原先都是封装在Matrix_Set()方法中进行的, 而这里我们为了绘制的方便,把世界变换从Matrix_Set()方法中拿出来,放到了Direct3D_Render() 之中。另外需要注意,制作出镜头旋转的方法,我们先用D3DXMatrixRotationY 方法制作一个根据系统时间绕Y 轴旋转的矩阵R, 就是这样写:
- D3DXMatrixRotationY(&R, ::timeGetTime() / 1440.0f);
然后在设置好物体的世界矩阵坐标后, 右乘一下我们制作好的矩阵R,再把结果矩阵设置为当前的世界矩阵,进行绘制就可以了。
-
- D3DXMatrixTranslation(&g_WorldMatrix[0], 3.0f, -3.0f, 0.0f);
- g_WorldMatrix[0] = g_WorldMatrix[0]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[0]);
- g_cube->DrawSubset(0);
完整的Direct3D_ Render()函数的代码如下:
-
-
-
- void Direct3D_Render(HWND hwnd)
- {
-
-
-
- g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
-
-
- RECT formatRect;
- GetClientRect(hwnd, &formatRect);
-
-
-
- g_pd3dDevice->BeginScene();
-
- Matrix_Set();
-
-
- if (::GetAsyncKeyState(0x31) & 0x8000f)
- g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_SOLID);
- if (::GetAsyncKeyState(0x32) & 0x8000f)
- g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
-
-
-
-
-
- D3DXMatrixRotationY(&R, ::timeGetTime() / 1440.0f);
-
-
- D3DXMatrixTranslation(&g_WorldMatrix[0], 3.0f, -3.0f, 0.0f);
- g_WorldMatrix[0] = g_WorldMatrix[0]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[0]);
- g_cube->DrawSubset(0);
-
-
- D3DXMatrixTranslation(&g_WorldMatrix[1], -3.0f, -3.0f, 0.0f);
- g_WorldMatrix[1] = g_WorldMatrix[1]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[1]);
- g_teapot->DrawSubset(0);
-
-
- D3DXMatrixTranslation(&g_WorldMatrix[2], 3.0f, 3.0f, 0.0f);
- g_WorldMatrix[2] = g_WorldMatrix[2]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[2]);
- g_torus->DrawSubset(0);
-
-
- D3DXMatrixTranslation(&g_WorldMatrix[3], -3.0f, 3.0f, 0.0f);
- g_WorldMatrix[3] = g_WorldMatrix[3]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[3]);
- g_sphere->DrawSubset(0);
-
-
- int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() );
- g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(255,39,136));
-
-
-
-
- g_pd3dDevice->EndScene();
-
-
-
- g_pd3dDevice->Present(NULL, NULL, NULL, NULL);
- }
最后,绘制完成之后,在程序准备退出的时候,就进入几何体绘制四步曲的第四步——释放了,这一步其实就是Direct3D_CleanUp()函数的实现代码:
-
-
-
- void Direct3D_CleanUp()
- {
-
- SAFE_RELEASE(g_torus)
- SAFE_RELEASE(g_sphere)
- SAFE_RELEASE(g_cube)
- SAFE_RELEASE(g_teapot)
- SAFE_RELEASE(g_pFont)
- SAFE_RELEASE(g_pd3dDevice)
- }
好了,我们来看一下示例运行的截图:
我们可以按下键盘上的“ 1 ”和“ 2”键,在实体填充和线框填充之间切换:
14.8 示例程序D3Ddemo7
我们讲完了如何快捷地在Direct3D 中绘制几何体,接下来, 是时候把光照和材质的知识变成实现代码,加入到刚刚我们写的那个几何体的快速绘制demo 中了。在上节demo 的基础上,我们自定义了一个Light_Set()函数,把光照相关的代码在其中进行了封装, 且我们在Objects_lnit()函数中先进行了材质的设置,并调用了一次Light_Set(),将默认情况下的光源设置为点光源。接着我们
在Direct3D_Render()自定义函数中渲染五步曲的第三步里添加了如下代码,以实现通过键盘上按键的按下动作,使当前使用的光源在三种光源类型之间切换:
-
- f (::GetAsyncKeyState(0x51) & 0x8000f)
- Light_Set(g_pd3dDevice, 1);
- f (::GetAsyncKeyState(0x57) & 0x8000f)
- Light_Set(g_pd3dDevice, 2);
- f (::GetAsyncKeyState(0x45) & 0x8000f)
- Light_Set(g_pd3dDevice, 3);
下面我们贴出示例程序的核心部分源代码:
-
-
-
- HRESULT Objects_Init(HWND hwnd)
- {
-
- if(FAILED(D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1, false, DEFAULT_CHARSET,
- OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("微软雅黑"), &g_pFont)))
- return E_FAIL;
- srand(timeGetTime());
-
-
- if(FAILED(D3DXCreateBox(g_pd3dDevice, 2, 2, 2, &g_cube, NULL)))
- return false;
- if(FAILED(D3DXCreateTeapot(g_pd3dDevice, &g_teapot, NULL)))
- return false;
- if(FAILED(D3DXCreateSphere(g_pd3dDevice, 1.5, 25, 25,
- &g_sphere, NULL))) return false;
- if(FAILED(D3DXCreateTorus(g_pd3dDevice, 0.5f, 1.2f, 25, 25,
- &g_torus, NULL))) return false;
-
-
- D3DMATERIAL9 mtrl;
- ::ZeroMemory(&mtrl, sizeof(mtrl));
- mtrl.Ambient = D3DXCOLOR(0.5f, 0.5f, 0.7f, 1.0f);
- mtrl.Diffuse = D3DXCOLOR(0.6f, 0.6f, 0.6f, 1.0f);
- mtrl.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 0.3f);
- mtrl.Emissive = D3DXCOLOR(0.3f, 0.0f, 0.1f, 1.0f);
- g_pd3dDevice->SetMaterial(&mtrl);
-
-
- Light_Set(g_pd3dDevice, 1);
- g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, true);
- g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true);
- g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true);
-
- g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
-
- return S_OK;
- }
-
-
-
-
- VOID Light_Set(LPDIRECT3DDEVICE9 pd3dDevice, UINT nType)
- {
-
- static D3DLIGHT9 light;
- ::ZeroMemory(&light, sizeof(light));
-
-
- switch (nType)
- {
- case 1:
- light.Type = D3DLIGHT_POINT;
- light.Ambient = D3DXCOLOR(0.6f, 0.6f, 0.6f, 1.0f);
- light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
- light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f);
- light.Position = D3DXVECTOR3(0.0f, 200.0f, 0.0f);
- light.Attenuation0 = 1.0f;
- light.Attenuation1 = 0.0f;
- light.Attenuation2 = 0.0f;
- light.Range = 300.0f;
- break;
- case 2:
- light.Type = D3DLIGHT_DIRECTIONAL;
- light.Ambient = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f);
- light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
- light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f);
- light.Direction = D3DXVECTOR3(1.0f, 0.0f, 0.0f);
- break;
- case 3:
- light.Type = D3DLIGHT_SPOT;
- light.Position = D3DXVECTOR3(100.0f, 100.0f, 100.0f);
- light.Direction = D3DXVECTOR3(-1.0f, -1.0f, -1.0f);
- light.Ambient = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f);
- light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
- light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 0.3f);
- light.Attenuation0 = 1.0f;
- light.Attenuation1 = 0.0f;
- light.Attenuation2 = 0.0f;
- light.Range = 300.0f;
- light.Falloff = 0.1f;
- light.Phi = D3DX_PI / 3.0f;
- light.Theta = D3DX_PI / 6.0f;
- break;
- }
-
- pd3dDevice->SetLight(0, &light);
- pd3dDevice->LightEnable(0, true);
- pd3dDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(36, 36, 36));
- }
-
-
-
-
- void Direct3D_Render(HWND hwnd)
- {
-
-
-
- g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
-
-
- RECT formatRect;
- GetClientRect(hwnd, &formatRect);
-
-
-
- g_pd3dDevice->BeginScene();
-
- Matrix_Set();
-
-
- if (::GetAsyncKeyState(0x31) & 0x8000f)
- g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_SOLID);
- if (::GetAsyncKeyState(0x32) & 0x8000f)
- g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
-
-
-
- if (::GetAsyncKeyState(0x51) & 0x8000f)
- Light_Set(g_pd3dDevice, 1);
- if (::GetAsyncKeyState(0x57) & 0x8000f)
- Light_Set(g_pd3dDevice, 2);
- if (::GetAsyncKeyState(0x45) & 0x8000f)
- Light_Set(g_pd3dDevice, 3);
-
-
-
-
-
- D3DXMatrixRotationY(&R, ::timeGetTime() / 1440.0f);
-
-
-
- D3DXMatrixTranslation(&g_WorldMatrix[0], 3.0f, -3.0f, 0.0f);
- g_WorldMatrix[0] = g_WorldMatrix[0]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[0]);
- g_cube->DrawSubset(0);
-
-
- D3DXMatrixTranslation(&g_WorldMatrix[1], -3.0f, -3.0f, 0.0f);
- g_WorldMatrix[1] = g_WorldMatrix[1]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[1]);
- g_teapot->DrawSubset(0);
-
-
- D3DXMatrixTranslation(&g_WorldMatrix[2], 3.0f, 3.0f, 0.0f);
- g_WorldMatrix[2] = g_WorldMatrix[2]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[2]);
- g_torus->DrawSubset(0);
-
-
- D3DXMatrixTranslation(&g_WorldMatrix[3], -3.0f, 3.0f, 0.0f);
- g_WorldMatrix[3] = g_WorldMatrix[3]*R;
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[3]);
- g_sphere->DrawSubset(0);
-
-
- int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() );
- g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(255,39,136));
-
-
-
-
- g_pd3dDevice->EndScene();
-
-
-
- g_pd3dDevice->Present(NULL, NULL, NULL, NULL);
- }
编译并运行上面的代码,我们可以得到如下截图中所示的显示效果。因为我们按键盘上的“1 ”和“ 2 ”在线框模式和实体填充模式之间切换,而按下键盘上的“ Q ”、“ W ”、“ E ”在点光源、方向光源和聚光灯光源之间切换,所以在这里截了6 张图片,分别代表了两种填充模式和三种光源能得到的6 种排列组合的显示效果:
第一张,实体填充模式 点光源 第二张,线框填充模式 点光源
第三张,实体填充模式 方向光源 第四张,线框填充模式 方向光源
第五张,实体填充模式 聚光灯光源 第六张,线框填充模式 聚光灯光源
14.9 章节小憩
看着自己亲手敲出来的代码实现了极具立体感的三维图形世界,是不是一股成就感油然而生呢?这就是游戏编程的魅力所在,把梦境中的事物变成了“现实” 。