在OpenGL中,glFrustum的分解

3

我正在尝试理解使用glFrustum()在OpenGL中创建的投影矩阵,以及将其转换为x=[-1,1]y=[-1,1]z=[-1,1]标准化设备坐标,以及进行的一系列4x4矩阵乘法,得出的投影矩阵。

我了解最终结果(在NDC中)是通过在应用连续变换后除以w组件获得的,但这些连续变换是什么?glFrustum()函数将视锥体截断为一个平截头体,并将其转换为正交投影矩阵。这个矩阵可以与模型视图矩阵相乘,从而得到剪裁空间坐标系下的顶点坐标。然后,透视除法将这些坐标转换为规范化设备坐标系下的坐标,最终呈现在屏幕上。
这是关于表达式 中的矩阵T(涉及nearfarleftrighttopdown变量)。每个T仅表示缩放、平移、旋转或剪切操作/变换吗?
我看过一篇与此相关的帖子:Is OpenGL LH or RH,以及Projection Matrix tutorial,但我仍然不明白基本操作(即缩放、平移、旋转或剪切操作)的具体实现。
1个回答

6
透视变换矩阵不能仅分解为缩放、平移、旋转和剪切操作。这些操作都是仿射的,而透视变换是投影的(不是仿射的;特别地,透视不会保留线的平行性)。
“目标体积”是规范化设备空间中的轴对齐立方体。在标准的OpenGL中,这个立方体在所有维度上从-1到1(Direct3D和Vulkan也使用稍微不同的约定:[-1,1]用于x和y,但[0,1]用于z。这意味着矩阵的第三行看起来有点不同,但概念相同)。
投影矩阵是这样构建的,使金字塔锥体被转换为规范化体积。OpenGL还使用了这样的约定,在眼睛空间中,摄像机朝向-z。要创建透视效果,只需通过将投影中心与所讨论点连接的射线与实际观察平面相交来投影每个点。
上述透视矩阵假设图像平面平行于xy平面(如果要不同,则可以应用一些仿射旋转)。在OpenGL眼睛空间中,投影中心始终位于原点。当你进行数学运算时,你会发现透视归结为截距定理的一个简单实例。如果要将所有点投影到距中心(“相机”)1个单位的平面上,则最终需要将x和y除以-z。
这可以用矩阵形式写成。
(  1   0   0   0 )
(  0   1   0   0 )
(  0   0   1   0 )
(  0   0  -1   0 )

它的工作原理是通过设置w_clip = -z_eye,因此当执行裁剪空间w除法时,我们会得到:

 x_ndc = x_clip / w_clip = - x_eye / z_eye
 y_ndc = y_clip / w_clip = - y_eye / z_eye

请注意,这也适用于 z:。
 z_ndc = z_clip / w_clip = - z_eye / z_eye = 1

通常情况下,这种矩阵不用于渲染,因为深度信息丢失了——所有点实际上都投影到一个平面上。通常,我们希望保留深度(可能以某种非线性偏差的方式)。为了做到这一点,我们可以调整z的公式(第三行)。由于我们不希望z依赖于x和y,因此只有最后一个元素可以进行调整。通过使用形如(0 0 A B)的行,我们得到以下方程:
z_ndc =  - A * z_eye / z_eye - B / z_eye = -A - B / z_eye

这只是眼空间z值的双曲变换,深度仍然保持不变,矩阵可逆。我们只需要计算A和B。
让我们将函数z_ndc(z_eye) = -A - B / z_eye称为Z(z_eye)。由于观察体积被z_ndc = -1(前平面)和z_ndc = 1所限制,并且近平面和远平面在眼空间的距离以参数给出,我们必须将近平面z_eye=-n映射到-1,将远平面z_eye=-f映射到1。为了选择A和B,我们必须解决一个2个非线性方程组的系统:
Z(-n) = -1
Z(-f) =  1

这将得到您在矩阵的第三行找到的两个系数。

对于x和y,我们想要控制两件事:视场角度和棱台的不对称性(类似于投影仪中所知道的“镜头移位”)。视场角由映射到NDC中的[-1,1]的图像平面上的x和y范围定义。因此,您可以想象一个与图像平面平行的任意平面上的轴对齐矩形。该矩形描述了从摄像机选择的距离映射到可见视口的场景的一部分。改变视场角只是在x和y中缩放该矩形。

概念上,镜头移位只是一个平移,因此您可能认为它应该放在最后一列。但是,由于除法w=-z将在矩阵乘法之后进行,我们必须首先将该平移乘以-z。这意味着平移部分现在在第三列中,我们有一个形式的矩阵:

( C   0   D   0 )
( 0   E   F   0 )
( 0   0   A   B )
( 0   0  -1   0 )

对于x,这给出了:
x_clip = (x_eye * C + D * z_eye ) / (-z_eye) = -x_eye / z_eye - D

现在我们只需要找到正确的系数C和D,将x_eye=l映射到x_ndc=-1,将x_eye=r映射到x_ndc=1。请注意,经典的GL截锥函数将此处的l和r值解释为近平面上的距离,因此我们必须针对z_eye=-n计算所有内容。解决这个新的2元方程组,您将得到在截锥体矩阵中看到的那些系数。

2
“特别要注意的是,这个变换不会保持线的平行性。”如果你想要技术上的解释,这个变换在四维空间中是完全线性的。而导致线不再平行的非线性部分则是由除以W所引起的。值得注意的是,这一步骤是在矩阵变换之后进行的。 - Nicol Bolas
@derhass:从您的代码行z_eye=-nz_eye=-f中,有点难以确定您是希望nf小于0还是-n-f小于0?也就是说,nf是作为正数给出还是负数给出的?我的理解是它们应该是负数,但我很难将其与在https://dev59.com/3F8e5IYBdhLWcg3wlbPx中给出的正数解释答案相一致。 - WoodMath
按照惯例,nf被赋予为视线方向中近平面和远平面的_距离_,因此它们始终为正值。由于视线方向通常为-z_eye,这些平面位于负的z_eye值处。 - derhass
@WoodMath:我觉得以这种方式来看待事物是有误导性的。特别是,在除法进行之前,z=+/-1并没有什么特别的。近平面和远平面分别映射到了-w和+w。毕竟,在4D齐次空间中,你有额外的自由度,点将被表示为线,而平面(包括剪裁平面)将被表示为超平面 - 实际上是三维体积。所以,是的,你可以将这样的矩阵分解成简单的仿射部分,即只进行缩放操作和旋转,你甚至不需要平移,但所有这些都是相对于一个4D空间来说的。 - derhass
@derhass:当nf被赋予正数时,nf更为正向(即n是否大于f)?我认为由于我们是沿着负_z_轴看,n确实会更为正向。然而,在Songho.ca的图表中,nf从它们的正值取反了,因此-f是最负的,这意味着原始的正值f大于n(即f大于n)。这似乎是违反直觉的。 - WoodMath
@WoodMath:在通常情况下,你选择 f > n。很容易看出,当使用带有正方向指向相机观察方向的有符号距离值时,距离的绝对值越大,点离得越远(无论点位于哪一侧)。在你前方10个单位的位置(f=10)比在你前方1个单位的位置(n=1)更远。还要注意的是,在该矩阵中,你不必设置 f > n。你只需要设置 f,n > 0 并且 f != n。如果你设置 0 < f < n,那么只需沿着 z 轴进行翻转即可。 - derhass

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接