教程 44 - 相机系统

上一篇:渲染目标和可配置渲染通道 | 下一篇:渲染视图系统 | 返回目录


📚 快速导航


目录


📖 简介

在之前的教程中,我们一直在硬编码相机的视图和投影矩阵。这限制了我们的灵活性 - 无法轻松改变视角、切换相机,或实现相机动画。

本教程将实现一个完整的相机系统 (Camera System),提供:

  • 灵活的相机组件
  • 多种投影模式(透视/正交)
  • 相机管理和切换
  • 常见的相机控制模式

Camera Controls 相机控制 Camera Component 相机组件 Camera System 相机系统 First Person
第一人称 Orbit
轨道 Free Fly
自由飞行 Transform
位置+旋转 Projection
投影参数 View Matrix
视图矩阵 Projection Matrix
投影矩阵 Camera System
相机系统 Camera 1
主相机 Camera 2
UI 相机 Camera 3
调试相机

为什么需要相机系统?

复制代码
硬编码相机 (旧方式):
┌────────────────────────────────────┐
│ mat4 view = mat4_look_at(...);     │
│ mat4 proj = mat4_perspective(...); │
│                                    │
│ 问题:                              │
│ ✗ 参数分散在代码中                 │
│ ✗ 无法保存/加载相机配置             │
│ ✗ 难以实现相机切换                 │
│ ✗ 无法管理多个相机                 │
└────────────────────────────────────┘

相机系统 (新方式):
┌────────────────────────────────────┐
│ camera* cam = camera_system_get_default(); │
│ mat4 view = camera_get_view(cam);  │
│ mat4 proj = camera_get_projection(cam); │
│                                    │
│ 优点:                              │
│ ✓ 相机是独立对象                   │
│ ✓ 统一管理和配置                   │
│ ✓ 轻松切换和动画                   │
│ ✓ 支持多相机场景                   │
└────────────────────────────────────┘

🎯 学习目标

目标 描述
理解相机概念 掌握视图变换和投影变换
实现相机组件 创建独立的相机对象
构建相机系统 管理多个相机
实现投影矩阵 透视和正交投影
相机控制 第一人称、轨道等控制模式

🎥 相机基础概念

什么是相机

在 3D 图形中,相机定义了观察者的视角:

复制代码
相机的作用:
┌────────────────────────────────────┐
│ World Space (世界空间)             │
│                                    │
│   🎄 Tree                          │
│       (10, 0, 5)                   │
│                                    │
│   🏠 House                         │
│       (0, 0, 0)                    │
│                                    │
│   📷 Camera                        │
│       Position: (5, 2, 10)        │
│       LookAt: (0, 0, 0)           │
└──────────────┬─────────────────────┘
               │ View Transform
               ▼
┌────────────────────────────────────┐
│ View Space (视图空间)              │
│                                    │
│   🎄 Tree                          │
│       (-5, -2, -5)                 │
│                                    │
│   🏠 House                         │
│       (-5, -2, -10)                │
│                                    │
│   📷 Camera                        │
│       (0, 0, 0) ← 原点             │
└──────────────┬─────────────────────┘
               │ Projection Transform
               ▼
┌────────────────────────────────────┐
│ Clip Space (裁剪空间)              │
│ [-1, 1] × [-1, 1] × [0, 1]        │
└────────────────────────────────────┘

相机空间变换

完整的相机变换管线:

glsl 复制代码
// Vertex Shader
vec4 world_pos = model * vec4(in_position, 1.0);
vec4 view_pos = view * world_pos;           // 视图变换
vec4 clip_pos = projection * view_pos;      // 投影变换
gl_Position = clip_pos;

变换矩阵:

矩阵 作用 输入空间 输出空间
Model 物体变换 模型空间 世界空间
View 相机变换 世界空间 视图空间
Projection 投影变换 视图空间 裁剪空间

MVP 矩阵:

c 复制代码
// 组合矩阵 (从右到左)
mat4 mvp = projection * view * model;

// Vertex Shader
gl_Position = mvp * vec4(in_position, 1.0);

投影类型

两种主要的投影模式:

1. 透视投影 (Perspective):

复制代码
透视投影 (有深度感):
        ╱│╲
       ╱ │ ╲
      ╱  │  ╲  ← 视锥体
     ╱   │   ╲
    ╱    │    ╲
   ╱─────┼─────╲
  📷 Camera    Far Plane

特点:
• 远处物体看起来更小
• 有灭点 (Vanishing Point)
• 适合 3D 场景

使用场景:
✓ 第一人称游戏
✓ 第三人称游戏
✓ 3D 场景渲染

2. 正交投影 (Orthographic):

复制代码
正交投影 (无深度感):
   ┌──────────┐
   │          │
   │          │  ← 盒子
   │          │
   │          │
   └──────────┘
  📷 Camera

特点:
• 远近物体大小相同
• 无灭点
• 适合 2D 渲染

使用场景:
✓ UI 渲染
✓ 2D 游戏
✓ CAD/建模工具
✓ 地图/小地图

📐 相机组件

相机数据结构

完整的相机组件定义:

