第21章 翱翔于三维空间——游戏摄像机的构建

来源:互联网 发布:linux配置两个tomcat 编辑:程序博客网 时间:2024/05/16 10:14
章节导读

本章我们以核心思想为突破口,从原理介绍到一个C++类的写法, 一步一步带领大家实现了一个第一人称三维摄像机的C++类。然后在这个摄像机类的帮助下,放出了一个几乎贯穿了我们之前学到的所有DirectX 相关知识的“三维场景漫游”示例程序,算是对我们之前学的固定功能流水线这套渲染体系的总结。这个“三维场景漫游”示例程序的代码量有一千行,包括Direct3D 初始化、Directlnput 输入处理、顶点缓存、光照与材质、文字输出、颜色、纹理贴图、四大变换、网格模型、X 文件载入等等知识(当然还有默认被开启的深度缓存〉。

21.1 对摄像机的一些概述

回想本章之前的示例程序, 都是通过封装的Directlnput 类来处理键盘和鼠标的输入,对应地改变我们人物模型的世界矩阵来达到移动物体, 改变观察点的效果。其实我们的观察方向乃至观察点都是没有变的,变的只是我们3D人物的位置。举个例子吧,之前的示例程序就像是在小区里面安放着的摄像头,位置和观察点都是固定的,变的只是来来往往的人群。
之前的示例程序, 和三维观察相关的四大矩阵变换都封装在了一个叫Matrix_Set()的函数中,而Matrix_Set()通常只用在初始化资源的时候调用一次而己。其中,四大变换中的取景变换,投影变换和视口变换都是一直老老实实地在Matrix_Set()中尽职尽责地呆着,当然,除了世界矩阵变换之外。世界矩阵由于实时处理的需要,自从第一次介绍完四大变换的时候让它在Matrix_Set() 中呆了一次, 在此之后的示例程序中,我们都是把世界矩阵的设置放在Direct3D_Update()以及Direct3D_Render()之中的。因为我们必须实时地处理输入,让人物模型的世界矩阵能根据我们的输入进行相应的调整,这样才能做出人物模型随着键盘和鼠标的输入,随心所欲地受我们控制的效果出来。
但是我们之前实现的那种所谓的“视角改变”还是来得不彻底不爽快,说白了就是用D3DXMatrixLookAtLH 在资源初始化时固定住视角, 在程序运行过程中接收到消息并改变三维人物模型的世界矩阵而已。本章的讲解要点就是手把手地教大家创建出一个可以在三维空间中自由移动的摄像机类, 这样利用这个摄像机类写出来的示例程序就像是我们身临其境地在三维空间中真正地自由朝翔。
我们把这个摄像机类取名为CameraClass, 这个类的功能非常适合用于三维飞行模拟类游戏(比如王牌空战)以及第一人称视角游戏( 比如反恐精英, 穿越火线)中。

21.2 开始设计摄像机类

首先,给大家介绍一下这个摄像机类的核心思想, 那就是用4个分量: 右分量( rightvector ) 、上分量( up vector )、

观察分量( lookvector )和位置分量( position vector ),来确定一个摄像机相对于世界坐标系的位置和朝向。并根据这4 个分量计算出一个取景变换矩阵,完全取代之前的示例程序用D3DXMatrixLookAtLH 创建的取景变换矩阵。
在世界坐标系中,这几个分量都是通过向量表示的, 并且实际上它们为摄像机定义了一个局部坐标系。
其中,摄像机的右分量、上分量和观察分量定义了摄像机在世界坐标系中的朝向,因此它们也被称为方向向量。在通常的情况下,方向向量都是单位向量(模为1),并且两两之间相互垂直,也就是我们常说的标准正交。
其实, 这3个向量我们完全可以理解为三维坐标系中的X , Y, Z 轴。

另外,我们需要了解标准正交矩阵的一个重要性质,那就是标准正交矩阵的逆矩阵与其转置矩阵相等。


用上面提到的右分量( right vector )、上分量( up vector )、观察分量( look vector )和位置分量( position vector )这4 个向量来描述摄像机的话,其中的位置分量其实我们可以把它看做一个描述位置的点,那么有用的就还有3 个分量, 每个分量我们可以进行沿着其平移和绕着其旋转两种操作,那么我们可以想到的方式就是2 × 3=6 种,就是以下这6 种运动方式:

  • 沿着右分量平移
  • 沿着上分量平移
  • 沿着观察分量平移
  • 绕着右分量旋转
  • 绕着上分量旋转
  • 绕着观察分量旋转

另外,新创建的这个摄像机类在我们的示例程序中完全“上位”了,取代了之前我们在Matrix_Set()中实现的取景变换和投影变换,所以在我们新的程序中, Matrix_Set()函数将不复存在,单单用一个摄像机类就行了。
根据上面的讲解,我们勾勒出这个CameraClass 类的轮廓如下:

//=============================================================================// Name: CameraClass.h//Des: 一个封装了实现虚拟摄像机的类的头文件//=============================================================================#pragma once#include <d3d9.h>#include <d3dx9.h>class CameraClass{private://成员变量的声明    D3DXVECTOR3m_vRightVector;        // 右分量向量    D3DXVECTOR3m_vUpVector;           // 上分量向量    D3DXVECTOR3m_vLookVector;         // 观察方向向量    D3DXVECTOR3m_vCameraPosition;        // 摄像机位置的向量D3DXVECTOR3m_vTargetPosition;        //目标观察位置的向量    D3DXMATRIXm_matView;          // 取景变换矩阵    D3DXMATRIXm_matProj;          // 投影变换矩阵       LPDIRECT3DDEVICE9m_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);  //析构函数};

21.3 关于向量计算的函数讲解

下面我们首先介绍一下Direct3D 中,与向量计算有关的这6 个函数,在完成Camera Class 类的过程中有用到。
1. D3DXVec3Normalize 函数
首先我们来介绍对向量进行规范化的D3DXVec3Normalize函数,在MSDN 中我们查到它原型如下:

 D3DXVECTOR3 * D3DXVec3Normalize(  __inout  D3DXVECTOR3 *pOut,  __in     const D3DXVECTOR3 *pV);
这个函数的第一个参数为输出的结果,在第二个参数中填想要被规范化的向量就行了。一般我们把这两个参数填一模一样的,就表示把填的这个向量规范化后的结果替代原来的向量。举个例子就是这样写:

 D3DXVec3Normalize(&m_vLookVector, &m_vLookVector);  //规范化观察分量
2. 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);    // 右向量与上向量垂直
3 . D3DXVec3Dot 函数
下面我们来讲一下用于计算向量点乘的D3DXVec3Dot 函数,在MSDN 中我查到它的原型如下:
 FLOAT  D3DXVec3Dot(  __in  const D3DXVECTOR3 *pV1,  __in  const D3DXVECTOR3 *pV2);
