一直对齐次坐标这个概念的理解不够彻底,只见大部分的书中说道“齐次坐标在仿射变换中非常的方便”,然后就没有了后文,今天在一个叫做“三百年 重生”的博客上看到一篇关于透视投影变换的探讨的文章,其中有对齐次坐标有非常精辟的说明,特别是针对这样一句话进行了有力的证明:“齐次坐标表示是计算机图形学的重要手段之一,它既能够用来明确区分向量和点,同时也更易用于进行仿射(线性)几何变换。”——F.S. Hill, JR。
由于作者对齐次坐标真的解释的不错,我就原封不动的摘抄过来:
对于一个向量v以及基oabc,可以找到一组坐标(v1,v2,v3),使得v = v1 a + v2 b + v3 c (1)
而对于一个点p,则可以找到一组坐标(p1,p2,p3),使得 p – o = p1 a + p2 b + p3 c (2),
从上面对向量和点的表达,我们可以看出为了在坐标系中表示一个点(如p),我们把点的位置看作是对这个基的原点o所进行的一个位移,即一个向量——p – o(有的书中把这样的向量叫做位置向量——起始于坐标原点的特殊向量),我们在表达这个向量的同时用等价的方式表达出了点p:p = o + p1 a + p2 b + p3 c (3)
(1)(3)是坐标系下表达一个向量和点的不同表达方式。这里可以看出,虽然都是用代数分量的形式表达向量和点,但表达一个点比一个向量需要额外的信息。如果我写出一个代数分量表达(1, 4, 7),谁知道它是个向量还是个点!
我们现在把(1)(3)写成矩阵的形式:v = (v1 v2 v3 0) X (a b c o)
p = (p1 p2 p3 1) X (a b c o),这里(a,b,c,o)是坐标基矩阵,右边的列向量分别是向量v和点p在基下的坐标。这样,向量和点在同一个基下就有了不同的表达:3D向量的第4个代数分量是0,而3D点的第4个代数分量是1。像这种这种用4个代数分量表示3D几何概念的方式是一种齐次坐标表示。
这样,上面的(1, 4, 7)如果写成(1,4,7,0),它就是个向量;如果是(1,4,7,1),它就是个点。下面是如何在普通坐标(Ordinary Coordinate)和齐次坐标(Homogeneous Coordinate)之间进行转换:
(1)从普通坐标转换成齐次坐标时
如果(x,y,z)是个点,则变为(x,y,z,1);
如果(x,y,z)是个向量,则变为(x,y,z,0)
(2)从齐次坐标转换成普通坐标时
如果是(x,y,z,1),则知道它是个点,变成(x,y,z);
如果是(x,y,z,0),则知道它是个向量,仍然变成(x,y,z)
以上是通过齐次坐标来区分向量和点的方式。从中可以思考得知,对于平移T、旋转R、缩放S这3个最常见的仿射变换,平移变换只对于点才有意义,因为普通向量没有位置概念,只有大小和方向.
而旋转和缩放对于向量和点都有意义,你可以用类似上面齐次表示来检测。从中可以看出,齐次坐标用于仿射变换非常方便。
此外,对于一个普通坐标的点P=(Px, Py, Pz),有对应的一族齐次坐标(wPx, wPy, wPz, w),其中w不等于零。比如,P(1, 4, 7)的齐次坐标有(1, 4, 7, 1)、(2, 8, 14, 2)、(-0.1, -0.4, -0.7, -0.1)等等。因此,如果把一个点从普通坐标变成齐次坐标,给x,y,z乘上同一个非零数w,然后增加第4个分量w;如果把一个齐次坐标转换成普通坐标,把前三个坐标同时除以第4个坐标,然后去掉第4个分量。
由于齐次坐标使用了4个分量来表达3D概念,使得平移变换可以使用矩阵进行,从而如F.S. Hill, JR所说,仿射(线性)变换的进行更加方便。由于图形硬件已经普遍地支持齐次坐标与矩阵乘法,因此更加促进了齐次坐标使用,使得它似乎成为图形学中的一个标准。
以上很好的阐释了齐次坐标的作用及运用齐次坐标的好处。其实在图形学的理论中,很多已经被封装的好的API也是很有研究的,要想成为一名专业的计算机图形学的学习者,除了知其然必须还得知其所以然。这样在遇到问题的时候才能迅速定位问题的根源,从而解决问题。
首先,谈到矩阵,就离不开坐标变换,而3D坐标变换的基础是来源于线性代数。以下摘抄自孟岩的blog:
1 线性空间中的任何一个对象,通过选取基和坐标的办法,都可以表达为向量的形式。这里只要把基看成是线性空间里的坐标系就可以了。在线性空间中选定基之后,向量刻画对象,矩阵刻画对象的运动,用矩阵与向量的乘法施加运动。所以,矩阵的本质是运动的描述。
2 矩阵是线性空间中的线性变换的一个描述。在一个线性空间中,只要我们选定一组基,那么对于任何一个线性变换,都能够用一个确定的矩阵来加以描述。
3 矩阵不仅可以作为线性变换的描述,而且可以作为一组基的描述。而作为变换的矩阵,不但可以把线性空间中的一个点给变换到另一个点去,而且也能够把线性空间中的一个坐标系(基)表换到另一个坐标系(基)去。而且,变换点与变换坐标系,具有异曲同工的效果。线性代数里最有趣的奥妙,就蕴含在其中。
我们先讨论3*3矩阵:
1 矩阵的行序和列序(也称行优先或列优先)仅仅是指矩阵的存储方式,即我们如果用一个4*4数组m存储矩阵,如果m[0][0]-m[0][3]连续存储了矩阵的第一行,那么就是行优先,反之就是列优先。无论是行优先还是列优先,它们代表的数学意义是相同的。
2 如果矩阵是行序,那么它的第一列(m[0][0],m[1][0],m[2][0])就代表X变换,第二列就是Y变换,第三列就是Z变换。
我们来看看为什么:
刚才提到矩阵把线性空间中的一个点给变换到另一个点,不妨称变换前的点为P1(x1,y1,z1),变换后的点为P2(x2,y2,z2):
那么根据线代中的矩阵乘法,P1 * M = P2展开就成了:
x2 = x1*m[0][0] + y1*m[1][0] + z1*m[2][0];
y2 = x1*m[0][1] + y1*m[1][1] + z1*m[2][1];
z2 = x1*m[0][2] + y1*m[1][2] + z1*m[2][2];
看出什么了吗?
如果把一个行序矩阵(接下来讨论的都是行序矩阵,就省略行序二字了)的每一列分别用X,Y,Z三个矢量来表示,那么M就表示为:XYZ三根轴。
现在,矩阵M看起来是不是很像一个坐标系?而P1到P2的变换就是P1分别与X,Y,Z的点积!
也就是矩阵M是把P1从老坐标系变换到新坐标系,而矩阵的三列(三根轴)就分别代表了新坐标系的三根轴在老坐标系中的坐标!
而点积的几何意义其实就是求取投影!所以坐标变换的几何本质(刚才已经讨论过其代数本质),就是把一个点分别投影到三根轴上去而已!
再仔细想一想,P1在XYZ三根轴上的投影,不正是它在新坐标系下的坐标吗?这样一来,坐标变换是不是就太容易理解了呢?
3 所以缩放矩阵M的三根轴,XYZ的模就不等于1,如果某一根轴的模不等于1,那么就有了对应的缩放。所以纯旋转矩阵,前三列的模必然等于1。
4 DX的矩阵是行序,OpenGL的矩阵是列序。我写的引擎,S3D的矩阵也是行序的。
最后说说4*4矩阵:
1 刚才讨论了旋转和缩放,平移如何表示呢?
x2 = x1*m[0][0] + y1*m[1][0] + z1*m[2][0] + Xt;
这里Xt就是表示x方向上的平移。Xt,Yt,Zt共同构成了一个4*4矩阵的第4行的前3列。
2 4*4矩阵的第4列初始情况下一般设置为0,0,0,1,这些值也被称为W,它代表了近大远小的比例关系。此时需要被变换的点(矢量),也必须增加一个元素,成为x,y,z,w,而w的初始值设置为1。注意这里的xyzw并不是四元数的概念,这里的w仅仅是为了配合4*4矩阵运算而存在的。
3 4*4矩阵实际上是为了把多步坐标变换并置而产生的,矩阵并置后可以用一个4*4矩阵代表多次坐标变换,从而显著提高顶点变换的效率。而且参与变换的W最终还可以用于透视校正和深度测试。
4 对于4*4矩阵的上三角阵(3*3矩阵)的理解,可以完全沿用刚才3*3矩阵的讨论。
注1:几何流水线坐标变换基本流程: Local->World->View->Projection->Homo->Viewport->Screen
注2:如果一个矩阵变换是线性的,那么它的转置与求逆等价。我们经常在一些逆变换(比如ShadowMap算法)中看到转置,不必疑惑,它的意义仍然是求逆。
注3:矩阵运算不满足交换律,所以会出现一些有趣的结果,比如M1代表一个旋转操作,M2代表一个平移操作,那么 P*M1*M2 就是先旋转再平移,而P*M2*M1就成了先平移在旋转。我们在实际3D编码中经常遇到这样的问题,就是一个物体是以自己为参照物旋转,还是以世界为参照物旋转,那么按照刚才的描述,假如Mw表示世界变换矩阵,如果Mw左乘M1,再作为最终变换矩阵,那么就是以自己为参照物旋转(很容易理解,因为是先旋转,才变换到世界坐标系嘛,所以这个旋转是定义在本地的),而如果Mw右乘M1,就是先变换到世界坐标系,再旋转了,这时当然就是以以世界为参照物旋转了。
注4:几个最基本的矩阵表达式,感兴趣的可以自己推导一下:
// | x 0 0 0 |
// | 0 y 0 0 |
// | 0 0 z 0 |
// | 0 0 0 1 | 缩放
// | 1 0 0 0 |
// | 0 1 0 0 |
// | 0 0 1 0 |
// | x y z 1 | 平移
// | 1 0 0 0 |
// | 0 c s 0 |
// | 0 -s c 0 |
// | 0 0 0 1 | X旋转 s,c表示旋转角的sin,cos值,下同
// | c 0 -s 0 |
// | 0 1 0 0 |
// | s 0 c 0 |
// | 0 0 0 1 | Y旋转
// | c s 0 0 |
// | -s c 0 0 |
// | 0 0 1 0 |
// | 0 0 0 1 | Z旋转
// | Sx*Sy*Sz + Cy*Cz Cx*Sz Sx*Cy*Sz - Sy*Cz 0 |
// | Sx*Sy*Cz - Cy*Sz Cx*Cz Sx*Cy*Cz + Sy*Sz 0 |
// | Cx*Sy -Sx Cx*Cy 0 |
// | 0 0 0 1 | ZXY旋转的并置