《逐梦旅程 WINDOWS游戏编程之从零开始》笔记9——游戏摄像机&三维地形的构建

第21章 游戏摄像机的构建

之前的程序示例,都是通过封装的DirectInput类来处理键盘和鼠标的输入,对应地改变我们人物模型的世界矩阵来达到移动物体,改变观察点的效果。其实我们的观察方向乃至观察点都是没有变的,变的只是我们3D人物的位置。说白了就是用D3DXMatrixLookAtLH在资源初始化时固定住视角,在程序运行过程中接收到消息并改变三维人物模型的世界矩阵而已。这章的主要内容就是创建出一个可以在三维空间中自由移动的摄像机类,我们准备给这个摄像机类取名为CameraClass。

设计摄像机类

摄像机类的核心思想,那就是用四个分量:右分量(rightvector)、上分量(up vector)、观察分量(lookvector)和位置分量(position vector),来确定一个摄像机相对于世界坐标系的位置和朝向。并根据这四个分量计算出一个取景变换矩阵,完全取代之前的示例程序用D3DXMatrixLookAtLH创建的取景变换矩阵。

在世界坐标系中,这几个分量都是通过向量表示的,并且实际上他们为摄像机定义了一个局部坐标系。

其中,摄像机的左分量、上分量和观察分量定义了摄像机在世界坐标系中的朝向,因此他们也被称为方向向量。在通常的情况下,方向向量都是单位向量(模为1),并且两两之间相互垂直,也就是我们常说的标准正交。

其实,这三个向量我们完全可以理解为三维坐标系中的X,Y,Z轴。

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

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

● 沿着右分量平移

● 沿着上分量平移

● 沿着观察分量平移

● 绕着右分量旋转

● 绕着上分量旋转

● 绕着观察分量旋转

根据以上勾勒出这个CameraClass类的轮廓如下:

#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);  //析构函数  

};  

关于向量计算的6个函数

因为我们的摄像机类要用到这6个函数,所以在这里要说明一下。

1. D3DXVec3Normalize函数

对向量进行规范化的D3DXVec3Normalize函数:

D3DXVECTOR3* D3DXVec3Normalize(
 _Inout_  D3DXVECTOR3 *pOut,
 _In_     const D3DXVECTOR3 *pV
);

这个函数的第一个参数为输出的结果,在第二个参数中填想要被规范化的向量就行了,一般我们把这两个参数填一摸一样的,就表示把填的这个向量规范化后的结果替代原来的向量。

实例:

//其中的m_vLookVector为向量
D3DXVec3Normalize(&m_vLookVector, &m_vLookVector);//规范化m_vLookVector向量 

2. D3DXVec3Cross函数

用于计算两个向量叉乘结果的D3DXVec3Cross函数:

D3DXVECTOR3* D3DXVec3Cross(
 _Inout_  D3DXVECTOR3 *pOut,
 _In_     const D3DXVECTOR3 *pV1,
 _In_     const D3DXVECTOR3 *pV2
); 

第一个参数依然是计算的结果。第二和第三两个参数当然就是填参加叉乘运算的两个向量了。

实例:

D3DXVec3Cross(&m_vRightVector, &m_vUpVector,&m_vLookVector);    // 右向量与上向量垂直  

3. D3DXVec3Dot函数

用于计算向量点乘的D3DXVec3Dot函数

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函数

以根据给定的矩阵来变换一个向量,并且把变换后的向量规范化后输出来:

D3DXVECTOR3* D3DXVec3TransformCoord(
 _Inout_  D3DXVECTOR3 *pOut,
 _In_     const D3DXVECTOR3 *pV,
 _In_     const D3DXMATRIX *pM
); 

第一个参数就是得到的结果向量了。第二个参数填要被变换的那个向量,而第三个参数填用于变换的矩阵。

实例:

D3DXVec3TransformCoord(&m_vUpVector, &m_vCameraPosition, &R);//让m_vCameraPosition向量绕m_vRightVector旋转fAngle个角度  

6. D3DXVec3Length函数

计算一个三维向量长度的D3DXVec3Length函数:

FLOAT D3DXVec3Length(
 _In_  const D3DXVECTOR3 *pV
);

唯一的一个参数填要计算长度的那个向量,返回值就是计算出的给定向量的三维长度。

实例:

float length=D3DXVec3Length(&m_vCameraPosition);

计算取景变换矩阵

看完整个CameraClass类的轮廓,下面就开始实现其中的各个函数。首先是计算取景变换矩阵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是:

可是这里我很纳闷,这里一个3维向量怎么可以以和4*4的矩阵相乘呢?怎就完成了相应的变换了呢?现在还不是很懂,以后来琢磨琢磨,暂且先记住结论。

下面我们实现计算取景变换矩阵的CalculateViewMatrix函数中,其实也就是用了一下最后我们求出的V矩阵的结果而已:

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;
}  

其中的pMatrix->_23表示pMatrix矩阵的第二行,第三行的元素,我们在计算出的取景变换矩阵V的矩阵结果中找到第二行第三列,它的值为ly,也就是上向量m_vLookVector的y坐标值,即m_vLookVector.y,那么第二行第三列就是这样写了。

其他行其他列就以此类推了,注意的是一共要写4x4=16个值。

摄像机类的其余细节

