作者: i_dovelemon
来源:CSDN
日期:2014 / 9 / 25
主题: View Space, Perspective Matrix
引言
在游戏中,我们能够很容易的在3D世界中漫游。要完成这样的功能,我们就需要定制自己的相机。在这里,我们来一起实现一个类似FPS游戏中的第一人称相机,让你能够自由自在的在3D世界中遨游。
相机功能
要想实现FPS的相机,那么我们首先要做的就是确定FPS游戏中相机的功能有哪些了。我们可以想象下,在CF或者COD中,我们能够通过鼠标的移动来改变相机对着的方向。更具体的说,我们将鼠标上下移动的时候,就好像我们在上下抬头的感觉。而当我们左右移动鼠标的时候,我们就感觉好像在左右的转动我们的脖子,所看到的景色。当我们按下前进键或者后退键的时候,我们就会朝着,我们观望的方向进行移动。按下左右移动键的时候,我们就会横向的左右移动。
通过上面简单的描述,大家基本应该明白了应该需要实现的功能。我们来将上面的文字转换成更加3D化的概念。
我们知道,在3D图形学中,我们是通过相机的三个基坐标在与世界坐标的关联向量来表示相机的方向的。也就是Right, Up, Look向量。他们分别对应了坐标系中的X,Y和Z坐标轴。
也就是说,我们想要实现如下的功能:
1.上下移动鼠标的时候,我们希望相机能够绕着它的Right(X轴)向量进行旋转;
2.左右移动鼠标的时候,我们希望相机能够绕着世界坐标的Up(Y轴)向量进行旋转;
3.按下左右移动键的时候,我们希望相机能够沿着Right(X轴)向量进行平移;
4.按下上下移动键的时候,我们希望能够沿着Look(Z轴)向量进行平移;
好了,在了解到我们需要完成的功能之后,我们来进行相机的设计。
相机类
下面是相机类的申明:
<span style="font-family:Microsoft YaHei;">//----------------------------------------------------------------------------------- // declaration : Copyright (c), by XJ , 2014 . All right reserved . // brief : This file will define the First Perspective Shooter(FPS) camera. // file : Camera.h // author : XJ // date : 2014 / 9 / 25 // version : 1.0 //---------------------------------------------------------------------------------- #pragma once #include<d3dx9.h> /** * Define the FPS Camera */ class Camera { public: Camera(); public: const D3DXMATRIX& view() const ; const D3DXMATRIX& proj() const ; const D3DXMATRIX& viewproj() const ; const D3DXVECTOR3& right() const ; const D3DXVECTOR3& up() const ; const D3DXVECTOR3& look() const ; D3DXVECTOR3& pos(); //Create the view matrix void lookAt(D3DXVECTOR3& pos, D3DXVECTOR3& target, D3DXVECTOR3& up); //Create the perspective matrix void setLens(float fov, float aspect, float nearZ, float farZ); //Set the camera speed void setSpeed(float s); // Set the mouse speed void setMouseSpeed(float s); //Update the camera void update(float dt); protected: void _buildView(); protected: //The matrix D3DXMATRIX m_mView; D3DXMATRIX m_mProj; D3DXMATRIX m_mViewProj; //The basis relative to the world space D3DXVECTOR3 m_vPosW ; D3DXVECTOR3 m_vRightW; D3DXVECTOR3 m_vLookW; D3DXVECTOR3 m_vUpW ; //Camera speed float m_fSpeed ; float m_fMouseSpeed ; };// end for class</span>
这个类,不仅仅保存了我们根据相机能够构造的View Transform Matrix(相机变换矩阵),而且还将对透视投影的矩阵的设置创建也包含了。除了上面我们讲解的相机的坐标系的基坐标在世界空间中的表示之外,我们还需要知道相机在世界坐标系中的位置。我们使用向量m_vPosW来进行保存。m_fSpeed这个值,保存了用户在按下上下左右按键时,相机移动的速度。而m_fMouseSpeed控制了用户滑动鼠标时镜头转向的灵敏程度。
下面我们来一一的看下这个函数的实现。
首先我们来看下构造函数:
<span style="font-family:Microsoft YaHei;">/** * Constructor */ Camera::Camera() { D3DXMatrixIdentity(&m_mView); D3DXMatrixIdentity(&m_mProj); D3DXMatrixIdentity(&m_mViewProj); m_fSpeed = 0.1f ; m_fMouseSpeed = 300.0f; m_vPosW = D3DXVECTOR3(0.0f, 0.0f, 0.0f); m_vUpW = D3DXVECTOR3(0.0f, 1.0f, 0.0f); m_vLookW = D3DXVECTOR3(0.0f, 0.0f, 1.0f); m_vRightW = D3DXVECTOR3(1.0f, 0.0f, 0.0f); }// end constructor</span>
构造函数很简单,只是将里面的数据成员做了一些初始化的设置工作。但是这个初始化的设置,并不能够完成一个基本的相机的功能,在设置了这些之后,我们还需要调用lookAt函数和setLens函数来对相机变换和透视投影变换进行设置。
<span style="font-family:Microsoft YaHei;">/** * Create the look at matrix */ void Camera::lookAt(D3DXVECTOR3& pos, D3DXVECTOR3& target, D3DXVECTOR3& up) { //Calculate the look vector D3DXVECTOR3 look = target - pos; D3DXVec3Normalize(&look, &look); m_vLookW = look ; //Calculate the right vector D3DXVECTOR3 right ; D3DXVec3Cross(&right, &up, &look); D3DXVec3Normalize(&right, &right); m_vRightW = right ; //Calculate the up vector D3DXVec3Cross(&up, &look, &right); D3DXVec3Normalize(&up, &up); m_vUpW = up ; //Fill in the view matrix float x = -D3DXVec3Dot(&right, &pos); float y = -D3DXVec3Dot(&up, &pos); float z = -D3DXVec3Dot(&look, &pos); m_mView(0,0) = right.x ; m_mView(1,0) = right.y ; m_mView(2,0) = right.z ; m_mView(3,0) = x ; m_mView(0,1) = up.x ; m_mView(1,1) = up.y ; m_mView(2,1) = up.z ; m_mView(3,1) = y ; m_mView(0,2) = look.x ; m_mView(1,2) = look.y ; m_mView(2,2) = look.z ; m_mView(3,2) = z ; m_mView(0,3) = 0.0f ; m_mView(1,3) = 0.0f ; m_mView(2,3) = 0.0f ; m_mView(3,3) = 1.0f ; m_mViewProj = m_mView * m_mProj ; m_vPosW = pos ; }// end for lookAt</span>
lookAt函数的实现,和DirectX中的LookAt函数实现一致。我们根据输入的相机位置pos,相机看向的目标target,以及世界坐标的up向量,来构建相机的基坐标相对于世界坐标系关联的向量m_vRightW, m_vUpW, m_vLookW。然后构建我们的相机矩阵。这个内容可以在DirectX SDK的文档中,关于函数D3DXMatrixLookAtLH的介绍中可以得到如下的公式:
<span style="font-family:Microsoft YaHei;">zaxis = normal(At - Eye) xaxis = normal(cross(Up, zaxis)) yaxis = cross(zaxis, xaxis) xaxis.x yaxis.x zaxis.x 0 xaxis.y yaxis.y zaxis.y 0 xaxis.z yaxis.z zaxis.z 0 -dot(xaxis, eye) -dot(yaxis, eye) -dot(zaxis, eye) 1 </span>
上面的实现,就是基于此公式来实现的。
<span style="font-family:Microsoft YaHei;">/** * Set the lens and then create the perspective matrix */ void Camera::setLens(float fov, float aspect, float nearZ, float farZ) { float yScale = 1.0f / (tan(fov/2)); float xScale = yScale / aspect ; D3DXMatrixIdentity(&m_mProj); m_mProj(0, 0) = xScale ; m_mProj(1, 1) = yScale ; m_mProj(2, 2) = farZ / (farZ - nearZ); m_mProj(3, 2) = - nearZ * farZ / (farZ - nearZ); m_mProj(3, 3) = 0 ; m_mProj(2, 3) = 1 ; }// end for setLens</span>
同样的,setLens函数,使用的方法也和DirectX中计算透视投影矩阵的公式一致。可以在DirectX SDK的文档中关于D3DXMatrixPerspectiveFovLH的解释中得到如下的公式:
<span style="font-family:Microsoft YaHei;">xScale 0 0 0 0 yScale 0 0 0 0 zf/(zf-zn) 1 0 0 -zn*zf/(zf-zn) 0 where: yScale = cot(fovY/2) xScale = yScale / aspect ratio </span>
这里的公式,只限于左手坐标系的3D空间系统,并且只是在矩阵对向量进行变换时使用的是行向量的方法时才适用。如果你的系统是基于OpenGL的,那么就需要将上面的公式进行转置,更复杂的还要进行其他的变换,来达到你的要求。
由于篇幅所限,这里将不向大家讲述计算相机变换矩阵和透视投影矩阵的数学原理。感兴趣的读者,可以查看书籍来寻找答案。我将在后期专门写一篇文章来讲解3D空间中的各种矩阵变换操作。
<span style="font-family:Microsoft YaHei;">/** * Update the camera */ void Camera::update(float dt) { //Get the net direction that the camera will travel D3DXVECTOR3 dir(0.0f, 0.0f, 0.0f); //Get the input device MyInput* pInput = MyInput::getMyInput(0,0) ; if(pInput->keyDown(DIK_W)) dir += m_vLookW ; else if(pInput->keyDown(DIK_S)) dir -= m_vLookW ; if(pInput->keyDown(DIK_A)) dir -= m_vRightW ; else if(pInput->keyDown(DIK_D)) dir += m_vRightW ; //Normalize the net direction vector D3DXVec3Normalize(&dir, &dir); dir *= m_fSpeed ; //Move the position along the direction by m_fSpeed m_vPosW += dir * m_fSpeed ; //Angle to rotate around right vector float ditch = pInput->mouseDY() / m_fMouseSpeed ; //Angle to rotate around world's up vector float yAngle = pInput->mouseDX() / m_fMouseSpeed ; //Rotate the camera's look and up vector around the camera's right vector D3DXMATRIX R ; D3DXMatrixRotationAxis(&R, &m_vRightW, ditch); D3DXVec3TransformCoord(&m_vLookW, &m_vLookW, &R); D3DXVec3TransformCoord(&m_vUpW, &m_vUpW, &R); //Rotate the camera's axis around the world's y axie D3DXMatrixRotationY(&R, yAngle); D3DXVec3TransformCoord(&m_vLookW, &m_vLookW, &R); D3DXVec3TransformCoord(&m_vUpW, &m_vUpW, &R); D3DXVec3TransformCoord(&m_vRightW, &m_vRightW, &R); //Rebuild the view matrix _buildView(); m_mViewProj = m_mView * m_mProj ; }// end for update /** * Build the view matrix */ void Camera::_buildView() { //Do some float-error fixing //We assume the look vector is accurate D3DXVec3Normalize(&m_vLookW, &m_vLookW); //Calcuate the up vector D3DXVec3Cross(&m_vUpW, &m_vLookW, &m_vRightW); D3DXVec3Normalize(&m_vUpW, &m_vUpW); //Calculate the right vector D3DXVec3Cross(&m_vRightW, &m_vUpW, &m_vLookW); D3DXVec3Normalize(&m_vRightW, &m_vRightW); //Fill in the view matrix float x = -D3DXVec3Dot(&m_vRightW, &m_vPosW); float y = -D3DXVec3Dot(&m_vUpW, &m_vPosW); float z = -D3DXVec3Dot(&m_vLookW, &m_vPosW); m_mView(0,0) = m_vRightW.x ; m_mView(1,0) = m_vRightW.y ; m_mView(2,0) = m_vRightW.z ; m_mView(3,0) = x ; m_mView(0,1) = m_vUpW.x ; m_mView(1,1) = m_vUpW.y ; m_mView(2,1) = m_vUpW.z ; m_mView(3,1) = y ; m_mView(0,2) = m_vLookW.x ; m_mView(1,2) = m_vLookW.y ; m_mView(2,2) = m_vLookW.z ; m_mView(3,2) = z ; m_mView(0,3) = 0.0f ; m_mView(1,3) = 0.0f ; m_mView(2,3) = 0.0f ; m_mView(3,3) = 1.0f ; }// end for _buildView </span>
update方法,是相机的更新方法。每一帧的时候,都要调用这个方法来检测用户的输入,进行判断。在update中,我使用了我自己的对Direct Input的包装类MyInput。读者可以替换成自己的代码。但是更好的设计是将检测用户的输入作为数据传递给相机,而不是将输入设备绑定在相机中。这里仅仅是为了方便起见,才这样做。
这个函数,就是根据我们的功能描述来完成我们想要完成的功能的。代码已近清晰明了,这里不再解释。
有一个地方需要注意,在上面我们说:进行鼠标左右移动的时候,是按照世界坐标的Y轴进行旋转,为什么不是绕着相机坐标的Y轴进行旋转了?
我们先来看下在使用绕相机坐标的Y轴进行旋转后的效果图:
读者可以看到,在进行多次旋转之后,我们就好像要摔倒了一样,是倾斜的看着这个世界的。我们当然不希望是这样的效果(至少这我们的例子中,我们不希望有这种效果,但是在游戏中可能会故意为了营造地震,眩晕等效果,故意倾斜相机),所以这就是为什么,我们要绕着世界坐标的Y坐标进行旋转。因为这样,我们就能够时刻的保证,我们的头部总是向上的和世界坐标的Y坐标轴方向一致。
好了,其他的几个函数,仅仅是对Camera类中的变量进行获取而已,没有什么好说的。
今天就记到这里!!!
话说,周围的同学都开始找工作了,不知道像我这样的能够找到什么工作,哎!!!