c 复制代码
// engine/src/renderer/camera.h

typedef enum camera_projection_type {
    CAMERA_PROJECTION_PERSPECTIVE = 0,
    CAMERA_PROJECTION_ORTHOGRAPHIC
} camera_projection_type;

typedef struct camera {
    u32 id;                           // 相机 ID
    char name[256];                   // 相机名称

    // 变换
    vec3 position;                    // 位置
    vec3 euler_rotation;              // 欧拉角旋转 (pitch, yaw, roll)

    // 投影参数
    camera_projection_type projection_type;
    union {
        struct {
            f32 fov;                  // 视野角 (Field of View,度数)
            f32 aspect_ratio;         // 宽高比
            f32 near_clip;            // 近裁剪面
            f32 far_clip;             // 远裁剪面
        } perspective;

        struct {
            f32 left, right;          // 左右边界
            f32 bottom, top;          // 上下边界
            f32 near_clip, far_clip;  // 近远裁剪面
        } orthographic;
    };

    // 缓存的矩阵
    mat4 view;                        // 视图矩阵
    mat4 projection;                  // 投影矩阵

    // Dirty 标记
    b8 is_dirty;                      // 是否需要重新计算矩阵
} camera;

设计要点:

  • Union:透视和正交参数互斥,节省内存
  • 矩阵缓存:避免每帧重新计算
  • Dirty 标记:仅在参数改变时重新计算

相机创建和销毁

c 复制代码
// engine/src/renderer/camera.c

/**
 * @brief 创建透视相机
 */
camera* camera_create_perspective(
    vec3 position,
    f32 fov,
    f32 aspect_ratio,
    f32 near_clip,
    f32 far_clip
) {
    camera* cam = kallocate(sizeof(camera), MEMORY_TAG_CAMERA);

    cam->position = position;
    cam->euler_rotation = (vec3){0, 0, 0};

    cam->projection_type = CAMERA_PROJECTION_PERSPECTIVE;
    cam->perspective.fov = fov;
    cam->perspective.aspect_ratio = aspect_ratio;
    cam->perspective.near_clip = near_clip;
    cam->perspective.far_clip = far_clip;

    cam->is_dirty = true;

    // 初始化矩阵
    camera_update(cam);

    return cam;
}

/**
 * @brief 创建正交相机
 */
camera* camera_create_orthographic(
    vec3 position,
    f32 left, f32 right,
    f32 bottom, f32 top,
    f32 near_clip, f32 far_clip
) {
    camera* cam = kallocate(sizeof(camera), MEMORY_TAG_CAMERA);

    cam->position = position;
    cam->euler_rotation = (vec3){0, 0, 0};

    cam->projection_type = CAMERA_PROJECTION_ORTHOGRAPHIC;
    cam->orthographic.left = left;
    cam->orthographic.right = right;
    cam->orthographic.bottom = bottom;
    cam->orthographic.top = top;
    cam->orthographic.near_clip = near_clip;
    cam->orthographic.far_clip = far_clip;

    cam->is_dirty = true;

    camera_update(cam);

    return cam;
}

/**
 * @brief 销毁相机
 */
void camera_destroy(camera* cam) {
    if (cam) {
        kfree(cam, sizeof(camera), MEMORY_TAG_CAMERA);
    }
}

相机变换

设置相机位置和旋转:

c 复制代码
/**
 * @brief 设置相机位置
 */
void camera_set_position(camera* cam, vec3 position) {
    if (!vec3_compare(cam->position, position, 0.0001f)) {
        cam->position = position;
        cam->is_dirty = true;
    }
}

/**
 * @brief 设置相机旋转 (欧拉角,度数)
 */
void camera_set_rotation(camera* cam, vec3 euler_degrees) {
    if (!vec3_compare(cam->euler_rotation, euler_degrees, 0.0001f)) {
        cam->euler_rotation = euler_degrees;
        cam->is_dirty = true;
    }
}

/**
 * @brief 平移相机
 */
void camera_translate(camera* cam, vec3 translation) {
    cam->position = vec3_add(cam->position, translation);
    cam->is_dirty = true;
}

/**
 * @brief 旋转相机
 */
void camera_rotate(camera* cam, vec3 euler_delta_degrees) {
    cam->euler_rotation = vec3_add(cam->euler_rotation, euler_delta_degrees);
    cam->is_dirty = true;
}

🔍 视图矩阵

LookAt矩阵

最常用的视图矩阵构建方法:

c 复制代码
/**
 * @brief 创建 LookAt 视图矩阵
 * @param position 相机位置
 * @param target 观察目标点
 * @param up 上方向 (通常是 (0, 1, 0))
 */