下面是这个摄像机类的代码,另外在这个类中视口变换并没有去实现,其实很多时候不用去设置视口的Direct3D就为我们默认好了,不去设置也没关系。

  1 #include "CameraClass.h"
  2
  3 #ifndef SCREEN_WIDTH
  4 #define SCREEN_WIDTH    800                     //为窗口宽度定义的宏,以方便在此处修改窗口宽度
  5 #define SCREEN_HEIGHT   600                 //为窗口高度定义的宏,以方便在此处修改窗口高度
  6 #endif
  7
  8 //-----------------------------------------------------------------------------
  9 // Desc: 构造函数
 10 //-----------------------------------------------------------------------------
 11 CameraClass::CameraClass(IDirect3DDevice9 *pd3dDevice)
 12 {
 13     m_pd3dDevice = pd3dDevice;
 14     m_vRightVector  = D3DXVECTOR3(1.0f, 0.0f, 0.0f);   // 默认右向量与X正半轴重合
 15     m_vUpVector     = D3DXVECTOR3(0.0f, 1.0f, 0.0f);   // 默认上向量与Y正半轴重合
 16     m_vLookVector   = D3DXVECTOR3(0.0f, 0.0f, 1.0f);   // 默认观察向量与Z正半轴重合
 17     m_vCameraPosition  = D3DXVECTOR3(0.0f, 0.0f, -250.0f);   // 默认摄像机坐标为(0.0f, 0.0f, -250.0f)
 18     m_vTargetPosition    = D3DXVECTOR3(0.0f, 0.0f, 0.0f);//默认观察目标位置为(0.0f, 0.0f, 0.0f);
 19
 20 }
 21
 22 // Desc: 根据给定的矩阵计算出取景变换矩阵
 23 //-----------------------------------------------------------------------------
 24 VOID CameraClass::CalculateViewMatrix(D3DXMATRIX *pMatrix)
 25 {
 26     //1.先把3个向量都规范化并使其相互垂直,成为一组正交矩阵
 27     D3DXVec3Normalize(&m_vLookVector, &m_vLookVector);  //规范化观察分量
 28     D3DXVec3Cross(&m_vUpVector, &m_vLookVector, &m_vRightVector);    // 上向量与观察向量垂直
 29     D3DXVec3Normalize(&m_vUpVector, &m_vUpVector);                // 规范化上向量
 30     D3DXVec3Cross(&m_vRightVector, &m_vUpVector, &m_vLookVector);    // 右向量与上向量垂直
 31     D3DXVec3Normalize(&m_vRightVector, &m_vRightVector);          // 规范化右向量
 32
 33
 34     // 2.创建出取景变换矩阵
 35     //依次写出取景变换矩阵的第一行
 36     pMatrix->_11 = m_vRightVector.x;           // Rx
 37     pMatrix->_12 = m_vUpVector.x;              // Ux
 38     pMatrix->_13 = m_vLookVector.x;            // Lx
 39     pMatrix->_14 = 0.0f;
 40     //依次写出取景变换矩阵的第二行
 41     pMatrix->_21 = m_vRightVector.y;           // Ry
 42     pMatrix->_22 = m_vUpVector.y;              // Uy
 43     pMatrix->_23 = m_vLookVector.y;            // Ly
 44     pMatrix->_24 = 0.0f;
 45     //依次写出取景变换矩阵的第三行
 46     pMatrix->_31 = m_vRightVector.z;           // Rz
 47     pMatrix->_32 = m_vUpVector.z;              // Uz
 48     pMatrix->_33 = m_vLookVector.z;            // Lz
 49     pMatrix->_34 = 0.0f;
 50     //依次写出取景变换矩阵的第四行
 51     pMatrix->_41 = -D3DXVec3Dot(&m_vRightVector, &m_vCameraPosition);    // -P*R
 52     pMatrix->_42 = -D3DXVec3Dot(&m_vUpVector, &m_vCameraPosition);       // -P*U
 53     pMatrix->_43 = -D3DXVec3Dot(&m_vLookVector, &m_vCameraPosition);     // -P*L
 54     pMatrix->_44 = 1.0f;
 55 }
 56
 57 VOID CameraClass::SetTargetPosition(D3DXVECTOR3 *pLookat)
 58 {
 59     //先看看pLookat是否为默认值NULL
 60     if (pLookat != NULL)  m_vTargetPosition = (*pLookat);
 61     else m_vTargetPosition = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
 62
 63     m_vLookVector = m_vTargetPosition - m_vCameraPosition;//观察点位置减摄像机位置,得到观察方向向量
 64     D3DXVec3Normalize(&m_vLookVector, &m_vLookVector);//规范化m_vLookVector向量
 65
 66     //正交并规范化m_vUpVector和m_vRightVector
 67     D3DXVec3Cross(&m_vUpVector, &m_vLookVector, &m_vRightVector);
 68     D3DXVec3Normalize(&m_vUpVector, &m_vUpVector);
 69     D3DXVec3Cross(&m_vRightVector, &m_vUpVector, &m_vLookVector);
 70     D3DXVec3Normalize(&m_vRightVector, &m_vRightVector);
 71 }
 72
 73 // Name:CameraClass::SetCameraPosition( )
 74 // Desc: 设置摄像机所在的位置
 75 //-----------------------------------------------------------------------------
 76 VOID CameraClass::SetCameraPosition(D3DXVECTOR3 *pVector)
 77 {
 78     D3DXVECTOR3 V = D3DXVECTOR3(0.0f, 0.0f, -250.0f);
 79     m_vCameraPosition = pVector ? (*pVector) : V;//三目运算符,如果pVector为真的话,返回*pVector的值(即m_vCameraPosition=*pVector),否则返回V的值(即m_vCameraPosition=V)
 80 }
 81
 82 // Desc: 设置取景变换矩阵
 83 //-----------------------------------------------------------------------------
 84 VOID CameraClass::SetViewMatrix(D3DXMATRIX *pMatrix)
 85 {
 86     //根据pMatrix的值先做一下判断
 87     if (pMatrix) m_matView = *pMatrix;
 88     else CalculateViewMatrix(&m_matView);
 89     m_pd3dDevice->SetTransform(D3DTS_VIEW, &m_matView);
 90     //把取景变换矩阵的值分下来分别给右分量,上分量,和观察分量
 91     m_vRightVector = D3DXVECTOR3(m_matView._11, m_matView._12, m_matView._13);
 92     m_vUpVector    = D3DXVECTOR3(m_matView._21, m_matView._22, m_matView._23);
 93     m_vLookVector  = D3DXVECTOR3(m_matView._31, m_matView._32, m_matView._33);
 94 }
 95
 96 VOID CameraClass::SetProjMatrix(D3DXMATRIX *pMatrix)
 97 {
 98     //判断值有没有,没有的话就计算一下
 99     if (pMatrix != NULL) m_matProj = *pMatrix;
100     else D3DXMatrixPerspectiveFovLH(&m_matProj, D3DX_PI / 4.0f, (float)((double)SCREEN_WIDTH/SCREEN_HEIGHT), 1.0f, 30000.0f);//视截体远景设为30000.0f,这样就不怕看不到远处的物体了
101     m_pd3dDevice->SetTransform(D3DTS_PROJECTION, &m_matProj);//设置投影变换矩阵
102 }
103
104 // Name:CameraClass::MoveAlongRightVec( )
105 // Desc: 沿右向量平移fUnits个单位
106 //-----------------------------------------------------------------------------
107 VOID CameraClass::MoveAlongRightVec(FLOAT fUnits)
108 {
109     //直接乘以fUnits的量来累加就行了
110     m_vCameraPosition += m_vRightVector * fUnits;
111     m_vTargetPosition   += m_vRightVector * fUnits;
112 }
113
114 //-----------------------------------------------------------------------------
115 // Name:CameraClass::MoveAlongUpVec( )
116 // Desc:  沿上向量平移fUnits个单位
117 //-----------------------------------------------------------------------------
118 VOID CameraClass::MoveAlongUpVec(FLOAT fUnits)
119 {
120     //直接乘以fUnits的量来累加就行了
121     m_vCameraPosition += m_vUpVector * fUnits;
122     m_vTargetPosition   += m_vUpVector * fUnits;
123 }
124
125 //-----------------------------------------------------------------------------
126 // Name:CameraClass::MoveAlongLookVec( )
127 // Desc:  沿观察向量平移fUnits个单位
128 //-----------------------------------------------------------------------------
129 VOID CameraClass::MoveAlongLookVec(FLOAT fUnits)
130 {
131     //直接乘以fUnits的量来累加就行了
132     m_vCameraPosition += m_vLookVector * fUnits;
133     m_vTargetPosition   += m_vLookVector * fUnits;
134 }
135
136 //-----------------------------------------------------------------------------
137 // Name:CameraClass::RotationRightVec( )
138 // Desc:  沿右向量旋转fAngle个弧度单位的角度
139 //-----------------------------------------------------------------------------
140 VOID CameraClass::RotationRightVec(FLOAT fAngle)
141 {
142     D3DXMATRIX R;
143     D3DXMatrixRotationAxis(&R, &m_vRightVector, fAngle);//创建出绕m_vRightVector旋转fAngle个角度的R矩阵
144     D3DXVec3TransformCoord(&m_vUpVector, &m_vUpVector, &R);//让m_vUpVector向量绕m_vRightVector旋转fAngle个角度
145     D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vRightVector旋转fAngle个角度
146
147     m_vTargetPosition = m_vLookVector * D3DXVec3Length(&m_vCameraPosition);//更新一下观察点的新位置(方向乘以模=向量)
148 }
149
150 //-----------------------------------------------------------------------------
151 // Name:CameraClass::RotationUpVec( )
152 // Desc:  沿上向量旋转fAngle个弧度单位的角度
153 //-----------------------------------------------------------------------------
154 VOID CameraClass::RotationUpVec(FLOAT fAngle)
155 {
156     D3DXMATRIX R;
157     D3DXMatrixRotationAxis(&R, &m_vUpVector, fAngle);//创建出绕m_vUpVector旋转fAngle个角度的R矩阵
158     D3DXVec3TransformCoord(&m_vRightVector, &m_vRightVector, &R);//让m_vRightVector向量绕m_vUpVector旋转fAngle个角度
159     D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vUpVector旋转fAngle个角度
160
161     m_vTargetPosition = m_vLookVector * D3DXVec3Length(&m_vCameraPosition);//更新一下观察点的新位置(方向乘以模=向量)
162 }
163
164 //-----------------------------------------------------------------------------
165 // Name:CameraClass::RotationLookVec( )
166 // Desc:  沿观察向量旋转fAngle个弧度单位的角度
167 //-----------------------------------------------------------------------------
168 VOID CameraClass::RotationLookVec(FLOAT fAngle)
169 {
170     D3DXMATRIX R;
171     D3DXMatrixRotationAxis(&R, &m_vLookVector, fAngle);//创建出绕m_vLookVector旋转fAngle个角度的R矩阵
172     D3DXVec3TransformCoord(&m_vRightVector, &m_vRightVector, &R);//让m_vRightVector向量绕m_vLookVector旋转fAngle个角度
173     D3DXVec3TransformCoord(&m_vUpVector, &m_vUpVector, &R);//让m_vUpVector向量绕m_vLookVector旋转fAngle个角度
174
175     m_vTargetPosition = m_vLookVector * D3DXVec3Length(&m_vCameraPosition);//更新一下观察点的新位置(方向乘以模=向量)
176 }
177
178
179 //-----------------------------------------------------------------------------
180 // Desc: 析构函数
181 //-----------------------------------------------------------------------------
182 CameraClass::~CameraClass(void)
183 {
184 }  

