本笔记是基于Microsfot DirectX 9.0 SDK Update的SimpleSample生成的框架.
在3D场景中,所有的对象和模型均由三角形构成;而三角形由三个顶点构成;每个顶点不仅包括其坐标信息,还包括顶点的颜色、法向量和贴图坐标等信息。
第一步:准备顶点信息
1. 要先定义个顶点信息结构。
struct CUSTOMVERTEX{
D3DXVECTOR3 position; // 位置坐标
D3DXVECTOR3 normal; // 法向量坐标,根据当前截面的法向量跟入射光源的夹角来计算光源照射的效果。
};
这里的CUSTOMVERTEX表示custom vertext(自定义顶点)。
2. 定义顶点格式的常量。
由于后面要将顶点数据送入Vertex Buffer,然后再转给D3D去做着色处理,所以要告诉D3D顶点数据里包含什么内容。鉴于使用方便的原因,我们这里定义一个常量来表示顶点的数据项,即顶点里包含了哪些数据。
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_NORMAL)
这里的D3DFVF表示Direct3D Flexible Vertex Format(D3D动态顶点格式)。因为顶点结构内的内容包含什么由设计者自行决定。
D3DFVF_XYZ和D3DFVF_NORMAL是D3D预先定义好的几个标识符常数,用来表示顶点的坐标位置和法向量。
3. 创建Vertex Buffer。
1). 先定义一个全局指针,用来指向Vextex Buffer。
LPDIRECT3DVERTEXBUFFER9 g_pVB; // Vertex Buffer
2). 然后在InitApp()里初始化它。
g_pVB = NULL;
3). 最后在OnCreateDevice(),即Device的Create事件里创建Vertex Buffer。
V_RETURN(pd3dDevice->CreateVertexBuffer(
3*2*sizeof(CUSTOMVERTEX), // 6个顶点的Buffer大小(字节),3个是三角形正面,3个是三角形反面
0, // 指定未来顶点信息的处理方式,这里暂且放0
D3DFVF_CUSTOMVERTEX, // 指明我们的顶点数据结构中包含了哪些顶点信息.
D3DPOOL_MANAGED, // 指明Vertex Buffer要存放的内存(显存或系统内存),这里指定由D3D自行管理
&g_pVB, // Vertex Buffer的指针
NULL // 保留参数
));
说明:在3D设计里,每个三角形都有正反面之分,而且只有正面的三角形才能被看见。所以,设计一个三角形需要正反各3个顶点共6个。区分一个三角形是正面还是反面取决于构成这个三角形三个顶点的输入顺序和采用哪种坐标系。在左手坐标系里,如果以顺时针方面输入三个顶点所构成的三角形则是正面,反之是反面;在右手坐标系里,如果以逆时针方面输入三个顶点所构成的三角形则是正面,反之是反面;
4. 将Vertex数据填入Vertex Buffer。
由于Windows的内存管理机制,Vertex Buffer有可能被系统转移出真实的物理内存。所以在对Vertex Buffer进行数据输入时,需要先将其锁定。输入完数据后,再将其解锁。在OnCreateDevice()里继续写下面的代码。
CUSTOMVERTEX* pVertices; // 定义一个顶点数据结构的指针,用于输入数据到Vertex Buffer时进行索引
V_RETURN(g_pVB->Lock(0, // 从Buffer的第0个Byte开始Lock
0, // 要锁定多少Bytes,若要锁定整块Buffer,则设为0
(VOID**)&pVertices, // Buffer要存放的顶点数据结构在Buffer里的索引指针
0 // Buffer存取时的限制,若没什么限制,则为0
));
/*========== 正面三角形的三个顶点(采用右手坐标系,逆时针输入) =============== */
pVertices[0].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f); // 左下角顶点坐标
pVertices[0].normal = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 左下角顶点法向量
pVertices[1].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f); // 右下角顶点坐标
pVertices[1].normal = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 右下角顶点法向量
pVertices[2].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f); // 中间上面顶点坐标
pVertices[2].normal = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 中间上面顶点法向量
/*========== 反面三角形的三个顶点(采用右手坐标系,顺时针输入) =============== */
pVertices[3].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f);
pVertices[3].normal = D3DXVECTOR3(0.0f, 0.0f, -1.0f);
pVertices[4].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f);
pVertices[4].normal = D3DXVECTOR3(0.0f, 0.0f, -1.0f);
pVertices[5].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f);
pVertices[5].normal = D3DXVECTOR3(0.0f, 0.0f, -1.0f);
g_pVB->Unlock(); // 数据输入完毕后,解锁。
说明: 一般来说,顶点的法向量等同于该顶点所构成的三角形的法向量,也就是说,顶点的法向量就是顶点所在三角形的平面法向量,而一个平面的法向量定义为垂直于这个平面的向量.向量的方向可以根据右手法则(在右手坐标系上)来获得,即按照顶点输入的顺序握手,然后伸直大拇指,大拇指的方向就是法向量的方向.所以,正面三角形的法向量为(0.0f, 0.0f, 1.0f),即原点指向该点的向量. 反面三角形同理.
5. 设定三角形的Material,即三角形表面的光特性。
构成三角形的顶点数据被存放入顶点缓冲区(Vertex Buffer)后,然后需要设置三角形的Material。Material指的是模型表面的材质对光的反应特性。在3D程序里,通常存在三种光源:
- Diffuse Reflection --- 由非直射/斜射光源产生的光线,比如其他物体的反射光线。
- Specular Reflection --- 由直射/斜射光源产生的光线
- Ambient Light --- 纯粹由其他物体表面所反射的微弱光源所产生的光线,它通常指场景里最基本的光源亮度。
Material通常包含Ambient、Specular、Diffuse、Emissive和Power。除了Power是浮点数,其他的都是RGB颜色值。它们在Material中代表的意思如下:
- Ambient -- 被Ambient Light照到时物体表面所表现出来的颜色。
- Specular -- 被Specular Reflection照到时物体表面所表现出来的颜色,因为是直射,所以光线比较强,通常该值常被设定为相当于白色或比较亮的颜色。
- Diffuse -- 被Diffuse Reflection照到时物体表面所表现出来的颜色,特别的,在Material设定里,Diffuse代表物体表面的颜色,因为在光线的Diffuse Reflection下,物体表面所反射的光的颜色就代表物体的颜色。
- Emissive -- 物体自己发光的颜色。
- Power -- 是Specular的相关值,Power越大,Specular所反映出来的光就越亮,越集中。
有了上面的基本知识,开始设置Material, 通常设置的过程放在OnResetDevice()中,即Device的Reset事件中处理。
D3DMATERIAL9 mtrl;
ZeroMemory(&mtrl, sizeof(D3DMATERIAL9));
mtrl.Diffuse.r = mtrl.Ambient.r = 1.0f;
mtrl.Diffuse.g = mtrl.Ambient.g = 0.0f;
mtrl.Diffuse.b = mtrl.Ambient.b = 0.0f;
mtrl.Diffuse.a = mtrl.Ambient.a = 1.0f;
pd3dDevice->SetMatrial(&mtrl);
上面的代码将Material的Diffuse和Ambient设置为RGBA(1,0,0,1),即红色不透明。其余的Specular、Emissive和Power均为0。
6. 设定三角形的Texture,即三角形表面的贴图。
Texture指的是模型上的贴图,因为真实世界里物体的质料太复杂,不可能用计算机仿真出来,最简单的方法,就是在模型的表面贴上该质料的图片。由于当前的三角形的表面不需要贴图,这部分可以略掉。
7. 着色状态值的设置
D3D结构本身是一个状态机,每次着色(Render)时,都会参考目前的状态值,因此要先设定一些状态值。D3D提供了一个设定状态值的函数SetRenderState。
我们在OnResetDevice()中设置了几个基本状态值:
pd3dDevice->SetRenderState(D3DRS_DITHERENABLE, FALSE); // 不需要颜色补差
pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE,FALSE); // 关闭Specular光源
pd3dDevice->SetRenderState(D3DRS_ZENABLE,TRUE); // 打开深度值
pd3dDevice->SetRenderState(D3DRS_AMBIENT,0x000F0F0F); // 打开基本环境光源
这里第一句是要关闭颜色补差,即取消Dither功能。Dither就是指颜色补差法,如果一个系统经过种种计算出来的颜色值不在目前的调色盘中,那么可以用近似的几种颜色来表示,这就是Dither。
最后一句是打开基本环境光源,0x000F0F0F是环境光源的ARGB(00,0F,0F,0F)。若当前环境内的物体的Material的Ambient不为0的话,它就会作用于当前环境内的物体。Material的Ambient为当前物体在基本环境光源的作用下所显示的颜色。
8. Matrix初始化.
Matrix在3D程序里,主要用来做坐标转换。常用的Matrix有3个,分别是World Matrix、View Matrix和Projection Matrix。在OnResetDevice()里需要对这三个Matrix进行初始化和设置。
1). World Matrix
它主要用于转换对象模型的坐标。这里暂时不需要,只需要将其填成单位矩阵(Identity Matrix)即可(必填).
D3DXMATRIX matIdentity; // 定义一个Matrix
D3DXMatrixIdentity(&matIdentity); // 将其转变成单位矩阵
pd3dDevice->SetTransform(D3DTS_WORLD, &matIdentity); // 设置World Matrix
2). View Matrix
它主要用来设定观察者,即观察者的View,也就是对Camera的位置设定。
D3DXMATRIX matView; // 定义一个Matrix
D3DXVECTOR3 vFromPt = D3DXVECTOR3(0.0f, 0.0f, 5.0f); // Camera位置,因为三角形位于z=0的XY平面上。
D3DXVECTOR3 vLookatPt = D3DXVECTOR3(0.0f, 0.0f, 0.0f); // 目标的位置(0,0,0),即三角形所在的平面位置
D3DXVECTOR3 vUpVec = D3DXVECTOR3(0.0f, 1.0f, 0.0f); // Camera的方向向量,朝正Y轴方向。即端正了照相机照相
D3DXMatrixLookAtRH(&matView, &vFromPt, &vLookatPt, &vUpVec); // 产生View Matrix
pd3dDevice->SetTransform(D3DTS_VIEW, &matView); // 用产生的View Matrix来设置系统的View Matrix
上述的过程主要是通过设定Camera的位置和其方向向量,还有Camera所要照相的目标位置来生成一个View Matrix,然后用它来设置系统里的View Matrix.
3). Projection Matrix
Projection Matrix几乎是每个3D程序必需的,它的主要用途是将3D场景投影到视窗上。
D3DXMATRIX matProj;
FLOAT fAspect = ((FLOAT)pBackBufferSurfaceDesc->Width)/pBackBufferSurfaceDesc->Height;
D3DXMatrixPerspectiveFovRH(&matProj, D3DX_PI/4, fAspect, 1.0f, 100.0f);
pd3dDevice->SetTransform(D3DTS_PROJECTION, &matProj);
3D的投影过程就是将Frustum(平顶菱椎体,即Near Plane和Far Plane之间的部分)中的景物投影到视窗(Near Plane)上,而要产生负责投影计算的Projection Matrix,需要Near, Far, FOV及Aspect等值。他们分别的含义如下:
- Near -- Near Plane到Camera的距离
- Far -- Far Plaen到Camera的距离
- FOV -- 指垂直方向的观察角度,也就是眼睛上下俯视的观察角度
- Aspect -- 视窗的宽高比
上面的代码里分别设置Near = 1.0f; Far = 100.0f; FOV = PI/4 = 45度; Aspect的值由调用OnResetDevice时传进来的pBackBufferSurfaceDesc的宽高比可的。然后通过D3DXMatrixPerspectiveFovRH计算获得Projection Matrix, 最后用这个Projection Maxtrix来设置系统。
9. 建立光源
D3D中主要有三种光源:
- Point Light(点光源) -- 指位于某个位置的发光体,它所照射的光线是全方位的。
- Directional Light(定向光源) -- 指只有方向性而无发光点位置的光线,且其照射的光线是平行光线,因其发光点位置可以假设为无穷远,例如生活中的太阳光。
- Spot Light(聚光灯) -- 指某种位置所照射出的光线,照射范围呈漏斗状。例如舞台上的灯光。
在3D程序中,计算光源明暗的主要原理就是先求出光线的向量与物体表面的法向量,然后再根据这两个向量的夹角来计算此物体表面的受光强度。在本例中。只设置了一种光源Directional Light。 代码也是在OnResetDevice()中实现。
D3DLIGHT9 light; // 定义一个光源变量
D3DXVECTOR3 vecLightDirUnnormalized(-1.0f, -1.0f, -2.0f); // 定义一个光线的向量,即原点到该点的向量
ZeroMemory(&light, sizeof(D3DLIGHT9));
light.Type = D3DLIGHT_DIRECTIONAL; // 设置光源类型
light.Diffuse.r = 1.0f; // 设置光源颜色
light.Diffuse.g = 1.0f;
light.Diffuse.b = 1.0f;
D3DXVec3Normalize((D3DXVECTOR3*)&light.Direction, &vecLightDirUnnormalized); // 设置向量单位化
pd3dDevice->SetLight(0,&light); // 0表示系统光源的编号,设置0号光源
pd3dDevice->LightEnable(0, TRUE); // 0表示系统光源的编号,启用0号光源
pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE); // 最后用光源对环境进行着色处理,让场景具备光源效果
10. 设置CullMode
三角形分正反面,为了提高计算效率,我们可以利用CullMode来不计算反面的三角形(因为看不到,没有计算的必要)。这也是在OnResetDevice()里设置。
pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // 删除顺时针的面。
11. 转动三角形
到此,基本上已经完成了对场景内三角形的各种属性的设置。如果需要在场景内控制三角形,则需要将控制代码放在OnFrameMove()里。我们这里要用上、下、左和右键来控制三角形。要先定义一个全局结构和几个变量。
struct UserInput{
BOOL bRotateUp; // 记录上键
BOOL bRotateDown; // 记录下键
BOOL bRotateLeft; // 记录左键
BOOL bRotateRight; // 记录右键
};
FLOAT g_fWorldRotX;
FLOAT g_fWorldRotY;
UserInput g_UserInput;
设计一个函数UpdateInput来更新用户输入。
void UpdateInput(UserInput* pUserInput)
{
pUserInput->bRotateUp = (GetAsyncKeyState(VK_UP)&0x8000) == 0x8000;
pUserInput->bRotateDown = (GetAsyncKeyState(VK_Down)&0x8000) == 0x8000;
pUserInput->bRotateLeft = (GetAsyncKeyState(VK_Left)&0x8000) == 0x8000;
pUserInput->bRotateRight = (GetAsyncKeyState(VK_Right)&0x8000) == 0x8000;
}
在InitApp()里初始化这几个全局变量
ZeroMemory(&g_UserInput, sizeof(UserInput));
g_fWorldRotX = 0.0f;
g_fWorldRotY = 0.0f;
最后在OnFrameMove()里实现三角形的转动控制,通过计算对Y轴和X轴的旋转计算来更新World Matrix。
UpdateInput(&g_UserInput);
D3DXMATRIX matWorld; // 定义一个World Matrix
D3DXMATRIX matRotY; // 定义一个Y轴 Matrix
D3DXMATRIX matRotX; // 定义一个X轴 Matrix
if(g_UserInput.bRotateLeft && !g_UserInput.bRotateRight)
g_fWorldRotY += fElamsedTime;
if(!g_UserInput.bRotateLeft && g_UserInput.bRotateRight)
g_fWorldRotY -= fElamsedTime;
if(g_UserInput.bRotateUp && !g_UserInput.bRotateDown)
g_fWorldRotX += fElamsedTime;
if(!g_UserInput.bRotateUp && g_UserInput.bRotateDown)
g_fWorldRotX += fElamsedTime;
D3DXMatrixRotationX(&matRotX, g_fWorldRotX); // 生成一个X轴Matrix
D3DXMatrixRotationY(&matRotY, g_fWorldRotY); // 生成一个Y轴Matrix
D3DXMatrixMultiply(&matWorld, &matRotX, &matRotY); // 生成一个World Matrix
pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld); // 设置系统的World Matrix
12. 画出三角形
在定义了三角形的各种属性和运动控制以后,现在就要在OnFrameRender()里画出三角形。
pd3dDevice->SetStreamSource(
0, // 将Vertex Buffer放入第0个流中
g_pVB, // Vertex Buffer的指针
0, // 从第0个Vertex开始
sizeof(CUSTOMVERTEX) // 每个Vertex的Bytes
);
pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX); // 告诉系统,Vertex Buffer里包含了哪些数据项
pd3dDevice->DrawPrimitive(
D3DPT_TRIANGLELIST, // 要画的类型
0, // 从第0个Vertex开始
2 // 要画2个三角形
); // 画出三角形
13. 释放资源
程序退出时,需要将Vertex Buffer释放掉. 这放到OnDestroyDevice()里处理.
SAFE_RELEASE(g_pVB);