《逐梦旅程 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就为我们默认好了,不去设置也没关系。
#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); } // 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;
} 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)
} // 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);
} 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)
{
}
其中要注意的是如何计算出观察方向向量的,观察点位置减摄像机位置得出观察方向向量,再正交规范化右向量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(,std::ios::end); //把文件指针移动到文件末尾
std::vector<BYTE> inData(inFile.tellg()); //用模板定义一个vector<BYTE>类型的变量inData并初始化,其值为缓冲区当前位置,即缓冲区大小 inFile.seekg(std::ios::beg); //将文件指针移动到文件的开头,准备读取高度信息
inFile.read((char*)&inData[],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=; 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 + ; //每行的顶点数
m_nVertsPerCol = m_nCellsPerRow + ; //每列的顶点数
m_nNumVertices = m_nVertsPerRow * m_nVertsPerCol; //顶点总数
另外,我们在计算地形顶点前,还需要将地形的高度值乘以一个缩放系数,以便能够调整高度的整体变化幅度,就是下面这两句代码:
// 通过一个for循环,逐个把地形原始高度乘以缩放系数,得到缩放后的高度
for(unsigned int i=;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, )))
return FALSE;
//2,加锁
TERRAINVERTEX *pVertices = NULL;
m_pVertexBuffer->Lock(, ,(void**)&pVertices, );
//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 = , i = , j = ;
for (float z = fStartZ; z > fEndZ; z -=m_fCellSpacing, i++) //Z坐标方向上起始顶点到结束顶点行间的遍历
{
j = ;
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 * *sizeof(WORD),
D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIndexBuffer, )))
return FALSE;
//2.加锁
WORD* pIndices = NULL;
m_pIndexBuffer->Lock(, , (void**)&pIndices, );
//3.访问,赋值
nIndex = ;
for(int row = ; row < m_nCellsPerRow-; row++) //遍历每行
{
for(int col = ; col < m_nCellsPerCol-; col++) //遍历每列
{
//三角形ABC的三个顶点
pIndices[nIndex] = row * m_nCellsPerRow + col; //顶点A
pIndices[nIndex+] = row * m_nCellsPerRow + col + ; //顶点B
pIndices[nIndex+] = (row+) * m_nCellsPerRow + col; //顶点C
//三角形CBD的三个顶点
pIndices[nIndex+] = (row+) * m_nCellsPerRow + col; //顶点C
pIndices[nIndex+] = row * m_nCellsPerRow + col + ; //顶点B
pIndices[nIndex+] = (row+) * m_nCellsPerRow + col + ;//顶点D
//处理完一个单元格,索引加上6
nIndex += ; //索引自加6
}
}
//4、解锁
m_pIndexBuffer->Unlock();
渲染出地形
地形的渲染封装在了一个名为RenderTerrain的函数中:
BOOLTerrainClass::RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bRenderFrame)
{
m_pd3dDevice->SetStreamSource(,m_pVertexBuffer, , sizeof(TERRAINVERTEX)); ///把包含的几何体信息的顶点缓存和渲染流水线相关联
m_pd3dDevice->SetFVF(TERRAINVERTEX::FVF);//指定我们使用的灵活顶点格式的宏名称
m_pd3dDevice->SetIndices(m_pIndexBuffer);//设置索引缓存
m_pd3dDevice->SetTexture(,m_pTexture);//设置纹理 m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE); //关闭光照
m_pd3dDevice->SetTransform(D3DTS_WORLD,pMatWorld); //设置世界矩阵
m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, , ,
m_nNumVertices, , m_nNumVertices * ); //绘制顶点 m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE); //打开光照
m_pd3dDevice->SetTexture(, ); //纹理置空 if (bRenderFrame) //如果要渲染出线框的话
{
m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式设为线框填充
m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, , ,
m_nNumVertices, , m_nNumVertices *); //绘制顶点
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 = ;
m_nCellsPerCol = ;
m_nVertsPerRow = ;
m_nVertsPerCol = ;
m_nNumVertices = ;
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(,std::ios::end); //把文件指针移动到文件末尾
std::vector<BYTE>inData(inFile.tellg()); //用模板定义一个vector<BYTE>类型的变量inData并初始化,其值为缓冲区当前位置,即缓冲区大小 inFile.seekg(std::ios::beg); //将文件指针移动到文件的开头,准备读取高度信息
inFile.read((char*)&inData[],inData.size()); //关键的一步,读取整个高度信息
inFile.close(); //操作结束,可以关闭文件了 m_vHeightInfo.resize(inData.size()); //将m_vHeightInfo尺寸取为缓冲区的尺寸
//遍历整个缓冲区,将inData中的值赋给m_vHeightInfo
for (unsigned int i=; 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 + ; //每行的顶点数
m_nVertsPerCol = m_nCellsPerRow + ; //每列的顶点数
m_nNumVertices = m_nVertsPerRow * m_nVertsPerCol; //顶点总数 // 通过一个for循环,逐个把地形原始高度乘以缩放系数,得到缩放后的高度
for(unsigned int i=;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, )))
return FALSE;
//2,加锁
TERRAINVERTEX *pVertices = NULL;
m_pVertexBuffer->Lock(, ,(void**)&pVertices, );
//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 = , i = , j = ;
for (float z = fStartZ; z > fEndZ; z -=m_fCellSpacing, i++) //Z坐标方向上起始顶点到结束顶点行间的遍历
{
j = ;
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 * *sizeof(WORD),
D3DUSAGE_WRITEONLY, D3DFMT_INDEX16,D3DPOOL_MANAGED, &m_pIndexBuffer, )))
return FALSE;
//2.加锁
WORD* pIndices = NULL;
m_pIndexBuffer->Lock(, , (void**)&pIndices,);
//3.访问,赋值
nIndex = ;
for(int row = ; row < m_nCellsPerRow-;row++) //遍历每行
{
for(int col = ; col <m_nCellsPerCol-; col++) //遍历每列
{
//三角形ABC的三个顶点
pIndices[nIndex] = row* m_nCellsPerRow + col; //顶点A
pIndices[nIndex+] = row * m_nCellsPerRow + col + ; //顶点B
pIndices[nIndex+] = (row+) *m_nCellsPerRow + col; //顶点C
//三角形CBD的三个顶点
pIndices[nIndex+] = (row+) *m_nCellsPerRow + col; //顶点C
pIndices[nIndex+] = row * m_nCellsPerRow + col + ; //顶点B
pIndices[nIndex+] = (row+) *m_nCellsPerRow + col + ;//顶点D
//处理完一个单元格,索引加上6
nIndex += ; //索引自加6
}
}
//4、解锁
m_pIndexBuffer->Unlock(); return TRUE;
} //--------------------------------------------------------------------------------------
// Name:TerrainClass::RenderTerrain()
// Desc: 绘制出地形,可以通过第二个参数选择是否绘制出线框
//--------------------------------------------------------------------------------------
BOOLTerrainClass::RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bRenderFrame)
{
m_pd3dDevice->SetStreamSource(,m_pVertexBuffer, , sizeof(TERRAINVERTEX)); ///把包含的几何体信息的顶点缓存和渲染流水线相关联
m_pd3dDevice->SetFVF(TERRAINVERTEX::FVF);//指定我们使用的灵活顶点格式的宏名称
m_pd3dDevice->SetIndices(m_pIndexBuffer);//设置索引缓存
m_pd3dDevice->SetTexture(,m_pTexture);//设置纹理 m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE); //关闭光照
m_pd3dDevice->SetTransform(D3DTS_WORLD,pMatWorld); //设置世界矩阵
m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, , ,
m_nNumVertices, , m_nNumVertices * ); //绘制顶点 m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE); //打开光照
m_pd3dDevice->SetTexture(, ); //纹理置空 if (bRenderFrame) //如果要渲染出线框的话
{
m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式设为线框填充
m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, , ,
m_nNumVertices, , m_nNumVertices *); //绘制顶点
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教程十六 三维地形系统的实现
《逐梦旅程 WINDOWS游戏编程之从零开始》笔记9——游戏摄像机&三维地形的构建的更多相关文章
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记10——三维天空的构建&三维粒子的实现&多游戏模型的载入
第23章 三维天空的构建 目前描述三维天空的技术主要包括三种类型,直接来介绍使用最广泛的模拟技术,详细的描述可以见作者的博文. 天空盒(Sky Box),即放到场景的是一个立方体.它是目前使用最广泛的 ...
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记5——Direct3D中的顶点缓存和索引缓存
第12章 Direct3D绘制基础 1. 顶点缓存 计算机所描绘的3D图形是通过多边形网格来构成的,网网格勾勒出轮廓,然后在网格轮廓的表面上贴上相应的图片,这样就构成了一个3D模型.三角形网格是构建物 ...
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记6——四大变换&光照与材质
第13章 四大变换 在Direct3D中,如果为进行任何空间坐标变换而直接绘图的话,图形将始终处于应用程序窗口的中心位置,默认这个位置就成为世界坐标系的原点(0,0,0).而且我们也不能改变观察图形的 ...
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记2——透明贴图,动画技术
第5章 透明贴图 像这样直接贴图会产生这种情况,所以我们需要透明贴图. 透明遮罩法:主要利用BitBlt函数中Raser(光栅)值的运算,需要准备素材图和遮罩图: 这个方法的原理解释见书131页. 示 ...
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记3——输入消息处理,物理建模与粒子系统初步
第7章 Windows游戏输入消息处理 1. 键盘消息处理 之前提到的窗口过程函数有两参数与消息输出有关——wParam和llParam LRESULT CALLBACK WindowProc( _I ...
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记4——Direct3D编程基础
第11章 Direct3D编程基础 2D游戏是贴图的艺术,3D游戏是渲染的艺术.这句话在我学过了之前的GDI编程之后,前一句算是有所体会,现在是来理解后一句的时候了. 安装DirectX SDK配置啥 ...
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记1——创建窗口&GDI
第1章 创建窗口 步骤: 窗口类的设计 窗口类的注册 窗口的正式创建 窗口的显示与更新 消息循环体系 窗口过程函数处理消息 1. 设计:使用WNDCLASSEX结构体,这里注意的是C++中的结构体中的 ...
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记8——载入三维模型&Alpha混合技术&深度测试与Z缓存
第17章 三维游戏模型的载入 主要是如何从3ds max中导出.X文件,以及如何从X文件加载三维模型到DirextX游戏程序里.因为复杂的3D物体,要用代码去实现,那太反人类了,所以我们需要一些建模软 ...
- 《逐梦旅程 WINDOWS游戏编程之从零开始》笔记7——DirectInput&纹理映射
第15章 DirectInput接口 DirectInput作为DirectX的组件之一,依然是一些COM对象的集合.DirectInput由IDirectinput8.IDirectInputDev ...
随机推荐
- UVA.540 Team Queue (队列)
UVA.540 Team Queue (队列) 题意分析 有t个团队正在排队,每次来一个新人的时候,他可以插入到他最后一个队友的身后,如果没有他的队友,那么他只能插入到队伍的最后.题目中包含以下操作: ...
- Codeforces Round #381 (Div. 2) D dfs序+树状数组
D. Alyona and a tree time limit per test 2 seconds memory limit per test 256 megabytes input standar ...
- 题解【luoguP1351 NOIp提高组2014 联合权值】
题目链接 题意:给定一个无根树,每个点有一个权值.若两个点 \(i,j\) 之间距离为\(2\),则有联合权值 \(w_i \times w_j\).求所有的联合权值的和与最大值 分析: 暴力求,每个 ...
- js 获取当前链接和获取域名
<script language="javascript"> //获取域名 host = window.location.host; host2=document.do ...
- aos.js让页面滚动变得丰富
(转)<script src="js/jquery-2.1.1.min.js" type="text/javascript"></script ...
- 关于微信内置浏览器安卓端session丢失问题
项目上线测试,发现微信安卓端存在用户登录无法验证session情况, 导致每次接口请求都无法识别,而苹果客户端不会出现此问题,非微信环境打开不会出现此问题,找到一些解决方案做下记录: 方案1: 由于微 ...
- 转:PriorityQueue
转自:PriorityQueue 本文github地址 Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示.本文从Queue接口函数出发,结合生动的图解,深入浅出地 分析 ...
- 数据结构:Rope
以BZOJ1507为例,这里仅仅展示动态区间问题的一些典型操作,包括插入删除和修改,查询的话不支持按顺序查询 使用起来很简单很方便 #include<cstdio> #include< ...
- [洛谷P2048] [NOI2010] 超级钢琴
洛谷题目链接:[NOI2010]超级钢琴 题目描述 小Z是一个小有名气的钢琴家,最近C博士送给了小Z一架超级钢琴,小Z希望能够用这架钢琴创作出世界上最美妙的音乐. 这架超级钢琴可以弹奏出n个音符,编号 ...
- Maven:Non-resolvable parent POM: Failure to find错误
使用Maven编译项目时遇到如下错误: [ERROR] The project dfjr.generic:dfjr-generic:1.0-SNAPSHOT (F:\workspace\DFJR-PE ...