其中要注意的是如何计算出观察方向向量的,观察点位置减摄像机位置得出观察方向向量,再正交规范化右向量m_vRightVector和上向量m_vUpVector

使用这个类的话,一般就是在给绘制做准备的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();  //设置投影变换矩阵  

示例程序见原博客:【Visual C++】游戏开发笔记四十七 浅墨DirectX教程十五 翱翔于三维世界:摄像机的实现



第22章 三维地形的构建

绘制思路

首先来看三幅图:

以上的三幅图就概括了三维地形模拟的大体走向与思路。

首先是第一幅图,我们在图中可以看到,图中描绘的就是在同一平面上的三角形网格组成的一个大的矩形区域。在这里我们把他看做是一张大的均匀的同一平面上的“渔网”,显然它是一个二维的平面。图中的每一个顶点都可以用一个二维的坐标(x,y)来唯一表示。

然后第二幅图,我们就像“揠苗助长”一样,拉着第一幅图中的“渔网”的某些顶点往上提(或者往下压)。这里往上提一点,那里提一点,这样,我们就为每一个顶点都赋予了一个高度(就算有的顶点没有移动,它的高度就为0),第一幅图中的渔网就变形了,成了三维图形了。每个顶点就都有了一个高度值。用z坐标来表示这个高度值的话,那么现在三维空间中这个变形的“渔网”中的每个顶点都可以用(x,y,z)来唯一表示。

最后第三幅图,在第二幅图中的三维“渔网”的表面我们“镀上”纹理不尽相同的“薄膜”,也就是进行了一个纹理包装的过程。

