试图理解 WebGL 中透视矩阵背后的数学原理

Trying to understand the math behind the perspective matrix in WebGL

WebGL 的所有矩阵库都有某种 perspective 函数,您可以调用该函数来获取场景的透视矩阵。
例如,mat4.js file that's part of gl-matrix 中的 perspective 方法编码如下:

mat4.perspective = function (out, fovy, aspect, near, far) {
    var f = 1.0 / Math.tan(fovy / 2),
        nf = 1 / (near - far);
    out[0] = f / aspect;
    out[1] = 0;
    out[2] = 0;
    out[3] = 0;
    out[4] = 0;
    out[5] = f;
    out[6] = 0;
    out[7] = 0;
    out[8] = 0;
    out[9] = 0;
    out[10] = (far + near) * nf;
    out[11] = -1;
    out[12] = 0;
    out[13] = 0;
    out[14] = (2 * far * near) * nf;
    out[15] = 0;
    return out;
};

我真的很想了解此方法中的所有数学运算实际上在做什么,但我在几点上犯了错误。

首先,如果我们有一个如下所示的 canvas,纵横比为 4:3,那么该方法的 aspect 参数实际上是 4 / 3 , 正确吗?

我还注意到 45° 似乎是一个常见的视野。如果是这样,那么 fovy 参数将是 π / 4 弧度,对吗?

综上所述,方法中的 f 变量是什么缩写,它的用途是什么?
我试图设想实际情况,我想象如下:

这样一想,我就明白了为什么你用fovy除以2,为什么你取那个比值的正切值,但是为什么它的倒数存储在f?同样,我很难理解 f 真正代表什么。

接下来,我得到 nearfar 的概念是沿 z 轴的裁剪点,这很好,但是如果我使用上图中的数字(即, π / 44 / 310100) 并将它们插入 perspective 方法,然后我得到如下矩阵:

其中 f 等于:

所以我还有以下问题:

  1. 什么是 f
  2. 分配给out[10](即110 / -90)的值代表什么?
  3. 分配给 out[11]-1 有什么作用?
  4. 分配给 out[14] 的值(即 2000 / -90)代表什么?

最后,我应该指出,我已经阅读了 Gregg Tavares's explanation on the perspective matrix,但毕竟,我仍然有同样的困惑。

f 是一个缩放 y 轴的因子,这样沿着视锥顶平面的所有点,post-透视分割,都有一个 1 的 y 坐标, 而那些在底平面上的 y 坐标为 -1。尝试沿着其中一个平面插入点(示例:0, 2.41, 12, 7.24, 3),您就会明白为什么会发生这种情况:因为它以等于齐次 w 的预除 y 结束。

让我们看看我能否解释一下,或者在阅读本文后您可以想出更好的解释方法。

首先要意识到的是 WebGL 需要 clipspace 坐标。它们在 x、y 和 z 中为 -1 <-> +1。因此,透视矩阵基本上被设计为将 frustum 内的 space 转换为 clipspace.

如果你看这张图

我们知道 tangent = opposite (y) over adjacent(z) 所以如果我们知道 z 我们可以计算出 y,对于给定的 fovY,它位于平截头体的边缘。

tan(fovY / 2) = y / -z

两边乘以-z

y = tan(fovY / 2) * -z

如果我们定义

f = 1 / tan(fovY / 2)

我们得到

y = -z / f

请注意,我们尚未完成从相机space 到剪辑space 的转换。我们所做的只是在相机 space 中针对给定的 z 计算视野边缘的 y。视野的边缘也是clipspace的边缘。由于 clipspace 只是 +1 到 -1,我们可以将 cameraspace y 除以 -z / f 得到 clipspace.

这有意义吗?再看看图。让我们假设蓝色 z 是 -5 并且对于某些给定的视野 y 出现 +2.34。我们需要将+2.34转换为+1clipspace。它的通用版本是

clipY = cameraY * f / -z

查看“makePerspective”

function makePerspective(fieldOfViewInRadians, aspect, near, far) {
  var f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewInRadians);
  var rangeInv = 1.0 / (near - far);

  return [
    f / aspect, 0, 0, 0,
    0, f, 0, 0,
    0, 0, (near + far) * rangeInv, -1,
    0, 0, near * far * rangeInv * 2, 0
  ];
};

