这些天对左边变换比较感兴趣。申明,本人对游戏编程没兴趣,只是近体做的项目用了DirectX而已,用了就要懂么!况且坐标变换也挺用脑的,我的大脑已经很久没用了,快锈死了,得动动了。
我的
directX编程学习(4)
今天开始看动画初步,其实主要就是
坐标变换。无论对于
directX也好,对于openGL也好,
坐标变换都是做基本最重要的部分。所以今天一定要好好学习学习。
先看一下D3D的矩阵表示形式:
m._11 m._12 m._13 m._14 ux uy uz 0
m._21 m._22 m._23 m._24 = vx vy vz 0
m._31 m._32 m._33 m._34 wx wy wz 0
m._41 m._42 m._43 m._44 tx ty tz 1
下面看一下D3D需要构造的几个基本矩阵:
1,平移矩阵
1 0 0 0
0 1 0 0
0 0 1 0
tx ty tz 1
平移矩阵可以由函数:VOID D3DMatrixTranslation(D3DMATRIX* m,FLOAT tx,FLOAT ty,FLOAT tz)来构造。
2,旋转矩阵
以绕Y轴旋转为例,矩阵形式如下:
cosR 0 -sinR 0
0 1 0 0
sinR 0 cosR 0
0 0 0 1
以上矩阵可以通过类似VOID D3DMatrixRotationY(D3DMATRIX* m,FLOAT r)来获得。同样可以得到绕X,Z轴旋转的矩阵。
3,缩放矩阵
sx 0 0 0
0 sy 0 0
0 0 sz 0
0 0 0 1
可以通过D3DMATRIX* D3DMatrixScaling(D3DMATRIX *pOut,FLOAT sx,FLOAT sy,FLOAT sz)来获得。
得到上述矩阵以后,可以通过矩阵乘法来累积变换,乘法形式同线性代数中所讲一样,不详述。调用形式如下:D3DMATRIX* D3DMatrixMultiply(D3DMATRIX *pOut,D3DMATRIX *pM1,D3DMATRIX *pM2).其中返回值同pOut值相同。
在C++中由于利用函数重载,还可以写成:pOut = pM1 * pM2的形式,当然也可以连乘:pOut = pM1 * pM2 * pM3.
获得了变换矩阵以后,要对顶点起作用,需要利用m_pDevice->SetTransform(D3DTS_WORLD,&m_matWorld)来设置。当然可以将D3DTS_WORLD换成D3DTS_VIEW,D3DTS_PROJECTION来设置视矩阵和投影矩阵。
下面看一个实际应用的例子:
HRESULT CMyD3DApplication::FrameMove()
{
// rotates the object about the y-axis
D3DXMATRIX matRotY;
D3DXMATRIX matRotZ;
D3DXMatrixRotationY( &matRotY, m_fTime * 0.5f);
D3DXMatrixRotationZ( &matRotZ, m_fTime * 1.5f);
D3DXMATRIX matTrans;
D3DXMatrixTranslation(&matTrans, 0.0f, 0.0f, 0.0f);
m_pd3dDevice->SetTransform( D3DTS_WORLD, &(matRotY * matRotZ * matTrans));
return S_OK;
}
我们用FrameMove()函数来制作动画,循环播放。我们只需要调用相应函数获得矩阵,然后乘起来,设置一下就可以了。
再看一小段代码:
// Each viewport fills a quarter of the window
m_RViewport.Width = MainViewport.Width / 2;
m_RViewport.Height = MainViewport.Height / 2;
m_RViewport.Y = m_SRTViewport.Y = 0;
m_RViewport.X = m_TRViewport.X = 0;
// Set the full Z range for each viewport
m_RViewport.MinZ = 0.0f;
m_RViewport.MaxZ = 1.0f;
m_pd3dDevice->SetViewport(&m_RViewport);
m_matWorld = RotationMatrix1;
m_pd3dDevice->SetTransform(D3DTS_WORLD, &m_matWorld);
m_pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST,
0,
0,
4, // number of vertices
0,
2); // number of primitives
我们只要把已经做好的矩阵设置成当前世界坐标矩阵,然后依次绘制图元就可以了。
从这个例子我们也可以想象出d3d也存在着类似于openGL那样的一个保存当前状态的自动机。
上边的代码中我们也看到了使用视口的例子,通过设置不同视口的位置和大小,我们可以在屏幕上显示出不同的内容,书中就是使用四个视口分别展示了四种不同的变换。
上次写blog的时候只是简单记录了一下D3D的坐标变换,以及让做好的矩阵应用到场景中的顶点上,并探索了一下使用视口的方法。这次我就把D3D坐标变换其余的部分补充完整。首先介绍一种物体的表示法,然后介绍两种任意变换的方法,介绍视变换和投影变换,最后介绍深度缓冲的使用。
上次介绍了对物体在三个坐标轴方向上做平移、缩放、旋转的情况,可以使用D3DX系列的API直接构造变换矩阵,那么再复杂一点的情况就是绕自身轴的旋转。使用上述基本变换实际上可以达到这一目的,只是有些麻烦,下面我们来探讨一下更为通用的表达方式。为了达到这一目的就必须利用更加复杂的矩阵变换。
首先我们借助一个结构来描述物体的定位:
struts Object
{
D3DMATRIX matLocal;
}
用该矩阵中的三个向量分别表示物体的朝向:Look,Up,Right,这个里的含义如同openGL里边相机的Look,Up,Right是一样的。然而实际上定位一个物体除了上述三个向量表示的姿态以外,还需要一个位置信息,于是我们用第四行来记录位置。将该矩阵设置为单位阵表示物体的变换从原点开始,沿坐标轴方向。这样表示以后,物体绕Look轴转就是横滚(pitch),绕Up轴转就是偏航(yaw),绕Right轴转就是俯仰(roll)。
------------------------------------
下面说一下用到的API。将一个向量按照指定矩阵变换的API是:D3DXVectorTransformCoord(D3DXVector* vNew,D3DXVector* vOld,D3DXMatrix* mat),那么旋转用的矩阵mat又来源于D3DMatrixRotationAxis(D3DXMatrix* mat,D3DXVector* vAxis,FLOAT fRad),表示绕某一向量旋转一个角度产生一个变换矩阵。有了这两个API我们就可以通过将三个姿态向量指定给vAxis和vOld来获取到新的姿态向量。
需要注意的是,由于计算精度问题,上述计算进行多次以后会存在舍入误差,使三个姿态向量不再垂直。为了解决这个问题需要在旋转之前对三个向量进行归一化。这里的归一化不是对三个向量各自归一,而是按照下述方式进行:
D3DXVec3Normalize(&vLook,&vLook);
D3DXVec3Cross(&vRight,&vUp,&vLook);
D3DXVec3Normalize(&vRight,&vRight);
D3DXVec3Cross(&vUp,&vLook,&vRight);
D3DXVec3Normalize(&vUp,&vUp);
可以看到是通过向量单独归一和叉乘的方式进行,既保证向量归一,又保证垂直。
matLocal矩阵的保存形式如下:第一行是Right,第二行是Up,第三行是Look,第四行是Position:
m_pObjects[0].matLocal._11 = vRight.x;
m_pObjects[0].matLocal._12 = vRight.y;
m_pObjects[0].matLocal._13 = vRight.z;
m_pObjects[0].matLocal._21 = vUp.x;
m_pObjects[0].matLocal._22 = vUp.y;
m_pObjects[0].matLocal._23 = vUp.z;
m_pObjects[0].matLocal._31 = vLook.x;
m_pObjects[0].matLocal._32 = vLook.y;
m_pObjects[0].matLocal._33 = vLook.z;
m_pObjects[0].matLocal._41 = vPos.x;
m_pObjects[0].matLocal._42 = vPos.y;
m_pObjects[0].matLocal._43 = vPos.z;
下面我们把上述变换过程总结一下:
- 确定旋转角度和旋转轴。
- 取出当前的vRight,vLook,vUp,vPos向量;
- 对三个向量进行归一化;
- 利用D3DMatrixRotationAxis(D3DXMatrix* mat,D3DXVector* vAxis,FLOAT fRad)产生旋转矩阵;
- 利用D3DXVectorTransformCoord(D3DXVector* vNew,D3DXVector* vOld,D3DXMatrix* mat)对当前的vRight,vLook,vUp向量进行变换,得到新的vRight,vLook,vUp向量。
- 移动位置,获得新的vPos;
- 将新的vRight,vLook,vUp,vPos向量设置到matLocal中。
----------------------------
上边的表示方法我们看到要7个过程,这略微有些复杂,那么下面我们来看另外一种简洁的计算方法-四元数(Quaternion)。
我们先对比一下实现的差别,然后再具体解释API的含义。
- 确定旋转角度和旋转轴。
- 利用D3DXQuaternionRotationYawPitchRoll(D3DXMatrix* mat,Float fYaw,FLOAT fPitch,FLOAT fRoll)的到变换矩阵。
- 把上述得到的矩阵同matLocal相乘得到新的matLocal;
- 做位置的变换。
四元数的原理有点复杂,由于速成关系我也没有怎么看,只是知道可以简单想象成一个向量加上一次旋转,具体的运算推导有机会再研究吧。但这个东西用途的确很广泛,因此被作为一种专门的方法被D3D介绍。
上边只用到了一个API,那就是D3DXQuaternionRotationYawPitchRoll(D3DXMatrix* mat,Float fYaw,FLOAT fPitch,FLOAT fRoll),给定绕三个轴的旋转角度,返回一个变换矩阵。
-----------------------------
下边看一下观察变换,观察矩阵同物体定位矩阵唯一不同的就是其存储方式,它采取列向量的存储方式。相机的各种变换同物体的变换没有任何不同,最后也是得到一个矩阵,只是D3D提供了一个根据视点位置,相机朝向和向上方向构造矩阵的函数:D3DXMatrixLookAtLH(D3DXMatrix* mat,D3DXVECTOR3* pEye,D3DXVECTOR3* pAt,D3DXVECTOR3* pUp),省着自己算了。最后用m_pd3Device->SetTransform(D3DTS_VIEW,&mat)设置一下就可以了。
这里需要注意的是D3DXMatrixLookAtLH()只适合于简单的头罩式显示或者视点跟随,对于具有复杂旋转的飞行模拟器这类相机最好还是自己来算。计算的方式同前边介绍的物体变换的方式一样,也有两种方式,一种是复杂的7步变换,一种是简单的四元数变换。最后将得到的向量按照列向量的形式赋给视矩阵,再利用SetTransform()设置一下就好了。这实际上是一种自己维护相机的方式。
------------------------------
下面看一下投影变换。提到投影就会想到视锥,就会有视域角(FOV-field of view)、宽高比(aspect)和远近裁减面这四个参数。在D3D里边可以利用D3DXMatrixPerspectiveFoVLH(D3DXMATRIX* pOut,FLOAT fovY,FLOAT Aspect,FLOAT zn,FLOAT zf),通过给定的四个参数获得投影矩阵,然后用m_pd3Device->SetTranform(D3DTS_PROJECTION,&pOut)来设置投影矩阵即可。
-------------------------------
视口的使用 上次 已经说过了,下面就看一下深度缓冲的使用。
在框架里边使用深度缓冲只要让m_d3dEnumeration.AppUsesDepthBuffer = TRUE.然后在每一帧绘制前用m_pd3Device->Clear()方法清空缓冲区。
HRESULT Clear(
DWORD Count,//矩形数量
const D3DRECT *pRects,//矩形指针
DWORD Flags,//要清除的缓冲类型
float Z,//Z缓冲设置的值
DWORD Stencil)//模板缓冲设置的值
书中还讲了一个深度缓冲精度影响渲染质量的问题,为了达到无错误的稳定效果可以是用W缓冲器。方法如下:
m_pd3Device->SetRenderState(D3DTS_ZNABLE,D3DZB_USEW).
但是这需要硬件的支持,为了稳妥起见还是使用Z缓冲比较好。
-------------------------------
总结来说,D3D要设置的矩阵分为三种:除了上次提到的世界坐标矩阵,其实我理解也就是openGL里边对应的模型视图矩阵中的模型矩阵,还有视矩阵和投影矩阵。它们的设置函数都是pDeviceObject->SetTransform(),只不过参数不同而已。所有的其他函数也好,表示也好最终都是为了获得这三个矩阵,这让我们拨开云雾见太阳,只要心中挂念这矩阵就可以了。