摄像机
摄像机决定了我们从哪个角度观察3D场景。本章将介绍摄像机的概念、实现方式以及如何创建可交互的摄像机系统。
摄像机基础
在OpenGL中,摄像机实际上并不存在。我们通过变换整个场景来模拟摄像机的移动,这称为"摄像机变换"。
视图矩阵
视图矩阵定义了摄像机的位置和方向:
glm::mat4 view = glm::lookAt(
cameraPos, // 摄像机位置
cameraTarget, // 观察目标
cameraUp // 上方向向量
);
摄像机坐标系
摄像机有自己局部坐标系:
- 右向量(Right):指向摄像机右侧
- 上向量(Up):指向上方
- 方向向量(Direction):指向观察方向(实际上是-Z方向)
// 摄像机位置
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
// 摄像机方向(指向原点)
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
// 右向量
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
// 上向量
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
自由移动摄像机
基本移动
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
// 视图矩阵
glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
键盘控制
float deltaTime = 0.0f;
float lastFrame = 0.0f;
float cameraSpeed = 2.5f;
void processInput(GLFWwindow* window) {
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
float speed = cameraSpeed * deltaTime;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += speed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= speed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * speed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * speed;
}
移动方向说明
W(前进)
↑
A(左移)← + → D(右移)
↓
S(后退)
前进/后退:沿cameraFront方向移动
左/右移动:沿cameraRight方向移动(cameraFront × cameraUp)
视角旋转
欧拉角
欧拉角用三个角度表示旋转:
- 俯仰角(Pitch):上下看
- 偏航角(Yaw):左右看
- 翻滚角(Roll):侧向倾斜
Pitch(俯仰)
↑
|
+----→ Yaw(偏航)
/
/
↓
Roll(翻滚)
计算方向向量
根据俯仰角和偏航角计算前向量:
float pitch = 0.0f;
float yaw = -90.0f; // 初始看向-Z方向
glm::vec3 front;
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(front);
鼠标控制
float lastX = 400.0f, lastY = 300.0f;
float sensitivity = 0.1f;
bool firstMouse = true;
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 反转Y轴
lastX = xpos;
lastY = ypos;
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(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(front);
}
// 注册回调
glfwSetCursorPosCallback(window, mouse_callback);
隐藏光标
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
缩放(视野调整)
通过鼠标滚轮调整视野(FOV):
float fov = 45.0f;
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
fov -= (float)yoffset;
if (fov < 1.0f) fov = 1.0f;
if (fov > 45.0f) fov = 45.0f;
}
glfwSetScrollCallback(window, scroll_callback);
// 在渲染循环中更新投影矩阵
glm::mat4 projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
摄像机类
封装一个完整的摄像机类:
// Camera.h
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
enum Camera_Movement {
FORWARD,
BACKWARD,
LEFT,
RIGHT
};
class Camera {
public:
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
float Yaw;
float Pitch;
float MovementSpeed;
float MouseSensitivity;
float Zoom;
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f),
float yaw = -90.0f, float pitch = 0.0f)
: Front(glm::vec3(0.0f, 0.0f, -1.0f)),
MovementSpeed(2.5f),
MouseSensitivity(0.1f),
Zoom(45.0f) {
Position = position;
WorldUp = up;
Yaw = yaw;
Pitch = pitch;
updateCameraVectors();
}
glm::mat4 GetViewMatrix() {
return glm::lookAt(Position, Position + Front, Up);
}
void ProcessKeyboard(Camera_Movement direction, float deltaTime) {
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
if (direction == LEFT)
Position -= Right * velocity;
if (direction == RIGHT)
Position += Right * velocity;
}
void ProcessMouseMovement(float xoffset, float yoffset, bool constrainPitch = true) {
xoffset *= MouseSensitivity;
yoffset *= MouseSensitivity;
Yaw += xoffset;
Pitch += yoffset;
if (constrainPitch) {
if (Pitch > 89.0f) Pitch = 89.0f;
if (Pitch < -89.0f) Pitch = -89.0f;
}
updateCameraVectors();
}
void ProcessMouseScroll(float yoffset) {
Zoom -= yoffset;
if (Zoom < 1.0f) Zoom = 1.0f;
if (Zoom > 45.0f) Zoom = 45.0f;
}
private:
void updateCameraVectors() {
glm::vec3 front;
front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
front.y = sin(glm::radians(Pitch));
front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
Front = glm::normalize(front);
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}
};
使用摄像机类
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float deltaTime = 0.0f;
float lastFrame = 0.0f;
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
}
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
static bool firstMouse = true;
static float lastX = 400.0f, lastY = 300.0f;
if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
camera.ProcessMouseScroll(yoffset);
}
int main() {
// 初始化...
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetScrollCallback(window, scroll_callback);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
while (!glfwWindowShouldClose(window)) {
processInput(window);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 使用摄像机
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom),
800.0f / 600.0f, 0.1f, 100.0f);
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
// 绘制...
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
摄像机移动策略
FPS风格
摄像机在一个平面上移动,不能上下移动:
void ProcessKeyboard(Camera_Movement direction, float deltaTime) {
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += glm::vec3(Front.x, 0.0f, Front.z) * velocity;
if (direction == BACKWARD)
Position -= glm::vec3(Front.x, 0.0f, Front.z) * velocity;
// ...
}
飞行风格
摄像机可以在任意方向移动:
void ProcessKeyboard(Camera_Movement direction, float deltaTime) {
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
// ...
// 添加上下移动
if (direction == UP)
Position += WorldUp * velocity;
if (direction == DOWN)
Position -= WorldUp * velocity;
}
平滑移动
使用插值实现平滑的摄像机移动:
glm::vec3 targetPosition;
void updateCamera(float deltaTime) {
float lerpFactor = 5.0f * deltaTime;
camera.Position = glm::mix(camera.Position, targetPosition, lerpFactor);
}
环绕摄像机
环绕摄像机围绕一个目标点旋转:
float radius = 10.0f;
float angle = 0.0f;
void updateOrbitCamera() {
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view = glm::lookAt(
glm::vec3(camX, 0.0f, camZ), // 摄像机位置
glm::vec3(0.0f, 0.0f, 0.0f), // 目标点
glm::vec3(0.0f, 1.0f, 0.0f) // 上向量
);
}
小结
摄像机系统是3D场景交互的核心:
- 视图矩阵定义了摄像机的位置和方向
- 欧拉角(俯仰、偏航)控制视角旋转
- 键盘控制移动,鼠标控制视角
- FOV控制视野范围
下一章我们将学习 光照,为场景添加真实感的光照效果。
摄像机调试
如果摄像机行为异常,检查以下几点:
- 前向量是否正确计算
- 俯仰角是否被限制在合理范围
- deltaTime是否正确计算
- 鼠标偏移方向是否正确