让我们看看我能不能解释一下,或者读完之后你可以想出一个更好的方法来解释它。
首先要实现的是 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 除,你的程序会崩溃,如果你这样做,没有除法,所以没有机会被零除。
看到-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)
当我希望 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的重写。