《逐梦旅程 WINDOWS游戏编程之从零开始》笔记9——游戏摄像机&三维地形的构建
Posted f91og
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《逐梦旅程 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(); //操作结束,可以关以上是关于《逐梦旅程 WINDOWS游戏编程之从零开始》笔记9——游戏摄像机&三维地形的构建的主要内容,如果未能解决你的问题,请参考以下文章
《逐梦旅程 WINDOWS游戏编程之从零开始》笔记7——DirectInput&纹理映射
《逐梦旅程 WINDOWS游戏编程之从零开始》笔记7——四大变换
《逐梦旅程 WINDOWS游戏编程之从零开始》笔记8——光照与材质
《逐梦旅程 WINDOWS游戏编程之从零开始》笔记5——Direct3D编程基础