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);
自发光就是对象自己发出的光,它是根据通过对象的自发光材质实现的。下面我们在讲解材质时讲到的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 结构体的名称,并在名称之前加一个“ &” 取地址符号就可以了。
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 材质
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 关于顶点法线
这里提出了顶点法线和面法线两个概念,我们首先需要清楚地理解并区分这两个概念。
面法线很容易理解,即垂直于三角形平面的一条法线。那顶点法线又从何而来呢,严格地从法线的定义上来说, 其实顶点是不存在法线的, 那为何又有顶点法线这个概念呢? 让顶点也拥有法线, 是为了在光照计算时,能够知晓光线到达表面时的入射角,以便在多面体的表面获得一种平滑的效 果。
顶点法线可以在定义的顶点结构中进行描述。我们需要在前面已经拥有的顶点结构体中添加一组用于描述顶点法向量的数据成员。当然修改了顶点结构体,对应的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 总结与升华
- 光照计算两要素:光照和材质。
- 四大光照:环境光、镜面反射光,漫反射光和自发光。
- 三大光源:点光源、平行光源和聚光灯。
- 光源属性:一个结构体D3DLIGHT9 、两个方法SetLight 和LightEnable 。
- 材质属性:一个结构体D3DMATERIAL9 、一个方法SetMaterial 和法线向量的计算。
14.7 几何体的快捷绘制
14.7.1 D3D中内置的几何体概述
我们可以在DirectX SDK( 以目前最新的Microsoft DirectX SDK (June 2010))为例找到这些函数的说明文档, 这是这些函数最官方最原始的参考资料了。
先给大家看看这几个函数的全家福, 后面我们会详细介绍使用方法:
14.7.2 D3D中几种内置的几何体绘制四步曲
想要通过这几个函数快捷绘制出一个几何体,需要以下几个步骤:
- 定义一个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 方法
HRESULT D3DXCreateBox(
__in LPDIRECT3DDEVICE9 pDevice, //Direct3D 设备对象
__in FLOAT Width, //盒子的宽度
__in FLOAT Height, //盒子的高度
__in FLOAT Depth, //盒子的深度
__out LPD3DXMESH *ppMesh, //布储着盒子网格的指针
__out LPD3DXBUFFER *ppAdjacency // 存储三角形索引的指针
);
下面是一个调用实例,用于创建一个长方体:
//用此函数产生一个长方体
ID3DXMesh* meshBox;
D3DXCreateBox (
g_pd3dDevice, //D3D 设备对象
2.0, //宽度
2.0f, // 高度
2.0f, // 深度
&meshBox, // 对应COM对象
0 // 指向ID3DXBuffer,存放相邻平面的信息,通常设置成0(或NULL )
) ;
g_pd3dDevice->BeginScene();
meshBox->DrawSubset(O);
( 2 )柱体的创建——D3DXCreateCylinder 方法
D3DXCreateCylinder 方法用于创建柱体。
HRESULT D3DXCreateCylinder(
__in LPDIRECT3DDEVICE9 pDevice, //D3D 设备对象
__in FLOAT Radius1, //Z 轴负方向的半径大小
__in FLOAT Radius2, //Z 轴正方向的半径大小
__in FLOAT Length, //沿Z轴的长度
__in UINT Slices, //外围设为60 个面
__in UINT Stacks, //两端间共有60 段
__out LPD3DXMESH *ppMesh, //对应的COM 对象
__out LPD3DXBUFFER *ppAdjacency //指向ID3DXBuffer,存放相邻平面的信息,通常设置成0 (或NULL)
);
- 第五个参数,UINT 类型的Slices ,表示柱体的外围有几个面, 举个例子,如果设为8 的话,我们创建的就是八角柱了。
- 第六个参数,UINT 类型的Stacks ,表示柱体的两端间共有几段,这个后面光照效果的时候需要用到的。
//创建一个柱子
ID3DXMesh* meshCylinder;
D3DXCreateCylinder (
g_pd3dDevice, //D3D 设备对象
280.f, // Z 轴负方向的半径大小
10.0f, // Z 轴正方向的半径大小
3000.0f, // 沿Z 轴的长度
60, // 外围设为60个面
60, // 两端间共有60 段
&meshBox, // 对应COM对象
0 // 指向ID3DXBuffer,存放相邻平面的信息,通常设置成0(或NULL )
) ;
g_pd3dDevice->BeginScene();
meshBox->DrawSubset(O);
( 3 ) 2D 多边形的创建——D3DXCreatePolygon 方法
D3DXCreatePolygon 方法用于快速创建一个2D 多边形。
HRESULT D3DXCreatePolygon(
__in LPDIRECT3DDEVICE9 pDevice, //D3D 设备对象
__in FLOAT Length, //每条边的长度
__in UINT Sides, //包含几个三角形
__out LPD3DXMESH *ppMesh, //对应的COM 对象
__out LPD3DXBUFFER *ppAdjacency //指向ID3DXBuffer,存放相邻平面的信息,通常设置成0 (或NULL)
);
下面看一个调用实例,方便大家快速掌握这个函数的用法:
//用此函数产生一个2D 多边体
ID3DXMesh* meshPolygon;
D3DXCreatePolygon(
g_pd3dDevice, //D3D 设备对象
2.0f, // 每条边的长度
6, // 包含几个三角形
&meshBox, // 对应COM对象
0 // 指向ID3DXBuffer,存放相邻平面的信息,通常设置成0(或NULL )
) ;
g_pd3dDevice->BeginScene();
meshBox->DrawSubset(O);
(4 )球面体创建一一D3DXCreateSphere 方法
D3DXCreateSphere 方法用于创建一个球面体。
HRESULT D3DXCreateSphere(
__in LPDIRECT3DDEVICE9 pDevice, //D3D 设备对象
__in FLOAT Radius, // 球面体半径
__in UINT Slices, // 用几条经线绘制
__in UINT Stacks, // 用几条维线绘制
__out LPD3DXMESH *ppMesh, //对应的COM 对象
__out LPD3DXBUFFER *ppAdjacency //指向ID3DXBuffer,存放相邻平面的信息,通常设置成0 (或NULL)
);
依然是一个调用实例:
//用此函数产生一个球面体
ID3DXMesh* meshSphere;
D3DXCreateSphere(
g_pd3dDevice, //D3D 设备对象
l.Of, // 球面体半径
10, //用几条经线绘制
10, // 用几条维线绘制
&meshBox, // 对应COM对象
0 // 指向ID3DXBuffer,存放相邻平面的信息,通常设置成0(或NULL )
) ;
g_pd3dDevice->BeginScene();
meshBox->DrawSubset(O);
( 5 )茶壶的创建——D3DXCreateTeapot 方法
HRESULT D3DXCreateTeapot(
__in LPDIRECT3DDEVICE9 pDevice,
__out LPD3DXMESH *ppMesh,
__out LPD3DXBUFFER *ppAdjacency
);
这个函数的调用实例如下:
//用此函数产生一个茶壶
ID3DXMesh* meshTeapot;
D3DXCreateTeapot(
g_pd3dDevice, //D3D 设备对象
&meshBox, // 对应COM对象
0 // 指向ID3DXBuffer,存放相邻平面的信息,通常设置成0(或NULL )
) ;
g_pd3dDevice->BeginScene();
meshBox->DrawSubset(O);
(6 〕圆环的创建一一D3DXCreateTorus 方法
HRESULT D3DXCreateTorus(
__in LPDIRECT3DDEVICE9 pDevice, //D3D 绘制对象
__in FLOAT InnerRadius, //圆环的内圈半径
__in FLOAT OuterRadius, //圆环的外圈半径
__in UINT Sides, //外圈有几个面
__in UINT Rings, //内圈和外圈间有几个面( 同心圆)
__out LPD3DXMESH *ppMesh,
__out LPD3DXBUFFER *ppAdjacency // 通常设置成0(或NULL)
);
- 第四个参数,UINT 类型的Sides, 表示我们创建的圆环的外圈有几个面, 也就是大圆的轮廓是几边形,这个值显然要大于等于3.
//用此函数产生一个圆环体
ID3DXMesh* meshTorus;
D3DXCreateTorus(
g_pd3dDevice, //D3D 设备对象
l.Of, //圆环的内圈半径
3.0f, // 圆环的外圈半径
10, //外圈有几个面
10, // 内圈和外圈间有几个面( 同心圆)
&meshBox, // 对应COM对象
0 // 指向ID3DXBuffer,存放相邻平面的信息,通常设置成0(或NULL )
) ;
g_pd3dDevice->BeginScene();
meshBox->DrawSubset(O);
最后做一下总结, 首先,我们这些方法的第一个参数和最后两个参数都是相同的。然后,这些函数的使用方法不用去强行记忆,用到的时候按D3DX+Create+几何体名字的命名方法去查一下SDK 文档或者稍微查看一下本小节的知识就可以了。
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 )应该这样理解:关掉这种需要通过光的反射才能看到物体的自然界现象。
//-----------------------------------【Object_Init( )函数】--------------------------------------
// 描述:渲染资源初始化函数,在此函数中进行要被渲染的物体的资源的初始化
//--------------------------------------------------------------------------------------------------
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()函数的代码如下:
//-----------------------------------【Direct3D_Render( )函数】-------------------------------
// 描述:使用Direct3D进行渲染
//--------------------------------------------------------------------------------------------------
void Direct3D_Render(HWND hwnd)
{
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之一】:清屏操作
//--------------------------------------------------------------------------------------
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
//定义一个矩形,用于获取主窗口矩形
RECT formatRect;
GetClientRect(hwnd, &formatRect);
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之二】:开始绘制
//--------------------------------------------------------------------------------------
g_pd3dDevice->BeginScene(); // 开始绘制
Matrix_Set();//调用封装了四大变换的函数,对Direct3D世界变换,取景变换,投影变换,视口变换进行设置
// 获取键盘消息并给予设置相应的填充模式
if (::GetAsyncKeyState(0x31) & 0x8000f) // 若数字键1被按下,进行实体填充
g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_SOLID);
if (::GetAsyncKeyState(0x32) & 0x8000f) // 若数字键2被按下,进行线框填充
g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之三】:正式绘制,利用顶点缓存绘制图形
//--------------------------------------------------------------------------------------
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));
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之四】:结束绘制
//--------------------------------------------------------------------------------------
g_pd3dDevice->EndScene(); // 结束绘制
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之五】:显示翻转
//--------------------------------------------------------------------------------------
g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示
}
最后,绘制完成之后,在程序准备退出的时候,就进入几何体绘制四步曲的第四步——释放了,这一步其实就是Direct3D_CleanUp()函数的实现代码:
//-----------------------------------【Direct3D_CleanUp( )函数】--------------------------------
// 描述:资源清理函数,在此函数中进行程序退出前资源的清理工作
//---------------------------------------------------------------------------------------------------
void Direct3D_CleanUp()
{
//释放COM接口对象
SAFE_RELEASE(g_torus)
SAFE_RELEASE(g_sphere)
SAFE_RELEASE(g_cube)
SAFE_RELEASE(g_teapot)
SAFE_RELEASE(g_pFont)
SAFE_RELEASE(g_pd3dDevice)
}
好了,我们来看一下示例运行的截图:
14.8 示例程序D3Ddemo7
// 根据键盘按下的情况设置相应的光照类型
if (::GetAsyncKeyState(0x51) & 0x8000f) // 若键盘上的按键Q被按下,光源类型设为点光源
Light_Set(g_pd3dDevice, 1);
if (::GetAsyncKeyState(0x57) & 0x8000f) // 若键盘上的按键W被按下,光源类型设为平行光源
Light_Set(g_pd3dDevice, 2);
if (::GetAsyncKeyState(0x45) & 0x8000f) // 若键盘上的按键E被按下,光源类型设为聚光灯
Light_Set(g_pd3dDevice, 3);
下面我们贴出示例程序的核心部分源代码:
//-----------------------------------【Object_Init( )函数】--------------------------------------
// 描述:渲染资源初始化函数,在此函数中进行要被渲染的物体的资源的初始化
//--------------------------------------------------------------------------------------------------
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;
}
//-----------------------------------【Light_Set( )函数】-----------------------------
// Desc: 封装了3种光源类型的函数,可以根据第二个参数选择光源类型
//--------------------------------------------------------------------------------------
VOID Light_Set(LPDIRECT3DDEVICE9 pd3dDevice, UINT nType)
{
//定义一个光照类型并初始化
static D3DLIGHT9 light;
::ZeroMemory(&light, sizeof(light));
//一个switch,给3种光源选项
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)); //设置一下环境光
}
//-----------------------------------【Direct3D_Render( )函数】-------------------------------
// 描述:使用Direct3D进行渲染
//--------------------------------------------------------------------------------------------------
void Direct3D_Render(HWND hwnd)
{
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之一】:清屏操作
//--------------------------------------------------------------------------------------
g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
//定义一个矩形,用于获取主窗口矩形
RECT formatRect;
GetClientRect(hwnd, &formatRect);
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之二】:开始绘制
//--------------------------------------------------------------------------------------
g_pd3dDevice->BeginScene(); // 开始绘制
Matrix_Set();//调用封装了四大变换的函数,对Direct3D世界变换,取景变换,投影变换,视口变换进行设置
// 根据键盘按下的情况设置相应的填充模式
if (::GetAsyncKeyState(0x31) & 0x8000f) // 若数字键1被按下,进行实体填充
g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_SOLID);
if (::GetAsyncKeyState(0x32) & 0x8000f) // 若数字键2被按下,进行线框填充
g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
// 根据键盘按下的情况设置相应的光照类型
if (::GetAsyncKeyState(0x51) & 0x8000f) // 若键盘上的按键Q被按下,光源类型设为点光源
Light_Set(g_pd3dDevice, 1);
if (::GetAsyncKeyState(0x57) & 0x8000f) // 若键盘上的按键W被按下,光源类型设为平行光源
Light_Set(g_pd3dDevice, 2);
if (::GetAsyncKeyState(0x45) & 0x8000f) // 若键盘上的按键E被按下,光源类型设为聚光灯
Light_Set(g_pd3dDevice, 3);
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之三】:正式绘制,利用顶点缓存绘制图形
//--------------------------------------------------------------------------------------
//物体的公转
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));
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之四】:结束绘制
//--------------------------------------------------------------------------------------
g_pd3dDevice->EndScene(); // 结束绘制
//--------------------------------------------------------------------------------------
// 【Direct3D渲染五步曲之五】:显示翻转
//--------------------------------------------------------------------------------------
g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示
}
编译并运行上面的代码,我们可以得到如下截图中所示的显示效果。因为我们按键盘上的“1 ”
和“ 2 ”在线框模式和实体填充模式之间切换,而按下键盘上的“ Q ”、“ W ”、“ E ”在点光源、
方向光源和聚光灯光源之间切换,所以在这里截了6 张图片,分别代表了两种填充模式和三种光源
能得到的6 种排列组合的显示效果:
第三张,实体填充模式+方向光源 第四张,线框填充模式+方向光源
第五张,实体填充模式+聚光灯光源 第六张,线框填充模式+聚光灯光源
14.9 章节小憩
看着自己亲手敲出来的代码实现了极具立体感的三维图形世界,是不是一股成就感油然而生呢?这就是游戏编程的魅力所在,把梦境中的事物变成了“现实” 。