mat4 mat4_look_at(vec3 position, vec3 target, vec3 up) {
    // 1. 计算相机坐标系的三个轴
    vec3 forward = vec3_normalized(vec3_sub(target, position));  // Z 轴 (前)
    vec3 right = vec3_normalized(vec3_cross(forward, up));        // X 轴 (右)
    vec3 actual_up = vec3_cross(right, forward);                  // Y 轴 (上)

    // 2. 构建旋转矩阵 (相机空间 → 世界空间的逆)
    mat4 result = mat4_identity();

    // 右向量
    result.data[0] = right.x;
    result.data[1] = actual_up.x;
    result.data[2] = -forward.x;  // OpenGL 使用右手坐标系,-Z 是前方

    // 上向量
    result.data[4] = right.y;
    result.data[5] = actual_up.y;
    result.data[6] = -forward.y;

    // 前向量
    result.data[8] = right.z;
    result.data[9] = actual_up.z;
    result.data[10] = -forward.z;

    // 3. 平移部分 (负相机位置)
    result.data[12] = -vec3_dot(right, position);
    result.data[13] = -vec3_dot(actual_up, position);
    result.data[14] = vec3_dot(forward, position);

    return result;
}

LookAt 原理:

复制代码
相机空间构建:
            ↑ Up (0, 1, 0)
            │
            │
     ───────●────── Right
           ╱│
          ╱ │
         ╱  │ Forward
        ╱   │
    Target  📷 Camera

步骤:
1. Forward = normalize(target - position)
2. Right = normalize(cross(forward, up))
3. Up = cross(right, forward)
4. 构建旋转 + 平移矩阵

欧拉角相机

使用欧拉角 (Pitch, Yaw, Roll) 控制相机:

c 复制代码
/**
 * @brief 从欧拉角计算视图矩阵
 */
void camera_update_view_euler(camera* cam) {
    // 1. 将欧拉角转换为弧度
    f32 pitch = deg_to_rad(cam->euler_rotation.x);
    f32 yaw = deg_to_rad(cam->euler_rotation.y);
    f32 roll = deg_to_rad(cam->euler_rotation.z);

    // 2. 计算前向量
    vec3 forward;
    forward.x = cosf(pitch) * sinf(yaw);
    forward.y = sinf(pitch);
    forward.z = cosf(pitch) * cosf(yaw);
    forward = vec3_normalized(forward);

    // 3. 计算目标点
    vec3 target = vec3_add(cam->position, forward);

    // 4. 使用 LookAt
    cam->view = mat4_look_at(cam->position, target, (vec3){0, 1, 0});
}

欧拉角定义:

复制代码
Pitch (俯仰): 绕 X 轴旋转
    ↑ (仰视)
   📷
    ↓ (俯视)

Yaw (偏航): 绕 Y 轴旋转
    ← (左转)
   📷
    → (右转)

Roll (翻滚): 绕 Z 轴旋转
    ↺ (逆时针)
   📷
    ↻ (顺时针)

四元数相机

使用四元数避免万向锁:

c 复制代码
/**
 * @brief 从四元数计算视图矩阵
 */
void camera_update_view_quaternion(camera* cam, quat rotation) {
    // 1. 将四元数转换为旋转矩阵
    mat4 rotation_matrix = quat_to_mat4(rotation);

    // 2. 提取前向量
    vec3 forward = {
        -rotation_matrix.data[8],
        -rotation_matrix.data[9],
        -rotation_matrix.data[10]
    };

    // 3. 计算目标点
    vec3 target = vec3_add(cam->position, forward);

    // 4. 提取上向量
    vec3 up = {
        rotation_matrix.data[4],
        rotation_matrix.data[5],
        rotation_matrix.data[6]
    };

    // 5. 使用 LookAt
    cam->view = mat4_look_at(cam->position, target, up);
}

📊 投影矩阵

透视投影

实现透视投影矩阵:

c 复制代码
/**
 * @brief 创建透视投影矩阵
 * @param fov_radians 垂直视野角 (弧度)
 * @param aspect_ratio 宽高比 (width / height)
 * @param near_clip 近裁剪面距离
 * @param far_clip 远裁剪面距离
 */
mat4 mat4_perspective(f32 fov_radians, f32 aspect_ratio, f32 near_clip, f32 far_clip) {
    f32 half_tan_fov = tanf(fov_radians * 0.5f);

    mat4 result = {0};  // 零矩阵

    result.data[0] = 1.0f / (aspect_ratio * half_tan_fov);  // X 缩放
    result.data[5] = 1.0f / half_tan_fov;                   // Y 缩放
    result.data[10] = -((far_clip + near_clip) / (far_clip - near_clip));  // Z 映射
    result.data[11] = -1.0f;  // W = -Z (透视除法)
    result.data[14] = -((2.0f * far_clip * near_clip) / (far_clip - near_clip));  // Z 偏移

    return result;
}

透视投影参数:

复制代码
FOV (Field of View 视野角):
┌──────────────────────────┐
│  ╱────╲  ← FOV = 90°     │
│ ╱      ╲ (宽视野)        │
│📷      │                 │
└──────────────────────────┘

┌──────────────────────────┐
│   ╱──╲   ← FOV = 45°     │
│  ╱    ╲  (窄视野)        │
│ 📷    │                  │
└──────────────────────────┘

推荐 FOV:
• 第一人称: 70-90°
• 第三人称: 60-75°
• 赛车游戏: 90-110°

Near/Far Clip:
┌────────────────────────────┐
│ Near Clip (0.1)            │
│   ├───────┤                │
│   📷                       │
│                            │
│             Far Clip (1000)│
│             ├──────────────┤
└────────────────────────────┘

