投影变换是整个渲染管线里,设计得最复杂的,也最巧妙的一次变换。
其实基本理解了这些变换,对整个渲染管线就有了大概的认识了。
毕竟是理科生,所以这一篇不直接给最终矩阵,我们一步步来讨论,
为什么投影变换矩阵要这么设计?
为什么裁剪在透视除法之前?
为什么观察坐标系是右手坐标系?
如何实现的近大远小?
如何实现的erlay-z?
深入探索透视投影变换_大数据_Popy007(Twinsen)的专栏-CSDN博客blog.csdn.net上面这篇博客是我认为写的最好的关于透视投影变换的文章。本文很多内容也直接抄过来了。既然有这篇文章,为什么我还要写一篇,主要是觉得部分推导顺序,以及关于 裁剪在透视除法之前等问题的原因不准确,所以放在后面讨论。
1.投影变换的目的
坐标转换到观察空间后,由于直接使用摄像机的平截头体进行裁剪比较复杂(平截头体的边界方程求交困难),所以需要将其转化到裁剪空间(Clip空间 )。
从观察空间到裁剪空间的变换叫做投影变换。
裁剪空间变换的思路是,对平截头体进行缩放,使近裁剪面和远裁剪面变成正方形,使坐标的w分量表示裁剪范围,此时,只需要简单的比较x,y,z和w分量的大小即可裁剪图元。
虽然叫做投影变换,但是投影变换并没有进行真正的投影。
这一篇主要讲透视投影
2.透视投影变换分2步:
1. 从Frustum内一点投影到近剪裁平面
2. 由近剪裁平面缩放成规则观察体(Canonical View Volume),CVV空间,得到clip坐标
(此时没有除以W变成3D坐标,是齐次坐标)
相机空间中的顶点,如果在视锥体中,则变换后就在CVV中。如果在视锥体外,变换后就在CVV外。CVV本身的规则性对于多边形的裁剪很有利。
3.透视投影推导:
投影坐标系有两种矩阵:透视矩阵 和 正交矩阵
我们选择OpenGL的透视投影变换进行分析:
第一步:投影到近剪裁平面
我们先从一个方向考察投影关系
设P(x,z)是经过相机变换之后的点,
视锥体由eye--眼睛位置,
np--近裁剪平面,
fp--远裁剪平面组成。
N是眼睛到近裁剪平面的距离,
F是眼睛到远裁剪平面的距离。
选择近裁剪平面作为投影平面。设P"(x",z")是投影之后的点,则有z’ = -N。通过相似三角形性质:
X/X' = Z/Z' = Z/ -N
所以
X' = -N * X/Z
同理,有
Y' = -N * Y/Z
这样,我们便得到了P投影后的点P"
p' = ( -N *x/z -N *y/z -N)
从上面可以看出,当Frustum内的点投影到近剪裁平面的时候,投影的结果z"始终等于-N,在投影面上。实际上,z"对于投影后的P"已经没有意义了。
3.1 充分利用Z’值,一步步对Z’改造
0)后面在进入片元操作之前还有erlay-Z测试,有必要把投影之前的z保存下来,方便后面使用。
由第一幅图可知,所有位于线段p'p上的点,最终都会投影到p'点,那么如果这条线段上有多个点,如何确定最终保留哪一个呢?当然是离观察这最近的这个了,也就是深度值(z值)最小的。所以z'坐标需要保存p点的z值。
那么 p' = ( -N *x/z -N *y/z Z)
又因为在光栅化之前,我们需要对z坐标进行插值
1)后面投影之后的光栅化阶段,要通过x'和y'对z进行线性插值,以求出三角形内部片元的z,进行z缓冲深度测试。
从X' = -N * X/Z 可以看出 ,投影后的x'和y',与z不是线性关系,与1/z才是线性关系。所以用1/z的线性组合值和x'、y'一起插值才是正确的。
2)同时为了保证近处精度更高,我们使用Z坐标的的倒数
z' = 1/z;
3)P"的3个代数分量统一地除以分母-z,易于使用齐次坐标变为普通坐标来完成。
所以我们写作: z' = -1/z;
所以:我们暂时得到的新的点P" 表示为:
P' = ( -N *x/z -N *y/z -1/z)
第2步:缩放到CVV空间
CVV是一个x,y,z的范围都为[-1,1]的规则体,便于进行多边形裁剪。
我们先不管x和y,先映射Z到[-1,1]之间。
要进行映射,常用的主要是wrap和线性映射
我们直接使用线性公式:
我们对 -1/z 适当的选择系数a和b,也就是
a + b*( -1/z )
这个式子在z = -N的时候值为-1,而在z = -F的时候值为1,从而在z方向上构建CVV。
所以最终记录的Pz值:
z' = -(a + b/z)
所以:我们得到的新的点 暂时表示为:
3)我们为了在GPU中运算更快,同时能合并之前的矩阵变换,所以我们最终要使用矩阵形式来做透视变换。 所以这一步还是要转为齐次坐标
所以:
p' = ( -N *x/z -N *y/z -(a + b/z) 1)
对于齐次坐标,我们可以对 所有项乘以一个相同的值,不会改变他的位置。
p' = p'* (-Z) = ( -N *x -N *y (az + b) -z)
所以我们可以凑出下面的矩阵乘法:
这一步在透视投影过程中称为透视除法(Perspective Division),这是透视投影变换的第2步,经过这一步,就丢弃了原始的z值(得到了CVV中对应的z值),顶点才算完成了投影。而在这两步之间的就是CVV裁剪过程,所以裁剪空间使用的是齐次坐标。
为什么不把透视除法整合到矩阵中?我们在后面详细讨论原因。
接下来我们就求出a和b:因为CVV是 -1到1范围,所以:
所以:
我们得到了透视投影矩阵的第一个版本:
现在映射X和Y到[-1,1]之间
上面版本的透视投影矩阵可以从z方向上构建CVV,但是x和y方向仍然没有限制在[-1,1]中。
为了能在x和y方向把顶点从Frustum情形变成CVV情形,我们开始对x和y进行线性插值映射到[-1, 1]:(在 x,y项没有乘以-Z之前的范围)
对于 :
p' = ( -N *x/z -N *y/z -(a + b/z) 1)
Nx / z的有效范围是[left, right]
Ny / z 为[bottom, top]
注:投影平面的左边界值(记为left)和右边界值(记为right)
把x,y代入第一个版本,我们得到了最终的投影点:
注意到我们之前对 p’乘以了 -z, 但上面的 X,Y 还没有乘以 -z
所以对 X,Y项乘以 -Z
则我们最终的透视变换矩阵:
注意到M的最后一行不是(0 0 0 1)而是(0 0 -1 0),因此可以看出透视变换不是一种仿射变换,它是非线性的。
将right、left、top以及bottom等参数去掉
fov即视野,是视锥体在xz平面或者yz平面的开角角度,具体哪个平面都可以。OpenGL和D3D都使用yz平面。
aspect即投影平面的宽高比。
near是近裁剪平面的距离
far是远裁剪平面的距离。
我们回头讨论下几个问题
为什么OpenGL和D3D计算FOV都使用yz平面?
上图中左边是在xz平面计算视锥体,右边是在yz平面计算视锥体。可以看到左边的第3步top = right / aspect使用了除法(图形程序员讨厌的东西),而右边第3步right = top x aspect使用了乘法,这就是为什么图形APIs采用yz平面的原因
为什么要先做裁剪,再做透视除法?
我们看到博客里说:“我们可以看出为什么要先做裁剪,再做透视除法,因为透视除法会丢失w’信息,也就是原始的z值,导致Z方向无法做裁剪。”
这个理解是不准确的。
我总结了三个原因:
1.第一个原因,避免裁剪出来的新三角形有畸变
对于齐次坐标, w'同时影响 x’,y‘,z’ 值。 因为 CVVx = x'/w CVVy = y'/w CVVz = z'/w
不是单纯导致Z方向无法做裁剪,因为X,Y平面也可以因为clip 切割三角形,而这个三角形,是在Z方向需要插值的。
硬件进行clip操作要在透视除法之前做,此时的视锥体xyz范围是[-w,w](d3d的 z裁剪范围是在(0,w)范围裁剪),是因为clip之后可能会产生很多新的三角形,所以也会得到新的顶点(新的顶点的 x‘ y' z' w' 都是相交计算得到的,w’要通过x' = Nx ; y' =Ny; z' = az+b ; 来插值得到,这样后面除以w’得到cvv坐标,才能还原成投影到近平面的近大远小效果,因为w‘= -z 是深度信息),之后再进行透视除法,转化到[-1,1]范围
如果除以w后,丢失w信息,再裁剪,那么只能计算新三角形的新顶点的x‘ y' z' 值,w'没有计算。
对于X,Y平面超出被切割的三角形是没关系的,但对于Z方向上被切割的三角形,因为w’信息丢失,
那么clip的边缘相交切割的得到的新三角形的顶点 插值,是基于-Nx/z ; -Ny/z; -(az+b)/z ; 来插值得到w 。 我们可以看到这个插值,不再基于z线性,而是基于 1/z 线性,那么得到的三角形就是有畸变的。
2. 进行透视除法之前会进行裁剪,会把z=0的部分剔除掉,从而保证透视除法的时候不会存在z=0的顶点。
3. 优化性能
裁剪能剔除部分顶点(即使生成了新顶点,很多时候剔除得更多),减少部分顶点进行 除以w的除法操作,提高性能。
而之后的深度测试,我们为了减少近处的z-fight效应,本来就想近处精度更高,所以要基于 1/z线性,
经过透视投影后,顶点的原始z信息丢了,但得到了[-1,1]中的z,顶点之间的z顺序不会有变,不会影响通过NDC的z光栅化得到的fragment的z进行depth test。
另外以下是忘记看的哪里说的了,不知道正确与否:
在真正的渲染管线里(比如DX),为了优化性能,
并不是裁剪后进行透视除法,得到clip坐标。
而是,直接把透视除法整合到投影矩阵里,直接得到Clip坐标, 然后 乘以1/W来得到CVV坐标来剔除
为什么本地坐标系,世界坐标系、投影坐标系都是左手坐标系,观察坐标系是右手坐标系?
实际上,在Opengl中,本地坐标系和世界坐标系都是右手坐标系,也就是说,直到投影空间中才变换为左手。而unity中的本地坐标系和世界坐标系是左手系统,所以显得观察坐标系比较的特别。
所以Opengl里 不需要转换Z轴, Unity(DX)里需要转换一次Z轴为负方向