投影矩阵与Reversed Z

投影矩阵与Reversed Z

导言

投影矩阵是图形学中实现物件渲染显示的关键要素,许多的效果实现都绕不开这一项。而由于其推导过程虽然简单,但也不是一目了然,因此在使用的时候经常会忘记其具体过程与结果,而需要反复查阅资料,费时费力;再加上DX API跟OPEN GL API的区别以及UE中惯用的Reversed Z手段,使得这个问题就更为复杂,往往上一次搞清楚之后,下次再碰到类似问题又要重头推导一遍,为了提高使用效率,特此将平时工作中的心得与经验整理出来。

推导

假设投影覆盖范围上下左右前后分别用来表示:

对于DX而言,经过投影矩阵转化后得到的上下左右(Y, X)的范围为[-1, 1],而前后(Z)的范围为[0,1];

对于GL而言,经过投影矩阵转化后得到的上下左右前后(Y, X)的范围都为[-1, 1];

DX API 正交投影矩阵推导

以X轴方向(左右)为例,为了满足条件,就要保证投影后对应的是-1,而对应的是1,且这个变换应该是线性的,那么给出的方程式应该为:

也就是说投影矩阵M的第一个元素 ,第四个元素 ,剩下两个元素为0;

同理,对于Y轴方向也有类似结论。

而对于Z轴方向,对应的是0,而对应的是1,因此有:

综上,可以给出正交投影矩阵的结果如下:

DX API 透视投影矩阵推导

透视矩阵相对于正交矩阵而言,要稍微复杂一点,这是因为透视矩阵其变换关系不再是线性的,通过数学公式描述会比较抽象,为了方便叙述,下面给出一个示意图:

如图,依然以X轴为例,注意,此时的对应的是近平面上的上下左右数值。

图中n为近平面裁剪距离,xs为采样点处的x坐标,与视点的连线与近平面的交点的x坐标为xn,根据三角形相似:

从而可以得出:

注意,这里不能通过三角形近似求得xs处的left&right,之后再仿照公式1求得当前点的x值,这是因为上图中的水平线条指的是z轴,即(0, 0, z),而非((r+l)/2, (t+b)/2, z)轴,因此三角形近似求得的结果会存在问题。

令xn = x,将公式5代入到公式1,得到:

同理:

可以看到,公式6&7中都带有一个分母z,为了得到这个分母,就需要将变换后的w分量变成z,通过归一化的方式来处理,这个修正方法会对z变换产生影响,为了保证变换后的z范围依然对应于[0,1],就有如下方程:

求得:

因此,整个投影矩阵可以写成:

假设n为z轴(深度方向)上的距离,r跟l分别是此深度值下左右边界,那么这个矩阵的[0][0]与[1][1]就分别表征的是水平方向与垂直方向上半个FOV角对应的正切函数的倒数:

在这个投影矩阵下,Device Depth与Linear Depth(Z)之间的变换关系可以表示为:

OpenGL 投影矩阵

OpenGL与DX的区别有以下两点:

  1. OpenGL采用的是右手坐标系,简单来说,相对于DX而言,Z轴方向相反

  2. Z轴投影后的范围也有所不同,DX为[0, 1],而OpenGL为[-1, 1]

按照上述区别来看,如果取[n, f]均为正值,那么对于OpenGL而言,就需要将[-n, -f]会映射到[-1, 1],即[n,f]映射到[1, -1],因此,仿照刚才的推导过程,对于OpenGL的正交投影矩阵,其XY轴不变,Z轴数值首先变成相反数(Z轴方向调转了),其次由于范围的变化,Z轴变换的两个关键参数,其形式将与XY轴保持一致,最终结果为:

对于透视投影矩阵,首先对Z轴(第三列)进行翻转,其次参考前面求取DX透视矩阵a,b的两个方式,可以求得第三行的两个关键参数,其最终结果为:

Reversed Z

在前面推导的透视矩阵中,最终计算得到的深度值d(指的是Device Depth)跟z(通常说的Linear Depth)之间的关系为:

这里的A对应的是矩阵第三行的第四个元素,而B对应的则是。在这种公式下得到的深度跟z的倒数呈线性关系,实际上,深度只是为了表征物件距离相机的远近,可以采用任何的公式实现d与z之间的映射,那么公式13有什么优越之处呢?取n = 5, f = 1005,那么对于DX来说,A = -5*1005/1000 = -5.025; B = 1005/1000 = 1.005;绘制d-z曲线如图所示:

