摄像机与观察空间详解
当我们讨论摄像机/观察空间(Camera/View Space)时,实际上是在以摄像机的视角(即摄像机作为场景原点)来描述场景中所有顶点的坐标。观察矩阵会将所有世界坐标变换为相对于摄像机位置和方向的观察坐标。
要定义一个摄像机,需明确以下几个向量:
- 摄像机的位置
- 观察的方向
- 右向量(指向摄像机右侧)
- 上向量(指向摄像机上方)
1. 摄像机的基本向量
1.1 摄像机位置
摄像机位置即为世界空间中的一个向量,表示摄像机所在的位置。
1.2 摄像机方向
摄像机方向描述摄像机正朝向哪个方向。通常用场景原点向量减去摄像机位置向量得到指向目标的向量。由于OpenGL中摄像机默认沿-z轴观察,为了统一方向,建议将相减顺序调整为“摄像机位置 - 目标点”:
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
1.3 右向量(Right Vector)
右向量代表摄像机空间的x轴正方向。计算方法是将上向量与方向向量叉乘:
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
1.4 上向量(Up Vector)
上向量可通过右向量与方向向量叉乘得到:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
2. LookAt 矩阵
利用三个互相垂直的轴(方向、右向量、上向量)和一个平移向量,可以构建LookAt矩阵,实现从世界空间到观察空间的变换。
glm::mat4 glm::lookAt( glm::vec3 const& eye, // 相机位置
glm::vec3 const& center, // 观察目标 glm::vec3 const& up // 上方向 );
3. 摄像机自由移动
将摄像机位置设置为cameraPos,观察方向为cameraPos + cameraFront,即可实现摄像机始终朝向目标。
LookAt函数示例:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
键盘输入控制摄像机移动:
void processInput(GLFWwindow *window){
...
float cameraSpeed = 0.05f; // 可根据需要调整
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
注意: 要对右向量进行标准化,确保不同方向移动时速度一致。
3.1 移动速度与deltaTime
移动速度设为常量可能导致不同设备上速度不一致。常见做法是引入deltaTime,即每帧的时间差,保证不同帧率下移动速度一致。
全局变量:
float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间
每帧计算deltaTime:
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
4. 视角移动(鼠标控制)
调整视角需要根据鼠标输入动态改变cameraFront向量。
4.1 欧拉角简介
欧拉角(Euler Angle)可用三个值描述3D空间的旋转:
- 俯仰角(Pitch): 上下看
- 偏航角(Yaw): 左右看
- 滚转角(Roll): 翻滚(一般用于特殊场景)
俯仰角控制上下观察,偏航角控制左右,滚转角则一般不在常规摄像机中使用。
4.2 方向向量计算
利用三角函数可根据欧拉角计算方向向量的各分量:
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
4.3 鼠标输入处理
- 偏航角和俯仰角由鼠标移动控制
- 储存上一帧鼠标位置,计算与当前帧的偏移量
- 通过GLFW设置鼠标隐藏与捕捉:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
- 注册鼠标移动回调:
glfwSetCursorPosCallback(window, mouse_callback);
-
鼠标回调函数处理流程:
- 计算鼠标偏移量
- 更新俯仰角和偏航角
- 限制俯仰角范围(避免视角翻转)
- 计算新的方向向量
示例代码:
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // y坐标自底向上增长
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
if(pitch > 89.0f) pitch = 89.0f;
if(pitch < -89.0f) pitch = -89.0f;
glm::vec3 front;
front.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
front.y = sin(glm::radians(pitch));
front.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = glm::normalize(front);
- 首次进入窗口时摄像机跳变问题:
用布尔变量firstMouse避免首次回调计算异常:
if(firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
5. 缩放(Zoom)
视野(Field of View,fov)定义了可见场景的范围。调整fov即可实现缩放效果。
- 鼠标滚轮回调实现缩放:
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
if(fov >= 1.0f && fov <= 45.0f)
fov -= yoffset;
if(fov <= 1.0f)
fov = 1.0f;
if(fov >= 45.0f)
fov = 45.0f;
}
- 在每帧上传透视投影矩阵,并使用fov变量:
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
- 注册鼠标滚轮回调:
glfwSetScrollCallback(window, scroll_callback);
提示: 使用欧拉角摄像机系统时需注意万向节死锁(Gimbal Lock)问题。