推荐值:
• Near: 0.1 ~ 1.0
• Far: 100 ~ 10000
• Near/Far 比率尽量大

正交投影

实现正交投影矩阵:

c 复制代码
/**
 * @brief 创建正交投影矩阵
 * @param left 左边界
 * @param right 右边界
 * @param bottom 底边界
 * @param top 顶边界
 * @param near_clip 近裁剪面
 * @param far_clip 远裁剪面
 */
mat4 mat4_orthographic(f32 left, f32 right, f32 bottom, f32 top, f32 near_clip, f32 far_clip) {
    mat4 result = mat4_identity();

    f32 lr = 1.0f / (left - right);
    f32 bt = 1.0f / (bottom - top);
    f32 nf = 1.0f / (near_clip - far_clip);

    result.data[0] = -2.0f * lr;                   // X 缩放
    result.data[5] = -2.0f * bt;                   // Y 缩放
    result.data[10] = 2.0f * nf;                   // Z 缩放

    result.data[12] = (left + right) * lr;         // X 平移
    result.data[13] = (top + bottom) * bt;         // Y 平移
    result.data[14] = (far_clip + near_clip) * nf; // Z 平移

    return result;
}

正交投影应用:

c 复制代码
// UI 相机 (屏幕空间)
camera* ui_cam = camera_create_orthographic(
    (vec3){0, 0, 0},  // 位置
    0, 1920,          // 左右 (屏幕宽度)
    0, 1080,          // 下上 (屏幕高度)
    -100, 100         // 近远
);

// 2D 游戏相机
camera* game_2d_cam = camera_create_orthographic(
    (vec3){0, 0, 0},
    -10, 10,          // 世界单位
    -10, 10,
    -1, 1
);

投影参数

设置和更新投影参数:

c 复制代码
/**
 * @brief 设置透视投影 FOV
 */
void camera_set_fov(camera* cam, f32 fov_degrees) {
    if (cam->projection_type == CAMERA_PROJECTION_PERSPECTIVE) {
        if (KABS(cam->perspective.fov - fov_degrees) > 0.001f) {
            cam->perspective.fov = fov_degrees;
            cam->is_dirty = true;
        }
    }
}

/**
 * @brief 设置宽高比
 */
void camera_set_aspect_ratio(camera* cam, f32 aspect_ratio) {
    if (cam->projection_type == CAMERA_PROJECTION_PERSPECTIVE) {
        if (KABS(cam->perspective.aspect_ratio - aspect_ratio) > 0.001f) {
            cam->perspective.aspect_ratio = aspect_ratio;
            cam->is_dirty = true;
        }
    }
}

/**
 * @brief 设置裁剪面
 */
void camera_set_clip_planes(camera* cam, f32 near_clip, f32 far_clip) {
    if (cam->projection_type == CAMERA_PROJECTION_PERSPECTIVE) {
        cam->perspective.near_clip = near_clip;
        cam->perspective.far_clip = far_clip;
    } else {
        cam->orthographic.near_clip = near_clip;
        cam->orthographic.far_clip = far_clip;
    }
    cam->is_dirty = true;
}

🎮 相机系统

系统架构

相机系统管理多个相机:

c 复制代码
// engine/src/systems/camera_system.h

#define MAX_CAMERAS 256

typedef struct camera_system_config {
    u32 max_camera_count;  // 最大相机数量
} camera_system_config;

typedef struct camera_system_state {
    camera_system_config config;
    camera* cameras;       // 相机数组
    u32 camera_count;      // 当前相机数量
    camera* default_camera; // 默认相机
} camera_system_state;

系统初始化:

c 复制代码
// engine/src/systems/camera_system.c

static camera_system_state* state = NULL;

/**
 * @brief 初始化相机系统
 */
b8 camera_system_initialize(u64* memory_requirement, void* memory, camera_system_config config) {
    if (config.max_camera_count == 0) {
        KFATAL("camera_system_initialize: max_camera_count must be > 0");
        return false;
    }

    // 计算内存需求
    u64 struct_requirement = sizeof(camera_system_state);
    u64 array_requirement = sizeof(camera) * config.max_camera_count;
    *memory_requirement = struct_requirement + array_requirement;

    if (!memory) {
        return true;  // 仅查询内存需求
    }

    // 分配状态
    state = memory;
    state->config = config;
    state->cameras = (camera*)((u8*)memory + struct_requirement);
    state->camera_count = 0;

    // 创建默认相机
    state->default_camera = camera_system_acquire("default");
    camera_set_position(state->default_camera, (vec3){0, 0, 10});

    return true;
}

相机管理

获取和释放相机:

c 复制代码
/**
 * @brief 获取相机 (如果不存在则创建)
 */