总的来说,这个公式 有以下几个优点:

  1. 这个公式可以很好的契合透视投影近大远小的特征,从上图中可以看到d在z较小的时候增长很快,到后期之后基本平稳,而对于渲染结果而言,则是近景处深度精度高,远景处深度精度低,符合实际需要

  2. 可以通过齐次矩阵借助硬件实现倒数计算

  3. 采用这个公式计算得到的深度d在屏幕空间是线性的,因此在光栅化的时候可以通过线性运算实现面片之间的深度数据插值计算。

  4. 采用这个公式,即使far值取无穷大,对于深度精度的影响也基本可忽略(当然,这里需要将裁剪与far值进行分开处理)

根据浮点数的表示规则,我们知道,浮点数在0附近的数值精度会远远高于其他位置的数值精度,且距离0越远,精度越低。浮点数的精度规则对应到深度计算上来,就会发现,在近平面处的浮点精度最高,如下图所示,在靠近0处占据的有效数据位数就越多,而远平面处的浮点精度最低,占据的有效数据位数就越少,乍听起来似乎还不错,不过由于通常情况下近平面处是没有任何物件的,距离相机最近的物件,通常距离近平面还有一段距离,这就使得近平面处的浮点数精度被浪费了,是否有办法将这些浪费的精度用起来呢?

有人提出了Reversed Z的深度表示方法,关于Reversed Z的历史,最早可以追溯到99年的一篇SIGGRAPH文章,之后在 Matt Pettineo跟Brano Kemen的博客中也有提到,最近的一次是雪崩工作室Emily Persson在SIGGRAPH 2012上的演讲中:Creating Vast Game Worlds 。

Reversed Z的基本思路就是对原有的d进行一次翻转操作:

上图是将之前绘制结果直接01翻转得到的结果,而下图则是采用reversed z的表现,可以看到两者的区别在于浮点数的有效位数在reversed z的时候分布比较均匀。

对于DX而言,reversed z的作用就是将此前[n, f]到[0, 1]的映射改为[1, 0],因此,其透视投影矩阵将变成如下形式:

而如果取r = -l = w, t = -b = h的话,这个矩阵可以简化为:

对于OpenGL而言,其深度d随着z变化的曲线示意图给出如下:

同样,如果将浮点精度有效刻度添加上去的话,结果如下图所示:

可以看到,浮点数有效范围也基本上集中在近平面处,而虽然最终存储到深度贴图中的数据会被转换到[0, 1]范围内,但是在此前一步通过投影矩阵得到的落在[-1, 1]范围的结果其实已经导致精度减半了,要想保留精度,就需要在投影矩阵阶段进行处理,那么,对于这种[-1, 1]的情况,要怎么进行修正呢?

我们尝试将[-n, -f]的映射范围从[-1,1]更改为[0, -2],之后应用reversed z,得到结果如上图,可以看到有效刻度聚集的情况依然没有好转。看起来对于OpenGL这种[-1, 1]的映射实现无法享受reversed z的优势了?在继续思考的同时,我们再来看下虚幻的深度计算公式是怎么样的。

Unreal Engine Depth Equation

先给出Unreal这边的Reversed Z的投影矩阵,其实现有多个重载版本,这里取其中一个有代表性的来举例(为了方便对比,下面给出的矩阵相对于UE源码中的矩阵是转置过的,这也说明,这里给出的矩阵实际上是OpenGL的右手坐标系的):

可以看到,UE矩阵相对于前面推导的DX Reversed Z的矩阵的区别就在于多了一个f是否等于n的判断,且在代码中追踪可以发现大部分情况下,传入的f都是跟n相同的,那么这种做法有什么意义呢?

可以得知,这种时候得到的d跟z之间的关系将变成如下所示:

非常的简洁,在z=n的时候,深度为1,而只有当z趋近于无穷大的时候,深度才为0。也就是说,这种表达方式,放弃了z = f的时候的下边界,而是取无穷远作为远平面。虽然有轻微的基本可以忽略的精度损失,但是可以避免距离过远导致的场景物件的裁剪(当然,不可能把相机可见的所有物件都纳入渲染的范围,UE还有其他手段对物件进行剔除,比如说ScreenSize等),使得渲染的结果更加真实。

另外,这里也可以看到,实际上UE并没有使用OpenGL的[-1,1]的投影范围,而是使用了DX的Reversed Z的投影方式,这样做的目的显然是为了最大程度的保留深度贴图的有效精度范围。

你可能感兴趣的:(投影矩阵与Reversed Z)