第二幅图中的那个“揠苗助长”常常是借助高度图来完成。

高度图

高度图说白了其实就是一组连续的数组,这个数组中的元素与地形网格中的顶点一一对应,且每一个元素都指定了地形网格的某个顶点的高度值。当然,高度图至少还有一种实现方案,就是用数值中的每一个元素来指定每个三角形栅格的高度值,而不是顶点的高度值。

高度图有多种可能的图形表示,其中最常用的一种是灰度图(grayscale map)。地形中某一点的海拔越高的话,相应地该点对应的灰度图中的亮度就越大。下面就是一幅灰度图:

我们通常只为每一个元素分配一个字节的存储空间,这样高度也就只能在0~255之间取值。因此,地形中最低点将用0表示,而最高点使用255表示。

这个范围大体上来反应地形中的高度变化完全没问题,但是在实际运用中,为了匹配3D世界的尺寸,可能需要对高度值进行比例变换,然而一进行比例变换,往往就可能超出上面的0~255这个区间。所以我们把高度数据加载到应用程序中时,我们重新分配一个整型或者浮点型的数组来存储这些高度值,这样我们就不必拘泥于0~255这个范围,这样就可以随心所欲地构建出我们心仪的三维世界了。

对于灰度图中的每个像素来说,同样使用0~~255之间的值来表示一个灰度。这样,我们就能把不同的灰度映射为高度,并且用像素索引表示不同网格。

要从高度图创建一个地形,我们需要创建一个与高度图相同大小的顶点网格,并使用高度图上每个像素的高度值作为顶点的高度。例如,我们可以使用一张6×6像素分辨率的高度图生成一个6×6大小的顶点网格。

网格上的顶点不仅包含位置,还包含诸如法线和纹理坐标的信息。下图就是一个在XZ平面中的6×6大小的顶点网格,其中每个顶点的高度对应在Y坐标上。

另外我们在设计三维地形模拟系统的时候,会指定一下相邻顶点的距离(水平距离和垂直距离一样)。这个距离在上图中用“Block Scale”表示。这个距离如果取小一点的话,会使顶点间的高度过渡平滑,但是会减少网格也就是三维地形的整体大小;反之,相邻间顶点的距离取大一点的话,顶点间的过渡会变得陡峭,同时网格也就是三维地形的整体尺寸会相对来说变大。

最常用的灰度图格式是后缀名为RAW,我们在这里使用的高度图文件格式就是RAW,这个格式不包含诸如图像类型和大小信息的文件头,所以易于被读取。RAW文件只是简单的二进制文件,只包含地形的高度数据。在一个8位高度图中,每个字节都表示顶点的高度。

在程序中读取高度图

以代码为例子:

// 从文件中读取高度信息
    std::ifstream inFile;
    inFile.open(pRawFileName,std::ios::binary);   //用二进制的方式打开文件  

    inFile.seekg(0,std::ios::end);                                              //把文件指针移动到文件末尾
    std::vector<BYTE> inData(inFile.tellg());                 //用模板定义一个vector<BYTE>类型的变量inData并初始化,其值为缓冲区当前位置,即缓冲区大小  

    inFile.seekg(std::ios::beg);                                                       //将文件指针移动到文件的开头,准备读取高度信息
    inFile.read((char*)&inData[0],inData.size());    //关键的一步,读取整个高度信息
    inFile.close();                                                                                          //操作结束,可以关闭文件了  

这里用到了C++中模板以及文件流的知识,

C++中的文件流:首先是那个ifstream,这是 C++ 中另一个标准库 fstream,它定义了三个新的数据类型:

数据类型 描述
ofstream 该数据类型表示输出文件流,用于创建文件并向文件写入信息。
ifstream 该数据类型表示输入文件流,用于从文件读取信息。
fstream 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

下面是 open() 函数的标准语法,open() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。

void open(const char *filename, ios::openmode mode);

istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg("seek get")和关于 ostream 的 seekp("seek put")。

seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。

C++中的模版:模板函数定义的一般形式如下所示

template <class type> ret-type func-name(parameter list)
{
   // 函数的主体
}

如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:

template <class type> class class-name {
.
.
.
}

回归正题,于保存在raw文件中的每个灰度数据只是用一个字节存储的,那么这样所表示的地形高度只能在[0,255]之间取值。我们显然不高兴这样。所以,我们继续将读取的高度信息重新保存到一个浮点型的模板类型中,这样就能舒心地取到任何范围的高度值了。注意下面这段代码中vHeightInfo的定义是在类头文件中的:

std::vector<FLOAT>   m_vHeightInfo;               // 用于存放高度信息
…
 m_vHeightInfo.resize(inData.size());                                //将m_vHeightInfo尺寸取为缓冲区的尺寸
    //遍历整个缓冲区,将inData中的值赋给m_vHeightInfo
 for (unsigned int i=0; i<inData.size();i++)
     m_vHeightInfo[i] = inData[i];  

地形类轮廓的书写

这个类我们取名为TerrainClass,它能通过载入二进制类型的文件(以raw格式为首)来得到地形的高度信息,通过载入图片得到地形所采用的纹理。载入文件的过程我们封装在一个名为LoadTerrainFromFile的函数中。

在上文中讲高度图的概念相关知识的时候我们就提到过,需要把高度图所传达的信息转化到顶点网格中去,这样才好绘制出来。所以在类中既是重点也是难点的就是这个“转化”的过程,这个过程我们放到一个名为InitTerrain的函数中。高度图到顶点的“转化”完成后,接下来当然需要把这些顶点配合着纹理都绘制出来,绘制的过程我们放在一个名为RenderTerrain的函数中。加上构造函数和析构函数,FVF顶点格式的定义以及若干必须的成员变量,我们就可以勾勒出TerrainClass类的轮廓如下,即下面贴出来的是Terrain.h头文件的全部代码:

#pragma once  

#include <d3d9.h>
#include <d3dx9.h>
#include <vector>
#include <fstream>
#include  "D3DUtil.h"  

class TerrainClass
{
private:
    LPDIRECT3DDEVICE9       m_pd3dDevice;           //D3D设备
    LPDIRECT3DTEXTURE9      m_pTexture;         //纹理
    LPDIRECT3DINDEXBUFFER9  m_pIndexBuffer;         //顶点缓存
    LPDIRECT3DVERTEXBUFFER9 m_pVertexBuffer;        //索引缓存  

