3D数学-裁剪空间与透视投影矩阵的推导
透视投影矩阵的变换本质,是将视锥体变换到裁剪空间中
视锥体的具有六个面,近裁剪面,远裁剪面,左裁剪面,右裁剪面,上裁剪面,下裁剪面
所有超出视锥体的都会被舍弃,也就是被裁剪,我们之后的操作都是对视锥体内部进行计算
接下来我们来分析并解析,如何推导透视投影矩阵
注意:这里我们对于坐标的矢量使用的是行矢量,如果你使用的是列矢量,那么透视投影矩阵要进行转置
在之前的章节中,我们已经知道了:
将点p
投影到z=d
的平面上,通过相似三角形的比例关系,可以获得p'
p[x,y,z]
--> p'[dx/z,dy/z,d]
我们使用齐次坐标理论去改变它
p'=[dx/z,dy/z,d]=[dx/z,dy/z,dz/z]=[x,y,z]/(z/d)
我们将这个分母放入w
,那么四维齐次坐标将会是:[x,y,z,z/d]
,之后我们要将齐次坐标转化为三维坐标只需要将各个分量除以w
即可。
实际上,d
值表示焦距,即从投影平面到投影中心的距离,它的值并不重要,我们将其选择为最方便的值:1
投影在平面上的z值我们并不关心,之所以选择使用d=1
,主要是原z
值对于后续的计算仍然有作用,因此我们要保存它
这样以来,齐次坐标就变为了:[x,y,z,z]
此时,透视投影矩阵是这样的:
[1 0 0 0]
[0 1 0 0]
[0 0 1 1]
[0 0 0 0]
现在,我们考虑裁剪空间的坐标[-1,1]
,之前我们的计算仅仅考虑到了将点投影在平面上,但是这仍然不够。
因为,实际上,我们希望将视锥体内的坐标,转化为裁剪空间下的坐标,而裁剪空间是什么坐标?
裁剪空间下的坐标,x[-1,1] y[-1,1] z[-1,1]
,因此,上述的透视投影矩阵还不够全面,不仅如此,我们还需要考虑到x,y的缩放,以及将z
的值映射到[-1,1]
的范围内。
现在,我们用一些字母参数重新描述透视投影矩阵
[zoomx 0 0 0]
[0 zoomy 0 0]
[0 0 a 1]
[0 0 b 0]
现在我们来解释一下这些字母参数
zoomx
与zoomy
分别表示x
轴与y
轴的缩放(根据相机的缩放控制),这很容易理解
a
和b
,用于将z值映射到[-1,1]
我们首先计算通过该矩阵变换后的齐次坐标
[x,y,z,1][zoomx 0 0 0] = [zoomx*x,zoomy*y,az+b,z]
[0 zoomy 0 0]
[0 0 a 1]
[0 0 b 0]
变换后的齐次坐标为:[zoomx*x,zoomy*y,az+b,z]
将齐次坐标转化为普通坐标:[zoom*x/z,zoomy*y/z,(az+b)/z,1]
之前我们说明了,我们要将视锥体中的点转换到裁剪空间中。现在先处理z
值,将其映射到[-1,1]
我们定义从原点到近裁剪平面的距离为n
,从原点到远裁剪平面的距离为f
。
我们可以列出方程:
(az+b)/z = -1 , z = n
(az+b)/z = 1 , z = f
我们可以求出a
和b
的值
a = (n+f)/(f-n);
b = -2nf/(f-n);
现在我们已经求出第一版的透视投影矩阵了,即
[zoomx 0 0 0]
[0 zoomy 0 0]
[0 0 (n+f)/(f-n) 1]
[0 0 -2nf/(f-n) 0]
我们还没有将x
和y
映射到[-1,1]
的范围内
这里我们首先介绍一种数学方法
简单的线性插值
这是在图形学中普遍使用的基本技巧,我们在很多地方都会用到,比如2D位图的放大、缩小,Tweening变换,以及我们即将看到的透视投影变换等等。基本思想是:给一个x
属于[a, b]
,找到y
属于[c, d]
,使得x
与a
的距离比上ab
长度所得到的比例,等于y
与c
的距离比上cd
长度所得到的比例,用数学表达式描述很容易理解:
(x - a)/(b - a)=(y - c)/(d - c)
这样,从a到b的每一个点都与c到d上的唯一一个点对应。有一个x,就可以求得一个y。
此外,如果x不在[a, b]内,比如x < a或者x > b,则得到的y也是符合y < c或者y > d,比例仍然不变,插值同样适用。
现在我们使用这种方法将x
和y
映射到[-1,1]
范围内
我们再次强调我们获得的齐次坐标:[zoomx*x,zoomy*y,az+b,z]
,我们暂时忽略zoomx
和zoomy
(x/z - left)/(right - left) = (x' - (-1))/(1- (-1))
(y/z - bottom)/(top - bottom) = (y' - (-1))/(1- (-1))
我们计算出x'
和y'
x' = (2x / z)/(right - left) - (right + left)/(right - left)
y' = (2y / z)/(top - bottom) - (top + bottom)/(top - bottom)
我们将重新z
乘回去,获得齐次坐标下的x
和y
x' = (2x)/(right - left) - (right + left)/(right - left)*z
y' = (2y)/(top - bottom) - (top + bottom)/(top - bottom)*z
我们将其写到矩阵里,最终我们就可以获得完整的透视投影矩阵了
[(2)/(right - left) 0 (right + left)/(right - left) 0]
[0 (2)/(top - bottom) (top + bottom)/(top - bottom) 0]
[0 0 (n+f)/(f-n) 1]
[0 0 -2nf/(f-n) 0]
我们将上面这个矩阵称为M(这里其实还不完整,记得把zoomx
,zoomy
乘回去)
最终从视锥体变换到裁剪空间的表示式
[x,y,z,1] M = [x',y',az+b,z]
转化为普通三维坐标
[x'/z,y'/z,(az+b)/z,1]
我们在将相机缩放参数加回去,就是完整的裁剪空间坐标
[zoomx*x'/z,zoomy*y'/z,(az+b)/z,1]
当然,在根据不同约定下,a
和b
的值会有所不同,例如在DirectX风格的约定下,z[0,1]
那么我们之前的计算就会有所不同,矩阵当然也有所不同。在OpenGL下,由于使用的是列矢量进行计算,因此相对于我们这里的矩阵,要进行转置
我们之前讨论的都是透视投影矩阵,对于正交投影矩阵其实很简单,就是最后一列的变化
[0,0,1,0]
–>[0,0,0,1]
,正交投影矩阵不需要根据距离的大小进行缩放。
屏幕空间
我们已经将视锥体裁剪到裁剪空间下了,那么最后我们需要将裁剪空间投影到屏幕空间,屏幕空间当然是2维空间。
首先就是进行标准化齐次除法,也就是除以w
(OpenGL中将此结果称为归一化设备坐标),其实就是将之前的齐次坐标转化为普通三维坐标,我们之前已经做过了。
然后就是缩放x
和y
坐标,以映射到输出窗口上。
输出窗口如下:
裁剪空间的坐标中x
和y
是[-1,1],但是输出窗口的坐标系和这不太一样,他的原点通常位于左上角。
而我们的窗口原点则为[winPosx,winPosy]
,[0,0]
则是屏幕的原点,因为我们的窗口并不一定覆盖整个屏幕设备
我们的画面是在窗口中的,也就是图中较小的框框内。
裁剪空间的齐次坐标:[x,y,z,w]
首先进行齐次除法
x' = x/w
y' = y/w
然后我们要将这个坐标映射到我们屏幕中窗口的坐标中
screenx = (x'*winResx)/2+winCenterx
screeny = -(y'*winResy)/2+winCentery
完整的写法
screenx = (x*winResx)/2w+winCenterx
screeny = -(y*winResy)/2w+winCentery
那么我们会有疑问,z
有什么用?它被存储在深度缓冲,并用于深度测试。这一点有一定基础的读者应该容易理解。
另外w值也还有作用,在光栅化阶段,还需要用到它。