教程 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 上手实验室

相关推荐
lucky67072 小时前
Windows 上彻底卸载 Node.js
windows·node.js
编程小白20262 小时前
从 C++ 基础到效率翻倍:Qt 开发环境搭建与Windows 神级快捷键指南
开发语言·c++·windows·qt·学习
凯子坚持 c4 小时前
CANN 性能剖析实战:从原始事件到交互式火焰图
windows·microsoft
开开心心就好5 小时前
发票合并打印工具,多页布局设置实时预览
linux·运维·服务器·windows·pdf·harmonyos·1024程序员节
獨枭5 小时前
PyCharm 跑通 SAM 全流程实战
windows
仙剑魔尊重楼5 小时前
音乐制作电子软件FL Studio2025.2.4.5242中文版新功能介绍
windows·音频·录屏·音乐·fl studio
PHP小志6 小时前
Windows 服务器怎么修改密码和用户名?账户被系统锁定如何解锁
windows
专注VB编程开发20年7 小时前
vb.net datatable新增数据时改用数组缓存
java·linux·windows
仙剑魔尊重楼7 小时前
专业音乐制作软件fl Studio 2025.2.4.5242中文版新功能
windows·音乐·fl studio
rjc_lihui9 小时前
Windows 运程共享linux系统的方法
windows