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

时间:2015-02-02 20:21:45

标签: math matrix opengl-es webgl perspectivecamera

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,对吗?

4:3 aspect ratio

我也注意到45°似乎是一个普通的视野。如果是这种情况,那么fovy参数将是π / 4弧度,是否正确?

总而言之,方法中的f变量是什么缩写,它的目的是什么?
我试图想象实际情况,我想象了以下内容:

Side view of [perspective in 3D scene

这样思考,我可以理解为什么你将fovy除以2,以及为什么你采用该比率的正切,但为什么存储在f中的反转呢?同样,我在理解f真正代表什么方面遇到了很多麻烦。

接下来,我得到nearfar的概念是沿z轴的剪切点,所以这很好,但如果我使用上图中的数字(即{{} 1}},π / 44 / 310)并将它们插入到100方法中,然后最终得到如下矩阵:

enter image description here

perspective等于:

enter image description here

所以我留下了以下问题:

  1. 什么是f
  2. 分配给f(即out[10])的值代表什么?
  3. 分配给110 / -90的{​​{1}}做了什么?
  4. 分配给-1(即out[11])的值代表什么?
  5. 最后,我应该注意到我已经阅读了Gregg Tavares's explanation on the perspective matrix,但在完成所有这些之后,我仍然有同样的困惑。

2 个答案:

答案 0 :(得分:14)

让我们看看我是否可以解释这一点,或者在阅读完之后,你可以想出一个更好的解释方法。

要实现的第一件事是WebGL需要剪辑空间坐标。他们去-1< - > x,y和z中的+1。因此,透视矩阵基本上是为了占据视锥体中的空间并将其转换为剪辑空间。

如果你看一下这个图

frustum-side

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

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)

为什么这样写?我猜是因为如果你有第一种风格并且棕褐色出现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< - >中转换cameraZ。 -zFar范围到-1中的clipZ - < - >。 + 1范围。

标准透视矩阵使用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)

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的重写。

答案 1 :(得分:0)

f是一个缩放y轴的因子,因此沿视角截面的顶平面的所有点(透视后分割)的y坐标为1,底部的y坐标为1。平面的y坐标为-1。尝试沿其中一个平面插入点(示例:0, 2.41, 12, 7.24, 3),你可以看到为什么会发生这种情况:因为它最终得到的预分割y等于同质w。