试图理解 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
真正代表什么。
接下来,我得到 near
和 far
的概念是沿 z 轴的裁剪点,这很好,但是如果我使用上图中的数字(即, π / 4
、4 / 3
、10
和 100
) 并将它们插入 perspective
方法,然后我得到如下矩阵:
其中 f
等于:
所以我还有以下问题:
- 什么是
f
?
- 分配给
out[10]
(即110 / -90
)的值代表什么?
- 分配给
out[11]
的 -1
有什么作用?
- 分配给
out[14]
的值(即 2000 / -90
)代表什么?
最后,我应该指出,我已经阅读了 Gregg Tavares's explanation on the perspective matrix,但毕竟,我仍然有同样的困惑。
f
是一个缩放 y 轴的因子,这样沿着视锥顶平面的所有点,post-透视分割,都有一个 1 的 y 坐标, 而那些在底平面上的 y 坐标为 -1。尝试沿着其中一个平面插入点(示例:0, 2.41, 1
、2, 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 你的程序会崩溃如果你这样做的话没有除法所以没有机会被零除。
看到 -1
在 matrix[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;
并求解 s
和 c
。在我们的例子中,我们知道
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
我们现在知道 s
和 c
。
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.
的重写
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
真正代表什么。
接下来,我得到 near
和 far
的概念是沿 z 轴的裁剪点,这很好,但是如果我使用上图中的数字(即, π / 4
、4 / 3
、10
和 100
) 并将它们插入 perspective
方法,然后我得到如下矩阵:
其中 f
等于:
所以我还有以下问题:
- 什么是
f
? - 分配给
out[10]
(即110 / -90
)的值代表什么? - 分配给
out[11]
的-1
有什么作用? - 分配给
out[14]
的值(即2000 / -90
)代表什么?
最后,我应该指出,我已经阅读了 Gregg Tavares's explanation on the perspective matrix,但毕竟,我仍然有同样的困惑。
f
是一个缩放 y 轴的因子,这样沿着视锥顶平面的所有点,post-透视分割,都有一个 1 的 y 坐标, 而那些在底平面上的 y 坐标为 -1。尝试沿着其中一个平面插入点(示例:0, 2.41, 1
、2, 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 你的程序会崩溃如果你这样做的话没有除法所以没有机会被零除。
看到 -1
在 matrix[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;
并求解 s
和 c
。在我们的例子中,我们知道
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
我们现在知道 s
和 c
。
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.
的重写