这个函数倒不是和我们上面刚讲的那两个函数一样有个用于存放结果的pOut,它的结果就存放在返回值中,而两个参数就填参与运算的两个向量。举一个实例吧:
 pMatrix->_42 = -D3DXVec3Dot(&m_vUpVector, &m_vCameraPosition);       // -P*U
4. D3DXMatrixRotationAxis 函数
接下来介绍可以创建一个绕任意轴旋转一定角度的矩阵的D3DXMatrixRotationAxis 函数。其函数原型如下:

 D3DXMATRIX * D3DXMatrixRotationAxis(  __inout  D3DXMATRIX *pOut,  __in     const D3DXVECTOR3 *pV,  __in     FLOAT Angle);
第一个参数显然就填生成好的矩阵了,第二个参数填要绕着旋转的那根轴,第三个参数就填上要绕指定的轴旋转的角度。
依然是一个实例:

 D3DXMatrixRotationAxis(&R, &m_vRightVector, fAngle);//创建出绕m_vRightVector旋转fAngle个角度的R矩阵
5. D3DXVec3TransformCoord 函数
下面我们看一个D3DXVec3TransformCoord 函数,它可以根据给的矩阵来变换一个向量,并且把变换后的向量规范化后输出来。这个函数原型如下:

 D3DXVECTOR3 * D3DXVec3TransformCoord(  __inout  D3DXVECTOR3 *pOut,  __in     const D3DXVECTOR3 *pV,  __in     const D3DXMATRIX *pM);
第一个参数就是得到的结果向量了。第二个参数填要被变换的那个向量,而第三个参数填用于变换的矩阵。
依然是一个实例:

 D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vRightVector旋转fAngle个角度
6. D3DXVec3Length 函数
最后我们介绍计算一个三维向量长度的D3DXVec3Length 函数,这个函数原型如下:
 FLOAT  D3DXVec3Length(  __in  const D3DXVECTOR3 *pV);
