矩阵分解中不可预测的浮点误差

时间:2018-01-07 06:55:06

标签: c opengl math matrix floating-point

我试图通过使用以下公式来分解透视矩阵的近距离和远距离:

near = m32 / (m22 - 1);
far  = m32 / (m22 + 1);

这里的透视矩阵测试参数:

  aspect   = 0.782f;
  fovy     = glm_rad(49.984f);
  nearDist = 0.1550385f;
  farDist  = 6000.340975f;

  glm_perspective(fovy, aspect, nearDist, farDist, proj);

这就是我为了获得近似值和远值而做的事情(proj是列主矩阵):

far  = proj[3][2] / (proj[2][2] + 1.0f);
near = proj[3][2] / (proj[2][2] - 1.0f)

结果:

near = 0.155039 
far  = 5993.506348 

近似似乎可以接受,但远不是:/如果我为far使用较小的值,那么我会得到更准确的结果(正确的值是分解的值):

farDist = 600.340975 (near, far): 0.155039 600.319885 
farDist = 60.340975f (near, far): 0.155039 60.340946 

数学有问题吗?我有什么选择(不使用双重存储矩阵)?

您可以在此处查看透视矩阵公式:https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/gluPerspective.xml

m22 = (near + far) / (near - far)
m32 = 2 * near * far / (near - far)

和实施(行号可能会随时间变化):https://github.com/recp/cglm/blob/master/include/cglm/cam.h#L211

2 个答案:

答案 0 :(得分:2)

问题在于far/near比率越大,从透视矩阵中提取far需要更多有效数字。

far/near比率增加时,m22 = (near+far)/(near-far)接近1.

例如,将doublenear=0.155far=6,000一起使用,我们得到m22 = 1.0000516680014233。如果将其存储为float,则会将其截断为1.0000516

结果的重要部分是分数。即使所有其他操作都以完美的准确度完成,此时您只剩下3位有效数字。这与catastrophic cancellation非常相似。

基本上,每次far/near乘以10时,您都会丢失一个重要数字。当far6,000,000时,m22的值将被截断为{{1}存储为1.0时,会丢失所有信息。

我试图在Jupyter Notebook中展示它。

但真正的问题不仅是在不失去精确度的情况下提取float是不可能的,而且背景矩阵本身并不准确。

如果你在far处取一个向量,则应用透视矩阵,你将得不到z = 1.0。相反,将透视矩阵应用于具有不正确值z=6,000far的向量将为您提供z=5993.506348。矩阵本身已经错了,因此没有提取z=1.0的方法可以提供帮助。

TL; DR:如果您想以合理的准确度从透视矩阵中提取farnear,则必须使用far

编辑:添加了对真实问题的解释,关于灾难性取消的原始答案只是二阶效应。

答案 1 :(得分:2)

投影矩阵描述了从场景的3D点到视口的2D点的映射。投影矩阵从视图空间变换到剪辑空间。剪辑空间坐标为Homogeneous coordinates。通过除以剪辑的w分量,剪辑空间中的坐标转换为范围(-1,-1,-1)到(1,1,1)范围内的规范化设备坐标(NDC)坐标。

enter image description here

因此,Z-cooridnate为-1的点位于近平面上,Z-cooridnate为1的点位于远平面上(在标准化的设备空间中)。

要计算到近平面和远平面的距离,您必须通过投影矩阵变换近平面上的点和远平面上的点。然后你必须做一个透视鸿沟。到近处或远处平面的距离是结果的反转Z坐标:

mat4 proj;

mat4 invProj = inverse( proj );

vec4 ndcNear(0, 0, -1, 1);
vec4 ndcFar(0, 0, 1, 1);

vec4 ptNear = invProj * ndcNear;
vec4 ptFar  = invProj * ndcFar;

near = - ptNear[2] / ptNear[3];
far  = - ptFar[2]  / ptFar[3];