从GLSL到C++:手把手教你用GLM在OpenGL/Vulkan中实现相机漫游(附完整代码) 从GLSL到C手把手教你用GLM在OpenGL/Vulkan中实现相机漫游附完整代码在图形学开发中相机控制是构建3D场景的基础能力。想象一下当你在游戏中探索虚拟世界时流畅的视角移动和旋转体验背后是一套精密的数学运算在支撑。本文将带你用GLM数学库从零实现一个专业级的第一人称相机控制器。1. 环境准备与基础概念1.1 GLM库的安装与配置GLMOpenGL Mathematics是一个专为图形学设计的C数学库其API设计完全模仿GLSL语法使得CPU端和GPU端的数学运算保持高度一致。安装非常简单# 使用vcpkg安装 vcpkg install glm # 或者直接包含头文件 git clone https://github.com/g-truc/glm.git在项目中引入GLM只需要包含核心头文件#include glm/glm.hpp #include glm/gtc/matrix_transform.hpp // 矩阵变换 #include glm/gtc/type_ptr.hpp // 内存操作1.2 相机系统的核心矩阵一个完整的相机系统需要三种关键矩阵矩阵类型作用GLM生成函数模型矩阵(Model)物体从局部坐标到世界坐标的变换glm::translate/rotate/scale观察矩阵(View)世界坐标到相机坐标的变换glm::lookAt投影矩阵(Projection)3D场景到2D平面的投影glm::perspective这三个矩阵的乘积MVP就是最终传递给着色器的变换矩阵glm::mat4 mvp projection * view * model;2. 构建相机类框架2.1 相机基础属性定义我们先定义一个Camera类来封装相机逻辑class Camera { public: // 构造函数初始化相机参数 Camera(glm::vec3 position glm::vec3(0.0f), glm::vec3 up glm::vec3(0.0f, 1.0f, 0.0f), float yaw -90.0f, float pitch 0.0f); // 获取观察矩阵 glm::mat4 GetViewMatrix(); // 处理键盘输入 void ProcessKeyboard(Camera_Movement direction, float deltaTime); // 处理鼠标输入 void ProcessMouseMovement(float xoffset, float yoffset); private: // 相机属性 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; };2.2 初始化相机朝向在构造函数中我们需要根据初始参数计算相机坐标系Camera::Camera(glm::vec3 position, glm::vec3 up, float yaw, float pitch) : 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(); } void Camera::updateCameraVectors() { // 计算新的Front向量 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和Up向量 Right glm::normalize(glm::cross(Front, WorldUp)); Up glm::normalize(glm::cross(Right, Front)); }3. 实现相机运动控制3.1 键盘移动控制第一人称相机通常支持WASD移动这里我们实现一个平滑的移动逻辑void Camera::ProcessKeyboard(Camera_Movement direction, float deltaTime) { float velocity MovementSpeed * deltaTime; switch (direction) { case FORWARD: Position Front * velocity; break; case BACKWARD: Position - Front * velocity; break; case LEFT: Position - Right * velocity; break; case RIGHT: Position Right * velocity; break; } }注意deltaTime是帧间隔时间用于实现与帧率无关的平滑移动3.2 鼠标视角控制鼠标控制需要处理两种输入视角旋转和滚轮缩放void Camera::ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch true) { xoffset * MouseSensitivity; yoffset * MouseSensitivity; Yaw xoffset; Pitch yoffset; // 确保俯仰角不会超过89度 if (constrainPitch) { if (Pitch 89.0f) Pitch 89.0f; if (Pitch -89.0f) Pitch -89.0f; } updateCameraVectors(); } void Camera::ProcessMouseScroll(float yoffset) { Zoom - yoffset; if (Zoom 1.0f) Zoom 1.0f; if (Zoom 45.0f) Zoom 45.0f; }4. 矩阵计算与着色器交互4.1 实时计算观察矩阵每次相机参数变化后我们需要重新计算观察矩阵glm::mat4 Camera::GetViewMatrix() { return glm::lookAt(Position, Position Front, Up); }4.2 投影矩阵配置投影矩阵通常只需要在窗口大小变化时更新// 窗口大小改变时调用 void UpdateProjection(int width, int height) { float aspect (float)width / (float)height; glm::mat4 projection glm::perspective(glm::radians(Zoom), aspect, 0.1f, 100.0f); // 传递给着色器 glUniformMatrix4fv(glGetUniformLocation(shaderID, projection), 1, GL_FALSE, glm::value_ptr(projection)); }4.3 着色器中的矩阵使用在GLSL着色器中我们这样使用这些矩阵#version 330 core layout (location 0) in vec3 aPos; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position projection * view * model * vec4(aPos, 1.0); }5. 高级功能扩展5.1 相机平滑插值为了避免相机移动生硬可以实现一个缓动函数void Camera::SmoothMoveTo(glm::vec3 targetPos, float duration) { glm::vec3 startPos Position; float elapsed 0.0f; while (elapsed duration) { float t elapsed / duration; t t * t * (3.0f - 2.0f * t); // 三次缓动 Position glm::mix(startPos, targetPos, t); elapsed deltaTime; updateCameraVectors(); } }5.2 相机路径录制与回放对于需要预定义相机路径的场景struct CameraKeyframe { glm::vec3 position; glm::vec3 rotation; float timestamp; }; std::vectorCameraKeyframe keyframes; void RecordCameraFrame() { keyframes.push_back({ Position, glm::vec3(Pitch, Yaw, 0.0f), currentTime }); } void PlaybackCamera() { // 在关键帧之间插值 // ... }5.3 碰撞检测集成在实际游戏中相机通常需要与环境碰撞检测void Camera::CheckCollision(const BoundingBox worldBounds) { // 简单的AABB碰撞检测 if (Position.x worldBounds.min.x) Position.x worldBounds.min.x; if (Position.y worldBounds.min.y) Position.y worldBounds.min.y; if (Position.z worldBounds.min.z) Position.z worldBounds.min.z; if (Position.x worldBounds.max.x) Position.x worldBounds.max.x; if (Position.y worldBounds.max.y) Position.y worldBounds.max.y; if (Position.z worldBounds.max.z) Position.z worldBounds.max.z; }6. 性能优化技巧6.1 矩阵计算优化避免每帧重复计算不变的内容// 只在必要时更新 if (cameraMoved) { viewMatrix camera.GetViewMatrix(); mvpMatrix projectionMatrix * viewMatrix * modelMatrix; cameraMoved false; }6.2 使用四元存储旋转对于频繁旋转的场景改用四元数可以提高性能glm::quat cameraOrientation; void UpdateFromQuaternion() { Front glm::normalize(glm::rotate(cameraOrientation, glm::vec3(0, 0, -1))); Right glm::normalize(glm::rotate(cameraOrientation, glm::vec3(1, 0, 0))); Up glm::normalize(glm::rotate(cameraOrientation, glm::vec3(0, 1, 0))); }6.3 多线程矩阵计算对于复杂场景可以将矩阵计算放到工作线程std::futureglm::mat4 futureMVP std::async(std::launch::async, [](){ return projection * view * model; }); // 渲染线程中 glm::mat4 mvp futureMVP.get();7. 跨API兼容性处理7.1 Vulkan适配注意事项Vulkan使用不同的坐标系系统需要进行矩阵调整// Vulkan专用的投影矩阵 glm::mat4 GetVulkanProjection() { glm::mat4 proj glm::perspective(glm::radians(fov), aspect, near, far); proj[1][1] * -1; // 翻转Y轴 return proj; }7.2 左右手坐标系转换不同图形API使用不同的坐标系系统API坐标系深度范围需要调整OpenGL右手[-1,1]无Vulkan右手[0,1]深度矩阵DirectX左手[0,1]投影矩阵// DirectX风格的左手投影矩阵 glm::mat4 GetDXProjection() { return glm::perspectiveLH(glm::radians(fov), aspect, near, far); }8. 完整代码实现以下是整合所有功能的完整Camera类实现// Camera.h #pragma once #include glm/glm.hpp #include glm/gtc/matrix_transform.hpp enum Camera_Movement { FORWARD, BACKWARD, LEFT, RIGHT }; class Camera { public: 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); glm::mat4 GetViewMatrix(); void ProcessKeyboard(Camera_Movement direction, float deltaTime); void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch true); void ProcessMouseScroll(float yoffset); // 相机属性 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; private: void updateCameraVectors(); }; // Camera.cpp #include Camera.h Camera::Camera(glm::vec3 position, glm::vec3 up, float yaw, float pitch) : 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 Camera::GetViewMatrix() { return glm::lookAt(Position, Position Front, Up); } void Camera::ProcessKeyboard(Camera_Movement direction, float deltaTime) { float velocity MovementSpeed * deltaTime; switch (direction) { case FORWARD: Position Front * velocity; break; case BACKWARD: Position - Front * velocity; break; case LEFT: Position - Right * velocity; break; case RIGHT: Position Right * velocity; break; } } void Camera::ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch) { 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 Camera::ProcessMouseScroll(float yoffset) { Zoom - yoffset; if (Zoom 1.0f) Zoom 1.0f; if (Zoom 45.0f) Zoom 45.0f; } void Camera::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)); }