camera* camera_system_acquire(const char* name) {
    // 1. 查找现有相机
    for (u32 i = 0; i < state->camera_count; ++i) {
        if (strings_equal(state->cameras[i].name, name)) {
            return &state->cameras[i];
        }
    }

    // 2. 创建新相机
    if (state->camera_count >= state->config.max_camera_count) {
        KERROR("camera_system_acquire: Maximum camera count reached");
        return NULL;
    }

    camera* new_cam = &state->cameras[state->camera_count];
    state->camera_count++;

    // 初始化相机
    string_ncopy(new_cam->name, name, 256);
    new_cam->id = state->camera_count - 1;

    // 默认为透视相机
    new_cam->projection_type = CAMERA_PROJECTION_PERSPECTIVE;
    new_cam->perspective.fov = 75.0f;
    new_cam->perspective.aspect_ratio = 1920.0f / 1080.0f;
    new_cam->perspective.near_clip = 0.1f;
    new_cam->perspective.far_clip = 1000.0f;

    new_cam->position = (vec3){0, 0, 0};
    new_cam->euler_rotation = (vec3){0, 0, 0};

    new_cam->is_dirty = true;
    camera_update(new_cam);

    KDEBUG("Camera acquired: %s (ID: %u)", name, new_cam->id);
    return new_cam;
}

/**
 * @brief 释放相机
 */
void camera_system_release(const char* name) {
    for (u32 i = 0; i < state->camera_count; ++i) {
        if (strings_equal(state->cameras[i].name, name)) {
            // 移动后续相机
            for (u32 j = i; j < state->camera_count - 1; ++j) {
                state->cameras[j] = state->cameras[j + 1];
            }
            state->camera_count--;
            KDEBUG("Camera released: %s", name);
            return;
        }
    }
}

默认相机

提供默认相机:

c 复制代码
/**
 * @brief 获取默认相机
 */
camera* camera_system_get_default() {
    if (state && state->default_camera) {
        return state->default_camera;
    }
    KERROR("camera_system_get_default: No default camera");
    return NULL;
}

/**
 * @brief 更新所有相机
 */
void camera_system_update(f32 delta_time) {
    for (u32 i = 0; i < state->camera_count; ++i) {
        camera* cam = &state->cameras[i];
        if (cam->is_dirty) {
            camera_update(cam);
        }
    }
}

🕹️ 相机控制

第一人称控制

实现 FPS 风格的相机控制:

c 复制代码
/**
 * @brief 第一人称相机控制器
 */
typedef struct fps_camera_controller {
    camera* camera;
    f32 move_speed;     // 移动速度
    f32 look_speed;     // 视角速度
    f32 mouse_sensitivity;
} fps_camera_controller;

/**
 * @brief 更新 FPS 相机
 */
void fps_camera_update(fps_camera_controller* controller, f32 delta_time) {
    camera* cam = controller->camera;

    // 1. 键盘移动
    vec3 forward = camera_get_forward(cam);
    vec3 right = camera_get_right(cam);

    if (input_is_key_down(KEY_W)) {
        vec3 move = vec3_mul_scalar(forward, controller->move_speed * delta_time);
        camera_translate(cam, move);
    }
    if (input_is_key_down(KEY_S)) {
        vec3 move = vec3_mul_scalar(forward, -controller->move_speed * delta_time);
        camera_translate(cam, move);
    }
    if (input_is_key_down(KEY_A)) {
        vec3 move = vec3_mul_scalar(right, -controller->move_speed * delta_time);
        camera_translate(cam, move);
    }
    if (input_is_key_down(KEY_D)) {
        vec3 move = vec3_mul_scalar(right, controller->move_speed * delta_time);
        camera_translate(cam, move);
    }

    // 2. 鼠标视角
    vec2 mouse_delta = input_get_mouse_delta();
    vec3 rotation = cam->euler_rotation;

    rotation.y += mouse_delta.x * controller->mouse_sensitivity;  // Yaw
    rotation.x += mouse_delta.y * controller->mouse_sensitivity;  // Pitch

    // 限制 Pitch (-89° ~ 89°)
    rotation.x = KCLAMP(rotation.x, -89.0f, 89.0f);

    camera_set_rotation(cam, rotation);
}

轨道相机

围绕目标旋转的相机:

c 复制代码
/**
 * @brief 轨道相机控制器
 */
typedef struct orbit_camera_controller {
    camera* camera;
    vec3 target;         // 观察目标
    f32 distance;        // 距离目标的距离
    f32 min_distance;    // 最小距离
    f32 max_distance;    // 最大距离
    f32 yaw;             // 水平旋转
    f32 pitch;           // 垂直旋转
    f32 orbit_speed;     // 旋转速度
    f32 zoom_speed;      // 缩放速度
} orbit_camera_controller;

/**
 * @brief 更新轨道相机
 */