唯一的一个参数填要计算长度的那个向量, 返回值就是计算出的给定向量的三维长度。
依然是一个实例:

float length = D3DXVec3Length (&m_vCameraPosition);

21.4 计算取景变换矩阵

看完整个CameraClass 类的轮廓, 本节开始讲解其中各个函数的实现代码应该怎么编写。
首先就是我们最关心的取代了之前用D3DXMatrixLookAtLH无脑计算出取景变换矩阵,而自立门户的CalculateViewMatrix 函数的写法。为了讲解方便, 我们令:
向量 p= (px, py, pz) 表示位置向量:
向量 r = (rx, ry, rz) 表示右向量:
向量 u= (ux,uy,uz )表示上向量:
向量 l = (lx, ly, lz )表示观察向量。
我们知道,取景变换所解决的其实就是世界坐标系中的物体在以摄像机为中心的坐标系中如何来表示的问题。这就是说, 需要将世界坐标系中的物体随着摄像机一起进行变换, 这样摄像机的坐标系就与世界坐标系完全重合了,如下图所示。



上面的(a)图到( b )图, 是一个平移的过程,而(b ) 图到( c ) 图则是一个旋转的过程。另外需要注意的一点是, 空间中的物体也应该随着摄像机一同进行变换,这样摄像机中看到景物才没有变化。
我们的目的, 就是通过一系列的矩阵变换,得到最终的取景变换矩阵V。
我们要得到取景变换矩阵V, 说白了就是能够满足如下的条件:

