本系列文章由zhmxy555(毛星云)编写,转载请注明出处。
作者:毛星云(浅墨) 邮箱: happylifemxy@163.com
本篇文章中,我们以核心思想为突破口,从原理介绍到一个C++类的写法,一步一步带领大家实现了一个第一人称三维摄像机的C++类。然后我们在这个摄像机类的帮助下,放出了一个几乎贯穿了我们之前学到的所有DirectX相关知识的“三维场景漫游”示例程序,算是对我们之前学的固定功能流水线这套渲染体系的总结。这个“三维场景漫游”示例程序的代码量有一千行,包括了Direct3D初始化,DirectInput输入处理,顶点缓存,光照与材质,文字输出,颜色,纹理贴图,四大变换,网格模型,X文件载入等等知识(当然还有默认被开启的深度缓存)。下面我们先放出一张运行截图:
好吧,正文开始,来卡看如何一步一步实现一个第一人称三维摄像机。
一、一些概述
回想我们之前文章的配套示例程序中,我们都是通过封装的DirectInput类来处理键盘和鼠标的输入,对应地改变我们人物模型的世界矩阵来达到移动物体,改变观察点的效果。其实我们的观察方向乃至观察点都是没有变的,变的只是我们3D人物的位置。举个例子吧,我们之前的示例程序就像是在小区里面安放着的摄像头,位置和观察点都是固定的,变的只是来来往往的人群。贴几张我们之前文章配套的示例程序截图来让大家体会一下:
之前的示例程序,和三维观察相关的四大矩阵变换我们都封装在了一个叫Matrix_Set()的函数中,而Matrix_Set()通常只用在初始化资源的时候调用一次而已。其中,四大变换中的取景变换,投影变换和视口变换都是一直老老实实地在Matrix_Set()中尽职尽责地呆着,当然,除了世界矩阵变换这个调皮的野孩子之外。世界矩阵由于实时处理的需要,自从第一次介绍完四大变换的时候让他在Matrix_Set()中呆了一次,在此之后的示例程序中我们都是把世界矩阵的设置放在Direct3D_Update()以及Direct3D_Render()之中的。因为我们必须实时地处理输入,让人物模型的世界矩阵能根据我们的输入进行相应的调整,这样才能做出人物模型随着键盘和鼠标的输入,随心所欲地受我们控制的效果出来。
但其实我们之前实现的那种所谓的“视角改变”还是来得不彻底不爽快,说白了就是用D3DXMatrixLookAtLH在资源初始化时固定住视角,在程序运行过程中接收到消息并改变三维人物模型的世界矩阵而已。我们这篇文章的主要讲解点就是手把手地教大家创建出一个可以在三维空间中自由移动的摄像机类,这样利用这个摄像机类写出来的示例程序就像是我们身临其境地在三维空间中真正地自由翱翔一样。浅墨怎么在写上面这段文字的时候,脑海中闪现的竟然是凤凰传奇的那句“在你的心上,自由地飞翔”,黑了- -
我们准备给这个摄像机类取名为CameraClass,这个类的功能非常适合用于三维飞行模拟类游戏(比如王牌空战)以及第一人称视角游戏(比如反恐精英(CS),穿越火线(CF))中。
二、开始设计摄像机类
首先,给大家介绍一下这个摄像机类的核心思想,那就是用四个分量:右分量(rightvector)、上分量(up vector)、观察分量(lookvector)和位置分量(position vector),来确定一个摄像机相对于世界坐标系的位置和朝向。并根据这四个分量计算出一个取景变换矩阵,完全取代之前的示例程序用D3DXMatrixLookAtLH创建的取景变换矩阵。
在世界坐标系中,这几个分量都是通过向量表示的,并且实际上他们为摄像机定义了一个局部坐标系。
其中,摄像机的左分量、上分量和观察分量定义了摄像机在世界坐标系中的朝向,因此他们也被称为方向向量。在通常的情况下,方向向量都是单位向量(模为1),并且两两之间相互垂直,也就是我们常说的标准正交。
其实,这三个向量我们完全可以理解为三维坐标系中的X,Y,Z轴。
另外,我们需要了解标准正交矩阵的一个重要性质,那就是标准正交矩阵的逆矩阵与其转置矩阵相等。
用上面提到的右分量(right vector)、上分量(up vector)、观察分量(look vector)和位置分量(position vector)这四个向量来描述摄像机的话,其中的位置分量其实我们可以把他看做一个描述位置的点,那么有用的就还3个分量,每个分量我们可以进行沿着其平移和绕着其旋转两种操作,那么我们可以想到的方式就是2x 3=6种,就是以下这六种运动方式:
● 沿着右分量平移
● 沿着上分量平移
● 沿着观察分量平移
● 绕着右分量旋转
● 绕着上分量旋转
● 绕着观察分量旋转
另外,我们新创建的这个摄像机类在我们的示例程序中完全“上位”了,取代了之前我们在Matrix_Set()中实现的取景变换和投影变换,所以在我们新的程序中,Matrix_Set()函数将不复存在,单单用一个摄像机类就行了。
根据上面的讲解,我们可以勾勒出这个CameraClass类的轮廓如下:
//============================================================================= // Name: CameraClass.h // Des: 一个封装了实现虚拟摄像机的类的头文件 // 2013年 3月10日 Create by 浅墨 //============================================================================= #pragma once #include <d3d9.h> #include <d3dx9.h> class CameraClass { private: //成员变量的声明 D3DXVECTOR3 m_vRightVector; // 右分量向量 D3DXVECTOR3 m_vUpVector; // 上分量向量 D3DXVECTOR3 m_vLookVector; // 观察方向向量 D3DXVECTOR3 m_vCameraPosition; // 摄像机位置的向量 D3DXVECTOR3 m_vTargetPosition; //目标观察位置的向量 D3DXMATRIX m_matView; // 取景变换矩阵 D3DXMATRIX m_matProj; // 投影变换矩阵 LPDIRECT3DDEVICE9 m_pd3dDevice; //Direct3D设备对象 public: //一个计算取景变换的函数 VOID CalculateViewMatrix(D3DXMATRIX *pMatrix); //计算取景变换矩阵 //三个Get系列函数 VOID GetProjMatrix(D3DXMATRIX *pMatrix) { *pMatrix = m_matProj; } //返回当前投影矩阵 VOID GetCameraPosition(D3DXVECTOR3 *pVector) { *pVector = m_vCameraPosition; } //返回当前摄像机位置矩阵 VOID GetLookVector(D3DXVECTOR3 *pVector) { *pVector = m_vLookVector; } //返回当前的观察矩阵 //四个Set系列函数,注意他们都参数都有默认值NULL的,调用时不写参数也可以 VOID SetTargetPosition(D3DXVECTOR3 *pLookat = NULL); //设置摄像机的目标观察位置向量 VOID SetCameraPosition(D3DXVECTOR3 *pVector = NULL); //设置摄像机所在的位置向量 VOID SetViewMatrix(D3DXMATRIX *pMatrix = NULL); //设置取景变换矩阵 VOID SetProjMatrix(D3DXMATRIX *pMatrix = NULL); //设置投影变换矩阵 public: // 沿各分量平移的三个函数 VOID MoveAlongRightVec(FLOAT fUnits); // 沿right向量移动 VOID MoveAlongUpVec(FLOAT fUnits); // 沿up向量移动 VOID MoveAlongLookVec(FLOAT fUnits); // 沿look向量移动 // 绕各分量旋转的三个函数 VOID RotationRightVec(FLOAT fAngle); // 绕right向量选择 VOID RotationUpVec(FLOAT fAngle); // 绕up向量旋转 VOID RotationLookVec(FLOAT fAngle); // 绕look向量旋转 public: //构造函数和析构函数 CameraClass(IDirect3DDevice9 *pd3dDevice); //构造函数 virtual ~CameraClass(void); //析构函数 };
三、关于向量计算的六个函数讲解
下面我们得先介绍一下Direct3D中,与向量计算有关的这六个函数,在写这个类的过程中有用到。
Ⅰ.D3DXVec3Normalize函数
首先我们来介绍对向量进行规范化的D3DXVec3Normalize函数 ,在MSDN中我们查到它原型如下:
D3DXVECTOR3* D3DXVec3Normalize( _Inout_ D3DXVECTOR3 *pOut, _In_ const D3DXVECTOR3 *pV );这个函数的第一个参数为输出的结果,在第二个参数中填想要被规范化的向量就行了,一般我们把这两个参数填一摸一样的,就表示把填的这个向量规范化后的结果替代原来的向量。
举个例子就是这样写:
//其中的m_vLookVector为向量 D3DXVec3Normalize(&m_vLookVector, &m_vLookVector);//规范化m_vLookVector向量
Ⅱ.D3DXVec3Cross函数
然后我们来介绍用于计算两个向量叉乘结果的D3DXVec3Cross函数,在MSDN中我们查到它原型如下:
D3DXVECTOR3* D3DXVec3Cross( _Inout_ D3DXVECTOR3 *pOut, _In_ const D3DXVECTOR3 *pV1, _In_ const D3DXVECTOR3 *pV2 );
第一个参数依然是计算的结果。第二和第三两个参数当然就是填参加叉乘运算的两个向量了。
另外需要注意,D3DXVec3Cross函数的返回值和第一个参数pOut 参数是一样的,为指向D3DXVECTOR3 结构的两个向量叉乘结果。
依然是一个实例:
D3DXVec3Cross(&m_vRightVector, &m_vUpVector,&m_vLookVector); // 右向量与上向量垂直
Ⅲ.D3DXVec3Dot函数
下面我们来讲一下用于计算向量点乘的D3DXVec3Dot函数,在MSDN中我查到它的原型如下:
FLOAT D3DXVec3Dot( _In_ const D3DXVECTOR3 *pV1, _In_ const D3DXVECTOR3 *pV2 );
这个函数倒不是和我们上面刚讲的那两个函数一样有个用于存放结果的pOut,它的结果就存放在返回值中,而两个参数就填参与运算的两个向量。
举一个实例吧:
pMatrix->_42 =-D3DXVec3Dot(&m_vUpVector, &m_vCameraPosition); // -P*U
Ⅳ.D3DXMatrixRotationAxis函数
接下来介绍可以创建一个绕任意轴旋转一定角度的矩阵的D3DXMatrixRotationAxis函数。其函数原型如下:
D3DXMATRIX* D3DXMatrixRotationAxis( _Inout_ D3DXMATRIX *pOut, _In_ const D3DXVECTOR3 *pV, _In_ FLOAT Angle );
第一个参数显然就填生成好的矩阵了,第二个参数填要绕着旋转的那根轴,第三个参数就填上要绕指定的轴旋转的角度。
依然是一个实例:
D3DXMatrixRotationAxis(&R,&m_vRightVector, fAngle);//创建出绕m_vRightVector旋转fAngle个角度的R矩阵
Ⅴ.D3DXVec3TransformCoord函数
下面我们看一个D3DXVec3TransformCoord函数,它可以根据给的的矩阵来变换一个向量,并且把变换后的向量规范化后输出来。这个函数原型如下:
D3DXVECTOR3* D3DXVec3TransformCoord( _Inout_ D3DXVECTOR3 *pOut, _In_ const D3DXVECTOR3 *pV, _In_ const D3DXMATRIX *pM );第一个参数就是得到的结果向量了。第二个参数填要被变换的那个向量,而第三个参数填用于变换的矩阵。
依然是一个实例:
D3DXVec3TransformCoord(&m_vUpVector, &m_vCameraPosition, &R);//让m_vCameraPosition向量绕m_vRightVector旋转fAngle个角度
Ⅵ.D3DXVec3Length函数
最后我们介绍计算一个三维向量长度的D3DXVec3Length函数,这个函数原型如下:
FLOAT D3DXVec3Length( _In_ const D3DXVECTOR3 *pV );唯一的一个参数填要计算长度的那个向量,返回值就是计算出的给定向量的三维长度。
依然是一个实例:
float length=D3DXVec3Length(&m_vCameraPosition);
四、计算取景变换矩阵
看完整个CameraClass类的轮廓,下面就准备开始讲解其中各个函数的实现代码应该怎么写。
首先就是我们最关心的取代了之前用D3DXMatrixLookAtLH无脑计算出取景变换矩阵,而自立门户的CalculateViewMatrix函数的写法。
为了讲解方便,我们令向量表示位置向量,
向量表示右向量,
向量表示上向量,
向量表示观察向量。
我们知道,取景变换所解决的其实就是世界坐标系中的物体在以摄像机为中心的坐标系中如何来表示的问题。这就是说,需要将世界坐标系中的物体随着摄像机一起进行变换,这样摄像机的坐标系就与世界坐标系完全重合了。
如下图所示:
上面的(a)图到(b)图,是一个平移的过程,而(b)图到(c)图则是一个旋转的过程。另外需要注意的一点是,空间中的物体也应该随着摄像机一同进行变换,这样摄像机中看到景物才没有变化。
我们的目的,就是通过一系列的矩阵变换,得到最终的取景变换矩阵V。
我们要得到取景变换矩阵V,说白了就是能够满足如下的条件:
pV=(0,0,0) 矩阵V将摄像机移动到世界坐标系的原点
rV=(1,0,0)矩阵V将摄像机的右向量与世界坐标系的x轴重合
uV=(0,1,0)矩阵V将摄像机的上向量与世界坐标系的y轴重合
lV=(0,0,1)矩阵V将摄像机的观察向量与世界坐标系的z轴重合
所以,想得到这个取景变换矩阵V,就是进行了先平移,后旋转,最后综合的三步曲操作,下面我们来对其各个击破:
1.平移
将摄像机的位置向量p平移到原点就是实现这个式子:pV=(0,0,0),即摄像机的位置分量乘以V之后等于(0,0,0)。那么很简单,我们假设取景变换矩阵V实现平移操作的的中间矩阵为T来简化一下。
而将摄像机的位置向量p平移到原点可以通过把他和它大小相等,方向相反的-p向量做加法轻松实现,即p-p=0,所以描述取景变换中的平移变换部分的T矩阵就是这样写:
2.旋转
想要让摄像机的各分量与世界坐标系的各轴重合的话,
即满足之前我们列出的这三个式子:
rV=(1,0,0)
uV=(0,1,0)
lV=(0,0,1)
实现起来,求出如下的一个3x3的矩阵A就可以了:
这里的矩阵A有很多种解法,最科学的解法在这里:
我们一眼就可以看出来A其实就是B矩阵的逆矩阵。而矩阵B刚好是个正交矩阵,我们文章前面就跟大家提到过要用到正交矩阵的这个性质:正交矩阵的逆矩阵等于其转置矩阵。用到我们这里,就是B矩阵的逆矩阵等于B矩阵的转置矩阵,而B矩阵的逆矩阵就是A矩阵,那么就是说A矩阵等于B矩阵的转置矩阵。
那么我们求一下B矩阵的转置矩阵,就是求出A矩阵了。
也就是:
3.综合前两步
我们需要把A矩阵扩展成4X4的,然后计算一下两矩阵的值TA=V就行了
即:
好了,取景变换矩阵V就被我们求出来了。
上面过程看不太懂不要紧,我们看一下最后V矩阵的结果就行了,下面我们实现计算取景变换矩阵的CalculateViewMatrix函数中,其实也就是用了一下最后我们求出的V矩阵的结果而已:
//----------------------------------------------------------------------------- // Name:CameraClass::CalculateViewMatrix( ) // Desc: 根据给定的矩阵计算出取景变换矩阵 //----------------------------------------------------------------------------- VOID CameraClass::CalculateViewMatrix(D3DXMATRIX *pMatrix) { //1.先把3个向量都规范化并使其相互垂直,成为一组正交矩阵 D3DXVec3Normalize(&m_vLookVector, &m_vLookVector); //规范化观察分量 D3DXVec3Cross(&m_vUpVector, &m_vLookVector, &m_vRightVector); // 上向量与观察向量垂直 D3DXVec3Normalize(&m_vUpVector, &m_vUpVector); // 规范化上向量 D3DXVec3Cross(&m_vRightVector, &m_vUpVector, &m_vLookVector); // 右向量与上向量垂直 D3DXVec3Normalize(&m_vRightVector, &m_vRightVector); // 规范化右向量 // 2.创建出取景变换矩阵 //依次写出取景变换矩阵的第一行 pMatrix->_11 = m_vRightVector.x; // Rx pMatrix->_12 = m_vUpVector.x; // Ux pMatrix->_13 = m_vLookVector.x; // Lx pMatrix->_14 = 0.0f; //依次写出取景变换矩阵的第二行 pMatrix->_21 = m_vRightVector.y; // Ry pMatrix->_22 = m_vUpVector.y; // Uy pMatrix->_23 = m_vLookVector.y; // Ly pMatrix->_24 = 0.0f; //依次写出取景变换矩阵的第三行 pMatrix->_31 = m_vRightVector.z; // Rz pMatrix->_32 = m_vUpVector.z; // Uz pMatrix->_33 = m_vLookVector.z; // Lz pMatrix->_34 = 0.0f; //依次写出取景变换矩阵的第四行 pMatrix->_41 = -D3DXVec3Dot(&m_vRightVector, &m_vCameraPosition); // -P*R pMatrix->_42 = -D3DXVec3Dot(&m_vUpVector, &m_vCameraPosition); // -P*U pMatrix->_43 = -D3DXVec3Dot(&m_vLookVector, &m_vCameraPosition); // -P*L pMatrix->_44 = 1.0f; }代码中浅墨已经注释很清楚了,我们先把3个向量都规范化并使其相互垂直,成为一组正交矩阵,然后对着我们计算出的取景变换矩阵V的结果,一行一行赋值就行了。
关于上面的赋值,我们随便抽一个出来讲一下:
比如这句:
pMatrix->_23 = m_vLookVector.y; // Ly
其中的pMatrix->_23表示pMatrix矩阵的第二行,第三行的元素,我们在计算出的取景变换矩阵V的矩阵结果中找到第二行第三列,它的值为ly,也就是上向量m_vLookVector的y坐标值,即m_vLookVector.y,那么第二行第三列就是这样写了。
其他行其他列就以此类推了,注意的是一共要写4x4=16个值。
五、类的其余实现细节
因为这个类浅墨基本上把代码都逐行注释了,所以类中其他的函数的实现方法大家直接看代码就行了。另外在这个类中视口变换并没有去实现,其实很多时候不用去设置视口的Direct3D就为我们默认好了,不去设置也无伤大雅的。
CameraClass.cpp的所有代码如下(友情声明:头文件CameraClass.h头文件在上头我们已经贴出来过了):
//============================================================================= // Name: CameraClass.cpp // Des: 一个封装了实现虚拟摄像机的类的源文件 // 2013年 3月10日 Create by 浅墨 //============================================================================= #include "CameraClass.h" #ifndef SCREEN_WIDTH #define SCREEN_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度 #define SCREEN_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度 #endif //----------------------------------------------------------------------------- // Desc: 构造函数 //----------------------------------------------------------------------------- CameraClass::CameraClass(IDirect3DDevice9 *pd3dDevice) { m_pd3dDevice = pd3dDevice; m_vRightVector = D3DXVECTOR3(1.0f, 0.0f, 0.0f); // 默认右向量与X正半轴重合 m_vUpVector = D3DXVECTOR3(0.0f, 1.0f, 0.0f); // 默认上向量与Y正半轴重合 m_vLookVector = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 默认观察向量与Z正半轴重合 m_vCameraPosition = D3DXVECTOR3(0.0f, 0.0f, -250.0f); // 默认摄像机坐标为(0.0f, 0.0f, -250.0f) m_vTargetPosition = D3DXVECTOR3(0.0f, 0.0f, 0.0f);//默认观察目标位置为(0.0f, 0.0f, 0.0f); } //----------------------------------------------------------------------------- // Name:CameraClass::CalculateViewMatrix( ) // Desc: 根据给定的矩阵计算出取景变换矩阵 //----------------------------------------------------------------------------- VOID CameraClass::CalculateViewMatrix(D3DXMATRIX *pMatrix) { //1.先把3个向量都规范化并使其相互垂直,成为一组正交矩阵 D3DXVec3Normalize(&m_vLookVector, &m_vLookVector); //规范化观察分量 D3DXVec3Cross(&m_vUpVector, &m_vLookVector, &m_vRightVector); // 上向量与观察向量垂直 D3DXVec3Normalize(&m_vUpVector, &m_vUpVector); // 规范化上向量 D3DXVec3Cross(&m_vRightVector, &m_vUpVector, &m_vLookVector); // 右向量与上向量垂直 D3DXVec3Normalize(&m_vRightVector, &m_vRightVector); // 规范化右向量 // 2.创建出取景变换矩阵 //依次写出取景变换矩阵的第一行 pMatrix->_11 = m_vRightVector.x; // Rx pMatrix->_12 = m_vUpVector.x; // Ux pMatrix->_13 = m_vLookVector.x; // Lx pMatrix->_14 = 0.0f; //依次写出取景变换矩阵的第二行 pMatrix->_21 = m_vRightVector.y; // Ry pMatrix->_22 = m_vUpVector.y; // Uy pMatrix->_23 = m_vLookVector.y; // Ly pMatrix->_24 = 0.0f; //依次写出取景变换矩阵的第三行 pMatrix->_31 = m_vRightVector.z; // Rz pMatrix->_32 = m_vUpVector.z; // Uz pMatrix->_33 = m_vLookVector.z; // Lz pMatrix->_34 = 0.0f; //依次写出取景变换矩阵的第四行 pMatrix->_41 = -D3DXVec3Dot(&m_vRightVector, &m_vCameraPosition); // -P*R pMatrix->_42 = -D3DXVec3Dot(&m_vUpVector, &m_vCameraPosition); // -P*U pMatrix->_43 = -D3DXVec3Dot(&m_vLookVector, &m_vCameraPosition); // -P*L pMatrix->_44 = 1.0f; } //----------------------------------------------------------------------------- // Name:CameraClass::SetTargetPosition( ) // Desc: 设置摄像机的观察位置 //----------------------------------------------------------------------------- VOID CameraClass::SetTargetPosition(D3DXVECTOR3 *pLookat) { //先看看pLookat是否为默认值NULL if (pLookat != NULL) m_vTargetPosition = (*pLookat); else m_vTargetPosition = D3DXVECTOR3(0.0f, 0.0f, 1.0f); m_vLookVector = m_vTargetPosition - m_vCameraPosition;//观察点位置减摄像机位置,得到观察方向向量 D3DXVec3Normalize(&m_vLookVector, &m_vLookVector);//规范化m_vLookVector向量 //正交并规范化m_vUpVector和m_vRightVector D3DXVec3Cross(&m_vUpVector, &m_vLookVector, &m_vRightVector); D3DXVec3Normalize(&m_vUpVector, &m_vUpVector); D3DXVec3Cross(&m_vRightVector, &m_vUpVector, &m_vLookVector); D3DXVec3Normalize(&m_vRightVector, &m_vRightVector); } //----------------------------------------------------------------------------- // Name:CameraClass::SetCameraPosition( ) // Desc: 设置摄像机所在的位置 //----------------------------------------------------------------------------- VOID CameraClass::SetCameraPosition(D3DXVECTOR3 *pVector) { D3DXVECTOR3 V = D3DXVECTOR3(0.0f, 0.0f, -250.0f); m_vCameraPosition = pVector ? (*pVector) : V;//三目运算符,如果pVector为真的话,返回*pVector的值(即m_vCameraPosition=*pVector),否则返回V的值(即m_vCameraPosition=V) } //----------------------------------------------------------------------------- // Name:CameraClass::SetViewMatrix( ) // Desc: 设置取景变换矩阵 //----------------------------------------------------------------------------- VOID CameraClass::SetViewMatrix(D3DXMATRIX *pMatrix) { //根据pMatrix的值先做一下判断 if (pMatrix) m_matView = *pMatrix; else CalculateViewMatrix(&m_matView); m_pd3dDevice->SetTransform(D3DTS_VIEW, &m_matView); //把取景变换矩阵的值分下来分别给右分量,上分量,和观察分量 m_vRightVector = D3DXVECTOR3(m_matView._11, m_matView._12, m_matView._13); m_vUpVector = D3DXVECTOR3(m_matView._21, m_matView._22, m_matView._23); m_vLookVector = D3DXVECTOR3(m_matView._31, m_matView._32, m_matView._33); } //----------------------------------------------------------------------------- // Name:CameraClass::SetProjMatrix( ) // Desc: 设置投影变换矩阵 //----------------------------------------------------------------------------- VOID CameraClass::SetProjMatrix(D3DXMATRIX *pMatrix) { //判断值有没有,没有的话就计算一下 if (pMatrix != NULL) m_matProj = *pMatrix; else D3DXMatrixPerspectiveFovLH(&m_matProj, D3DX_PI / 4.0f, (float)((double)SCREEN_WIDTH/SCREEN_HEIGHT), 1.0f, 30000.0f);//视截体远景设为30000.0f,这样就不怕看不到远处的物体了 m_pd3dDevice->SetTransform(D3DTS_PROJECTION, &m_matProj);//设置投影变换矩阵 } //----------------------------------------------------------------------------- // Name:CameraClass::MoveAlongRightVec( ) // Desc: 沿右向量平移fUnits个单位 //----------------------------------------------------------------------------- VOID CameraClass::MoveAlongRightVec(FLOAT fUnits) { //直接乘以fUnits的量来累加就行了 m_vCameraPosition += m_vRightVector * fUnits; m_vTargetPosition += m_vRightVector * fUnits; } //----------------------------------------------------------------------------- // Name:CameraClass::MoveAlongUpVec( ) // Desc: 沿上向量平移fUnits个单位 //----------------------------------------------------------------------------- VOID CameraClass::MoveAlongUpVec(FLOAT fUnits) { //直接乘以fUnits的量来累加就行了 m_vCameraPosition += m_vUpVector * fUnits; m_vTargetPosition += m_vUpVector * fUnits; } //----------------------------------------------------------------------------- // Name:CameraClass::MoveAlongLookVec( ) // Desc: 沿观察向量平移fUnits个单位 //----------------------------------------------------------------------------- VOID CameraClass::MoveAlongLookVec(FLOAT fUnits) { //直接乘以fUnits的量来累加就行了 m_vCameraPosition += m_vLookVector * fUnits; m_vTargetPosition += m_vLookVector * fUnits; } //----------------------------------------------------------------------------- // Name:CameraClass::RotationRightVec( ) // Desc: 沿右向量旋转fAngle个弧度单位的角度 //----------------------------------------------------------------------------- VOID CameraClass::RotationRightVec(FLOAT fAngle) { D3DXMATRIX R; D3DXMatrixRotationAxis(&R, &m_vRightVector, fAngle);//创建出绕m_vRightVector旋转fAngle个角度的R矩阵 D3DXVec3TransformCoord(&m_vUpVector, &m_vUpVector, &R);//让m_vUpVector向量绕m_vRightVector旋转fAngle个角度 D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vRightVector旋转fAngle个角度 m_vTargetPosition = m_vLookVector * D3DXVec3Length(&m_vCameraPosition);//更新一下观察点的新位置(方向乘以模=向量) } //----------------------------------------------------------------------------- // Name:CameraClass::RotationUpVec( ) // Desc: 沿上向量旋转fAngle个弧度单位的角度 //----------------------------------------------------------------------------- VOID CameraClass::RotationUpVec(FLOAT fAngle) { D3DXMATRIX R; D3DXMatrixRotationAxis(&R, &m_vUpVector, fAngle);//创建出绕m_vUpVector旋转fAngle个角度的R矩阵 D3DXVec3TransformCoord(&m_vRightVector, &m_vRightVector, &R);//让m_vRightVector向量绕m_vUpVector旋转fAngle个角度 D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vUpVector旋转fAngle个角度 m_vTargetPosition = m_vLookVector * D3DXVec3Length(&m_vCameraPosition);//更新一下观察点的新位置(方向乘以模=向量) } //----------------------------------------------------------------------------- // Name:CameraClass::RotationLookVec( ) // Desc: 沿观察向量旋转fAngle个弧度单位的角度 //----------------------------------------------------------------------------- VOID CameraClass::RotationLookVec(FLOAT fAngle) { D3DXMATRIX R; D3DXMatrixRotationAxis(&R, &m_vLookVector, fAngle);//创建出绕m_vLookVector旋转fAngle个角度的R矩阵 D3DXVec3TransformCoord(&m_vRightVector, &m_vRightVector, &R);//让m_vRightVector向量绕m_vLookVector旋转fAngle个角度 D3DXVec3TransformCoord(&m_vUpVector, &m_vUpVector, &R);//让m_vUpVector向量绕m_vLookVector旋转fAngle个角度 m_vTargetPosition = m_vLookVector * D3DXVec3Length(&m_vCameraPosition);//更新一下观察点的新位置(方向乘以模=向量) } //----------------------------------------------------------------------------- // Desc: 析构函数 //----------------------------------------------------------------------------- CameraClass::~CameraClass(void) { }
怎么样,注释够详细吧,类的实现代码其实就是对着写好的头文件挨着挨着把函数扩展实现出来就OK了。
这个类的使用方面的话,我们一般就是在在给绘制做准备的Objects_Init()函数中调用一下,这就是这样子写:
// 创建并初始化虚拟摄像机 g_pCamera = new CameraClass(g_pd3dDevice); g_pCamera->SetCameraPosition(&D3DXVECTOR3(0.0f, 200.0f, -300.0f)); //设置摄像机所在的位置 g_pCamera->SetTargetPosition(&D3DXVECTOR3(0.0f, 300.0f, 0.0f)); //设置目标观察点所在的位置 g_pCamera->SetViewMatrix(); //设置取景变换矩阵 g_pCamera->SetProjMatrix(); //设置投影变换矩阵
好了,看完摄像机类的实现,下面我们就放出这个综合性非常强的“场景漫游”示例程序。
五、详细注释的源代码欣赏
在为大家写这个示例程序的时候,浅墨感觉自己在创造整个世界,非常地有成就感。首先是选择天蓝色的清屏颜色,这样我们的“世界”看起来就像是在蓝天晴空的背景下了。然后是选择翠绿色的可以无缝衔接的草坪纹理,用之前学的纹理映射和顶点缓存的知识把纹理重复铺上个几百张(具体张数是50x 50=250张),这样就是一大片绿油油的草坪了。接着就是我们几节之前刚讲的载入X文件模型,浅墨选的人物模型是PS3版《真三国无双6》中的王元姬,一袭蓝色的衣服,非常好看。再接着我们还用D3DXCreateCylinder函数创建了一根柱子(后面用for循环绘制了12根一摸一样的柱子),最后就是设置光照,设置渲染状态什么的。
先是用的纹理素材: 尺寸是512x512
然后是PS3版《真三国无双6》中的王元姬在3DS Max中的渲染效果图:
也先放一张程序的运行截图:
本篇文章的配套源代码包含了6个文件,主要用于公共辅助宏定义的D3DUtil.h,用于封装了DirectInput输入控制API的DirectInputClass.h和DirectInputClass.cpp,以及封装了虚拟摄像机类的CameraClass.h和CameraClass.cpp,当还还有核心代码main.cpp。
DirectInputClass.h和DirectInputClass.cpp较之前的文章中依然没有任何修改,依然不再贴出,CameraClass.cpp和CameraClass.h在上面讲解的过程中以及贴出来了,这里也不贴出,我们依然只贴核心代码main.cpp,大家要看得爽的话,源代码在文章末尾有下载链接,下回去用VisualStuido看就行了。下面就是main.cpp的全部代码:
//***************************************************************************************** // //【Visual C++】游戏开发笔记系列配套源码四十七 浅墨DirectX教程十五 翱翔于三维空间:第一人称摄像机的实现 // VS2010版 // 2013年 3月10日 Create by 浅墨 //图标素材出处: VAMPIRE_SWEETIE //背景音乐素材出处:雅尼-兰花 //人物模型素材出处:真三国无双6 王元姬 仿制版 // //***************************************************************************************** //***************************************************************************************** // Desc: 宏定义部分 //***************************************************************************************** #define SCREEN_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度 #define SCREEN_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度 #define WINDOW_TITLE _T("【Visual C++】游戏开发笔记系列配套示例程序四十七 浅墨DirectX教程十五 翱翔于三维空间:第一人称摄像机的实现") //为窗口标题定义的宏 //***************************************************************************************** // Desc: 头文件定义部分 //***************************************************************************************** #include <d3d9.h> #include <d3dx9.h> #include <tchar.h> #include <time.h> #include "DirectInputClass.h" #include "CameraClass.h" //***************************************************************************************** // Desc: 库文件定义部分 //***************************************************************************************** #pragma comment(lib,"d3d9.lib") #pragma comment(lib,"d3dx9.lib") #pragma comment(lib, "dinput8.lib") // 使用DirectInput必须包含的库文件,注意这里有8 #pragma comment(lib,"dxguid.lib") #pragma comment(lib, "winmm.lib") //定义顶点结构体 struct CUSTOMVERTEX { FLOAT _x, _y, _z; FLOAT _nx, _ny, _nz; FLOAT _u, _v; CUSTOMVERTEX(FLOAT x, FLOAT y, FLOAT z, FLOAT nx, FLOAT ny, FLOAT nz, FLOAT u, FLOAT v) { _x = x, _y = y, _z = z; _nx = nx, _ny = ny, _nz = nz; _u = u, _v = v; } }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1) //***************************************************************************************** // Desc: 全局变量声明部分 //***************************************************************************************** LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; //Direct3D设备对象 LPD3DXFONT g_pTextFPS =NULL; //字体COM接口 LPD3DXFONT g_pTextAdaperName = NULL; // 显卡信息的2D文本 LPD3DXFONT g_pTextHelper = NULL; // 帮助信息的2D文本 LPD3DXFONT g_pTextInfor = NULL; // 绘制信息的2D文本 float g_FPS = 0.0f; //一个浮点型的变量,代表帧速率 wchar_t g_strFPS[50]={0}; //包含帧速率的字符数组 wchar_t g_strAdapterName[60]={0}; //包含显卡名称的字符数组 D3DXMATRIX g_matWorld; //世界矩阵 DInputClass* g_pDInput = NULL; //一个DInputClass类的指针 CameraClass* g_pCamera = NULL; LPD3DXMESH g_pMesh = NULL; // 网格对象 D3DMATERIAL9* g_pMaterials = NULL; // 网格的材质信息 LPDIRECT3DTEXTURE9* g_pTextures = NULL; // 网格的纹理信息 DWORD g_dwNumMtrls = 0; // 材质的数目 LPD3DXMESH g_cylinder = NULL; //柱子网格对象 D3DMATERIAL9 g_MaterialCylinder; //材质 LPDIRECT3DVERTEXBUFFER9 g_pVertexBuffer = NULL; //绘制草地的顶点缓存对象 LPDIRECT3DTEXTURE9 g_pTexture = NULL; //绘制草地的纹理对象 //***************************************************************************************** // Desc: 全局函数声明部分 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance); HRESULT Objects_Init(); void Direct3D_Render( HWND hwnd); void Direct3D_Update( HWND hwnd); void Direct3D_CleanUp( ); float Get_FPS(); void HelpText_Render(HWND hwnd); //***************************************************************************************** // Name: WinMain( ) // Desc: Windows应用程序入口函数 //***************************************************************************************** int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd) { //开始设计一个完整的窗口类 WNDCLASSEX wndClass = { 0 }; //用WINDCLASSEX定义了一个窗口类,即用wndClass实例化了WINDCLASSEX,用于之后窗口的各项初始化 wndClass.cbSize = sizeof( WNDCLASSEX ) ; //设置结构体的字节数大小 wndClass.style = CS_HREDRAW | CS_VREDRAW; //设置窗口的样式 wndClass.lpfnWndProc = WndProc; //设置指向窗口过程函数的指针 wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; //指定包含窗口过程的程序的实例句柄。 wndClass.hIcon=(HICON)::LoadImage(NULL,_T("icon.ico"),IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE); //从全局的::LoadImage函数从本地加载自定义ico图标 wndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); //指定窗口类的光标句柄。 wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH); //为hbrBackground成员指定一个灰色画刷句柄 wndClass.lpszMenuName = NULL; //用一个以空终止的字符串,指定菜单资源的名字。 wndClass.lpszClassName = _T("ForTheDreamOfGameDevelop"); //用一个以空终止的字符串,指定窗口类的名字。 if( !RegisterClassEx( &wndClass ) ) //设计完窗口后,需要对窗口类进行注册,这样才能创建该类型的窗口 return -1; HWND hwnd = CreateWindow( _T("ForTheDreamOfGameDevelop"),WINDOW_TITLE, //喜闻乐见的创建窗口函数CreateWindow WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, SCREEN_WIDTH, SCREEN_HEIGHT, NULL, NULL, hInstance, NULL ); //Direct3D资源的初始化,调用失败用messagebox予以显示 if (!(S_OK==Direct3D_Init (hwnd,hInstance))) { MessageBox(hwnd, _T("Direct3D初始化失败~!"), _T("浅墨的消息窗口"), 0); //使用MessageBox函数,创建一个消息窗口 } PlaySound(L"雅尼 - 兰花.wav", NULL, SND_FILENAME | SND_ASYNC|SND_LOOP); //循环播放背景音乐 MoveWindow(hwnd,200,50,SCREEN_WIDTH,SCREEN_HEIGHT,true); //调整窗口显示时的位置,窗口左上角位于屏幕坐标(200,50)处 ShowWindow( hwnd, nShowCmd ); //调用Win32函数ShowWindow来显示窗口 UpdateWindow(hwnd); //对窗口进行更新,就像我们买了新房子要装修一样 //进行DirectInput类的初始化 g_pDInput = new DInputClass(); g_pDInput->Init(hwnd,hInstance,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE); //消息循环过程 MSG msg = { 0 }; //初始化msg while( msg.message != WM_QUIT ) //使用while循环 { if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) //查看应用程序消息队列,有消息时将队列中的消息派发出去。 { TranslateMessage( &msg ); //将虚拟键消息转换为字符消息 DispatchMessage( &msg ); //该函数分发一个消息给窗口程序。 } else { Direct3D_Update(hwnd); //调用更新函数,进行画面的更新 Direct3D_Render(hwnd); //调用渲染函数,进行画面的渲染 } } UnregisterClass(_T("ForTheDreamOfGameDevelop"), wndClass.hInstance); return 0; } //***************************************************************************************** // Name: WndProc() // Desc: 对窗口消息进行处理 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) //窗口过程函数WndProc { switch( message ) //switch语句开始 { case WM_PAINT: // 客户区重绘消息 Direct3D_Render(hwnd); //调用Direct3D_Render函数,进行画面的绘制 ValidateRect(hwnd, NULL); // 更新客户区的显示 break; //跳出该switch语句 case WM_KEYDOWN: // 键盘按下消息 if (wParam == VK_ESCAPE) // ESC键 DestroyWindow(hwnd); // 销毁窗口, 并发送一条WM_DESTROY消息 break; case WM_DESTROY: //窗口销毁消息 Direct3D_CleanUp(); //调用Direct3D_CleanUp函数,清理COM接口对象 PostQuitMessage( 0 ); //向系统表明有个线程有终止请求。用来响应WM_DESTROY消息 break; //跳出该switch语句 default: //若上述case条件都不符合,则执行该default语句 return DefWindowProc( hwnd, message, wParam, lParam ); //调用缺省的窗口过程来为应用程序没有处理的窗口消息提供缺省的处理。 } return 0; //正常退出 } //***************************************************************************************** // Name: Direct3D_Init( ) // Desc: 初始化Direct3D // Point:【Direct3D初始化四步曲】 // 1.初始化四步曲之一,创建Direct3D接口对象 // 2.初始化四步曲之二,获取硬件设备信息 // 3.初始化四步曲之三,填充结构体 // 4.初始化四步曲之四,创建Direct3D设备接口 //***************************************************************************************** HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance) { //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之一,创接口】:创建Direct3D接口对象, 以便用该Direct3D对象创建Direct3D设备对象 //-------------------------------------------------------------------------------------- LPDIRECT3D9 pD3D = NULL; //Direct3D接口对象的创建 if( NULL == ( pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) //初始化Direct3D接口对象,并进行DirectX版本协商 return E_FAIL; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之二,取信息】:获取硬件设备信息 //-------------------------------------------------------------------------------------- D3DCAPS9 caps; int vp = 0; if( FAILED( pD3D->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps ) ) ) { return E_FAIL; } if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) vp = D3DCREATE_HARDWARE_VERTEXPROCESSING; //支持硬件顶点运算,我们就采用硬件顶点运算,妥妥的 else vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING; //不支持硬件顶点运算,无奈只好采用软件顶点运算 //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之三,填内容】:填充D3DPRESENT_PARAMETERS结构体 //-------------------------------------------------------------------------------------- D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.BackBufferWidth = SCREEN_WIDTH; d3dpp.BackBufferHeight = SCREEN_HEIGHT; d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; d3dpp.BackBufferCount = 2; d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE; d3dpp.MultiSampleQuality = 0; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = hwnd; d3dpp.Windowed = true; d3dpp.EnableAutoDepthStencil = true; d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; d3dpp.Flags = 0; d3dpp.FullScreen_RefreshRateInHz = 0; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之四,创设备】:创建Direct3D设备接口 //-------------------------------------------------------------------------------------- if(FAILED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, vp, &d3dpp, &g_pd3dDevice))) return E_FAIL; //获取显卡信息到g_strAdapterName中,并在显卡名称之前加上“当前显卡型号:”字符串 wchar_t TempName[60]=L"当前显卡型号:"; //定义一个临时字符串,且方便了把"当前显卡型号:"字符串引入我们的目的字符串中 D3DADAPTER_IDENTIFIER9 Adapter; //定义一个D3DADAPTER_IDENTIFIER9结构体,用于存储显卡信息 pD3D->GetAdapterIdentifier(0,0,&Adapter);//调用GetAdapterIdentifier,获取显卡信息 int len = MultiByteToWideChar(CP_ACP,0, Adapter.Description, -1, NULL, 0);//显卡名称现在已经在Adapter.Description中了,但是其为char类型,我们要将其转为wchar_t类型 MultiByteToWideChar(CP_ACP, 0, Adapter.Description, -1, g_strAdapterName, len);//这步操作完成后,g_strAdapterName中就为当前我们的显卡类型名的wchar_t型字符串了 wcscat_s(TempName,g_strAdapterName);//把当前我们的显卡名加到“当前显卡型号:”字符串后面,结果存在TempName中 wcscpy_s(g_strAdapterName,TempName);//把TempName中的结果拷贝到全局变量g_strAdapterName中,大功告成~ if(!(S_OK==Objects_Init())) return E_FAIL; SAFE_RELEASE(pD3D) //LPDIRECT3D9接口对象的使命完成,我们将其释放掉 return S_OK; } HRESULT Objects_Init() { //创建字体 D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1000, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("Calibri"), &g_pTextFPS); D3DXCreateFont(g_pd3dDevice, 20, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"华文中宋", &g_pTextAdaperName); D3DXCreateFont(g_pd3dDevice, 23, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"微软雅黑", &g_pTextHelper); D3DXCreateFont(g_pd3dDevice, 26, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"黑体", &g_pTextInfor); // 从X文件中加载网格数据 LPD3DXBUFFER pAdjBuffer = NULL; LPD3DXBUFFER pMtrlBuffer = NULL; D3DXLoadMeshFromX(L"WYJ.X", D3DXMESH_MANAGED, g_pd3dDevice, &pAdjBuffer, &pMtrlBuffer, NULL, &g_dwNumMtrls, &g_pMesh); // 读取材质和纹理数据 D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); //创建一个D3DXMATERIAL结构体用于读取材质和纹理信息 g_pMaterials = new D3DMATERIAL9[g_dwNumMtrls]; g_pTextures = new LPDIRECT3DTEXTURE9[g_dwNumMtrls]; for (DWORD i=0; i<g_dwNumMtrls; i++) { //获取材质,并设置一下环境光的颜色值 g_pMaterials[i] = pMtrls[i].MatD3D; g_pMaterials[i].Ambient = g_pMaterials[i].Diffuse; //创建一下纹理对象 g_pTextures[i] = NULL; D3DXCreateTextureFromFileA(g_pd3dDevice, pMtrls[i].pTextureFilename, &g_pTextures[i]); } SAFE_RELEASE(pAdjBuffer) SAFE_RELEASE(pMtrlBuffer) // 创建一片草坪,50X50=250张纹理 g_pd3dDevice->CreateVertexBuffer(4 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_MANAGED, &g_pVertexBuffer, 0); CUSTOMVERTEX *pVertices = NULL; g_pVertexBuffer->Lock(0, 0, (void**)&pVertices, 0); pVertices[0] = CUSTOMVERTEX(-500.0f, 0.0f, -500.0f, 0.0f, 1.0f, 0.0f, 0.0f, 50.0f); pVertices[1] = CUSTOMVERTEX(-500.0f, 0.0f, 500.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f); pVertices[2] = CUSTOMVERTEX( 500.0f, 0.0f, -500.0f, 0.0f, 1.0f, 0.0f, 50.0f, 50.0f); pVertices[3] = CUSTOMVERTEX( 500.0f, 0.0f, 500.0f, 0.0f, 1.0f, 0.0f, 50.0f, 0.0f); g_pVertexBuffer->Unlock(); // 创建地板纹理 D3DXCreateTextureFromFile(g_pd3dDevice, L"grass.jpg", &g_pTexture); //创建柱子 D3DXCreateCylinder(g_pd3dDevice, 10.0f, 10.0f, 500.0f, 60, 60, &g_cylinder, 0); g_MaterialCylinder.Ambient = D3DXCOLOR(0.9f, 0.0f, 0.8f, 1.0f); g_MaterialCylinder.Diffuse = D3DXCOLOR(0.9f, 0.0f, 0.8f, 1.0f); g_MaterialCylinder.Specular = D3DXCOLOR(0.9f, 0.2f, 0.9f, 0.9f); g_MaterialCylinder.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.9f, 1.0f); // 设置光照 D3DLIGHT9 light; ::ZeroMemory(&light, sizeof(light)); light.Type = D3DLIGHT_DIRECTIONAL; light.Ambient = D3DXCOLOR(0.7f, 0.7f, 0.7f, 1.0f); light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f); light.Direction = D3DXVECTOR3(1.0f, 0.0f, 0.0f); g_pd3dDevice->SetLight(0, &light); g_pd3dDevice->LightEnable(0, true); g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true); g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true); // 创建并初始化虚拟摄像机 g_pCamera = new CameraClass(g_pd3dDevice); g_pCamera->SetCameraPosition(&D3DXVECTOR3(0.0f, 200.0f, -300.0f)); //设置摄像机所在的位置 g_pCamera->SetTargetPosition(&D3DXVECTOR3(0.0f, 300.0f, 0.0f)); //设置目标观察点所在的位置 g_pCamera->SetViewMatrix(); //设置取景变换矩阵 g_pCamera->SetProjMatrix(); //设置投影变换矩阵 // 设置纹理过滤和纹理寻址方式 g_pd3dDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); g_pd3dDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); g_pd3dDevice->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR); return S_OK; } void Direct3D_Update( HWND hwnd) { //使用DirectInput类读取数据 g_pDInput->GetInput(); // 沿摄像机各分量移动视角 if (g_pDInput->IsKeyDown(DIK_A)) g_pCamera->MoveAlongRightVec(-0.3f); if (g_pDInput->IsKeyDown(DIK_D)) g_pCamera->MoveAlongRightVec( 0.3f); if (g_pDInput->IsKeyDown(DIK_W)) g_pCamera->MoveAlongLookVec( 0.3f); if (g_pDInput->IsKeyDown(DIK_S)) g_pCamera->MoveAlongLookVec(-0.3f); if (g_pDInput->IsKeyDown(DIK_I)) g_pCamera->MoveAlongUpVec( 0.3f); if (g_pDInput->IsKeyDown(DIK_K)) g_pCamera->MoveAlongUpVec(-0.3f); //沿摄像机各分量旋转视角 if (g_pDInput->IsKeyDown(DIK_LEFT)) g_pCamera->RotationUpVec(-0.003f); if (g_pDInput->IsKeyDown(DIK_RIGHT)) g_pCamera->RotationUpVec( 0.003f); if (g_pDInput->IsKeyDown(DIK_UP)) g_pCamera->RotationRightVec(-0.003f); if (g_pDInput->IsKeyDown(DIK_DOWN)) g_pCamera->RotationRightVec( 0.003f); if (g_pDInput->IsKeyDown(DIK_J)) g_pCamera->RotationLookVec(-0.001f); if (g_pDInput->IsKeyDown(DIK_L)) g_pCamera->RotationLookVec( 0.001f); //鼠标控制右向量和上向量的旋转 g_pCamera->RotationUpVec(g_pDInput->MouseDX()* 0.001f); g_pCamera->RotationRightVec(g_pDInput->MouseDY() * 0.001f); //鼠标滚轮控制观察点收缩操作 static FLOAT fPosZ=0.0f; fPosZ += g_pDInput->MouseDZ()*0.03f; //计算并设置取景变换矩阵 D3DXMATRIX matView; g_pCamera->CalculateViewMatrix(&matView); g_pd3dDevice->SetTransform(D3DTS_VIEW, &matView); //把正确的世界变换矩阵存到g_matWorld中 D3DXMatrixTranslation(&g_matWorld, 0.0f, 0.0f, fPosZ); //以下这段代码用于限制鼠标光标移动区域 POINT lt,rb; RECT rect; GetClientRect(hwnd,&rect); //取得窗口内部矩形 //将矩形左上点坐标存入lt中 lt.x = rect.left; lt.y = rect.top; //将矩形右下坐标存入rb中 rb.x = rect.right; rb.y = rect.bottom; //将lt和rb的窗口坐标转换为屏幕坐标 ClientToScreen(hwnd,<); ClientToScreen(hwnd,&rb); //以屏幕坐标重新设定矩形区域 rect.left = lt.x; rect.top = lt.y; rect.right = rb.x; rect.bottom = rb.y; //限制鼠标光标移动区域 ClipCursor(&rect); } //***************************************************************************************** // Name: Direct3D_Render() // Desc: 进行图形的渲染操作 // Point:【Direct3D渲染五步曲】 // 1.渲染五步曲之一,清屏操作 // 2.渲染五步曲之二,开始绘制 // 3.渲染五步曲之三,正式绘制 // 4.渲染五步曲之四,结束绘制 // 5.渲染五步曲之五,翻转显示 //***************************************************************************************** void Direct3D_Render(HWND hwnd) { //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之一】:清屏操作 //-------------------------------------------------------------------------------------- g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL, D3DCOLOR_XRGB(50, 100, 250), 1.0f, 0); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之二】:开始绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->BeginScene(); // 开始绘制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之三】:正式绘制 //-------------------------------------------------------------------------------------- //绘制人物 g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_matWorld);//设置模型的世界矩阵,为绘制做准备 // 用一个for循环,进行模型的网格各个部分的绘制 for (DWORD i = 0; i < g_dwNumMtrls; i++) { g_pd3dDevice->SetMaterial(&g_pMaterials[i]); //设置此部分的材质 g_pd3dDevice->SetTexture(0, g_pTextures[i]);//设置此部分的纹理 g_pMesh->DrawSubset(i); //绘制此部分 } // 绘制草坪 D3DXMATRIX matWorld; D3DXMatrixTranslation(&matWorld, 0.0f, 0.0f, 0.0f); g_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld); g_pd3dDevice->SetTexture(0, g_pTexture); g_pd3dDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(CUSTOMVERTEX)); g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX); g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); //绘制柱子 D3DXMATRIX TransMatrix, RotMatrix, FinalMatrix; D3DXMatrixRotationX(&RotMatrix, -D3DX_PI * 0.5f); g_pd3dDevice->SetMaterial(&g_MaterialCylinder); for(int i = 0; i < 6; i++) { D3DXMatrixTranslation(&TransMatrix, -100.0f, 0.0f, -150.0f + (i * 75.0f)); FinalMatrix = RotMatrix * TransMatrix ; g_pd3dDevice->SetTransform(D3DTS_WORLD, &FinalMatrix); g_cylinder->DrawSubset(0); D3DXMatrixTranslation(&TransMatrix, 100.0f, 0.0f, -150.0f + (i * 75.0f)); FinalMatrix = RotMatrix * TransMatrix ; g_pd3dDevice->SetTransform(D3DTS_WORLD, &FinalMatrix); g_cylinder->DrawSubset(0); } HelpText_Render(hwnd); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之四】:结束绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->EndScene(); // 结束绘制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之五】:显示翻转 //-------------------------------------------------------------------------------------- g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示 } void HelpText_Render(HWND hwnd) { //定义一个矩形,用于获取主窗口矩形 RECT formatRect; GetClientRect(hwnd, &formatRect); //在窗口右上角处,显示每秒帧数 formatRect.top = 5; int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); g_pTextFPS->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_RGBA(0,239,136,255)); //显示显卡类型名 g_pTextAdaperName->DrawText(NULL,g_strAdapterName, -1, &formatRect, DT_TOP | DT_LEFT, D3DXCOLOR(1.0f, 0.5f, 0.0f, 1.0f)); // 输出帮助信息 formatRect.left = 0,formatRect.top = 380; g_pTextInfor->DrawText(NULL, L"控制说明:", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(235,123,230,255)); formatRect.top += 35; g_pTextHelper->DrawText(NULL, L" W:向前飞翔 S:向后飞翔 ", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" A:向左飞翔 D:向右飞翔", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" I:垂直向上飞翔 K:垂直向下飞翔", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" J:向左倾斜 L:向右倾斜", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 上、下、左、右方向键、鼠标移动:视角变化 ", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 鼠标滚轮:人物模型Y轴方向移动", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" ESC键 : 退出程序", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); } //***************************************************************************************** // Name:Get_FPS()函数 // Desc: 用于计算帧速率 //***************************************************************************************** float Get_FPS() { //定义四个静态变量 static float fps = 0; //我们需要计算的FPS值 static int frameCount = 0;//帧数 static float currentTime =0.0f;//当前时间 static float lastTime = 0.0f;//持续时间 frameCount++;//每调用一次Get_FPS()函数,帧数自增1 currentTime = timeGetTime()*0.001f;//获取系统时间,其中timeGetTime函数返回的是以毫秒为单位的系统时间,所以需要乘以0.001,得到单位为秒的时间 //如果当前时间减去持续时间大于了1秒钟,就进行一次FPS的计算和持续时间的更新,并将帧数值清零 if(currentTime - lastTime > 1.0f) //将时间控制在1秒钟 { fps = (float)frameCount /(currentTime - lastTime);//计算这1秒钟的FPS值 lastTime = currentTime; //将当前时间currentTime赋给持续时间lastTime,作为下一秒的基准时间 frameCount = 0;//将本次帧数frameCount值清零 } return fps; } //***************************************************************************************** // Name: Direct3D_CleanUp() // Desc: 对Direct3D的资源进行清理,释放COM接口对象 //***************************************************************************************** void Direct3D_CleanUp() { //释放COM接口对象 for (DWORD i = 0; i<g_dwNumMtrls; i++) SAFE_RELEASE(g_pTextures[i]); SAFE_DELETE(g_pTextures); SAFE_DELETE(g_pMaterials); SAFE_DELETE(g_pDInput); SAFE_RELEASE(g_cylinder); SAFE_RELEASE(g_pMesh); SAFE_RELEASE(g_pd3dDevice); SAFE_RELEASE(g_pTextAdaperName) SAFE_RELEASE(g_pTextHelper) SAFE_RELEASE(g_pTextInfor) SAFE_RELEASE(g_pTextFPS) SAFE_RELEASE(g_pd3dDevice) }
本篇文章的示例程序相对来说比较综合,里面融合了Direct3D的初始化,DirectInput输入处理,顶点缓存,文字输出,颜色,光照与材质,纹理贴图,四大变换,网格模型,X文件载入等等知识(当然还有默认被开启的深度缓存)。
虽然这听起来怎么这么地,但是却是很好理解的,就是在HRESULT Objects_Init()函数中把要绘制的物体都准备好,也把光照、材质、摄像机都初始化好,然后在Direct3D_Render( HWND hwnd);函数中绘制出来就可以了。说白了也就是相对于之前的demo中多绘制了一点东西而已,没什么特别的。
摄像机类的使用的话,就是在Objects_Init()顺带着中初始化一下,然后在Direct3D_Update( HWND hwnd)函数中配合着DirectInput类对输出进行处理就好了。
另外需要注意的是,我们把文字帮助信息和FPS的输出都封装在了一个叫HelpText_Render的函数中了。因为我们绘制的东西越来越多,为了Direct3D_Render看起来整洁一点,而不是里面大一串内容,是时候把文字帮助信息封装起来了。
下面来看一下运行截图,这次的运行效果就可以天马行空了,可以在无限的空间中任意地遨游,想去哪儿就飞去哪儿。
程序运行后就是这样,同时一曲唯美的出自轻音乐大师雅尼的《兰花》映入耳帘:
我们开始在键盘上W,A,S,D,I,J,K,L,↑,↓,←,→12个键乱按,加上鼠标乱晃:
越飞越远:
摄像机在蓝天之下,草原之上盘旋:
来张正面照:
暮然回首,那人却在灯火阑珊处:
隔近了看草地就是这样的:
这一袭蓝衣真美:
更多图片就不贴了,大家在文章末尾下源代码自己拿回去玩去吧~
另外我们在Direct3D_Update函数中涉及到了将鼠标限制在某一区域之内的写法,这个其实很好实现的,我们在讲解Windows鼠标消息处理的那篇文章中用到过了。
细心的朋友们应该可以发现,这个程序运行起来还是有一点小问题的,当我们对摄像机进行旋转操作时,视图有时候会发生倾斜,如果对于飞行射击游戏,这样的倾斜是完全没问题的,但是如果是做CF,CS,鬼泣这类的第一人称的游戏的话,那就不应该发生这样的视觉倾斜了。
摄像机之所以发生倾斜是因为我们构造的摄像机的几个分量是两两之间互相垂直的。要想解决这样的问题,就需要保证每次进行摄像机的左右旋转时,是绕着垂直于世界坐标系的轴的向量,而这根轴的向量就是世界坐标系的Y轴。这是我们以后待完成的工作了。
文章最后,依旧是放出本篇文章配套源代码的下载:
本节笔记配套源代码请点击这里下载:
【浅墨DirectX提高班】配套源代码之十五下载
以上就是本节笔记的全部内容,更多精彩内容,且听下回分解。
浅墨在这里,希望喜欢游戏开发系列文章的朋友们能留下你们的评论,每次浅墨登陆博客看到大家的留言的时候都会非常开心,感觉自己正在传递一种信仰,一种精神。
文章最后,依然是【每文一语】栏目,今天的句子是:
我们所有的梦想都可以成真,只要我们有勇气去追求它们。
All our dreams can come true, if we have the courageto pursue them.
————沃尔特·迪斯尼
下周一,让我们离游戏开发的梦想更近一步。
下周一,游戏开发笔记,我们,不见不散。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。