void orbit_camera_update(orbit_camera_controller* controller, f32 delta_time) {
    // 1. 鼠标拖拽旋转
    if (input_is_button_down(BUTTON_RIGHT)) {
        vec2 mouse_delta = input_get_mouse_delta();
        controller->yaw -= mouse_delta.x * controller->orbit_speed;
        controller->pitch -= mouse_delta.y * controller->orbit_speed;

        // 限制 Pitch
        controller->pitch = KCLAMP(controller->pitch, -89.0f, 89.0f);
    }

    // 2. 鼠标滚轮缩放
    f32 scroll = input_get_mouse_wheel();
    controller->distance -= scroll * controller->zoom_speed;
    controller->distance = KCLAMP(controller->distance,
                                   controller->min_distance,
                                   controller->max_distance);

    // 3. 计算相机位置
    f32 yaw_rad = deg_to_rad(controller->yaw);
    f32 pitch_rad = deg_to_rad(controller->pitch);

    vec3 offset;
    offset.x = controller->distance * cosf(pitch_rad) * sinf(yaw_rad);
    offset.y = controller->distance * sinf(pitch_rad);
    offset.z = controller->distance * cosf(pitch_rad) * cosf(yaw_rad);

    vec3 position = vec3_add(controller->target, offset);
    camera_set_position(controller->camera, position);

    // 4. 看向目标
    camera_look_at(controller->camera, controller->target);
}

自由飞行

无重力的自由飞行相机:

c 复制代码
/**
 * @brief 自由飞行相机控制器
 */
typedef struct freefly_camera_controller {
    camera* camera;
    f32 move_speed;
    f32 boost_multiplier;  // Shift 加速倍数
    f32 look_speed;
} freefly_camera_controller;

/**
 * @brief 更新自由飞行相机
 */
void freefly_camera_update(freefly_camera_controller* controller, f32 delta_time) {
    camera* cam = controller->camera;

    // 速度调整
    f32 speed = controller->move_speed;
    if (input_is_key_down(KEY_LSHIFT)) {
        speed *= controller->boost_multiplier;
    }

    // 六自由度移动
    vec3 forward = camera_get_forward(cam);
    vec3 right = camera_get_right(cam);
    vec3 up = (vec3){0, 1, 0};  // 世界上方向

    // 前后
    if (input_is_key_down(KEY_W)) {
        camera_translate(cam, vec3_mul_scalar(forward, speed * delta_time));
    }
    if (input_is_key_down(KEY_S)) {
        camera_translate(cam, vec3_mul_scalar(forward, -speed * delta_time));
    }

    // 左右
    if (input_is_key_down(KEY_A)) {
        camera_translate(cam, vec3_mul_scalar(right, -speed * delta_time));
    }
    if (input_is_key_down(KEY_D)) {
        camera_translate(cam, vec3_mul_scalar(right, speed * delta_time));
    }

    // 上下
    if (input_is_key_down(KEY_SPACE)) {
        camera_translate(cam, vec3_mul_scalar(up, speed * delta_time));
    }
    if (input_is_key_down(KEY_LCONTROL)) {
        camera_translate(cam, vec3_mul_scalar(up, -speed * delta_time));
    }

    // 鼠标视角
    vec2 mouse_delta = input_get_mouse_delta();
    vec3 rotation = cam->euler_rotation;
    rotation.y += mouse_delta.x * controller->look_speed;
    rotation.x += mouse_delta.y * controller->look_speed;
    rotation.x = KCLAMP(rotation.x, -89.0f, 89.0f);
    camera_set_rotation(cam, rotation);
}

🎮 实际应用

完整的游戏相机示例:

c 复制代码
// game.h
typedef struct game_state {
    camera* main_camera;
    fps_camera_controller* camera_controller;
} game_state;

// game.c
b8 game_initialize(game* game_inst) {
    game_state* state = (game_state*)game_inst->state;

    // 创建主相机
    state->main_camera = camera_system_acquire("main");

    // 设置初始位置
    camera_set_position(state->main_camera, (vec3){0, 2, 10});
    camera_set_rotation(state->main_camera, (vec3){0, 0, 0});

    // 创建控制器
    state->camera_controller = kallocate(sizeof(fps_camera_controller), MEMORY_TAG_GAME);
    state->camera_controller->camera = state->main_camera;
    state->camera_controller->move_speed = 5.0f;
    state->camera_controller->look_speed = 0.2f;
    state->camera_controller->mouse_sensitivity = 0.1f;

    return true;
}

b8 game_update(game* game_inst, f32 delta_time) {
    game_state* state = (game_state*)game_inst->state;

    // 更新相机控制器
    fps_camera_update(state->camera_controller, delta_time);

    // 更新相机系统
    camera_system_update(delta_time);

    return true;
}

b8 game_render(game* game_inst, f32 delta_time) {
    game_state* state = (game_state*)game_inst->state;

    // 获取相机矩阵
    mat4 view = camera_get_view(state->main_camera);
    mat4 projection = camera_get_projection(state->main_camera);

    // 传递给渲染器
    shader_set_uniform(shader, "view", &view);
    shader_set_uniform(shader, "projection", &projection);

    // 渲染场景
    render_scene(state->scene);

    return true;
}

❓ 常见问题

1. 如何避免万向锁?

问题:

c 复制代码
// 欧拉角 Pitch = 90° 时出现万向锁
camera_set_rotation(cam, (vec3){90, 0, 0});
// Yaw 和 Roll 轴重合,失去一个自由度

解决方案:

c 复制代码
// 方案 1: 限制 Pitch 角度
rotation.x = KCLAMP(rotation.x, -89.0f, 89.0f);  // 避免 ±90°

