相机的核心构造一个是glm::lookAt函数,一个是glm::perspective函数,本文相机的一切运动都在于如何构建相应的参数传入上述两个函数里。
cpp
glm::mat4 glm::lookAt(
glm::vec3 const &eye,//相机所在位置
glm::vec3 const ¢er,//要凝视的点
glm::vec3 const &up //相机上向量
);
cpp
glm::mat4 perspective(
float fovy,
float aspect,
float near,
float far);
首先我们通常默认相机的Front向量为(0,0,-1),位置eye为(0,0,0),则center=eye+Front=(0,0,-1)。
默认up向量为(0,1,0),通过Front向量和up向量可以叉积出right向量。

1.视角抬起与左右环视
通过修改Front向量即可。简单点可直接用欧拉角,相机引入yaw跟pitch,麻烦点就直接用向量做。可知相机自身的Front向量,right向量以及叉积出来的Up向量(注意不是up向量),这三组向量构成一组坐标基。
cpp
//上下抬动theta
Front=Front*cos(theta)+Up*sin(theta)
center=eye+Front;
//up 不变,eye不变
//上下旋转动beta
Front=Front*cos(beta)+right*sin(beta)
center=eye+Front;
//up 不变,eye不变
2.视角平移
前后平移即通过Front向量平移,左右平移即通过right向量平移。将eye跟center加减Front向量和right向量即可实现平移。
cpp
//前后方向平移
eye+=Front*k;//k为平移系数
center+=Front*k;
//up 不变
//左右方向平移
eye+=right*k;//k为平移系数
center+=right*k;
//up 不变
3.zoomIn/zoomOut
1)普通的视野变换
直接通过修改fov即可,fov越大则视野越宽看到的东西更多,但是屏幕上呈现的物体会变小;反之则视野越窄看到的东西更少,但是屏幕上呈现的物体会变大
2)在鼠标位置进行区域的视野放大缩小
这时候直接修改fov就不管用了,需要通过平移相机的方式来实现。
获取鼠标所在点映射的世界空间点P,然后让相机沿着有eye与P两点构成的向量前后移动即可达到zoomIn/zoomOut的效果。其本质还是利用了相似的性质,这种实现方法可以巧妙地保证鼠标所在位置对应的三维空间点永远会在鼠标所对应的屏幕像素点上。
cpp
glm::vec3 dir=P-eye;
//k为视野缩放系数,正数时zoomIn视野变大,负数则相反
eye+=dir*k;
center+=dir*k;
//up 不变
4.聚焦到某个物体
首先获取到这个物体的包围盒中心点P,然后用相机当前的Front向量与之加减获得到新的相机的eye。然后将相机的center设置为P即可。
cpp
//k为系数,可以根据包围盒的大小设置,包围盒越大可以让相机离远点
eye=k*(P-Front);
center=P;
//up 不变
5.绕某个物体旋转
可以先聚焦到这个物体。接下来获取到这个物体的包围盒中心点P,这个P可固定为相机center,然后P与eye的距离为r。然后如果向上旋转theta,相机的eye会绕着以P为球心,r为半径的面沿着相机当前Up向量方向旋转theta。如果向右旋转,相机的eye会绕着以P为球心,r为半径的面沿着相机当前right向量方向旋转beta。当然,如果相机引入yaw跟pitch的话会相对更简单一些。
cpp
// 计算单位球面上点P绕切线n旋转theta角后的新坐标
// O: 球心
// P: 球面上的点
// n: 切线方向向量(需单位化且与OP垂直)
// theta: 旋转角度(弧度)
glm::vec3 rotatePointOnSphere(
const glm::vec3& O,
const glm::vec3& P,
const glm::vec3& n,
float theta
)
{
// 验证输入条件
glm::vec3 OP = P - O;
float opLength = glm::length(OP);
// 检查是否为单位球面(允许微小误差)
if (std::abs(opLength - 1.0f) > 1e-6f) {
std::cerr << "警告:输入点不在单位球面上,将进行归一化处理" << std::endl;
}
// 检查切线是否与半径垂直
float dotProduct = glm::dot(OP, n);
if (std::abs(dotProduct) > 1e-6f) {
std::cerr << "警告:输入的切线方向不与半径垂直,将重新计算垂直分量" << std::endl;
}
// 确保OP是单位向量
glm::vec3 unitOP = glm::normalize(OP);
// 确保n是单位向量且与OP垂直
glm::vec3 tangent = glm::normalize(n - dotProduct * unitOP);
// 创建旋转四元数:绕切线方向旋转theta角
glm::quat rotation = glm::angleAxis(theta, tangent);
// 执行旋转:首先将点平移到原点,旋转后再平移回球心
glm::vec3 P_origin = P - O; // 点P相对于球心的坐标
glm::vec3 P_rotated_origin = rotation * P_origin; // 旋转
glm::vec3 P_rotated = P_rotated_origin + O; // 平移回球心
// 由于浮点误差,可能需要重新归一化以确保在单位球面上
return O + glm::normalize(P_rotated - O);
}
//绕P点上下转动theta
center=P;
eye=rotatePointOnSphere(P,eye,Up,theta);
//up向量不变
//绕P点左右转动beta
center=P;
eye=rotatePointOnSphere(P,eye,right,beta);
//up向量不变