OpenGL - 鼠标坐标到空间坐标的转换

6

我的目标是将一个球体放置在鼠标指向的位置(Z坐标为0)。

我看到了这个问题,但我还没有理解MVP矩阵的概念,所以我进行了一些研究,现在我有两个问题:

如何从相机设置(例如查找、眼睛和上向量)创建视图矩阵?

我也阅读了这篇关于几种相机类型的教程以及WebGL的这篇文章

尽管我已经阅读了以上内容,但我仍然无法将它们整合起来,我不知道如何获取投影矩阵...

我应该采取哪些步骤来实现所有这些?

1个回答

21
在渲染中,场景中的每个网格通常都会通过模型矩阵、视图矩阵和投影矩阵进行转换。
投影矩阵: 投影矩阵描述了场景的3D点到视口的2D点的映射。投影矩阵从视图空间转换到剪辑空间,剪辑空间中的坐标通过除以剪辑坐标的w分量来转换为归一化设备坐标(NDC),其范围为(-1,-1,-1)到(1,1,1)。
视图矩阵: 视图矩阵描述了观察场景时的方向和位置。视图矩阵将从世界空间转换到视图(眼睛)空间。在视口的坐标系中,X轴指向左,Y轴向上,Z轴指向视图外(注意,在右手坐标系中,Z轴是X轴和Y轴的叉积)。
模型矩阵: 模型矩阵定义了场景中网格的位置、方向和相对大小。模型矩阵将网格的顶点位置从局部空间转换为世界空间。
模型矩阵如下所示:
( X-axis.x, X-axis.y, X-axis.z, 0 )
( Y-axis.x, Y-axis.y, Y-axis.z, 0 )
( Z-axis.x, Z-axis.y, Z-axis.z, 0 )
( trans.x,  trans.y,  trans.z,  1 ) 

视图

在视口中,X轴指向左侧,Y轴指向上方,Z轴指向视图之外(注意,在右手坐标系中,Z轴是X轴和Y轴的叉积)。

视图坐标

以下代码定义了一个矩阵,完全包含了计算场景观察所需的步骤:

  • 将模型坐标转换为视口坐标。
  • 旋转,以朝向视图方向。
  • 移动到眼睛位置。

以下代码与 gluLookAtglm::lookAt 的功能相同:

using TVec3  = std::array< float, 3 >;
using TVec4  = std::array< float, 4 >;
using TMat44 = std::array< TVec4, 4 >;

TVec3 Cross( TVec3 a, TVec3 b ) { return { a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] }; }
float Dot( TVec3 a, TVec3 b ) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }
void Normalize( TVec3 & v )
{
    float len = sqrt( v[0] * v[0] + v[1] * v[1] + v[2] * v[2] );
    v[0] /= len; v[1] /= len; v[2] /= len;
}

TMat44 Camera::LookAt( const TVec3 &pos, const TVec3 &target, const TVec3 &up )
{ 
    TVec3 mz = { pos[0] - target[0], pos[1] - target[1], pos[2] - target[2] };
    Normalize( mz );
    TVec3 my = { up[0], up[1], up[2] };
    TVec3 mx = Cross( my, mz );
    Normalize( mx );
    my = Cross( mz, mx );

    TMat44 v{
        TVec4{ mx[0], my[0], mz[0], 0.0f },
        TVec4{ mx[1], my[1], mz[1], 0.0f },
        TVec4{ mx[2], my[2], mz[2], 0.0f },
        TVec4{ Dot(mx, pos), Dot(my, pos), -Dot(mz, pos), 1.0f }
    };

    return v;
}

投影

投影矩阵描述了场景中 3D 点到视口 2D 点的映射。它将从眼空间到裁剪空间的变换,和裁剪空间中的坐标通过除以剪裁坐标的w分量来转换为规范化设备坐标 (NDC)。NDC 的范围是 (-1,-1,-1) 到 (1,1,1)。
所有超出 NDC 范围的几何图形都会被剪切掉。

摄像机视锥体近平面和远平面之间的对象将映射到 NDC 的(-1,1) 范围内。


正交投影

在正交投影下,眼空间中的坐标被线性映射到规范化设备坐标。

Orthographic Projection

正交投影矩阵:

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

2/(r-l)         0               0               0
0               2/(t-b)         0               0
0               0               -2/(f-n)        0
-(r+l)/(r-l)    -(t+b)/(t-b)    -(f+n)/(f-n)    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

地点:

a = w / h
ta = tan( fov_y / 2 );

2 * n / (r-l) = 1 / (ta * a)
2 * n / (t-b) = 1 / ta

如果投影是对称的,也就是说视线在视口的中心位置,视野没有偏移,那么矩阵可以简化:

1/(ta*a)  0     0              0
0         1/ta  0              0
0         0    -(f+n)/(f-n)   -1    
0         0    -2*f*n/(f-n)    0

以下函数将计算与gluPerspective相同的投影矩阵:
#include <array>

const float cPI = 3.14159265f;
float ToRad( float deg ) { return deg * cPI / 180.0f; }

using TVec4  = std::array< float, 4 >;
using TMat44 = std::array< TVec4, 4 >;

TMat44 Perspective( float fov_y, float aspect )
{
    float fn = far + near
    float f_n = far - near;
    float r = aspect;
    float t = 1.0f / tan( ToRad( fov_y ) / 2.0f );

    return TMat44{ 
        TVec4{ t / r, 0.0f,  0.0f,                 0.0f },
        TVec4{ 0.0f,  t,     0.0f,                 0.0f },
        TVec4{ 0.0f,  0.0f, -fn / f_n,            -1.0f },
        TVec4{ 0.0f,  0.0f, -2.0f*far*near / f_n,  0.0f }
    };
}

三种解决透视投影下视图空间位置恢复的方法

  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[2][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 * vec4(ndc_x, ndc_y, 2.0*depth - 1.0, 1.0)
vec3 viewPos       = viewPos.xyz / viewPos.w;


查看更多:


谢谢您的详细回答,但我仍然不明白如何将球体放置在我想要的坐标位置,以及如何从鼠标位置开始。 - gdf31
1
尽管我还没有实现它,但我将您的答案标记为已接受。我猜这是太多信息需要吸收了,但您给了我重要的知识。 “Mouse picking miss”视频真的很有帮助,但我对两件事感到困惑。我不知道如何获取所需的信息以获取视图和投影矩阵。 好吧,我将该信息分别传递给gluLookAtgluPerspective。我感到有点羞愧,因为我没有阅读我的旧CS课程代码,并忘记了那两行在做什么。OpenGL文档也很好地解释了这些功能。 - gdf31

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