// 方案 2: 使用四元数
typedef struct camera {
    quat rotation;  // 用四元数代替欧拉角
    // ...
} camera;

2. 如何实现平滑相机移动?

线性插值 (Lerp):

c 复制代码
/**
 * @brief 平滑移动相机
 */
void camera_smooth_move_to(camera* cam, vec3 target_pos, f32 speed, f32 delta_time) {
    f32 t = KMIN(speed * delta_time, 1.0f);
    vec3 new_pos = vec3_lerp(cam->position, target_pos, t);
    camera_set_position(cam, new_pos);
}

// 使用
vec3 target = {10, 2, 5};
camera_smooth_move_to(main_camera, target, 5.0f, delta_time);

弹簧阻尼:

c 复制代码
typedef struct smooth_camera {
    vec3 position;
    vec3 velocity;
    f32 damping;  // 阻尼系数 (0.1 ~ 0.3)
} smooth_camera;

void smooth_camera_update(smooth_camera* sc, vec3 target, f32 delta_time) {
    vec3 displacement = vec3_sub(target, sc->position);
    vec3 spring_force = vec3_mul_scalar(displacement, 10.0f);  // 弹簧力
    vec3 damping_force = vec3_mul_scalar(sc->velocity, -sc->damping);  // 阻尼力

    vec3 acceleration = vec3_add(spring_force, damping_force);
    sc->velocity = vec3_add(sc->velocity, vec3_mul_scalar(acceleration, delta_time));
    sc->position = vec3_add(sc->position, vec3_mul_scalar(sc->velocity, delta_time));
}

3. 如何实现相机抖动效果?

简单抖动:

c 复制代码
typedef struct camera_shake {
    f32 intensity;      // 强度
    f32 duration;       // 持续时间
    f32 elapsed;        // 已经过时间
} camera_shake;

void camera_shake_update(camera* cam, camera_shake* shake, f32 delta_time) {
    if (shake->elapsed >= shake->duration) {
        return;  // 抖动结束
    }

    shake->elapsed += delta_time;

    // 衰减强度
    f32 t = shake->elapsed / shake->duration;
    f32 current_intensity = shake->intensity * (1.0f - t);

    // 随机偏移
    vec3 offset;
    offset.x = (random_float() - 0.5f) * 2.0f * current_intensity;
    offset.y = (random_float() - 0.5f) * 2.0f * current_intensity;
    offset.z = 0;

    camera_translate(cam, offset);
}

// 触发抖动
camera_shake shake = {0};
shake.intensity = 0.5f;
shake.duration = 0.3f;
shake.elapsed = 0.0f;

4. 如何实现相机碰撞检测?

简单球体碰撞:

c 复制代码
void camera_handle_collisions(camera* cam, vec3 desired_position, f32 radius) {
    vec3 actual_position = desired_position;

    // 检查与场景中所有碰撞体
    for (u32 i = 0; i < collision_object_count; ++i) {
        collision_object* obj = &collision_objects[i];

        vec3 closest_point = find_closest_point_on_object(obj, desired_position);
        f32 distance = vec3_distance(closest_point, desired_position);

        if (distance < radius) {
            // 碰撞!推开相机
            vec3 push_dir = vec3_normalized(vec3_sub(desired_position, closest_point));
            vec3 push = vec3_mul_scalar(push_dir, radius - distance);
            actual_position = vec3_add(actual_position, push);
        }
    }

    camera_set_position(cam, actual_position);
}

5. 如何优化相机矩阵计算?

Dirty 标记优化:

c 复制代码
void camera_update(camera* cam) {
    if (!cam->is_dirty) {
        return;  // 无需更新
    }

    // 更新视图矩阵
    camera_update_view_euler(cam);

    // 更新投影矩阵
    if (cam->projection_type == CAMERA_PROJECTION_PERSPECTIVE) {
        cam->projection = mat4_perspective(
            deg_to_rad(cam->perspective.fov),
            cam->perspective.aspect_ratio,
            cam->perspective.near_clip,
            cam->perspective.far_clip
        );
    } else {
        cam->projection = mat4_orthographic(
            cam->orthographic.left,
            cam->orthographic.right,
            cam->orthographic.bottom,
            cam->orthographic.top,
            cam->orthographic.near_clip,
            cam->orthographic.far_clip
        );
    }

    cam->is_dirty = false;
}

性能对比:

方式 每帧计算次数 性能
无缓存 每帧重新计算
缓存 + Dirty 仅在改变时计算 快 10-100x

📝 练习

练习 1: 实现第三人称相机

任务: 创建跟随角色的第三人称相机。

c 复制代码
typedef struct third_person_camera {
    camera* camera;
    transform* target;     // 跟随目标
    f32 distance;          // 距离
    f32 height_offset;     // 高度偏移
    f32 follow_speed;      // 跟随速度
} third_person_camera;

