跳到主要内容

摄像机

摄像机决定了我们从哪个角度观察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控制视野范围

下一章我们将学习 光照,为场景添加真实感的光照效果。

摄像机调试

如果摄像机行为异常,检查以下几点:

  1. 前向量是否正确计算
  2. 俯仰角是否被限制在合理范围
  3. deltaTime是否正确计算
  4. 鼠标偏移方向是否正确