    int                         m_nCellsPerRow;     // 每行的单元格数
    int                         m_nCellsPerCol;         // 每列的单元格数
    int                         m_nVertsPerRow;     // 每行的顶点数
    int                         m_nVertsPerCol;         // 每列的顶点数
    int                         m_nNumVertices;     // 顶点总数
    FLOAT                       m_fTerrainWidth;        // 地形的宽度
    FLOAT                       m_fTerrainDepth;        // 地形的深度
    FLOAT                       m_fCellSpacing;         // 单元格的间距
    FLOAT                       m_fHeightScale;         // 高度缩放系数
    std::vector<FLOAT>   m_vHeightInfo;           // 用于存放高度信息  

    //定义一个地形的FVF顶点格式
    struct TERRAINVERTEX
    {
        FLOAT _x, _y, _z;
        FLOAT _u, _v;
        TERRAINVERTEX(FLOAT x, FLOAT y, FLOAT z, FLOAT u, FLOAT v)
            :_x(x), _y(y), _z(z), _u(u), _v(v) {}
        static const DWORD FVF = D3DFVF_XYZ | D3DFVF_TEX1;
    };  

public:
    TerrainClass(IDirect3DDevice9 *pd3dDevice); //构造函数
    virtual ~TerrainClass(void);        //析构函数  

public:
    BOOL LoadTerrainFromFile(wchar_t *pRawFileName, wchar_t *pTextureFile);     //从文件加载高度图和纹理的函数
    BOOL InitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale);  //地形初始化函数
    BOOL RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bDrawFrame=FALSE);  //地形渲染函数
};  

地形顶点的计算

在计算顶点之前,还需要做一些准备工作。在创建地形时,需要通过指定地形的行数、列数以及顶点间的距离来指定地形的大小。上面我们在给类写轮廓的时候刚贴出来过,封装着地形顶点计算的InitTerrain函数的原型是这样的:

BOOLInitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale);  //地形初始化函数 

其中前两个参数分别为地形的行数和列数,需要我们在初始化时指定。也就是说在计算地形的时候行数和列数是已知的,那么,地形在x方向和z方向上的顶点数也就明了了,也就是z方向上顶点数为地形的行数加1,而在x方向上的顶点数为地形列数加上1.

第三个fSpace为顶点间的间隔,第四个参数fScale为缩放的系数。

关于顶点的计算思路,我们通过下面这幅图:

对每行的单元格数目、每列的单元格数目、单元格间的间距、高度缩放系数、地形的宽度、地形的深度、每行的顶点数、每列的顶点数、顶点总数各个击破,就写出了下面这几句代码:

 m_nCellsPerRow  = nRows; //每行的单元格数目
 m_nCellsPerCol  = nCols; //每列的单元格数目
 m_fCellSpacing  = fSpace;    //单元格间的间距
 m_fHeightScale  = fScale; //高度缩放系数
 m_fTerrainWidth = nRows * fSpace;  //地形的宽度
 m_fTerrainDepth = nCols * fSpace;  //地形的深度
 m_nVertsPerRow  = m_nCellsPerCol + 1;  //每行的顶点数
 m_nVertsPerCol  = m_nCellsPerRow + 1; //每列的顶点数
 m_nNumVertices  = m_nVertsPerRow * m_nVertsPerCol;  //顶点总数  

另外,我们在计算地形顶点前,还需要将地形的高度值乘以一个缩放系数,以便能够调整高度的整体变化幅度,就是下面这两句代码:

// 通过一个for循环,逐个把地形原始高度乘以缩放系数,得到缩放后的高度
    for(unsigned int i=0;i<m_vHeightInfo.size(); i++)
        m_vHeightInfo[i] *= m_fHeightScale;

接着,就是顶点的正式计算:

// 处理地形的顶点
//---------------------------------------------------------------
   //1,创建顶点缓存
if(FAILED(m_pd3dDevice->CreateVertexBuffer(m_nNumVertices * sizeof(TERRAINVERTEX),
    D3DUSAGE_WRITEONLY, TERRAINVERTEX::FVF,D3DPOOL_MANAGED, &m_pVertexBuffer, 0)))
    return FALSE;
   //2,加锁
TERRAINVERTEX *pVertices = NULL;
m_pVertexBuffer->Lock(0, 0,(void**)&pVertices, 0);
   //3,访问,赋值
FLOAT fStartX = -m_fTerrainWidth / 2.0f,fEndX =  m_fTerrainWidth / 2.0f;          //指定起始点和结束点的X坐标值
FLOAT fStartZ =  m_fTerrainDepth / 2.0f, fEndZ =-m_fTerrainDepth / 2.0f;  //指定起始点和结束点的Z坐标值
FLOAT fCoordU = 3.0f /(FLOAT)m_nCellsPerRow;     //指定纹理的横坐标值
FLOAT fCoordV = 3.0f /(FLOAT)m_nCellsPerCol;           //指定纹理的纵坐标值  

int nIndex = 0, i = 0, j = 0;
for (float z = fStartZ; z > fEndZ; z -=m_fCellSpacing, i++)          //Z坐标方向上起始顶点到结束顶点行间的遍历
{
    j = 0;
    for (float x = fStartX; x < fEndX; x+= m_fCellSpacing, j++)  //X坐标方向上起始顶点到结束顶点行间的遍历
    {
        nIndex = i * m_nCellsPerRow + j;         //指定当前顶点在顶点缓存中的位置
        pVertices[nIndex] =TERRAINVERTEX(x, m_vHeightInfo[nIndex], z, j*fCoordU, i*fCoordV); //把顶点位置索引在高度图中对应的各个顶点参数以及纹理坐标赋值给赋给当前的顶点
        nIndex++;                                                                          //索引数自加1
    }
}
   //4,解锁
m_pVertexBuffer->Unlock();  

顶点值算完了,当然还需要接着计算顶点的索引。顶点索引的计算关键是推导出一个用于求构成第i行,第j列的顶点处右下方两个三角形的顶点索引的通用公式

