OpenGL - 鼠标坐标到 Space 坐标
OpenGL - Mouse coordinates to Space coordinates
我的目标是在鼠标指向的地方放置一个球体(Z 坐标为 0)。
我看到了这个question但是我还没有理解MVP矩阵的概念,所以我研究了一下,现在我有两个问题:
如何根据查找、眼睛和向上矢量等相机设置创建视图矩阵?
我也看了this tutorial about several camera types and this one for webgl。
我仍然可以把它们放在一起我也不知道如何获得投影矩阵...
我应该采取哪些步骤来实现所有这些?
在渲染中,场景的每个网格通常由模型矩阵、视图矩阵和投影矩阵进行变换。
投影矩阵:
投影矩阵描述了从场景的 3D 点到视口的 2D 点的映射。投影矩阵从视图space转换为剪辑space,剪辑space中的坐标转换为范围为(-1, -1)的归一化设备坐标(NDC) , -1) 到 (1, 1, 1) 除以剪辑坐标的 w 分量。
查看矩阵:
视图矩阵描述了观察场景的方向和位置。视图矩阵从世界 space 转换为视图(眼睛)space。在视口的坐标系中,X 轴指向左侧,Y 轴指向上方,Z 轴指向视图外(请注意,在右手系统中,Z 轴是 X 轴的叉积轴和 Y 轴)。
模型矩阵:
模型矩阵定义场景中网格的位置、方向和相对大小。模型矩阵将顶点位置从网格变换到世界 space.
模型矩阵如下所示:
( 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 轴)。
下面的代码定义了一个矩阵,它精确地封装了计算场景外观所需的步骤:
- 正在将模型坐标转换为视口坐标。
- 旋转,看向视图的方向。
- 眼睛位置的移动
以下代码与 gluLookAt
或 glm::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 点的映射。它从眼睛space变换到剪辑space,剪辑space中的坐标通过除以剪辑坐标。 NDC 的范围为 (-1,-1,-1) 到 (1,1,1)。
NDC 之外的每个几何体都被剪裁。
相机视锥的近平面和远平面之间的物体被映射到NDC的范围(-1, 1)。
正射投影
在正交投影中,眼睛中的坐标 space 线性映射到归一化设备坐标。
正交投影矩阵:
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 点的映射。
相机平截头体(截棱锥)中的眼睛 space 坐标映射到立方体(归一化设备坐标)。
透视投影矩阵:
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 }
};
}
3 在透视投影中恢复视图 space 位置的解决方案
- 有视野和纵横比
由于投影矩阵是由视野和纵横比定义的,因此可以通过视野和纵横比恢复视口位置。前提是是对称透视投影,归一化设备坐标,已知深度和近远平面。
恢复视图中的 Z 距离 space:
z_ndc = 2.0 * depth - 1.0;
z_eye = 2.0 * n * f / (f + n - z_ndc * (f - n));
通过 XY 归一化设备坐标恢复视图 space 位置:
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 距离 space:
A = prj_mat[2][2];
B = prj_mat[3][2];
z_ndc = 2.0 * depth - 1.0;
z_eye = B / (A + z_ndc);
通过 XY 归一化设备坐标恢复视图 space 位置:
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;
进一步查看:
- How to render depth linearly in modern OpenGL with gl_FragCoord.z in fragment shader?
- How to compute the size of the rectangle that is visible to the camera at a given coordinate?
- How to recover view space position given view space depth value and ndc xy
- Is it possble get which surface of cube will be click in OpenGL?
我的目标是在鼠标指向的地方放置一个球体(Z 坐标为 0)。
我看到了这个question但是我还没有理解MVP矩阵的概念,所以我研究了一下,现在我有两个问题:
如何根据查找、眼睛和向上矢量等相机设置创建视图矩阵?
我也看了this tutorial about several camera types and this one for webgl。
我仍然可以把它们放在一起我也不知道如何获得投影矩阵...
我应该采取哪些步骤来实现所有这些?
在渲染中,场景的每个网格通常由模型矩阵、视图矩阵和投影矩阵进行变换。
投影矩阵:
投影矩阵描述了从场景的 3D 点到视口的 2D 点的映射。投影矩阵从视图space转换为剪辑space,剪辑space中的坐标转换为范围为(-1, -1)的归一化设备坐标(NDC) , -1) 到 (1, 1, 1) 除以剪辑坐标的 w 分量。查看矩阵:
视图矩阵描述了观察场景的方向和位置。视图矩阵从世界 space 转换为视图(眼睛)space。在视口的坐标系中,X 轴指向左侧,Y 轴指向上方,Z 轴指向视图外(请注意,在右手系统中,Z 轴是 X 轴的叉积轴和 Y 轴)。模型矩阵:
模型矩阵定义场景中网格的位置、方向和相对大小。模型矩阵将顶点位置从网格变换到世界 space.
模型矩阵如下所示:
( 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 轴)。
下面的代码定义了一个矩阵,它精确地封装了计算场景外观所需的步骤:
- 正在将模型坐标转换为视口坐标。
- 旋转,看向视图的方向。
- 眼睛位置的移动
以下代码与 gluLookAt
或 glm::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 点的映射。它从眼睛space变换到剪辑space,剪辑space中的坐标通过除以剪辑坐标。 NDC 的范围为 (-1,-1,-1) 到 (1,1,1)。
NDC 之外的每个几何体都被剪裁。
相机视锥的近平面和远平面之间的物体被映射到NDC的范围(-1, 1)。
正射投影
在正交投影中,眼睛中的坐标 space 线性映射到归一化设备坐标。
正交投影矩阵:
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 点的映射。
相机平截头体(截棱锥)中的眼睛 space 坐标映射到立方体(归一化设备坐标)。
透视投影矩阵:
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 }
};
}
3 在透视投影中恢复视图 space 位置的解决方案
- 有视野和纵横比
由于投影矩阵是由视野和纵横比定义的,因此可以通过视野和纵横比恢复视口位置。前提是是对称透视投影,归一化设备坐标,已知深度和近远平面。
恢复视图中的 Z 距离 space:
z_ndc = 2.0 * depth - 1.0;
z_eye = 2.0 * n * f / (f + n - z_ndc * (f - n));
通过 XY 归一化设备坐标恢复视图 space 位置:
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 距离 space:
A = prj_mat[2][2];
B = prj_mat[3][2];
z_ndc = 2.0 * depth - 1.0;
z_eye = B / (A + z_ndc);
通过 XY 归一化设备坐标恢复视图 space 位置:
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;
进一步查看:
- How to render depth linearly in modern OpenGL with gl_FragCoord.z in fragment shader?
- How to compute the size of the rectangle that is visible to the camera at a given coordinate?
- How to recover view space position given view space depth value and ndc xy
- Is it possble get which surface of cube will be click in OpenGL?