void third_person_camera_update(third_person_camera* tpc, f32 delta_time) {
    // 计算理想位置
    vec3 target_pos = tpc->target->position;
    vec3 target_forward = transform_get_forward(tpc->target);

    vec3 ideal_pos = vec3_sub(target_pos, vec3_mul_scalar(target_forward, tpc->distance));
    ideal_pos.y += tpc->height_offset;

    // 平滑跟随
    vec3 current_pos = tpc->camera->position;
    vec3 new_pos = vec3_lerp(current_pos, ideal_pos, tpc->follow_speed * delta_time);

    camera_set_position(tpc->camera, new_pos);

    // 看向目标
    vec3 look_target = vec3_add(target_pos, (vec3){0, 1.0f, 0});  // 看向目标的头部
    camera_look_at(tpc->camera, look_target);
}

练习 2: 实现相机路径动画

任务: 沿预定义路径移动相机。

c 复制代码
typedef struct camera_path {
    vec3* waypoints;       // 路径点
    u32 waypoint_count;
    f32 duration;          // 总时长
} camera_path;

typedef struct camera_path_animator {
    camera* camera;
    camera_path* path;
    f32 time;              // 当前时间
    b8 is_playing;
    b8 loop;
} camera_path_animator;

void camera_path_animator_update(camera_path_animator* anim, f32 delta_time) {
    if (!anim->is_playing) return;

    anim->time += delta_time;

    // 循环
    if (anim->time >= anim->path->duration) {
        if (anim->loop) {
            anim->time = fmodf(anim->time, anim->path->duration);
        } else {
            anim->is_playing = false;
            return;
        }
    }

    // 计算当前段
    f32 t = anim->time / anim->path->duration;
    f32 segment_length = 1.0f / (anim->path->waypoint_count - 1);
    u32 segment = (u32)(t / segment_length);
    f32 segment_t = (t - segment * segment_length) / segment_length;

    // Catmull-Rom 样条插值
    vec3 p0 = anim->path->waypoints[KMAX(0, segment - 1)];
    vec3 p1 = anim->path->waypoints[segment];
    vec3 p2 = anim->path->waypoints[KMIN(segment + 1, anim->path->waypoint_count - 1)];
    vec3 p3 = anim->path->waypoints[KMIN(segment + 2, anim->path->waypoint_count - 1)];

    vec3 position = vec3_catmull_rom(p0, p1, p2, p3, segment_t);
    camera_set_position(anim->camera, position);

    // 切线方向
    vec3 tangent = vec3_normalized(vec3_sub(p2, p1));
    vec3 target = vec3_add(position, tangent);
    camera_look_at(anim->camera, target);
}

练习 3: 实现多相机切换

任务: 支持在多个相机间平滑切换。

c 复制代码
typedef struct camera_switcher {
    camera* cameras[8];
    u32 camera_count;
    u32 active_index;
    f32 blend_duration;
    f32 blend_time;
    b8 is_blending;
    camera blend_from;
} camera_switcher;

void camera_switcher_switch_to(camera_switcher* cs, u32 index) {
    if (index >= cs->camera_count) return;

    cs->blend_from = *cs->cameras[cs->active_index];  // 保存当前状态
    cs->active_index = index;
    cs->is_blending = true;
    cs->blend_time = 0.0f;
}

void camera_switcher_update(camera_switcher* cs, f32 delta_time) {
    if (!cs->is_blending) return;

    cs->blend_time += delta_time;
    f32 t = KMIN(cs->blend_time / cs->blend_duration, 1.0f);

    camera* target = cs->cameras[cs->active_index];

    // 插值位置
    vec3 pos = vec3_lerp(cs->blend_from.position, target->position, t);
    camera_set_position(target, pos);

    // 插值旋转 (四元数 Slerp)
    quat from_rot = euler_to_quat(cs->blend_from.euler_rotation);
    quat to_rot = euler_to_quat(target->euler_rotation);
    quat blended_rot = quat_slerp(from_rot, to_rot, t);
    vec3 euler = quat_to_euler(blended_rot);
    camera_set_rotation(target, euler);

    if (t >= 1.0f) {
        cs->is_blending = false;
    }
}

camera* camera_switcher_get_active(camera_switcher* cs) {
    return cs->cameras[cs->active_index];
}

恭喜!你已经掌握了相机系统! 🎉

返回目录


Tutorial written by 上手实验室

相关推荐
松涛和鸣3 小时前
Linux Makefile : From Basic Syntax to Multi-File Project Compilation
linux·运维·服务器·前端·windows·哈希算法
gugugu.6 小时前
Redis ZSet类型深度解析:有序集合的原理与实战应用
网络·windows·redis
开开心心就好8 小时前
免费卸载工具,可清理残留批量管理启动项
linux·运维·服务器·windows·随机森林·pdf·1024程序员节
淼淼7639 小时前
Qt调度 程序
开发语言·c++·windows·qt
Poetinthedusk9 小时前
设计模式-命令模式
windows·设计模式·c#·wpf·命令模式
whm277710 小时前
Visual Basic Data控件
windows·visual studio
feiduoge10 小时前
教程 43 - 渲染目标和可配置渲染通道
windows·游戏引擎·图形渲染
BoBoZz1910 小时前
RotationAroundLine 模型的旋转
python·vtk·图形渲染·图形处理