从物体的本体坐标系到二维的显示器进行显示,需要经历一系列的变换,从模型变换、视图变换、投影变换到最后的视口变换。整个过程与我们日常的拍摄照片类似,例如我们去照相馆拍全家福,我们可以分如下三步:
- 首先需要排好顺序,摆好造型,这步操作就是模型变换(Model Transformation)。
- 然后摄像师放好照相机,将相机对准拍摄对象 (相机自身的旋转),进行相机的调焦和对焦(选择投影平面)。这相当于的视图变换(View/Camera Transformation)
- 最后就是按下相机的快门,拍出照片,这相当于投影变换(Projection Transformation)
这三种变换通常称为MVP变换,其对应的矩阵即MVP矩阵。
模型变换
模型变换将单个模型从本体坐标系转换到世界坐标系,主要包括位移、旋转和缩放等操作。D3DXMatrixTranslation进行平移操作,D3DXMatrixRotation*( X,Y,Z,Axis )//进行模型的旋转操作,
D3DXMatrixScaling进行模型的缩放操作。多个操作按顺序进行,使用D3DXMatrixMultiply生成最终的变换矩阵。
视图变换
视图变换就是在世界坐标系中设置一个摄像机的过程,并将顶点由世界坐标系转换到视图空间,在视图空间中,摄像机位于坐标原点,UP方向、右方向与观察方向构成左手坐标系。
在世界坐标系中,摄像机并不一定位于坐标原点,并且观察方向不一定指向Z轴正方向,对于投影变换及其他的一些操作来说,如果不满足这两个条件,后续的的操作就会变得非常低效,所以为了提高效率,我们需要进行视图变换。根据运动的相对性,视图变换主要有下面两个作用。
- 移动摄像头,使其位于world space的坐标原点
- 旋转摄像头,使将摄像机的三个向量分别与坐标轴对齐。其朝向z轴正方向,也就是视线由原点指向z轴正方向。上方向与Y轴重合,右方向与X轴重合。
将摄像机看作世界坐标系的一个原本位于原点的模型,进行视图变换时,摄像机不动,所有位于世界坐标系空间中的模型都进行一次摄像机世界变换的逆变换。摄像机看的内容与物体不动,摄像机架在世界坐标系中的结果相同。
位移矩阵T将摄像机位置由P点移动到原点,
旋转矩阵A将摄像机坐标轴与世界坐标轴对齐。
写成矩阵形式如下:
所以A为矩阵B的逆矩阵。由于B为正交矩阵,它的逆矩阵等于他的转置矩阵。可得:
最终的矩阵先进行移动,再旋转,由矩阵乘法可得:
转置表示逆变换,负号表示逆变换,点积表示位移在各个坐标轴上的投影。
D3DXVECTOR3 position(0, 10, 0); //摄像机上移动10.
// the camera is targetted at the origin of the world
D3DXVECTOR3 target(0.0f, 10.0f, 10.0f);
// the worlds up vector
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
D3DXMATRIX v1;
D3DXMatrixLookAtLH(&v1, &position, &target, &up);
D3DXMATRIX v2;
D3DXMatrixTranslation(&v2, 0, -10, 0);//把场景中的所有物体下移10
//v1==v2
D3DXMATRIX v3;
D3DXVECTOR3 up2(0.0f, -1.0f, 0.0f);//上方向沿X轴顺时针旋转180°。
D3DXMatrixLookAtLH(&v3, &position, &target, &up2);
D3DXMATRIX v4,v5;
D3DXMatrixRotationX(&v4, -D3DX_PI);//把场景中的所有物体逆时针旋转180°
D3DXMatrixMultiply(&v5, &v2, &v4);
//v3==v5
以观察方向look(矢量d)为基准(Z轴),通过叉积计算另外两个坐标轴的方向,right(矢量)对应X轴,up(矢量u)对应Y轴,XYZ构成左手坐标系。视图变换代码:
void Camera::getViewMatrix(D3DXMATRIX* V)
{
//以观察方向的基准,其他轴与它正交。
//构成左手坐标系。
// Keep camera's axes orthogonal to eachother
D3DXVec3Normalize(&_look, &_look);
D3DXVec3Cross(&_up, &_look, &_right);
D3DXVec3Normalize(&_up, &_up);
D3DXVec3Cross(&_right, &_up, &_look);
D3DXVec3Normalize(&_right, &_right);
// Build the view matrix:
float x = -D3DXVec3Dot(&_right, &_pos);
float y = -D3DXVec3Dot(&_up, &_pos);
float z = -D3DXVec3Dot(&_look, &_pos);
(*V)(0,0) = _right.x; (*V)(0, 1) = _up.x; (*V)(0, 2) = _look.x; (*V)(0, 3) = 0.0f;
(*V)(1,0) = _right.y; (*V)(1, 1) = _up.y; (*V)(1, 2) = _look.y; (*V)(1, 3) = 0.0f;
(*V)(2,0) = _right.z; (*V)(2, 1) = _up.z; (*V)(2, 2) = _look.z; (*V)(2, 3) = 0.0f;
(*V)(3,0) = x; (*V)(3, 1) = y; (*V)(3, 2) = z; (*V)(3, 3) = 1.0f;
}
视图变换的逆变换:
前三行代表摄像机的三个轴向量,最后行代表摄像机的位置坐标点。
投影变换(Projection transformation)
摄像机对好要拍摄的物体后,就差最后按下快门变成照片这一步了,而这一步也就是我们的投影变换,即从三维变成二维。在图形学中,投影变换分为正交投影和透视投影。
透视投影(Perspective projection)
透视投影会有近大远小的现象,类似于我们的人眼或者照相机拍照,这种投影更具有真实感,被广泛使用。从图中我们可以看出Far clip plane的面积要大于Near clip plane,因此我们摄像机所观测的区域不再是一个长方体,而是变成了一个四棱台(即四棱锥去掉顶部),也称为视锥体(Frustum )。视图变换后,透视投影在坐标系中的样子如下图,D3D中变换后的为对称视锥体,N和F均为两个平面的中心点,即L1N = -NR1, NB1 = -NT1,且近平面也是投影平面。
投影变换是把相机观测的视锥体压缩成一个标准立方体,变换过程分为两步:
- 我们先通过某种变换把这个视锥体压缩成一个长方体
- 再把长方体移到原点,再压缩成标准立方体,即进行正交投影变换
最终的变换矩阵为第一步的压缩变换矩阵,再乘以一个正交投影矩阵即可。由于前面的视图变换已经将摄像机设置到原点,方向与世界坐标轴对齐,后续的变换只涉及平移和缩放,不涉及旋转。
需要将T2-B2压扁到T1-B1的高度即可。由相似三角形可得:。
直线T1T2 上的任意一点(x,y,z),其Y值只需乘以 z/n 即可与T1点的Y值相等。对XZ平面上的点同理可求。因此对应视锥体里的任意一点 (x,y,z),我们只需要将其x和y的值乘以 z/n 即可使该视锥体变为长方体。
此处,变换后的深度值还是未知的。这里需要引入齐次坐标的概念,设我们有个常量k,通过齐次坐标定义可知 (x,y,z,1) 是等价于 (kx,ky,kz,k) 的,那么:
因此我们可以把上面的矩阵修改为:
利用近平面和远平面的深度值不变性可得:
1.Near clip plane上的任意点,设为(i,j,n),它的z值不变依旧为 n。
2.Far clip plane上的任意点,设为(k,l,f),它的z值不变依旧为 f。
由于z值与i、j,k、l无关,所有前面两个?均为0,另外两个设为M3 和M4
M3*n+M4=n*n
M3*f+M4 =f*f
计算可得压缩矩阵为:
矩阵中,蓝色表示压缩比与深度相关,不是常量。红色和蓝色是对X的压缩,绿色和蓝色是对Y的压缩,黄色和蓝色是对深度Z的拉伸。 因为在变换过程中,除了远平面和近平面上的点外,其他任意点的深度值Z都会变大。视锥体中的点 (x,y,z)的,Z值的变化为:
可见视锥体中的点在变换后离摄像机更远了。
正交投影(Orthographic projection)
正交投影示意图如下:
正交投影没有近大远小的现象,视线是互相平行的,类似于平行光照,工程中的三视图就是这种变换。图中的Near clip plane和一个Far clip plane,分别代表该摄像机能看见的最近的距离以及最远距离,即摄像机只能看见两个平面之间的物体。在正交投影中,Near clip plane的大小等于Far clip plane,能被摄像机拍摄到的空间即为一个长方体。
由于已经进行过了视图变换,即摄像机在原点,看向-z轴方向,摄像机的y轴与世界坐标y轴重叠。因此我们的所拍摄的空间也能够确定下来,如下图:
该空间的宽度即为L点和R点的距离,设L点和R点的x轴坐标分别为 l 和 r ,高度即为T点和B点的距离,设T点和B点的y轴坐标分别为 t 和 b ,长度即为N点和F点的距离,设N点和F点的z轴坐标分别为 n 和 f 。在D3D中:r = -l 和 t = - b,且 f > n,因为是看向z的正方向,所以更远的位置z的值越大。
上面的定义也证明了我们视图变换的必要性,如果没有视图变换把摄像机给规定到原点等,我们就很难用上面这些变量来形容出摄像机所观测的空间。
从图中我们可以看出,把空间中的物体的z轴都设置为0,那么空间中的所有物体都会被投影到xy平面上,也就是变为二维的了。
然而更规范化的做法是将上诉摄像机所观测的空间(即长方体,设为 S )变换成一个标准立方体(canonical cube)。何为标准立方体?即以原点为中心,边长为2的立方体,也就是立方形在x,y轴上都是从-1到1,z轴为0到1(D3D的设置,其他库可能为-1到1)。
整个变换过程就是我们的正交投影变换,其对应矩阵即为正交投影变换矩阵。该变换我们可以分为如下两步(这也体现了视图变换的重要性,使得我们做投影变换变得很简单):
- 平移变换,将长方体S的中心点平移到原点。
- 缩放变换,将长方体S的长、高缩放到2,宽度(深度)方向缩放到1
同样的,我们来看看这两个变换的矩阵:
首先是移到原点的平移变换,即把长方体S近平面的中心点移动到原点,在D3D中:近平面中心点X、Y坐标均为0 ,Z坐标为n。因此平移矩阵(设为 M )即为:(只将Z轴平移-n个单位)
然后将长方体S变为边长为2的立方体的缩放变换,我们设x轴方向的为宽度,即为 r-l ,y轴方向的为高度,即为 t-b ,z轴方向的为宽度,即为 n-f 。设垂直fov为2∅,宽高比为a。则:
缩放矩阵(设为 S )为
最终的正交投影矩阵为:
红框表示先位移再缩放。
注:该变化会导致物体的被拉伸(因为长方体变成了立方体),在后续的视口变换过程中会再对此进行处理。
透视投影的最终矩阵为压缩矩阵与正交变换矩阵的乘积:
代码如下:
D3DXMATRIX project;
D3DXMatrixPerspectiveFovLH(&project, D3DX_PI/3.0f, width / (float)height, 1.0f, 1000.0f);
D3DXMATRIX project2 = GetPerspectiveMatrix(D3DX_PI / 3.0f, width / (float)height, 1.0f, 1000.0f);
//project = project2
D3DXMATRIX D3DDemo::GetPerspectiveMatrix(float fov, float a, float n, float f)
{
D3DXMATRIX proj;
ZeroMemory(&proj, sizeof(D3DXMATRIX));
proj.m[0][0] = 1 / (a * tan(fov * 0.5));
proj.m[1][1] = 1 / (tan(fov * 0.5));
proj.m[2][2] = f / (f-n);
proj.m[2][3] = 1.0f ;
proj.m[3][2] = (-n*f) / (f-n);
return proj;
}
视口变换(viewport transformation)
完成透视投影变换后的标准立方体需要转换到屏幕坐标,通常是屏幕上的一个显示区域。屏幕的左上角为坐标原点,需要将x,y 由[-1,1]变换到[0,width]和[0,height],y正方向由↑变成↓。深度z由[0,1]变换到[MinZ,MaxZ]。
变换时先进行缩放变换,再进行位移变换。变换矩阵如下:
X和Y为视平面在左上角的坐标点。Width为视平面的宽度,Height为视平面的高度,MinDepth为顶点的最小深度(Z缓存),MaxDepth为顶点的最大深度(Z缓存),一般MinDepth为0,MaxDepth为1。对角线元素表示缩放,负号表示反向。第四行表示原点的位移,变换后原点移到视口中心,深度为MinZ。
经过上述四个变换,我们就已经成功的把模型由本体坐标系转换到屏幕上了!。
附录:在窗口进行鼠标点击,选取三维空间中的物体时,需要进行逆向变换,将视口中的点变换回投影窗口中的点。
逆变换先进行位移,再进行缩放变换。变换矩阵为:
代码如下:proj(0,0)为投影变换x的缩放系数,proj(1,1)为投影变换y的缩放系数。
d3d::Ray CalcPickingRay(int x, int y)
{
float px = 0.0f;
float py = 0.0f;
D3DVIEWPORT9 vp;
Device->GetViewport(&vp);
D3DXMATRIX proj;
Device->GetTransform(D3DTS_PROJECTION, &proj);
px = ((( 2.0f*x) / vp.Width) - 1.0f) / proj(0, 0);
py = (((-2.0f*y) / vp.Height) + 1.0f) / proj(1, 1);
d3d::Ray ray;
ray._origin = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
ray._direction = D3DXVECTOR3(px, py, 1.0f);
return ray;
}