【问题标题】:Trying to understand the math behind the perspective matrix in WebGL试图理解 WebGL 中透视矩阵背后的数学
【发布时间】:2015-04-01 21:21:39
【问题描述】:

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;
};

我真的很想了解这个方法中的所有数学实际上在做什么,但我在几个点上绊倒了。

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

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

说了这么多,方法中的f变量是什么,它的作用是什么?
我试图想象实际的场景,我想像以下内容:

这样想,我可以理解你为什么将fovy 除以2 以及为什么取那个比率的正切,但是为什么它的倒数存储在f 中?再说一次,我很难理解 f 真正代表什么。

接下来,我知道nearfar 是沿z 轴的剪切点,这很好,但如果我使用上图中的数字(即π / 4、@987654345 @、10100) 并将它们插入到 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,但毕竟,我留下了同样的困惑。

【问题讨论】:

  • 也许this link 有点帮助。这稍微引用了过时的固定功能 GL,但数学仍然有效。
  • 对不起,derhass,但该链接比我迄今为止查看的所有其他链接更令人困惑。我想我所要求的不仅仅是数学解释,而是对正在发生的事情的概念性解释,以及在实际情况下矩阵是如何形成的。

标签: math matrix opengl-es webgl perspectivecamera


【解决方案1】:

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

【讨论】:

  • Sneftel,感谢您的回答。 “右平面”和“左平面”是什么意思?此外,您的回答听起来像 f 只是对与 y 值相关的所有内容进行规范化,但我不明白为什么以及这与同质 w 有什么关系。另外,您能否对上述问题#2-#4 提供一些见解?非常感谢。
  • 对不起,我应该说“顶部或底部”平面。考虑空间中的所有点,这些点在渲染时将被绘制为屏幕最顶部或底部的像素。它们形成了世界上的平面。
  • 对于 2 和 4,就像 ff/aspect 将 x 和 y 缩放到 (-1,1) 范围,将 z 缩放到 (-1, 1) 范围。三是设置透视划分。我认为您可能需要在纸上多花点时间才能了解这里发生了什么。特别是,看看你是否能弄清楚为什么 z 坐标大的点比 z 坐标小的点靠得更近。
【解决方案2】:

让我们看看我能不能解释一下,或者读完之后你可以想出一个更好的方法来解释它。

首先要实现的是 WebGL 需要剪辑空间坐标。它们在 x、y 和 z 中变为 -1 +1。因此,透视矩阵的设计基本上是为了获取 frustum 内的空间并将其转换为剪辑空间。

如果你看这张图

我们知道 tangent = 相反 (y) 而不是相邻 (z),所以如果我们知道 z,我们就可以计算出对于给定 fovY 位于平截头体边缘的 y。

tan(fovY / 2) = y / -z

两边都乘以-z

y = tan(fovY / 2) * -z

如果我们定义

f = 1 / tan(fovY / 2)

我们得到

y = -z / f

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

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

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 来表示somethingc 来表示常量。

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)

当我希望 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的重写。

【讨论】:

  • 自从你几天前发布它以来,我几乎一直在思考你的答案,最后,它开始融合在一起,尽管我承认我仍然有点困惑。我也非常看重your post 以及对透视矩阵的解释...
  • This bookthis book 以便更好地了解正在发生的事情。综上所述,这是我目前的理解,如果我错了,请纠正:
  • Math.tan(fovy / 2)本质上是描述yz之间的关系。 (毕竟,这只是切线的基本三角函数定义。)因此,它的倒数等于maxZ / maxY。因此,当您将顶点的 y 部分乘以它时,除以 maxY 本质上具有将 y 从 0 标准化为 1 的效果。从那里,Z 越大,y 值最终变得越大。另外,我明白为什么你对 x 做同样的事情,但也会考虑纵横比。没关系。现在,我仍然很困惑的是 z 部分。一个...
  • 你在上面写了zeroToOne = (someY - near) * rangeInv;,但我想知道someY实际上是否应该是someZ。请让我知道你对此的看法。谢谢你。此外,我完全了解您如何计算 clipspace-11。这完全没问题。但是,我不明白clipspace 计算如何映射到out[10]out[14]。具体来说,zNear + zFarout[10] 中做了什么,zNear * zFarout[14] 中做了什么?抱歉有这些问题。您的帖子非常有帮助,但是正如您所看到的,我仍然有些困惑。谢谢。
  • 添加了一些,但我自己也很困惑:p
猜你喜欢
  • 2015-08-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多