我们可以看到 f 在这种情况下

tan(Math.PI * 0.5 - 0.5 * fovY)

其实和

是一样的
1 / tan(fovY / 2)

为什么这样写?我猜是因为如果你有第一种风格并且 tan 变成 0 你会除以 0 你的程序会崩溃如果你这样做的话没有除法所以没有机会被零除。

看到 -1matrix[11] 点意味着我们都完成了

matrix[5]  = tan(Math.PI * 0.5 - 0.5 * fovY)
matrix[11] = -1

clipY = cameraY * matrix[5] / cameraZ * matrix[11]

对于clipX,我们基本上做完全相同的计算,除了按纵横比缩放。

matrix[0]  = tan(Math.PI * 0.5 - 0.5 * fovY) / aspect
matrix[11] = -1

clipX = cameraX * matrix[0] / cameraZ * matrix[11]

最后,我们必须将 -zNear <-> -zFar 范围内的 cameraZ 转换为 -1 <-> + 1 范围内的 clipZ。

标准透视矩阵使用 reciprocal function 执行此操作,因此靠近相机的 z 值比远离相机的 z 值获得更高的分辨率。该公式为

clipZ = something / cameraZ + constant

让我们使用 s 作为 something,使用 c 作为常量。

clipZ = s / cameraZ + c;

并求解 sc。在我们的例子中,我们知道

s / -zNear + c = -1
s / -zFar  + c =  1

所以,将 `c' 移到另一边

s / -zNear = -1 - c
s / -zFar  =  1 - c

乘以 -zXXX

s = (-1 - c) * -zNear
s = ( 1 - c) * -zFar

这两个东西现在彼此相等所以

(-1 - c) * -zNear = (1 - c) * -zFar

扩大数量

(-zNear * -1) - (c * -zNear) = (1 * -zFar) - (c * -zFar)

简化

zNear + c * zNear = -zFar + c * zFar

向右移动zNear

c * zNear = -zFar + c * zFar - zNear

向左移动c * zFar

c * zNear - c * zFar = -zFar - zNear

简化

c * (zNear - zFar) = -(zFar + zNear)

除以 (zNear - zFar)

c = -(zFar + zNear) / (zNear - zFar)

求解 s

s = (1 - -((zFar + zNear) / (zNear - zFar))) * -zFar

简化

s = (1 + ((zFar + zNear) / (zNear - zFar))) * -zFar

1更改为(zNear - zFar)

s = ((zNear - zFar + zFar + zNear) / (zNear - zFar)) * -zFar

简化

s = ((2 * zNear) / (zNear - zFar)) * -zFar

再简化一些

s = (2 * zNear * zFar) / (zNear - zFar)

dang 我希望 stackexchange 像他们的数学网站一样支持数学:(

回到顶部。我们的论坛是

s / cameraZ + c

我们现在知道 sc

clipZ = (2 * zNear * zFar) / (zNear - zFar) / -cameraZ -
        (zFar + zNear) / (zNear - zFar)

让我们把 -z 移到外面

clipZ = ((2 * zNear * zFar) / zNear - ZFar) +
         (zFar + zNear) / (zNear - zFar) * cameraZ) / -cameraZ

我们可以把/ (zNear - zFar)改成* 1 / (zNear - zFar)所以

rangeInv = 1 / (zNear - zFar)
clipZ = ((2 * zNear * zFar) * rangeInv) +
         (zFar + zNear) * rangeInv * cameraZ) / -cameraZ

回顾 makeFrustum 我们看到它最终会成为

clipZ = (matrix[10] * cameraZ + matrix[14]) / (cameraZ * matrix[11])

看上面的公式符合

rangeInv = 1 / (zNear - zFar)
matrix[10] = (zFar + zNear) * rangeInv
matrix[14] = 2 * zNear * zFar * rangeInv
matrix[11] = -1
clipZ = (matrix[10] * cameraZ + matrix[14]) / (cameraZ * matrix[11])

我希望这是有道理的。注意:其中大部分只是我对 this article.

的重写