我的目标是将一个球体放置在鼠标指向的位置(Z坐标为0)。
我看到了这个问题,但我还没有理解MVP矩阵的概念,所以我进行了一些研究,现在我有两个问题:
如何从相机设置(例如查找、眼睛和上向量)创建视图矩阵?
我也阅读了这篇关于几种相机类型的教程以及WebGL的这篇文章。
尽管我已经阅读了以上内容,但我仍然无法将它们整合起来,我不知道如何获取投影矩阵...
我应该采取哪些步骤来实现所有这些?
我的目标是将一个球体放置在鼠标指向的位置(Z坐标为0)。
我看到了这个问题,但我还没有理解MVP矩阵的概念,所以我进行了一些研究,现在我有两个问题:
如何从相机设置(例如查找、眼睛和上向量)创建视图矩阵?
我也阅读了这篇关于几种相机类型的教程以及WebGL的这篇文章。
尽管我已经阅读了以上内容,但我仍然无法将它们整合起来,我不知道如何获取投影矩阵...
我应该采取哪些步骤来实现所有这些?
( 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 点的映射。它将从眼空间到裁剪空间的变换,和裁剪空间中的坐标通过除以剪裁坐标的w
分量来转换为规范化设备坐标 (NDC)。NDC 的范围是 (-1,-1,-1) 到 (1,1,1)。
所有超出 NDC 范围的几何图形都会被剪切掉。
摄像机视锥体近平面和远平面之间的对象将映射到 NDC 的(-1,1) 范围内。
正交投影
在正交投影下,眼空间中的坐标被线性映射到规范化设备坐标。
正交投影矩阵:
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 }
};
}
由于投影矩阵是由视角和宽高比定义的,因此可以通过视角和宽高比来恢复视口位置。前提是对称透视投影和已知归一化设备坐标、深度以及近、远平面。
恢复视图空间中的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;
查看更多:
gluLookAt
和gluPerspective
。我感到有点羞愧,因为我没有阅读我的旧CS课程代码,并忘记了那两行在做什么。OpenGL文档也很好地解释了这些功能。 - gdf31