• pV= ( 0, 0 , 0 ) , 矩阵V 将摄像机移动到世界坐标系的原点.
• rV= ( 1 , 0 , 0 ),矩阵V 将摄像机的右向量与世界坐标系的x 轴重合。
• uV= ( 0 , 1 , 0 ) , 矩阵V 将摄像机的上向量与世界坐标系的y 轴重合。
• l V= ( 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 . 旋转
想要让摄像机的各分量与世界坐标系的各轴重合的话,即要将right 、up 和look 向量分别与x,y 以及z 轴对齐, 这个矩阵将满足如下三个等式:

实现起来,求出如下的一个3 × 3 的矩阵A 就可以了。


这里的矩阵A 有很多种解法, 最科学的解法如下。
我们一眼就可以看出来A 其实就是B 矩阵的逆矩阵。而矩阵B 刚好是个正交矩阵,我们文章前面就跟大家提到过要用到正交矩阵的这个性质: 正交矩阵的逆矩阵等于其转置矩阵。用到我们这里, 就是B 矩阵的逆矩阵等于B 矩阵的转置矩阵,而B 矩阵的逆矩阵就是A 矩阵,那么就是说A矩阵等于B 矩阵的转置矩阵。
那么我们求一下B 矩阵的转置矩阵, 就求出A 矩阵了。即,


3 . 综合前两步
我们需要把A 矩阵扩展成4 × 4 的,然后计算一下两矩阵的值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 , 那么第二行第三列就是这样写了。
其他行其他列就以此类推就可以了,注意一共要写4 × 4= 16 个值。

21.5 类的其余实现细节

因为这个类我们基本上把代码都逐行注释了,所以类中其他的函数的实现方法大家直接看代码就行了。另外在这个类中视口变换并没有实现,其实很多时候不用去设置视口的, Direct3D 会为我们默认设置好了,不去设置也无伤大雅的。
CameraClass.cpp 的所有代码如下(CameraClass.h 头文件在前面已经贴出来过了〉:

//=============================================================================// Name: CameraClass.cpp//Des: 一个封装了实现虚拟摄像机的类的源文件//=============================================================================#include "CameraClass.h"#ifndef WINDOW_WIDTH#define WINDOW_WIDTH800//为窗口宽度定义的宏,以方便在此处修改窗口宽度#define WINDOW_HEIGHT600//为窗口高度定义的宏,以方便在此处修改窗口高度#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)WINDOW_WIDTH/WINDOW_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){}
这个类的使用方法,我们一般就是在给绘制做准备的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();  //设置投影变换矩阵

21.6 示例程序D3Ddemo16

首先是选择天蓝色的清屏颜色,这样我们的“世界”看起来就像是在蓝天晴空的背景下了。然后是选择翠绿色的可以无缝衔接的草坪纹理,用之前学的纹理映射和顶点缓存的知识把纹理重复铺上个几百张(具体张数是5 0 × 5 0=250 张),这样就是一大片绿油油的草坪了。接着就是我们之前刚讲的载入X 文件模型。再接着,我们还用D3DXCreateCylinder 函数创建了一根柱子(后面用for 循环绘制了12 根一模一样的柱子) ,最后就是设置光照,设置渲染状态什么的。

先是用的纹理素材,尺寸是5 12 × 5 12 ,如下图所示。


本节的示例源代码包含了6 个文件, 主要用于公共辅助宏定义的D3DUtil.h , 用于封装了Directlnput 输入控制API的DirectlnputClass.h 和DirectlnputClass.cpp ,以及封装了虚拟摄像机类的CameraClass.h 和CameraClass.cpp ,当然还有核心代码main.cpp 。
DirectlnputClass.h 和Directl叩utClass.cpp 较之前的示例程序没有任何修改,不再贴出,
CameraClass.cpp 和CameraClass.h 在上面讲解的过程中已经贴出来了,这里也不贴出。我们主要看一下main.cpp 中比较核心的几个函数:
首先是Object_Init() 函数,通过这个函数我们可以看到到底载入了哪些游戏资源:

//-----------------------------------【Object_Init( )函数】--------------------------------------//描述:渲染资源初始化函数,在此函数中进行要被渲染的物体的资源的初始化//--------------------------------------------------------------------------------------------------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;}
Direct3D_Render()函数的实现代码:

//-----------------------------------【Direct3D_Render( )函数】-------------------------------//描述:使用Direct3D进行渲染//--------------------------------------------------------------------------------------------------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);  // 翻转与显示 }
本节的示例程序相对来说比较综合,里面融合了Direct3D 的初始化、Directlnput 输入处理、顶点缓存、文字输出、颜色、光照与材质、纹理贴图、四大变换、网格模型、X 文件载入等等知识(当然还有默认被开启的深度缓存〉。
表面上看涉及的内容很多, 但是不难理解,就是在HRESULT Objects_lnit()函数中把要绘制的物体都准备好,也把光照、材质、摄像机都初始化好,然后在Direct3D_Render( HWND hwnd): 函数中绘制出来就可以了。说白了也就是相对于之前的demo 多绘制了一点东西而己,没什么特别的。
摄像机类的使用,就是在Objects_lnit()顺带着中初始化一下,然后在Direct3D_Update(HWNDhwnd)函数中配合着Directlnput 类对输出进行处理就好了。
另外需要注意的是,我们把文字帮助信息和FPS 的输出都封装在了一个叫HelpText_Render的函数中了。因为绘制的东西越来越多,为了Direct3D_Render 看起来整洁一点,而不是里面一大串内容,是时候把文字帮助信息封装起来了。

//-----------------------------------【HelpText_Render( )函数】-------------------------------//描述:封装了帮助信息的函数//--------------------------------------------------------------------------------------------------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"    R:垂直向上飞翔     F:垂直向下飞翔", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255));formatRect.top += 25;g_pTextHelper->DrawText(NULL, L"    Q:向左倾斜       E:向右倾斜", -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));}
程序运行后截图如下所示,我们开始在键盘上W 、A 、S 、D 、I、J 、K 、L、↑、↓、←、→ 12 个键上随便按;

    

另外我们在Direct3D_ Update 函数中涉及到了将鼠标限制在某一区域之内的写法,这个其实很好实现的,我们在讲解Windows 鼠标消息处理时用到过了。细心的朋友应该可以发现,这个程序运行起来还是有一点小问题的,当我们对摄像机进行旋转操作时,视图有时候会发生倾斜,如果对于飞行射击游戏,这样的倾斜是完全没问题的,但是如果是做CF 、cs 、鬼泣这类的第一人称的游戏的话,那就不应该发生这样的视觉倾斜了。
摄像机之所以发生倾斜是因为我们构造的摄像机的几个分量是两两之间互相垂直的。要想解决这样的问题,就需要保证每次进行摄像机的左右旋转时,是绕着垂直于世界坐标系的轴向量的,而这根轴的向量就是世界坐标系的Y 轴。

21.7 章节小憩

看到了本章示例的运行效果,书本前的你有没有很兴奋呢?这次,我们用自己亲手敲出来的C++代码创造出了一个近乎真实的三维场景,可以控制视角在这个场景中自由朝翔。




原创粉丝点击