图片来源:Joachim Baecker.
术语
在大多数3D工作中,我们参照的依据是欧几里得几何学中的三维空间(X, Y, Z)。但在某些情况下,参照投影几何更适用,除了 X, Y, Z 分量外,增加一个 W 分量,这个四维空间叫做“投影空间”,在四维空间中的坐标叫“齐次坐标”。
为了达到3D软件的目的,“投影的” 和 “齐次的” 可以理解为 "4D"。
不是四元数
虽然四元数跟齐次坐标长得很像,都是 4D 矢量,通常用(X, Y, Z, W)来表示。但是,四元数和齐次坐标是不同的概念,适用的领域也不同。
这篇文章跟“四元数”没有一毛钱关系。
类比2D
了解3D之前,我们先看看2D的投影是怎么回事儿。
想象投影仪在一个屏幕上投影一张2D图片,很容易就可以得到投影图片的 X, Y 分量。
现在,看投影仪和屏幕之间,你就可以发现 W 分量了。
W 分量是投影仪到屏幕的距离
那么 W 分量的作用是什么呢?想象一下,如果我们移动投影仪的位置,来增加或减少 W分量的值,那么投影出的2D图片会发生什么?如果将投影仪靠近屏幕,2D图片缩小,如果投影仪远离屏幕,2D图片放大。没错,这就是 W 分量的作用。
W 分量的值,影响了投影出 2D 图片的大小
应用到3D
到目前为止,还没有一个3D投影仪,很难想象3D中的投影几何,但是 3D 下的 W 分量与 2D 的作用相同。当 W 增大,坐标被拉伸;W 缩小,坐标被压缩, W 对3D坐标做缩放变换。
W = 1时
通常,给3D编程初学者的建议是,无论什么时候将 3D 坐标转换为 4D 坐标时,让 W=1。原因是,当缩放坐标的 W 为1时,坐标不会增大或缩小,保持原有的大小。所以,当 W=1,不会影响到 X, Y, Z 分量的值。
因此,当谈论到 3D 计算机图形学时,当坐标中 W=1时被称作“正确”。如果渲染使用 W>1 的坐标,每一个3D物体看起来都会变大,反之,W<1 的坐标中,3D物体会变小;如果渲染时试图让 W=0,那么你的程序会奔溃,当做透视除法的时候除数为0;如果 W<0,每一个物体都会上下翻转,水平翻转。
在数学中,没有所谓的“不正确”的齐次坐标,使用齐次坐标时让 W=1 仅仅是用于计算机图形学中的投影转换。
数学原理
现在,让我们来看一些例子,了解数学原理
在距屏幕3米远的位置放一个投影仪,投影出一个点(15, 21)在 2D 图像中,相应的投影坐标中的向量为 (X, Y, W)=(15, 21, 3)。
现在,想象推动投影仪向屏幕靠近,直到距离1米,越靠近屏幕投影,投影出的图像越小。投影仪靠近了3倍,因此图像缩小了3倍。如果我们将原向量的 X, Y, W 分量都除以 3,我们得到一个新向量 W=1:
投影出的点在坐标中的新位置(5, 7)
这就是怎样将一个“不正确”齐次坐标转换到一个“正确”坐标的方法:所有分量除以 W,这个过程对 2D 和 3D 同样适用。
通过给向量乘以 W 的倒数,来实现向量的所有分量除以 W,下面是一个4D的例子:
用 GLM 库编写,类似如下实现:
glm::vec4 coordinate(10, 20, 30, 5);
glm::vec4 correctCoordinate = (1.0/coordinate.w) * coordinate;
//now, correctCoordinate == (2,4,6,1)
在计算机图形学中使用齐次坐标
就像开始提到的,针对3D 计算机图形学中,有些情况下使用齐次坐标很有用,下面我们来看看这些情况:
3D 坐标中的转换矩阵
旋转和缩放的转换矩阵只需要3列,但是为了处理平移,至少需要4列矩阵,这就是为什么矩阵变换通常用4×4的矩阵。然而,4列矩阵不能与3维向量相乘,只能与4维向量相乘,这就是为什么我们使用齐次的4维向量取代3维向量。
4列矩阵只能与4维向量相乘,这就是为什么我们常常使用齐次的4维向量取代3维向量。
通过齐次坐标处理矩阵变化,第4维 W 分量通常不用改变。从3D转换到4D,只需将 W 分量设置为1,并且经过变换矩阵处理后,W 分量的值任为1,这意味着我们忽略 W 分量即可转换回 3D坐标。这个对目前为止大多数的矩阵变化都适用,如平移、旋转、缩放。需要注意的例外是投影矩阵会影响 W 分量。
透视变换
在 3D 世界,物体离相机越远看起来越小,这个现象叫做透视。在镜头中,如果猫离相机足够近,远处的大山会比猫看上去小。
在3D计算机图形学中,透视是通过变换矩阵改变向量的 W 分量来实现的。在变换到相机空间后(对向量应用了相机矩阵),还没有进行投影变换(还没有对向量应用投影矩阵),每个向量的 Z 分量表示了距离相机的距离。因此,Z 分量越大,矢量应该越小。W 分量影响这个缩放,所以投影矩阵用 Z 分量的值改变 W 分量的值。
在3D计算机图形学中,透视是通过投影矩阵变换,改变每一个向量中 W 分量的值来实现透视的
下面看一个透视例子,通过投影矩阵变换到齐次坐标。
注意投影矩阵是怎样用 Z 分量改变 W 分量的。
经过投影矩阵透视变换后,每一个向量即经过了“透视除法”。
透视除法只是将齐次坐标中的 W 分量转换为1的专用名词
继续上面的例子,透视除法这步如下:
完成透视除法后,W分量就没用了,我们就得到了一个完全符合3D透视投影规则的3D坐标。
在GLM中,透视投影矩阵可以通过使用 glm::perspective
或 glm::frustum
方法来创建。在老式的 OpenGL 中,通常使用gluPerspective
或 gluFrustum
方法。在 OpenGL 中,在顶点 shader 作用了每一个顶点后,自动进行透视除法。这就是顶点 shader 中 main 方法输出的 gl_position 变量,是4维向量,而不是3维向量。
设置平行光
齐次坐标中的一个属性,是允许有一个无限远的点(或无限长的向量),在3D坐标中这个是不允许的。当 W=0 时,这点表示无限远的一个点。如果你尝试将一个 W=0 的齐次坐标转换为一个普通的 W=1的齐次坐标,这会导致4次除0操作:
这意味着,不能将 W=0 的齐次坐标转换到 3D 坐标。
那这有什么用呢?用处说来就来了,平行光可以认为是一个无限远处的点光源。当一个点光源在无限远的位置,光线就会变成平行的,并且所有光线都在同一方向,这就是平行光的基本定义。想想太阳吧。
所以在传统的3D图形中,平行光可以通过改变点光源位置向量中的 W 分量来表示,当 W= 1时,是一个点光源;当 W= 0 时,是一个平行光。
在实现光照代码时,这更多的是一种传统的约定,但不是一种有用的方法。因为平行光和点光源的行为不同,通常分开实现。一个经典的光照 shader 实现如下:
if (lightPosition.w == 0.0) {
//directional light code here
} else {
//point light code here
}
总结
齐次坐标有一个额外的维度叫 W 分量,用来缩放X, Y, Z三个分量的值。平移和透视投影的矩阵变换只能在齐次坐标中使用,所以在3D计算机图形学中,当 W=1时,X, Y, Z 分量被称为“正确的”。任何齐次坐标,只要 W 不为0,都可以通过将每一个分量除以 W 来转换到 W=1的向量。当 W=0 时,这个坐标表示无穷远的一个点(或者表示无限长的一个向量),通常用于表示平行光的方向。