原文转自:
http://www.gesoftfactory.com/developer/Transform.htm
(提示:原文有图片。)
三维变换
在使用三维图形的应用程序中,可以用变换做以下事情:
- 描述一个物体相对于另一个物体的位置。
- 旋转并改变物体的大小。
- 改变观察的位置、方向和视角。
可以用一个4 x 4矩阵将任意点(x,y,z)变换为另一个点(x',y',z')。
对(x,y,z)和矩阵执行以下操作产生点(x',y',z')。
最常见的变换是平移、旋转和缩放。可以将产生这些效果的矩阵合并成单个矩阵,这样就可以一次计算多种变换。例如,可以构造单个矩阵,对一系列的点进行平移和旋转。更多信息,请参阅矩阵串接。
矩阵以行列顺序书写。一个沿每根轴均匀缩放顶点的矩阵,也称为统一缩放,用如下数学符号表示。
在C++应用程序中,Microsoft® Direct3D®使用D3DMATRIX结构,将矩阵声明为一个二维数组。以下示例代码显示了如何初始化一个D3DMATRIX结构,使之成为一个统一缩放矩阵。
// 本例中,s为浮点类型的变量
D3DMATRIX scale = {
s, 0.0f, 0.0f, 0.0f,
0.0f, s, 0.0f, 0.0f,
0.0f, 0.0f, s, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
平移
以下变换将点(x,y,z)平移到一个新的点(x',y',z')。
可以在C++应用程序中手工创建一个平移矩阵。以下示例代码显示了的一个函数的源码,该函数创建一个矩阵用于平移顶点。
D3DXMATRIX Translate(const float dx, const float dy, const float dz) {
D3DXMATRIX ret;
D3DXMatrixIdentity(&ret); // 由Direct3DX实现
ret(3, 0) = dx;
ret(3, 1) = dy;
ret(3, 2) = dz;
return ret;
} // 平移结束
为了方便,Direct3DX工具库提供了D3DXMatrixTranslation函数。
缩放
以下变换用指定值缩放点(x,y,z)的x-,y-和z-方向,产生新的点(x',y',z')。
旋转
这里描述的变换是基于左手坐标系的,也许和别处见到的变换矩阵不同。更多信息,请参阅三维坐标系。
以下变换将点(x,y,z)围绕x轴旋转,产生新的点(x',y',z')。
以下变换将点围绕y轴旋转。
以下变换将点围绕z轴旋转。
在这些示例矩阵中,希腊字母(θ)表示旋转的角度,以弧度为单位。当沿着旋转轴朝原点看时,角度以顺时针方向计量。
C++应用程序可以用Direct3D扩展(D3DX)工具库提供的D3DXMatrixRotationX,D3DXMatrixRotationX和D3DXMatrixRotationX函数创建旋转矩阵。以下示例是D3DXMatrixRotationX函数的源码。
D3DXMATRIX* WINAPI D3DXMatrixRotationX
( D3DXMATRIX *pOut, float angle )
{
#if DBG
if(!pOut)
return NULL;
#endif
float sin, cos;
sincosf(angle, &sin, &cos); // 计算角度的正弦和余弦值。
pOut->_11 = 1.0f; pOut->_12 = 0.0f; pOut->_13 = 0.0f; pOut->_14 = 0.0f;
pOut->_21 = 0.0f; pOut->_22 = cos; pOut->_23 = sin; pOut->_24 = 0.0f;
pOut->_31 = 0.0f; pOut->_32 = -sin; pOut->_33 = cos; pOut->_34 = 0.0f;
pOut->_41 = 0.0f; pOut->_42 = 0.0f; pOut->_43 = 0.0f; pOut->_44 = 1.0f;
return pOut;
}
( D3DXMATRIX *pOut, float angle )
if(!pOut)
return NULL;
float sin, cos;
sincosf(angle, &sin, &cos); // Determine sin and cos of angle.
pOut->_11 = 1.0f; pOut->_12 = 0.0f; pOut->_13 = 0.0f; pOut->_14 = 0.0f;
pOut->_21 = 0.0f; pOut->_22 = cos; pOut->_23 = sin; pOut->_24 = 0.0f;
pOut->_31 = 0.0f; pOut->_32 = -sin; pOut->_33 = cos; pOut->_34 = 0.0f;
pOut->_41 = 0.0f; pOut->_42 = 0.0f; pOut->_43 = 0.0f; pOut->_44 = 1.0f;
return pOut;
矩阵串接
使用矩阵的一个优势就是可以通过把两个以上的矩阵相乘,将它们的效果合并在一起。这意味着,要先旋转一个建模然后把它平移到某个位置,无需使用两个矩阵,只要把旋转矩阵和平移矩阵相乘,产生一个包含了所有效果的合成矩阵。这个过程被称为矩阵串接,可以写成以下公式。
在这个公式中,C是将被创建的合成矩阵,M1到Mn是矩阵C包含的单个矩阵。虽然大多数情况下,只需要串接两三个矩阵,但实际上并没有数量上的限制。
可以使用D3DXMatrixMultiply函数进行矩阵乘法。
进行矩阵乘法时先后次序是至关重要的。前面的公式反映了矩阵串接从左到右的规则。也就是说,用来创建合成矩阵的每个矩阵产生的直观效果会按从左到右的次序出现。下面显示了一个典型的世界变换矩阵。想象一下给一个旋转飞行的碟子创建世界变换矩阵。应用程序也许想让飞行的碟子绕它的中心——建模空间中的y轴——旋转,然后把它平移到场景中的另一个位置。要实现这样的效果,首先创建一个旋转矩阵,然后将它与平移矩阵相乘,如以下公式所示。
在这个公式中,Ry是绕y轴的旋转矩阵,Tw是平移到世界坐标中某个位置的矩阵。
矩阵相乘的顺序很重要,因为矩阵乘法是不可交换的,这和两个标量相乘不同。将矩阵以相反的顺序相乘会产生这样的直观效果:先把飞行中的碟子平移到世界空间中的某个位置,然后将它围绕世界坐标的原点旋转。
无论创建何种类型的矩阵,都要记住从左到右的规则,这样才能保证得到想要的效果。
世界变换
对世界变换的讨论介绍了基本概念,并提供了如何在Microsoft® Direct3D®应用程序中设置世界变换矩阵的细节。
什么是世界变换
世界变换将坐标从建模空间,在这个空间中的顶点相对于建模的局部原点定义,转变到世界空间,在这个空间中的顶点相对于场景中所有物体共有的原点定义。本质上,世界变换将一个建模放到世界中,并由此而得名。下图描绘了世界坐标系统和建模的局部坐标系统间的关系。
世界变换可以包含任意数量的平移、旋转和缩放的合并。有关对变换的数学讨论,请参阅三维变换。
设置世界矩阵
同任何其它变换一样,通过将一系列变换矩阵串接成单个矩阵,应用程序可以创建包含这些矩阵全部效果的世界矩阵。最简单的情况下,建模在世界的原点并且它的局部坐标轴与世界空间的方向相同,这时世界矩阵就是单位矩阵。更通常的情况下,世界矩阵是一系列矩阵的合成,包含一个平移矩阵,并且根据需要可能有一个以上的旋转矩阵。
以下示例,来自一个用C++编写的假想三维建模类,使用Direct3D扩展(D3DX)工具库提供的帮助函数创建了一个世界矩阵,这个世界矩阵包含了三个旋转矩阵,用于调整三维建模的方向,以及一个平移矩阵,用来根据建模在世界空间中的相对坐标重新确定它的位置。
/*
* 根据本示例的目的,假设以下变量都是有效的并经过初始化。
*
* 变量m_xPos,m_yPos,m_zPos包含了建模在世界坐标中的位置。
*
* 变量m_fPitch,m_fYaw和m_fRoll为浮点数,包含了建模的方向,
* 用pitch,yaw和roll旋转角表示,以弧度为单位。
*/
void C3DModel::MakeWorldMatrix( D3DXMATRIX* pMatWorld )
{
D3DXMATRIX MatTemp; // 用于旋转的临时矩阵
D3DXMATRIX MatRot; // 最终的旋转矩阵,应用于pMatWorld.
// 使用从左到右的矩阵串接顺序,在旋转之前对物体在世界空间中
// 的位置进行平移。
D3DXMatrixTranslation(pMatWorld, m_xPos, m_yPos, m_zPos);
D3DXMatrixIdentity(&MatRot);
// 现在将方向变量应用于世界矩阵
if(m_fPitch || m_fYaw || m_fRoll) {
// 产生并合成旋转矩阵。
D3DXMatrixRotationX(&MatTemp, m_fPitch); // Pitch
D3DXMatrixMultiply(&MatRot, &MatRot, &MatTemp);
D3DXMatrixRotationY(&MatTemp, m_fYaw); // Yaw
D3DXMatrixMultiply(&MatRot, &MatRot, &MatTemp);
D3DXMatrixRotationZ(&MatTemp, m_fRoll); // Roll
D3DXMatrixMultiply(&MatRot, &MatRot, &MatTemp);
// 应用旋转矩阵,得到最后的世界矩阵。
D3DXMatrixMultiply(pMatWorld, &MatRot, pMatWorld);
}
}
当准备好世界变换矩阵后,应该调用IDirect3DDevice9::SetTransform方法设置它,并把第一个参数指定D3DTS_WORLD宏。
注意 Direct3D使用应用程序设置的世界和观察矩阵配置许多内部数据结构。应用程序每次设置新的世界或观察矩阵时,系统都要重新计算相关的内部数据结构。频繁地设置这些矩阵——例如,每帧上千次——是计算量很大的。通过将世界矩阵和观察矩阵串接成一个世界/观察矩阵,并将该矩阵设置为世界矩阵,然后将观察矩阵设置为单位矩阵,应用程序可以将所需的计算次数减到最少。最好保存一份单独的世界矩阵和观察矩阵的副本在高速缓存中,这样就可以根据需要修改、串接及重置世界矩阵。为清晰起见,本文档中的Direct3D示例很少使用这项优化。
观察变换
本节介绍观察变换的基本概念,并提供有关如何在Microsoft® Direct3D®应用程序中设置观察矩阵的细节。信息被分为以下主题。
什么是观察变换?
观察变换根据观察者在世界空间中的位置,把顶点变换到摄像机空间。在摄像机空间中,摄像机,或观察者,位于原点,朝sz轴的正向看去。再次提醒一下,因为Direct3D使用左手坐标系,所以z轴的正向是朝着场景的。观察矩阵根据摄像机的位置——摄像机空间的原点——和方向重定位世界中的所有物体。
有两方法可以创建观察矩阵。所有情况下,摄像机在世界空间中的逻辑位置和方向会被用作起始点来创建观察矩阵,得到的观察矩阵会被应用于场景中的三维建模。观察矩阵平移并旋转物体,将它们放入摄像机空间中,摄像机位于原点。创建观察矩阵的一种方法是把平移矩阵和围绕每根坐标轴旋转的旋转矩阵合并。这种方法使用了以下通用矩阵公式。
在这个公式中,V是要创建的观察矩阵,T是在世界中重定位物体的平移矩阵,Rx到Rz分别是绕x轴,y轴和z轴旋转物体的旋转矩阵。平移和旋转矩阵基于摄像机在世界空间中逻辑位置和方向。因此,如果摄像机在世界中的逻辑位置是<10,20,100>,那么平移矩阵的目的是沿x轴移动物体-10单位,沿y轴移动-20单位,沿z轴移动-100单位。公式中的旋转矩阵基于摄像机的方向,根据摄像机空间的坐标轴与世界空间的坐标轴间的夹角决定。例如,如果前面提到的摄像机是垂直向下放的,那么它的z轴与世界空间的z轴有90度夹角,如下图所示。
旋转矩阵将角度相同但方向相反的旋转量应用于场景中的建模。这个摄像机的观察矩阵包含了一个绕x轴-90度的旋转。旋转矩阵与平移矩阵合并生成观察矩阵,观察矩阵调整物体在场景中的位置和方向,使它们的顶部朝着摄像机,看起来就好像摄像机在建模的上方一样。
设置观察矩阵
D3DXMatrixLookAtLH和D3DXMatrixLookAtRH辅助函数根据摄像机的位置和被观察点创建一个观察矩阵。
以下示例代码创建了一个用于右手系的观察矩阵。
D3DXMATRIX out;
D3DXVECTOR3 eye(2,3,3);
D3DXVECTOR3 at(0,0,0);
D3DXVECTOR3 up(0,1,0);
D3DXMatrixLookAtRH(&out, &eye, &at, &up);
Direct3D使用应用程序设置的世界矩阵和观察矩阵配置许多内部数据结构。每次应用程序设置一个新的世界矩阵或观察矩阵,系统都要重新计算相关的内部数据结构。频繁地设置这些矩阵——例如,每帧20,000次——计算量非常大。通过将世界矩阵和观察矩阵串接成一个世界/观察矩阵,并将之设置为世界矩阵,然后将观察矩阵设为单位矩阵,应用程序可以将所需的计算量减到最小。最好保存一份单独的世界矩阵和观察矩阵的副本在高速缓存中,这样就可以根据需要修改、串接及重置世界矩阵。为清晰起见,Direct3D示例很少使用这项优化。
投影变换
可以认为投影变换是控制摄像机的内部参数,这选择摄像机的镜头有些相似。这是三种类型的变换中最为复杂的。对投影变换的讨论被分为以下主题。
什么是投影变换?
典型的投影变换就是一个缩放和透视投影。投影变换将视棱锥转变为一个立方体。因为视棱锥的近端比远端小,所以这就产生了离摄像机近的物体被放大的效果,这就是透视如何被应用于场景的。
在视棱锥中,摄像机与观察变换空间的原点之间的距离被定义为D,因此投影矩阵看起来是这样:
通过在z方向平移-D,观察矩阵将摄像机平移到原点。平移矩阵如下所示:
将平移矩阵与投影矩阵相乘(T*P),得到合成的投影矩阵。如下:
下图描绘了透视投影如何将视棱锥转变到新的坐标空间。注意棱锥变成了立方体,同时原点从场景的右上角移到了中心。(译注:应该是从前裁剪平面的中心移到了原点)
在透视变换中,x和y方向的边界值是-1和1。Z方向的边界值分别是,0对应于前平面,1对应于后平面。
这个矩阵根据指定的从摄像机到近裁剪平面的距离,平移并缩放物体,但没有考虑视角(fov),并且用它为远处物体产生的z值可能几乎相同,这使深度比较变得困难。以下矩阵解决了这些问题,并根据视区的纵横比调整顶点,这使它成为透视投影的一个很好的选择。
在这个矩阵中,Zn是近裁剪平面的z值。变量w,h和Q有以下含义。注意fovw和fovh表示视区在水平和垂直方向上的视角,以弧度为单位。
对应用程序而言,使用视角的角度定义x和y的比例系数可能不如使用视区在水平和垂直方向上的大小(在摄像机空间中)方便。可以用数学推导,得出下面两个使用视区大小计算w和h的公式,它们与前面的公式是等价的。
在这两个公式中,Zn表示近裁剪平面的位置,Vw和Vh变量表示视区在摄像机空间的宽和高。
对于C++应用程序而言,这两个大小直接对应于D3DVIEWPORT9结构的Width和Height成员。(译注:虽然直接对应,但是不等价的,因为Vw和Vh位于摄像机空间,而Width和Height位于屏幕空间)
无论决定使用什么公式,非常重要的一点是要尽可能将Zn设得大,因为接近摄像机的z值变化不大。这使得用16位z缓存的深度比较变得有点复杂。
同世界变换和观察变换一样,应用程序调用IDirect3DDevice9::SetTransform方法设置投影矩阵。
设置投影矩阵
以下ProjectionMatrix示例函数设置了前后裁剪平面,以及在水平和垂直方向上视角的角度。此处的代码与什么是投影矩阵?主题中讨论的方法相似。视角应该小于弧度π。
D3DXMATRIX
ProjectionMatrix(const float near_plane, // 到近裁剪平面的距离
const float far_plane, // 到远裁剪平面的距离
const float fov_horiz, // 水平视角,用弧度表示
const float fov_vert) // 垂直视角,用弧度表示
{
float h, w, Q;
w = (float)1/tan(fov_horiz*0.5); // 1/tan(x) == cot(x)
h = (float)1/tan(fov_vert*0.5); // 1/tan(x) == cot(x)
Q = far_plane/(far_plane - near_plane);
D3DXMATRIX ret;
ZeroMemory(&ret, sizeof(ret));
ret(0, 0) = w;
ret(1, 1) = h;
ret(2, 2) = Q;
ret(3, 2) = -Q*near_plane;
ret(2, 3) = 1;
return ret;
} // End of ProjectionMatrix
在创建矩阵之后,调用IDirect3DDevice9::SetTransform方法设置投影矩阵,要将第一个参数设为D3DTS_PROJECTION。
Direct3D扩展(D3DX)工具库提供了以下函数,帮助应用程序设置投影矩阵。
- D3DXMatrixPerspectiveLH
- D3DXMatrixPerspectiveRH
- D3DXMatrixPerspectiveFovLH
- D3DXMatrixPerspectiveFovRH
- D3DXMatrixPerspectiveOffCenterLH
- D3DXMatrixPerspectiveOffCenterRH
W友好投影矩阵
对于已经用世界、观察和投影矩阵变换过的顶点,Microsoft® Direct3D®使用顶点的W成员进行深度缓存中基于深度的计算或计算雾效果。类似这样的计算要求应用程序归一化投影矩阵,使生成的W与世界空间中的Z相同。简而言之,如果应用程序的投影矩阵包含的(3,4)系数不为1,那么为了生成适用的矩阵,应用程序必须用(3,4)系数的倒数缩放所有的系数。如果应用程序不提供符合这样要求的矩阵,那么会造成雾效果和深度缓存不正确。什么是投影矩阵?中推荐的矩阵符合基于w的计算的要求。
下图显示了不符合要求的投影矩阵,以及对同一个矩阵进行缩放,这样就可以启用基于视点的雾。
我们假设在前面的矩阵中,所有变量都不为零。更多有关相对于视点的雾的信息,请参阅基于视点的深度与基于Z的深度的比较。更多有关基于w的深度缓存,请参阅深度缓存。
注意 Direct3D在基于w的深度计算中使用当前设置的投影矩阵。因此,即使应用程序不使用Direct3D变换流水线,但是为了使用基于w的特性,应用程序必须设置一个符合要求的投影矩阵。