如何根据视图空间深度值和NDC xy恢复视图空间位置

9

我正在编写延迟渲染器,并尝试更紧密地打包我的gbuffer。然而,我似乎无法正确计算给定视图空间深度的视图位置。

// depth -> (gl_ModelViewMatrix * vec4(pos.xyz, 1)).z; where pos is the model space position
// fov -> field of view in radians (0.62831855, 0.47123888)
// p -> ndc position, x, y [-1, 1]
vec3 getPosition(float depth, vec2 fov, vec2 p)
{
    vec3 pos;
    pos.x = -depth * tan( HALF_PI - fov.x/2.0 ) * (p.x);
    pos.y = -depth * tan( HALF_PI - fov.y/2.0 ) * (p.y);
    pos.z = depth;
    return pos;
}

计算出来的位置是错误的。我知道这是因为我仍然在gbuffer中存储着正确的位置,并且使用它进行测试。

你期望返回的位置在哪个空间中? - Nicol Bolas
我希望它在视图空间中。 - aCuria
计算公式是从这里抄袭的 http://developer.amd.com/gpu_assets/01gdc09ad3ddstalkerclearsky210309.ppt - aCuria
3个回答

17

3种解决透视投影中视图空间位置恢复的方案

投影矩阵描述了场景中3D点到视口中2D点的映射关系。它将从视图(眼睛)空间转换为裁剪空间,而裁剪空间中的坐标会通过裁剪坐标的w分量除以得到归一化设备坐标(NDC)。NDC位于范围内(-1,-1,-1)到(1,1,1)。

在透视投影中,投影矩阵描述了从针孔相机所看到的世界中的3D点到视口中2D点的映射关系。
相机截锥体中的视图空间坐标被映射到一个立方体(归一化设备坐标)中。

透视投影矩阵:

r = right, l = left, b = bottom, t = top, n = near, f = far

2*n/(r-l)      0              0               0
0              2*n/(t-b)      0               0
(r+l)/(r-l)    (t+b)/(t-b)    -(f+n)/(f-n)    -1    
0              0              -2*f*n/(f-n)    0

it follows:

aspect = w / h
tanFov = tan( fov_y * 0.5 );

prjMat[0][0] = 2*n/(r-l) = 1.0 / (tanFov * aspect)
prjMat[1][1] = 2*n/(t-b) = 1.0 / tanFov

在透视投影中,Z分量是通过有理函数计算得出的:
z_ndc = ( -z_eye * (f+n)/(f-n) - 2*f*n/(f-n) ) / -z_eye

深度(gl_FragCoord.zgl_FragDepth)的计算如下:

z_ndc = clip_space_pos.z / clip_space_pos.w;
depth = (((farZ-nearZ) * z_ndc) + nearZ + farZ) / 2.0;

1. 视野和纵横比

由于投影矩阵是由视野和纵横比定义的,因此可以通过视野和纵横比来恢复视口位置。前提是它是对称透视投影,并且已知归一化设备坐标、深度和近远平面。

恢复视图空间中的Z距离:

z_ndc = 2.0 * depth - 1.0;
z_eye = 2.0 * n * f / (f + n - z_ndc * (f - n));

通过XY归一化设备坐标恢复视图空间位置:

ndc_x, ndc_y = xy normalized device coordinates in range from (-1, -1) to (1, 1):

viewPos.x = z_eye * ndc_x * aspect * tanFov;
viewPos.y = z_eye * ndc_y * tanFov;
viewPos.z = -z_eye; 

2. 投影矩阵

投影矩阵中存储了由视野和长宽比定义的投影参数。因此,可以通过对称透视投影的投影矩阵值来恢复视口位置。

请注意投影矩阵、视野和长宽比之间的关系:

prjMat[0][0] = 2*n/(r-l) = 1.0 / (tanFov * aspect);
prjMat[1][1] = 2*n/(t-b) = 1.0 / tanFov;

prjMat[2][2] = -(f+n)/(f-n)
prjMat[3][2] = -2*f*n/(f-n)

在视图空间中恢复Z距离:

A     = prj_mat[2][2];
B     = prj_mat[3][2];
z_ndc = 2.0 * depth - 1.0;
z_eye = B / (A + z_ndc);

通过XY归一化设备坐标恢复视图空间位置:

viewPos.x = z_eye * ndc_x / prjMat[0][0];
viewPos.y = z_eye * ndc_y / prjMat[1][1];
viewPos.z = -z_eye; 

3. 逆投影矩阵

当然,视口位置可以通过逆投影矩阵得到。

mat4 inversePrjMat = inverse( prjMat );
vec4 viewPosH      = inversePrjMat * vec3( ndc_x, ndc_y, 2.0 * depth - 1.0, 1.0 )
vec3 viewPos       = viewPos.xyz / viewPos.w;


请参考以下问题的答案:


4

最终我成功解决了问题,由于这种方法与上文提到的不同,所以我会详细说明,以便任何看到这篇文章的人都有解决方案。

  • Pass 1: Store the depth value in view space to the gbuffer
  • To re-create the (x, y, z) position in the second pass:
  • Pass the horizontal and vertical field of view in radians into the shader.
  • Pass the near plane distance (near) to the shader. (distance from camera position to near plane)
  • Imagine a ray from the camera to the fragment position. This ray intersects the near plane at a certain position P. We have this position in the ndc space and want to compute this position in view space.
  • Now, we have all the values we need in view space. We can use the law of similar triangles to find the actual fragment position P'

    P = P_ndc * near * tan(fov/2.0f) // computation is the same for x, y
    // Note that by law of similar triangles, P'.x / depth = P/near  
    P'.xy = P/near * -depth; // -depth because in opengl the camera is staring down the -z axis
    P'.z = depth;
    

3

我写了一个延迟渲染器,并使用以下代码重新计算屏幕空间位置:

vec3 getFragmentPosition()
{
     vec4 sPos = vec4(gl_TexCoord[0].x, gl_TexCoord[0].y, texture2D(depthTex, gl_TexCoord[0].xy).x, 1.0);
     sPos.z = 2.0 * sPos.z - 1.0;
     sPos = invPersp * sPos;

     return sPos.xyz / sPos.w;
}

这里,depthTex指代存储深度信息的纹理,invPersp是预先计算的透视矩阵的逆矩阵。通过将屏幕片段位置与逆透视矩阵相乘,可以得到模型视图坐标。然后,通过除以w得到齐次坐标。乘以2并减去1是为了将深度从[0,1](存储在纹理中)缩放到[-1,1]。

此外,根据您使用的MRT类型,重新计算的结果可能与存储的信息不完全相等,因为您会失去浮点精度。


你的深度纹理是以一种格式存储的吗?这样你对所有坐标执行的2*p-1操作才能产生正确的值? - John Calsbeek
你正在将数据存储到depthTex中的哪个深度空间? - aCuria
我将其存储在我的GBuffer着色器中,作为 gl_FragCoord.z - wquist
为什么你必须将深度从[0-1]缩放到[-1,1]?这对我来说毫无意义...一些快速的谷歌搜索表明,gl_FragCoord.z是投影后的z值,在[0-1] ndc空间中。 - aCuria
你想要在投影之前得到z值,当它仅为模型视图时。我不是该领域的专家,所以我不知道你需要[-1, 1]而不是[0, 1]的确切原因。 - wquist
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接