OpenGL摄像机系统完全解析:从零实现3D场景漫游
作为一名OpenGL初学者,当我第一次看到三维场景中能够自由移动的摄像机时,内心充满了好奇。今天,我将带大家深入理解OpenGL中的摄像机系统,并通过一个完整的代码示例,手把手教你实现3D场景漫游。
一、为什么需要摄像机?
在现实生活中,我们通过眼睛观察世界。在3D图形学中,摄像机就扮演着“眼睛”的角色。它决定了:
-
我们从什么位置观察场景(位置)
-
我们看向什么方向(朝向)
-
哪个方向是“上”(上向量)
没有摄像机,我们只能看到固定的画面,无法从不同角度欣赏3D场景。
二、核心概念:三大向量
摄像机系统的核心由三个向量构成:
2.1摄像机位置(Camera Position)
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
这是摄像机在世界坐标系中的位置。上面的代码将摄像机放在(0,0,3),即原点后方3个单位处。
2.2 摄像机朝向(Camera Direction/Front)
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
这表示摄像机看向的方向。这里指向Z轴负方向,意味着摄像机正对着原点。
重要概念:实际计算中,我们通常使用“方向向量”而不是“目标点”。方向向量 = 目标点 - 位置。
2.3上向量(Up Vector)
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
这定义了哪边是“向上”。通常是(0,1,0),即Y轴正方向。
三、LookAt矩阵:摄像机的灵魂
glm::lookAt函数是摄像机系统的核心,它根据上述三个要素生成一个视图矩阵:
glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
参数解析:
-
第一个参数:摄像机位置
-
第二个参数:摄像机看向的点(位置+朝向)
-
第三个参数:上向量
这个矩阵会将世界坐标转换到摄像机视角,实现“从摄像机角度看世界”的效果。
四、动态摄像机:自由移动
4.1 移动控制(WASD)
float cameraSpeed = static_cast<float>(2.5 * deltaTime);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
关键技巧:
-
向前移动:位置 + 朝向 × 速度
-
向后移动:位置 - 朝向 × 速度
-
左右移动:位置 ± (朝向 × 上向量) × 速度
注意这里的叉积:glm::cross(cameraFront, cameraUp) 计算出向右向量。
4.2 帧速率独立移动
float deltaTime = currentFrame - lastFrame;
float cameraSpeed = 2.5f * deltaTime;
这是非常重要的技巧!deltaTime表示上一帧到当前帧的时间差。乘以这个值可以确保:
-
无论电脑快慢,移动速度恒定
-
快电脑每帧时间短,移动量小
-
慢电脑每帧时间长,移动量大
五、鼠标控制:旋转视角
5.1 欧拉角
我们使用欧拉角(Yaw和Pitch)来表示旋转:
-
Yaw(偏航角):左右旋转,绕Y轴
-
Pitch(俯仰角):上下旋转,绕X轴
float yaw = -90.0f; // 初始指向-Z方向
float pitch = 0.0f; // 初始水平看向前方
5.2 鼠标输入处理
void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{
float xoffset = xpos - lastX; // 计算X轴偏移
float yoffset = lastY - ypos; // 反转Y轴(屏幕坐标Y向下)
xoffset *= sensitivity; // 应用灵敏度
yoffset *= sensitivity;
yaw += xoffset; // 更新偏航角
pitch += yoffset; // 更新俯仰角
// 限制俯仰角,防止万向锁和视野翻转
if(pitch > 89.0f) pitch = 89.0f;
if(pitch < -89.0f) pitch = -89.0f;
}
为什么限制pitch为±89度?
-
超过90度会导致摄像机“翻转”,体验不佳
-
避免万向锁问题(虽然现代四元数可以解决,但欧拉角简单够用)
5.3 从欧拉角计算方向向量
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);
数学原理:
-
X分量:cos(yaw) × cos(pitch)
-
Y分量:sin(pitch)
-
Z分量:sin(yaw) × cos(pitch)
这本质上是单位球面上的坐标转换。
六、视野控制:滚轮缩放
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;
}
FOV(Field of View,视野)影响透视投影:
-
大FOV:视野广,有“鱼眼”效果
-
小FOV:视野窄,类似长焦镜头
-
限制在1-45度避免极端情况
七、投影矩阵:透视效果
glm::mat4 projection = glm::perspective(
glm::radians(fov), // 视野角度
(float)SCR_WIDTH / SCR_HEIGHT, // 宽高比
0.1f, // 近平面
100.0f // 远平面
);
透视投影实现“近大远小”效果:
-
近平面:距离摄像机0.1单位内的物体被裁剪
-
远平面:距离摄像机100单位外的物体被裁剪
八、完整工作流程
while (!glfwWindowShouldClose(window))
{
// 1. 更新时间差
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 2. 处理输入(移动和旋转)
processInput(window);
// 3. 清除缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 4. 计算投影矩阵(基于FOV和宽高比)
glm::mat4 projection = glm::perspective(...);
// 5. 计算视图矩阵(基于摄像机位置和朝向)
glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
// 6. 对每个物体计算模型矩阵并渲染
for (unsigned int i = 0; i < 10; i++)
{
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, cubePositions[i]);
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
// 传递三个矩阵给着色器
ourShader.setMat4("model", model);
ourShader.setMat4("view", view);
ourShader.setMat4("projection", projection);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
}
九、常见问题与解决方案
9.1 摄像机抖动
原因:deltaTime计算不准确或速度系数过大
解决:确保速度系数适中(如2.5),检查时间计算逻辑
9.2 旋转异常
原因:俯仰角未限制或鼠标坐标未反转
解决:限制pitch在±89度,正确处理Y轴反转
9.3 物体被裁剪
原因:物体超出近/远平面范围
解决:调整透视投影的近/远平面参数,或将摄像机移近物体
十、进阶学习方向
掌握了基础摄像机后,你可以探索:
-
四元数旋转:避免万向锁,实现平滑插值
-
第一人称与第三人称切换:改变摄像机相对位置
-
摄像机路径动画:沿样条曲线移动
-
后期处理效果:景深、运动模糊
总结
OpenGL的摄像机系统本质上是通过矩阵变换实现的“虚拟眼睛”。理解三个核心向量(位置、朝向、上向量)和LookAt矩阵是关键。通过WASD控制移动、鼠标控制旋转、滚轮控制视野,就能实现完整的3D场景漫游体验。
记住最重要的公式:
最终顶点位置 = 投影矩阵 × 视图矩阵 × 模型矩阵 × 原始顶点位置
这个矩阵乘法链条,就是3D图形学的核心精髓。希望这篇文章能帮助你迈出OpenGL学习的重要一步!
完整代码
// 包含OpenGL加载库,用于在运行时加载OpenGL函数指针
#include <glad.h>
// 包含GLFW窗口库,用于创建窗口和处理输入
#include <glfw3.h>
// 包含stb_image图像加载库,用于加载纹理图片
#include <stb_image.h>
// GLM数学库核心组件,提供向量和矩阵类型
#include <glm.hpp>
// 矩阵变换相关函数(平移、旋转、缩放、透视等)
#include <gtc/matrix_transform.hpp>
// 将glm矩阵转换为普通数组的工具
#include <gtc/type_ptr.hpp>
// 自定义着色器类的头文件,封装了着色器编译和使用的功能
#include <shader.h>
// 标准输入输出流,用于控制台输出
#include <iostream>
// 函数声明:窗口大小改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
// 函数声明:鼠标移动时的回调函数,用于控制摄像机视角
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
// 函数声明:鼠标滚轮滚动时的回调函数,用于控制视野缩放
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
// 函数声明:处理键盘输入的函数
void processInput(GLFWwindow* window);
// 设置:窗口宽度(像素)
const unsigned int SCR_WIDTH = 800;
// 设置:窗口高度(像素)
const unsigned int SCR_HEIGHT = 600;
// 摄像机位置(世界坐标系中的坐标)
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);
// 是否是第一次移动鼠标(用于初始化上次鼠标位置)
bool firstMouse = true;
// 偏航角(左右旋转角度),-90度表示初始指向Z轴负方向
float yaw = -90.0f;
// 俯仰角(上下旋转角度),0度表示水平向前
float pitch = 0.0f;
// 上一帧鼠标的X坐标
float lastX = 800.0f / 2.0;
// 上一帧鼠标的Y坐标
float lastY = 600.0 / 2.0;
// 视野角度(Field of View),45度为正常视角
float fov = 45.0f;
// 时间相关:当前帧与上一帧的时间差
float deltaTime = 0.0f;
// 上一帧的时间戳
float lastFrame = 0.0f;
int main()
{
// ========== GLFW初始化与配置 ==========
// 初始化GLFW库
glfwInit();
// 设置OpenGL主版本号为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
// 设置OpenGL次版本号为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// 使用核心模式(不兼容旧版本特性)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
// macOS系统需要额外设置向前兼容
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// ========== 创建GLFW窗口 ==========
// 创建窗口对象:宽度、高度、标题、全屏选项、共享资源
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate(); // 终止GLFW,释放资源
return -1;
}
// 将创建的窗口设置为当前线程的OpenGL上下文
glfwMakeContextCurrent(window);
// 注册窗口大小改变的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 注册鼠标移动的回调函数
glfwSetCursorPosCallback(window, mouse_callback);
// 注册鼠标滚轮回调函数
glfwSetScrollCallback(window, scroll_callback);
// 告诉GLFW捕获鼠标光标(隐藏光标并锁定在窗口内)
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// ========== 初始化GLAD ==========
// GLAD用于加载OpenGL函数指针,必须在使用任何OpenGL函数之前调用
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// ========== 配置OpenGL全局状态 ==========
// 启用深度测试:让远处的物体被近处的遮挡
glEnable(GL_DEPTH_TEST);
// ========== 编译着色器程序 ==========
// 加载并编译顶点着色器和片段着色器
Shader ourShader("7.3.camera.vs", "7.3.camera.fs");
// ========== 设置顶点数据 ==========
// 顶点数组:每个顶点包含3个位置坐标(x,y,z)和2个纹理坐标(u,v)
// 总共36个顶点(6个面 × 6个顶点/面 = 36)
float vertices[] = {
// 前面(Z = -0.5)的6个顶点(2个三角形)
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, // 左下角顶点
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, // 右下角顶点
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, // 右上角顶点
0.5f, 0.5f, -0.5f, 1.0f, 1.0f, // 重复,用于第二个三角形
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, // 左上角顶点
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, // 左下角顶点(重复)
// 后面(Z = 0.5)的6个顶点
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
// 左面(X = -0.5)的6个顶点
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
// 右面(X = 0.5)的6个顶点
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
// 下面(Y = -0.5)的6个顶点
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
// 上面(Y = 0.5)的6个顶点
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
// 10个立方体的世界坐标位置
glm::vec3 cubePositions[] = {
glm::vec3(0.0f, 0.0f, 0.0f), // 第1个:原点
glm::vec3(2.0f, 5.0f, -15.0f), // 第2个:右上方远处
glm::vec3(-1.5f, -2.2f, -2.5f), // 第3个:左下方稍远
glm::vec3(-3.8f, -2.0f, -12.3f), // 第4个:左方远处
glm::vec3(2.4f, -0.4f, -3.5f), // 第5个:右方稍远
glm::vec3(-1.7f, 3.0f, -7.5f), // 第6个:左上方较远
glm::vec3(1.3f, -2.0f, -2.5f), // 第7个:右下方稍远
glm::vec3(1.5f, 2.0f, -2.5f), // 第8个:右上方稍远
glm::vec3(1.5f, 0.2f, -1.5f), // 第9个:右方较近
glm::vec3(-1.3f, 1.0f, -1.5f) // 第10个:左方较近
};
// ========== 创建顶点缓冲对象和顶点数组对象 ==========
unsigned int VBO, VAO;
// 生成1个顶点数组对象(VAO)
glGenVertexArrays(1, &VAO);
// 生成1个顶点缓冲对象(VBO)
glGenBuffers(1, &VBO);
// 绑定VAO,后续的顶点属性配置会记录到这个VAO中
glBindVertexArray(VAO);
// 绑定VBO为GL_ARRAY_BUFFER类型的目标
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 将顶点数据复制到VBO中
// GL_STATIC_DRAW表示数据不会改变且频繁用于绘制
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// ========== 配置顶点属性指针 ==========
// 位置属性(location = 0):3个float,步长5个float,偏移0
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
// 启用位置属性
glEnableVertexAttribArray(0);
// 纹理坐标属性(location = 1):2个float,步长5个float,偏移3个float
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
// 启用纹理坐标属性
glEnableVertexAttribArray(1);
// ========== 加载和创建纹理 ==========
unsigned int texture1, texture2;
// ----- 纹理1 -----
glGenTextures(1, &texture1); // 生成纹理对象
glBindTexture(GL_TEXTURE_2D, texture1); // 绑定纹理,后续操作作用于该纹理
// 设置纹理环绕方式(超出纹理坐标范围时的处理)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // S轴(U)重复
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // T轴(V)重复
// 设置纹理过滤方式(放大缩小时的采样方式)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 缩小:线性过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大:线性过滤
int width, height, nrChannels; // 图片宽度、高度、通道数
stbi_set_flip_vertically_on_load(true); // 翻转Y轴,使纹理坐标原点在左下角
// 加载图片数据
unsigned char* data = stbi_load("雷欧.png", &width, &height, &nrChannels, 0);
if (data)
{
// 生成二维纹理图像
// 参数:目标、Mipmap等级、内部格式、宽、高、边框、输入格式、数据类型、图像数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
// 生成Mipmap(多级渐远纹理,提高渲染性能)
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data); // 释放图像内存
// ----- 纹理2 -----
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
if (data)
{
// 注意:awesomeface.png有透明通道,所以使用GL_RGBA格式
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
// ========== 设置着色器uniform变量 ==========
ourShader.use(); // 激活着色器程序
ourShader.setInt("texture1", 0); // 设置纹理单元0对应texture1
ourShader.setInt("texture2", 1); // 设置纹理单元1对应texture2
// ========== 主渲染循环 ==========
while (!glfwWindowShouldClose(window)) // 循环直到窗口被关闭
{
// ----- 计算帧时间差 -----
float currentFrame = static_cast<float>(glfwGetTime()); // 获取当前时间(秒)
deltaTime = currentFrame - lastFrame; // 计算两帧之间的时间差
lastFrame = currentFrame; // 更新上一帧时间
// ----- 处理输入 -----
processInput(window); // 响应键盘输入(WASD移动、ESC退出)
// ----- 渲染 -----
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清空颜色(深蓝绿色)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清空颜色缓冲和深度缓冲
// ----- 绑定纹理到纹理单元 -----
glActiveTexture(GL_TEXTURE0); // 激活纹理单元0
glBindTexture(GL_TEXTURE_2D, texture1); // 绑定纹理1到当前激活单元
glActiveTexture(GL_TEXTURE1); // 激活纹理单元1
glBindTexture(GL_TEXTURE_2D, texture2); // 绑定纹理2到当前激活单元
// ----- 激活着色器 -----
ourShader.use();
// ----- 计算并传递投影矩阵 -----
// 透视投影:视野角度、宽高比、近平面、远平面
glm::mat4 projection = glm::perspective(glm::radians(fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
ourShader.setMat4("projection", projection); // 传递给着色器
// ----- 计算并传递视图矩阵(摄像机视角)-----
// lookAt矩阵:摄像机位置、观察点位置、上向量
glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
ourShader.setMat4("view", view);
// ----- 渲染所有立方体 -----
glBindVertexArray(VAO); // 绑定VAO
for (unsigned int i = 0; i < 10; i++) // 循环10次,绘制10个立方体
{
// 计算模型矩阵
glm::mat4 model = glm::mat4(1.0f); // 初始化为单位矩阵
model = glm::translate(model, cubePositions[i]); // 平移到指定位置
float angle = 20.0f * i; // 每个立方体旋转角度不同
// 绕轴(1.0, 0.3, 0.5)旋转
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", model); // 传递模型矩阵给着色器
glDrawArrays(GL_TRIANGLES, 0, 36); // 绘制36个顶点(一个立方体)
}
// ----- 交换缓冲区和处理事件 -----
glfwSwapBuffers(window); // 交换前后缓冲区(双缓冲技术)
glfwPollEvents(); // 处理所有待处理的事件(键盘、鼠标等)
}
// ========== 清理资源 ==========
glDeleteVertexArrays(1, &VAO); // 删除VAO
glDeleteBuffers(1, &VBO); // 删除VBO
glfwTerminate(); // 终止GLFW,释放所有资源
return 0;
}
// 处理键盘输入的函数
void processInput(GLFWwindow* window)
{
// 按ESC键退出程序
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
// 根据帧时间计算移动速度(保证不同帧率下移动速度一致)
float cameraSpeed = static_cast<float>(2.5 * deltaTime);
// W键:向前移动(沿摄像机朝向方向)
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
// S键:向后移动
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
// A键:向左移动(向右向量反方向)
// 向右向量 = 摄像机朝向 × 上向量
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
// D键:向右移动
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
// 窗口大小改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// 设置OpenGL视口大小(渲染区域)
glViewport(0, 0, width, height);
}
// 鼠标移动时的回调函数(控制摄像机视角)
void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{
float xpos = static_cast<float>(xposIn); // 当前鼠标X坐标
float ypos = static_cast<float>(yposIn); // 当前鼠标Y坐标
// 如果是第一次移动,初始化上次位置,避免画面跳跃
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
// 计算鼠标移动偏移量
float xoffset = xpos - lastX; // X轴偏移(左右)
float yoffset = lastY - ypos; // Y轴偏移(上下),注意反转方向
lastX = xpos; // 更新上次X坐标
lastY = ypos; // 更新上次Y坐标
// 鼠标灵敏度(控制旋转速度)
float sensitivity = 0.1f;
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)); // X分量
front.y = sin(glm::radians(pitch)); // Y分量
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); // Z分量
cameraFront = glm::normalize(front); // 归一化后赋值给摄像机朝向
}
// 鼠标滚轮滚动时的回调函数(控制视野缩放)
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
// 滚轮上下滚动改变视野角度
fov -= (float)yoffset; // 向上滚动减小FOV(拉近视角),向下滚动增大FOV(拉远视角)
// 限制视野范围:1度到45度
if (fov < 1.0f)
fov = 1.0f;
if (fov > 45.0f)
fov = 45.0f;
}
核心知识点总结
-
OpenGL初始化流程:GLFW初始化 → 创建窗口 → GLAD初始化
-
顶点数据处理:VAO存储顶点属性配置,VBO存储实际顶点数据
-
纹理加载:stb_image加载图片 → 生成OpenGL纹理 → 设置过滤和环绕方式
-
摄像机系统:位置(Pos)、朝向(Front)、上向量(Up) → LookAt矩阵
-
投影矩阵:透视投影实现近大远小效果
-
MVP矩阵:模型(Model)、视图(View)、投影(Projection)三个变换矩阵
-
输入处理:键盘控制移动(帧速率独立)、鼠标控制视角(欧拉角)、滚轮控制视野
-
渲染循环:处理输入 → 更新状态 → 清空缓冲 → 绑定资源 → 绘制 → 交换缓冲
希望这份详细的注释能帮助你理解OpenGL的基础知识和摄像机系统的实现原理!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)