对顶点缓存中的任意一点A,如果该点位于地形中的第i行、第j列的话,那么该点在顶点缓存中所对应的位置应该就是i*m+j(m为每行的顶点数)。如果A点在索引缓存中的位置为k的话,那么A点为起始点构成的三角形ABC中,B、C顶点在顶点缓存中的位置就为(i+1)x m+j和i x m+(j+1)。且B点索引值为k+1,C点索引值为k+2.这样。这样,公式就可以推导为如下:

三角形ABC=【i*每行顶点数+j,i*每行顶点数+(j+1),(i+1)*行顶点数+j】

三角形CBD=【(i+1)*每行顶点数+j,i*每行顶点数+(j+1),(i+1)*行顶点数+(j+1)】

通过上面我们推导出的这个公式,就可以写出下面计算索引缓存的相关代码:

// 处理地形的索引
//---------------------------------------------------------------
    //1.创建索引缓存
    if (FAILED(m_pd3dDevice->CreateIndexBuffer(m_nNumVertices * 6 *sizeof(WORD),
        D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIndexBuffer, 0)))
        return FALSE;
    //2.加锁
    WORD* pIndices = NULL;
    m_pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0);
    //3.访问,赋值
    nIndex = 0;
    for(int row = 0; row < m_nCellsPerRow-1; row++)   //遍历每行
    {
        for(int col = 0; col < m_nCellsPerCol-1; col++)  //遍历每列
        {
            //三角形ABC的三个顶点
            pIndices[nIndex]   =  row * m_nCellsPerRow + col;           //顶点A
            pIndices[nIndex+1] =  row * m_nCellsPerRow + col + 1;  //顶点B
            pIndices[nIndex+2] = (row+1) * m_nCellsPerRow + col;    //顶点C
            //三角形CBD的三个顶点
            pIndices[nIndex+3] = (row+1) * m_nCellsPerRow + col;        //顶点C
            pIndices[nIndex+4] =  row * m_nCellsPerRow + col + 1;       //顶点B
            pIndices[nIndex+5] = (row+1) * m_nCellsPerRow + col + 1;//顶点D
            //处理完一个单元格,索引加上6
            nIndex += 6;  //索引自加6
        }
    }
    //4、解锁
    m_pIndexBuffer->Unlock();

渲染出地形

地形的渲染封装在了一个名为RenderTerrain的函数中:

BOOLTerrainClass::RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bRenderFrame)
{
    m_pd3dDevice->SetStreamSource(0,m_pVertexBuffer, 0, sizeof(TERRAINVERTEX));  ///把包含的几何体信息的顶点缓存和渲染流水线相关联
    m_pd3dDevice->SetFVF(TERRAINVERTEX::FVF);//指定我们使用的灵活顶点格式的宏名称
   m_pd3dDevice->SetIndices(m_pIndexBuffer);//设置索引缓存
    m_pd3dDevice->SetTexture(0,m_pTexture);//设置纹理  

   m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);      //关闭光照
    m_pd3dDevice->SetTransform(D3DTS_WORLD,pMatWorld); //设置世界矩阵
   m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,
        m_nNumVertices, 0, m_nNumVertices * 2);              //绘制顶点  

   m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE);  //打开光照
    m_pd3dDevice->SetTexture(0, 0); //纹理置空  

    if (bRenderFrame)  //如果要渲染出线框的话
    {
       m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式设为线框填充
       m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,
            m_nNumVertices, 0, m_nNumVertices *2);       //绘制顶点
       m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);    //把填充模式调回实体填充
    }
    return TRUE;
}  

完整的地形类

#include"TerrainClass.h"  

//-----------------------------------------------------------------------------
// Desc: 构造函数
//-----------------------------------------------------------------------------
TerrainClass::TerrainClass(IDirect3DDevice9*pd3dDevice)
{
       //给各个成员变量赋初值
    m_pd3dDevice = pd3dDevice;
    m_pTexture = NULL;
    m_pIndexBuffer = NULL;
    m_pVertexBuffer = NULL;
    m_nCellsPerRow = 0;
    m_nCellsPerCol = 0;
    m_nVertsPerRow = 0;
    m_nVertsPerCol = 0;
    m_nNumVertices = 0;
    m_fTerrainWidth = 0.0f;
    m_fTerrainDepth = 0.0f;
    m_fCellSpacing = 0.0f;
    m_fHeightScale = 0.0f;
}  

//--------------------------------------------------------------------------------------
// Name:TerrainClass::LoadTerrainFromFile()
// Desc: 加载地形高度信息以及纹理
//--------------------------------------------------------------------------------------
BOOLTerrainClass::LoadTerrainFromFile(wchar_t *pRawFileName, wchar_t *pTextureFile)
{
    // 从文件中读取高度信息
    std::ifstream inFile;
    inFile.open(pRawFileName,std::ios::binary);   //用二进制的方式打开文件  

    inFile.seekg(0,std::ios::end);                                              //把文件指针移动到文件末尾
    std::vector<BYTE>inData(inFile.tellg());                 //用模板定义一个vector<BYTE>类型的变量inData并初始化,其值为缓冲区当前位置,即缓冲区大小  

    inFile.seekg(std::ios::beg);                                                       //将文件指针移动到文件的开头,准备读取高度信息
    inFile.read((char*)&inData[0],inData.size());    //关键的一步,读取整个高度信息
    inFile.close();                                                                                          //操作结束,可以关闭文件了  

    m_vHeightInfo.resize(inData.size());                                //将m_vHeightInfo尺寸取为缓冲区的尺寸
       //遍历整个缓冲区,将inData中的值赋给m_vHeightInfo
    for (unsigned int i=0; i<inData.size();i++)
        m_vHeightInfo[i] = inData[i];  

    // 加载地形纹理
    if (FAILED(D3DXCreateTextureFromFile(m_pd3dDevice,pTextureFile, &m_pTexture)))
        return FALSE;  

    return TRUE;
}  

