转载自 深入探索透视投影变换
作者:Twinsen
原文:https://blog.csdn.net/popy007/article/details/1797121
感谢大神,让我对透视投影变换有了一点比较深刻的理解.
一个顶点从相机空间到到视平面的坐标变化过程
- 透视投影是3D固定流水线的重要组成部分.是将相机空间的点从 视椎体(frustum)变换到规则观察体(Canonical View Volume) ,待裁剪完毕后进行 透视除法.
- 在算法中它是通过 透视矩阵乘法 和 透视除法 两步完成的.
齐次坐标系表示
- 透视投影变换是在齐次坐标下进行的
- 用4个代数分量表示3D几何概念的方式是一种齐次坐标表示
- “齐次坐标表示是计算机图形学的重要手段之一,它既能够用来明确区分向量和点,同时也更易用于进行仿射(线性)几何变换。”—— F.S. Hill, JR
举例:
- (1, 4, 7)如果写成(1,4,7,0),它就是个向量;如果是(1,4,7,1),它就是个点。
下面是如何在普通坐标(Ordinary Coordinate)和齐次坐标(Homogeneous Coordinate)之间进行转换.
- 从普通坐标转换成齐次坐标时
如果(x,y,z)是个点,则变为(x,y,z,1);
如果(x,y,z)是个向量,则变为(x,y,z,0); - 从齐次坐标转换成普通坐标时
如果是(x,y,z,1),则知道它是个点,变成(x,y,z);
如果是(x,y,z,0),则知道它是个向量,仍然变成(x,y,z);
以上是通过齐次坐标来区分向量和点的方式。
从中可以思考得知,对于平移T、旋转R、缩放S这3个最常见的仿射变换,平移变换只对于点才有意义,因为普通向量没有位置概念,只有大小和方向,这可以通过下面的式子清楚地看出:
而旋转和缩放对于向量和点都有意义,你可以用类似上面齐次表示来检测。从中可以看出,齐次坐标用于仿射变换非常方便。
简单的线性插值
- 线性插值,这是在图形学中普遍使用的基本技巧,我们在很多地方都会用到,比如2D位图的放大、缩小,Tweening变换,以及我们即将看到的透视投影变换等等。
- 基本思想是:给一个x属于[a, b],找到y属于[c, d],使得x与a的距离比上ab长度所得到的比例,等于y与c的距离比上cd长度所得到的比例,用数学表达式描述很容易理解:
这样,从a到b的每一个点都与c到d上的唯一一个点对应。有一个x,就可以求得一个y。
此外,如果x不在[a, b]内,比如x < a或者x > b,则得到的y也是符合y < c或者y > d,比例仍然不变,插值同样适用。
透视投影变换
- 选择OpenGL的透视投影变换进行分析
- 主要的思想是:
- 经过相机矩阵的变换,顶点被变换到了相机空间。这时的多边形也许会被视锥体裁剪,但在这个不规则的体中进行裁剪并非那么容易的事情,所以经过图形学前辈们的精心分析,裁剪被安排到规则观察体(Canonical View Volume, CVV)中进行。
- CVV是一个正方体,x, y, z的范围都是[-1,1],多边形裁剪就是用这个规则体完成的。
- 所以,事实上是透视投影变换由两步组成:
- 用透视变换矩阵把顶点从视锥体中变换到裁剪空间的CVV中。
- CVV裁剪完成后进行透视除法
用透视变换矩阵把顶点从视锥体中变换到裁剪空间的CVV中
我们一步一步来,我们先从一个方向考察投影关系。
上图是右手坐标系中顶点在相机空间中的情形。设P(x,z)是经过相机变换之后的点,视锥体由eye——眼睛位置,np——近裁剪平面,fp——远裁剪平面组成。N是眼睛到近裁剪平面的距离,F是眼睛到远裁剪平面的距离。投影面可以选择任何平行于近裁剪平面的平面,这里我们选择近裁剪平面作为投影平面。设P’(x’,z’)是投影之后的点,则有z’ = -N。通过相似三角形性质,我们有关系:
同理,有
这样,我们便得到了P投影后的点P’
从上面可以看出,投影的结果z’始终等于-N,在投影面上。实际上,z’对于投影后的P’已经没有意义了,这个信息点已经没用了。
但对于3D图形管线来说,为了便于进行后面的片元操作,例如z缓冲消隐算法,有必要把投影之前的z保存下来,方便后面使用。因此,我们利用这个没用的信息点存储z,处理成:
这个形式最大化地使用了3个信息点,达到了最原始的投影变换的目的,但是它太直白了,有一点蛮干的意味,我感觉我们最终的结果不应该是它,你说呢?
我们开始结合CVV进行思考,把它写得在数学上更优雅一致,更易于程序处理。假入能够把上面写成这个形式:
那么我们就可以非常方便的用矩阵以及齐次坐标理论来表达投影变换:
其中
注意在把变成
的一步我们是使用齐次坐标变普通坐标的规则完成的。
这一步在透视投影过程中称为透视除法(Perspective Division)。就丢弃了原始的z值(得到了CVV中对应的z值,后面解释),顶点才算完成了投影。
而在这两步之间的就是CVV裁剪过程,所以裁剪空间使用的是齐次坐标主要原因在于透视除法会损失一些必要的信息(如原始z,第4个-z保留的)从而使裁剪变得更加难以处理
这里我们不讨论CVV裁剪的细节,只关注透视投影变换的两步。
小结一下就是:
- 对齐次坐标 (x,y,z,1)进行 投影变换,乘以Project矩阵,变换到了(Nx,Ny,az+b,-z);
- 然后进行 CVV裁剪 ,然后进行 透视除法(Perspective Division)。
矩阵
即我们第一个版本的投影矩阵。你一定会问为什么把Z写成
有三个原因:
-
投影之后的 光栅化阶段 ,要通过 x’和y’对z进行线性插值,以 求出三角形内部片元的z,进行 z缓冲深度测试。
在数学上,投影后的x’和y’,与z不是线性关系,与1/z才是线性关系。而正是1/z的线性关系,即-a+b/z。用这个1/z的线性组合值和x’、y’进行插值才是正确的。详情看《深入探索透视纹理映射》 -
P’的3个代数分量统一地除以分母-z,易于使用齐次坐标变为普通坐标来完成,使得处理更加一致、高效。
-
后面的CVV是一个x,y,z的范围都为[-1,1]的规则体,便于进行多边形裁剪。而我们可以适当的选择系数a和b,使得这个式子在z = -N的时候值为-1,而在z = -F的时候值为1,从而在z方向上构建CVV。
求解a和b
这样我们就得到了透视投影矩阵的第一个版本:
使用这个版本的透视投影矩阵 可以从z方向上构建CVV,但是x和y方向仍然没有限制在[-1,1]中,我们的透视投影矩阵的下一个版本就要解决这个问题。
为了能在x和y方向把顶点从Frustum情形变成CVV情形,先来观察我们目前得到的最终变换结果:
求出出来的z’在[-1,1]范围内,那么就要是对x’和y’进行处理。
我们知道-Nx / z的有效范围是投影平面的左边界值(记为left)和右边界值(记为right),即[left, right],-Ny / z则为[bottom, top]。
而现在我们想把-Nx / z属于[left, right]映射到x属于[-1, 1]中,-Ny / z属于[bottom, top]映射到y属于[-1, 1]中。
你想到了什么?哈,就是我们简单的线性插值,你都已经掌握了!我们解决掉它:
则我们得到了最终的投影点:
下面要做的就是从这个 新形式出发反推出下一个版本的透视投影矩阵。
注意到是
经过 透视除法 的形式,而P’只是变化了x和y分类的形式。所以这里做一个透视除法的逆处理。乘以一个 -z。有:
而这个结果又是这么来的:
最终得到:
M就是最终的透视变换矩阵。相机空间中的顶点,如果在视锥体中,则变换后就在CVV中。如果在视锥体外,变换后就在CVV外。而CVV本身的规则性对于多边形的裁剪很有利。OpenGL在构建透视投影矩阵的时候就使用了M的形式。注意到M的最后一行不是(0 0 0 1)而是(0 0 -1 0),因此可以看出透视变换不是一种仿射变换,它是非线性的。
另外一点你可能已经想到,对于投影面来说,它的宽和高大多数情况下不同,即宽高比不为1,比如640/480。
而CVV的宽高是相同的,即宽高比永远是1。这就 造成了多边形的失真 现象
- 比如一个投影面上的正方形在CVV的面上可能变成了一个长方形。
解决这个问题的方法就是在对多变形进行 透视变换、裁剪、透视除法之后 ,在 归一化的设备坐标(Normalized Device Coordinates)上 进行的 视口(viewport)变换中进行校正,它会把归一化的顶点之间按照和投影面上相同的比例变换到视口中,从而解除透视投影变换带来的失真现象。进行校正前提就是要使投影平面的宽高比和视口的宽高比相同。
便利的投影矩阵生成函数
projection = glm::perspective(glm::radians(45.0f), (float)screenWidth / screenHeight, 0.1f, 100.0f);
Perspective(fov, aspect, near, far)
- fov 即视野,是视锥体在xz平面或者yz平面的 开角角度,具体哪个平面都可以。OpenGL和D3D都使用yz平面
- aspect 即投影平面的宽高比
- near 是近裁剪平面的距离
- far 是远裁剪平面的距离
上图中左边是在xz平面计算视锥体,右边是在yz平面计算视锥体。可以看到左边的第3步top = right / aspect使用了除法(图形程序员讨厌的东西),
而右边第3步right = top x aspect使用了乘法,这也许就是为什么图形APIs采用yz平面的原因吧!