让我们看看我是否能够解释清楚这个问题,或者在阅读完之后你能想出更好的解释方式。
首先要意识到的是WebGL需要剪辑空间坐标。它们在x、y和z方向上为-1 <-> +1。因此,透视矩阵基本上是设计用来将
截锥体内部的空间转换为剪辑空间。
如果您查看此图示:
https://istack.dev59.com/nRgAD.webp
我们知道正切 = 对边(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,导致程序崩溃。而如果你采用这种方式,就不会有除法,也就不会出现除以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]
最后,我们需要将相机Z在-zNear <-> -zFar范围内转换为-1 <-> +1范围内的剪辑Z。标准透视矩阵使用倒数函数完成这个过程,使得靠近相机的z值比离相机远的z值获得更高的分辨率。该公式为:
Multiplicative_inverse
clipZ = something / cameraZ + constant
让我们使用s
代表 something
, 使用c
代表 constant。
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)
我希望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])
我希望这有意义。注意:大部分内容只是我对这篇文章的改写。