//--------------------------------------------------------------------------------------
// Name:TerrainClass::InitTerrain()
// Desc: 初始化地形的高度, 填充顶点和索引缓存
//--------------------------------------------------------------------------------------
BOOLTerrainClass::InitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale)
{
    m_nCellsPerRow  = nRows; //每行的单元格数目
    m_nCellsPerCol  = nCols; //每列的单元格数目
    m_fCellSpacing  = fSpace;    //单元格间的间距
    m_fHeightScale  = fScale; //高度缩放系数
    m_fTerrainWidth = nRows * fSpace;  //地形的宽度
    m_fTerrainDepth = nCols * fSpace;  //地形的深度
    m_nVertsPerRow  = m_nCellsPerCol + 1;  //每行的顶点数
    m_nVertsPerCol = m_nCellsPerRow + 1; //每列的顶点数
    m_nNumVertices  = m_nVertsPerRow * m_nVertsPerCol;  //顶点总数  

    // 通过一个for循环,逐个把地形原始高度乘以缩放系数,得到缩放后的高度
    for(unsigned int i=0;i<m_vHeightInfo.size(); i++)
        m_vHeightInfo[i] *= m_fHeightScale;
    //---------------------------------------------------------------
    // 处理地形的顶点
    //---------------------------------------------------------------
       //1,创建顶点缓存
    if(FAILED(m_pd3dDevice->CreateVertexBuffer(m_nNumVertices *sizeof(TERRAINVERTEX),
        D3DUSAGE_WRITEONLY, TERRAINVERTEX::FVF,D3DPOOL_MANAGED, &m_pVertexBuffer, 0)))
        return FALSE;
       //2,加锁
    TERRAINVERTEX *pVertices = NULL;
    m_pVertexBuffer->Lock(0, 0,(void**)&pVertices, 0);
       //3,访问,赋值
    FLOAT fStartX = -m_fTerrainWidth / 2.0f,fEndX =  m_fTerrainWidth / 2.0f;          //指定起始点和结束点的X坐标值
    FLOAT fStartZ =  m_fTerrainDepth / 2.0f, fEndZ =-m_fTerrainDepth / 2.0f;  //指定起始点和结束点的Z坐标值
    FLOAT fCoordU = 3.0f /(FLOAT)m_nCellsPerRow;     //指定纹理的横坐标值
    FLOAT fCoordV = 3.0f /(FLOAT)m_nCellsPerCol;           //指定纹理的纵坐标值  

    int nIndex = 0, i = 0, j = 0;
    for (float z = fStartZ; z > fEndZ; z -=m_fCellSpacing, i++)          //Z坐标方向上起始顶点到结束顶点行间的遍历
    {
        j = 0;
        for (float x = fStartX; x < fEndX; x+= m_fCellSpacing, j++)  //X坐标方向上起始顶点到结束顶点行间的遍历
        {
            nIndex = i * m_nCellsPerRow + j;         //指定当前顶点在顶点缓存中的位置
            pVertices[nIndex] =TERRAINVERTEX(x, m_vHeightInfo[nIndex], z, j*fCoordU, i*fCoordV); //把顶点位置索引在高度图中对应的各个顶点参数以及纹理坐标赋值给赋给当前的顶点
            nIndex++;                                                                          //索引数自加1
        }
    }
       //4,解锁
    m_pVertexBuffer->Unlock();  

    //---------------------------------------------------------------
    // 处理地形的索引
    //---------------------------------------------------------------
       //1.创建索引缓存
    if(FAILED(m_pd3dDevice->CreateIndexBuffer(m_nNumVertices * 6 *sizeof(WORD),
        D3DUSAGE_WRITEONLY, D3DFMT_INDEX16,D3DPOOL_MANAGED, &m_pIndexBuffer, 0)))
        return FALSE;
       //2.加锁
    WORD* pIndices = NULL;
    m_pIndexBuffer->Lock(0, 0, (void**)&pIndices,0);
       //3.访问,赋值
    nIndex = 0;
    for(int row = 0; row < m_nCellsPerRow-1;row++)   //遍历每行
    {
        for(int col = 0; col <m_nCellsPerCol-1; col++)  //遍历每列
        {
                     //三角形ABC的三个顶点
            pIndices[nIndex]   =  row* m_nCellsPerRow + col;                  //顶点A
            pIndices[nIndex+1] =  row * m_nCellsPerRow + col + 1;  //顶点B
            pIndices[nIndex+2] = (row+1) *m_nCellsPerRow + col;      //顶点C
                     //三角形CBD的三个顶点
            pIndices[nIndex+3] = (row+1) *m_nCellsPerRow + col;             //顶点C
            pIndices[nIndex+4] =  row * m_nCellsPerRow + col + 1;           //顶点B
            pIndices[nIndex+5] = (row+1) *m_nCellsPerRow + col + 1;//顶点D
                     //处理完一个单元格,索引加上6
            nIndex += 6;  //索引自加6
        }
    }
       //4、解锁
    m_pIndexBuffer->Unlock();  

    return TRUE;
}  

//--------------------------------------------------------------------------------------
// Name:TerrainClass::RenderTerrain()
// Desc: 绘制出地形,可以通过第二个参数选择是否绘制出线框
//--------------------------------------------------------------------------------------
BOOLTerrainClass::RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bRenderFrame)
{
    m_pd3dDevice->SetStreamSource(0,m_pVertexBuffer, 0, sizeof(TERRAINVERTEX));  ///把包含的几何体信息的顶点缓存和渲染流水线相关联
   m_pd3dDevice->SetFVF(TERRAINVERTEX::FVF);//指定我们使用的灵活顶点格式的宏名称
   m_pd3dDevice->SetIndices(m_pIndexBuffer);//设置索引缓存
    m_pd3dDevice->SetTexture(0,m_pTexture);//设置纹理  

   m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);      //关闭光照
    m_pd3dDevice->SetTransform(D3DTS_WORLD,pMatWorld); //设置世界矩阵
   m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,
        m_nNumVertices, 0, m_nNumVertices * 2);              //绘制顶点  

   m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE);  //打开光照
    m_pd3dDevice->SetTexture(0, 0); //纹理置空  

    if (bRenderFrame)  //如果要渲染出线框的话
    {
       m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式设为线框填充
       m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,
            m_nNumVertices, 0, m_nNumVertices *2);       //绘制顶点
       m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);    //把填充模式调回实体填充
    }
    return TRUE;
}  

//-----------------------------------------------------------------------------
// Desc: 析构函数
//-----------------------------------------------------------------------------
TerrainClass::~TerrainClass(void)
{
       SAFE_RELEASE(m_pTexture);
       SAFE_RELEASE(m_pIndexBuffer);
       SAFE_RELEASE(m_pVertexBuffer);
} 

这章看到顶点计算那里就有点云里雾里的,数学什么的,太蛋疼了。

示例程序见参考博客:【Visual C++】游戏开发四十八 浅墨DirectX教程十六 三维地形系统的实现

时间: 2024-10-07 05:31:13

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记9——游戏摄像机&三维地形的构建的相关文章

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记6——Direct3D中的顶点缓存和索引缓存

第12章 Direct3D绘制基础 1. 顶点缓存 计算机所描绘的3D图形是通过多边形网格来构成的,网网格勾勒出轮廓,然后在网格轮廓的表面上贴上相应的图片,这样就构成了一个3D模型.三角形网格是构建物体模型的基本单元,而一个三角形有3个顶点,为了能够使用大量三角形组成三角形网格来描述物体,需要首先定义号三角形的顶点(Vertex),3个顶点确定一个三角形,顶点除了定义每个顶点的坐标位置外,还还含有颜色等其他属性. 在Direct3D中,顶点的具体表现形式是顶点缓存,顶点缓存保存了顶点数据的内存空

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记10——三维天空的构建&amp;三维粒子的实现&amp;多游戏模型的载入

第23章 三维天空的构建 目前描述三维天空的技术主要包括三种类型,直接来介绍使用最广泛的模拟技术,详细的描述可以见作者的博文. 天空盒(Sky Box),即放到场景的是一个立方体.它是目前使用最广泛的三维天空模拟技术,网络上素材丰富,所以这次就用教大家用天空盒来模拟三维天空.天空盒经常是由24个顶点.六个面组成的立方体(或者直接从做好的X模型文件载入天空盒),并经常会随着视点的移动而移动,来刻画极远处玩家无法达到位置的天空 天空盒的设计 1.准备天空盒纹理素材 天空盒的纹理自然就是我们这个天空盒

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记8——光照与材质

第14章 绘制出质感的世界--光照与材质 1. 光照与光源 在Direct3D中的光源类型和光照类型是不同的两个概念,光照模型描述的是光线的反射特征,而光源类型主要强调的是能够产生这些光照模型的方式以及光线的位置,方向,强度等特征. 四大光照类型 环境光:基于整个自然界环境的整体亮度,称为环境光或者背景光,没有位置或者方向上的特征,只有一个颜色亮度值,不会衰减,在所有方向和所有物体表面上投射的环境光的数量是恒定不变的(有点像我们白天的自然光).在Direc3D中设置环境光可以直接使用setRen

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记5——Direct3D编程基础

第11章 Direct3D编程基础 2D游戏是贴图的艺术,3D游戏是渲染的艺术.这句话在我学过了之前的GDI编程之后,前一句算是有所体会,现在是来理解后一句的时候了. 安装DirectX SDK配置啥的就不说了,直接进入正题,先来个典型的Direct3D程序框架图: 主要分为5个部分: 创建一个Windows窗口 Direct3D的初始化 消息循环 渲染图形 结束应用程序,清除在初始化阶段锁创建的COM对象,退出程序 至于COM (Component Object Model, 组件对象模型)

《逐梦旅程 WINDOWS游戏编程之从零开始》读书笔记1——创建窗口

步骤: 窗口类的设计 窗口类的注册 窗口的正式创建 窗口的显示与更新 1. 设计:使用WNDCLASSEX结构体,这里注意的是C++中的结构体中的成员默认是共有的,所以可以直接通过 . 来调用. typedef struct tagWNDCLASSEX { UINT cbSize; //UINT类型的cbSize,表示该结构体的字节数大小 UINT style; //指定窗口的风格样式 WNDPROC lpfnWndProc; //指向窗口过程函数的函数指针 int cbClsExtra; //

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记7——四大变换

第13章 世界变换,取景变换,投影变换,视口变换 在Direct3D中,如果为进行任何空间坐标变换而直接绘图的话,图形将始终处于应用程序窗口的中心位置,默认这个位置就成为世界坐标系的原点(0,0,0).而且我们也不能改变观察图形的视角方向.默认情况下的观察方向是世界坐标系的z轴正向方向. 世界变换运算是为了能在世界空间中的指定位置来绘制图形 取景变换运算是为了以不同的视角观察图形 投影变换为了将相对较远的图形投影到同一个平面上并体现出"近大远小"的真实视觉效果 视口变换是为了控制显示图

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记8——载入三维模型&amp;Alpha混合技术&amp;深度测试与Z缓存

第17章 三维游戏模型的载入 主要是如何从3ds max中导出.X文件,以及如何从X文件加载三维模型到DirextX游戏程序里.因为复杂的3D物体,要用代码去实现,那太反人类了,所以我们需要一些建模软件. 对于3ds max,要到出.X文件,要装个Panda插件.然后就是作者推荐的一个3D模型资源网站:http://www.cgmodel.com/. 网格模型接口ID3DXMesh 这个接口表示网格,继承自ID3DXBaseMesh.ID3DXMesh接口中的D3DXCreateMesh()可用

《逐梦旅程 WINDOWS游戏编程之从零开始》笔记7——DirectInput&amp;纹理映射

第15章 DirectInput接口 DirectInput作为DirectX的组件之一,依然是一些COM对象的集合.DirectInput由IDirectinput8.IDirectInputDevice8和IDirectInputEffect这3个接口组成.其中IDirectInput8作为DirectInput API中最主要的接口,用于初始化系统以及创建输入设备接口,DirectInput中其他所有接口都需要依赖于我们的IDirectInput8之上,都是通过这个接口进行查询的.而Dir

《逐梦旅程 WINDOWS游戏编程之从零开始》源码分析2——GDI

GDI: 图形设备接口 1. 取得设备环境的句柄(如屏幕) 使用BeginPaint和EndPaint这两个函数,或者使用GetDC和ReleaseDC这两个函数.关于函数的具体说明可以参考mdsn文档. 一个GDI程序通用框架: 1 #include <windows.h> 2 3 #define WINDOW_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度 4 #define WINDOW_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度 5 #