1. 功能介绍
1.1 相机的基础概念
| 概念名称 | 作用说明 |
|---|---|
| 相机位置 | 相机所在的位置,表示站在哪里进行观察 |
| 目标点 | 相机看向的目标位置,表示看向谁、朝向谁 |
| 上方向向量 | 相机的头部朝向,用于表示相机头顶朝向哪个方向,控制相机倾斜 / 翻转 |
以目标点 为原点建立坐标系,其参照图如下:

1.2 球坐标与笛卡尔坐标的转换
向量的概念请在基础课程《线性代数》中进行基础的理解与学习
| 参数名称 | 作用说明 |
|---|---|
| 前方向向量 | 你眼睛看出去的 "视线方向"------ 比如你盯着墙上的钟,从你眼球(相机位置)到钟(目标点)的那根 "无形的线",就是前方向向量。 |
| 偏航角(Yaw) | 你现在盯着墙上的钟(目标点),向右走动,这时候为了保证看向钟,你会摆头向左看,这时所产生的角度为偏航角 |
| 俯仰角(Pitch) | 你还是盯着墙上的钟(目标点),向上飞起,这时候为了保证看向钟,你会低头向下看,这时所产生的角度为俯仰角 |
以目标点 建立坐标系,其参照图如下

1.3 投影矩阵
投影矩阵(Projection Matrix)是 3D 渲染中将 3D 空间坐标 "挤压" 到 2D 屏幕上的关键矩阵,它解决了 "如何把立体的 3D 场景变成平面图像" 的问题。在glm库中的perspective 函数用于创建透视投影矩阵,模拟人眼观察世界的 "近大远小" 效果(比如远处的物体看起来更小)
- 垂直视野角度(Field of View, FOV)
含义:相机 "视野的上下角度范围",类似人眼的 "视野开阔程度"。
效果:角度越大(比如 90°),视野越宽,能看到的范围越多,但物体看起来更小;角度越小(比如 30°),视野越窄,物体看起来更大更 "近"。 - 宽高比(Aspect Ratio)
含义:屏幕宽度与高度的比例(比如 800x600 的屏幕,宽高比 = 4:3)。
作用:确保 3D 物体在屏幕上不会被 "拉伸变形"。如果宽高比设置错误,例如圆形会被拉成椭圆。 - 近平面(Near Plane)
含义:相机能看到的 "最近距离"。距离小于 0.1 的物体(比如贴在镜头上的物体)会被裁剪掉,不显示。
作用:避免近距离物体遮挡视线,同时提升渲染效率(不用渲染过近的物体)。 - 远平面(Far Plane)
含义:相机能看到的 "最远距离"。距离大于 100 的物体(比如极远处的山)会被裁剪掉,不显示。
作用:避免渲染过远的物体(人眼也看不清),节省计算资源。

2. 基础环境
-
下载 glm库
GLM(Generalized Linear Models)库是一个用于广义线性模型的C++数学库,主要设计用于图形渲染和游戏开发中的向量、矩阵运算。它提供与GLSL(OpenGL Shading Language)相似的语法和功能,便于开发者实现高性能的数学计算。
3. 项目结构
前置项目:OpenGL使用C++ 绘制三角形,xmake配置不变
- 将glm库中的glm文件夹拷贝到include文件夹下
- 补充glcamera.h和glcamera.cpp文件,用以实现相机模块功能
bash
opengl-xmake-demo/
├─ include/ # 头文件目录
│ ├─ ***
│ ├─ glm/ # glm库
│ ├─ glcamera.h # 相机模块
│ └─ glwindow.h # OpenGL核心功能模块
├─ src/
│ ├─ ***
│ ├─ glcamera.cpp # 相机模块
│ ├─ glwindow.cpp # OpenGL核心功能模块
│ └─ main.cpp # 主程序代码(测试代码见下文)
└─ xmake.lua # XMake 配置文件(核心)
4. 源代码实现
4.1 相机模块
glcamera.h
cpp
#ifndef GLCAMERA_H
#define GLCAMERA_H
#include <glm/glm.hpp>
#include <GLFW/glfw3.h>
// 相机类:处理视角旋转、缩放和矩阵计算
class GLCamera
{
public:
// 构造函数:初始化相机参数(窗口宽高用于投影矩阵)
GLCamera(int windowWidth, int windowHeight,
const glm::vec3 &target = glm::vec3(0.0f, 0.0f, 1.0f),
float initialDistance = 3.0f);
// 析构函数
~GLCamera();
public:
// 根据角度更新相机前方向向量
void updateCameraVectors();
// 更新视图矩阵(内部调用,也可外部强制更新)
void updateViewMatrix();
// 鼠标按键回调(处理右键按下/释放)
void mouseButtonCallback(GLFWwindow *window, int button, int action, int mods);
// 鼠标移动回调(处理右键拖动旋转)
void mouseMotionCallback(GLFWwindow *window, double xpos, double ypos);
// 鼠标滚轮回调(处理缩放)
void scrollCallback(double xoffset, double yoffset);
// 窗口大小变化时更新投影矩阵
void resize(int width, int height);
// 获取视图矩阵
const glm::mat4 &getViewMatrix() const { return view; }
// 获取投影矩阵
const glm::mat4 &getProjectionMatrix() const { return projection; }
private:
// 相机属性
glm::vec3 cameraPos; // 相机位置
glm::vec3 cameraTarget; // 目标点(看向的点)
glm::vec3 cameraUp; // 上方向向量
glm::vec3 cameraFront; // 前方向向量(指向目标点)
// 视角控制参数
float yaw; // 偏航角(左右旋转,初始-90°指向Z轴负方向)
float pitch; // 俯仰角(上下旋转)
float distance; // 相机到目标点的距离
float sensitivity; // 鼠标灵敏度
float zoomSpeed; // 缩放速度
// 鼠标状态
bool isRightMouseDown; // 右键是否按下
double lastX, lastY; // 上一帧鼠标位置
// 矩阵
glm::mat4 view; // 视图矩阵
glm::mat4 projection; // 投影矩阵
};
#endif // GLCAMERA_H
glcamera.cpp
cpp
#include "glcamera.h"
#include <iostream>
#include <glm/gtc/matrix_transform.hpp>
GLCamera::GLCamera(int windowWidth, int windowHeight, const glm::vec3 &target, float initialDistance)
: cameraTarget(target), distance(initialDistance),
yaw(0.0f), pitch(0.0f), sensitivity(0.1f), zoomSpeed(0.2f),
isRightMouseDown(false), lastX(windowWidth / 2.0f), lastY(windowHeight / 2.0f)
{
// 初始化上方向为Y轴正方向
cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
// 计算初始相机位置
updateCameraVectors();
// 初始化视图矩阵
updateViewMatrix();
// 初始化投影矩阵
resize(windowWidth, windowHeight);
}
GLCamera::~GLCamera()
{
}
// 根据偏航角和俯仰角更新相机前方向向量
void GLCamera::updateCameraVectors()
{
glm::vec3 front;
front.x = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = -cos(glm::radians(yaw)) * cos(glm::radians(pitch));
// 浮点计算会存在误差,所以front的平方和结果不一定为1
// 将前方向向量(front)"归一化",确保它的长度始终为 1(单位向量)
cameraFront = glm::normalize(front);
// 更新相机位置
cameraPos = cameraTarget - cameraFront * distance;
}
// 更新视图矩阵(相机位置 = 目标点 - 前方向 * 距离)
void GLCamera::updateViewMatrix()
{
cameraPos = cameraTarget - cameraFront * distance;
view = glm::lookAt(cameraPos, cameraTarget, cameraUp);
}
// 鼠标按键事件:记录右键状态和初始位置
void GLCamera::mouseButtonCallback(GLFWwindow *window, int button, int action, int mods)
{
if (button == GLFW_MOUSE_BUTTON_RIGHT)
{
if (action == GLFW_PRESS)
{
isRightMouseDown = true;
glfwGetCursorPos(window, &lastX, &lastY); // 记录按下时的鼠标位置
}
else if (action == GLFW_RELEASE)
{
isRightMouseDown = false;
}
}
}
// 鼠标移动事件:右键拖动时旋转视角
void GLCamera::mouseMotionCallback(GLFWwindow *window, double xpos, double ypos)
{
// 仅在右键按下时处理
if (!isRightMouseDown)
return;
// 计算鼠标位移(当前位置 - 上一帧位置)
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // Y轴反向(上移为正)
lastX = xpos;
lastY = ypos;
// 应用灵敏度
xoffset *= sensitivity;
yoffset *= sensitivity;
// 更新角度
yaw += xoffset; // 偏航角(左右旋转)
pitch += yoffset; // 俯仰角(上下旋转)
// 限制俯仰角(-89° ~ 89°,避免相机翻转)
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
// 更新方向向量和视图矩阵
updateCameraVectors();
updateViewMatrix();
}
// 鼠标滚轮事件:缩放视角(调整相机到目标点的距离)
void GLCamera::scrollCallback(double xoffset, double yoffset)
{
distance -= (float)yoffset * zoomSpeed; // 滚轮Y偏移控制距离
if (distance < 0.5f)
distance = 0.5f; // 限制最小距离(避免穿过模型)
updateViewMatrix(); // 更新视图矩阵
}
// 窗口大小变化时更新投影矩阵(保持宽高比)
void GLCamera::resize(int width, int height)
{
projection = glm::perspective(
glm::radians(45.0f), // 1. 垂直视野角度(FOV)
(float)width / height, // 2. 宽高比(屏幕宽度/高度)
0.1f, // 3. 近平面(最近可见距离)
100.0f); // 4. 远平面(最远可见距离)
}
4.2 坐标轴与三棱锥
glwindow.h
cpp
#ifndef GLWINDOW_H
#define GLWINDOW_H
#include <iostream>
#include <string>
#include <GLFW/glfw3.h>
#include "glcamera.h"
class GLWindow
{
public:
GLWindow(int w, int h, const std::string &title);
~GLWindow();
void render();
void update();
bool shouldClose();
void processInput();
private:
bool initGLFW();
bool initGLAD();
void initShaders();
void initBuffers();
// 窗口大小和鼠标事件回调函数(静态成员函数)
static void framebufferSizeCallback(GLFWwindow *window, int width, int height);
static void mouseButtonCallback(GLFWwindow *window, int button, int action, int mods);
static void mouseMotionCallback(GLFWwindow *window, double xpos, double ypos);
static void scrollCallback(GLFWwindow *window, double xoffset, double yoffset);
private:
// 窗口相关信息
GLFWwindow *window;
int width;
int height;
std::string title;
// 渲染相关
unsigned int shaderProgram;
// 坐标轴
unsigned int coordVAO, coordVBO;
// 三棱锥
unsigned int pyramidVAO, pyramidVBO;
// 相机对象
GLCamera camera;
};
#endif // GLWINDOW_H
glwindow.cpp
cpp
// glad必须在glwindow.h之前包含
#include <glad/glad.h>
#include "glwindow.h"
GLWindow::GLWindow(int w, int h, const std::string &title)
: width(w), height(h), title(title), camera(w, h)
{
if (!initGLFW())
{
throw std::runtime_error("GLFW初始化失败!");
}
if (!initGLAD())
{
throw std::runtime_error("GLAD初始化失败!");
}
initShaders();
initBuffers();
}
GLWindow::~GLWindow()
{
// 释放VAO、VBO和着色器程序
glDeleteVertexArrays(1, &coordVAO);
glDeleteBuffers(1, &coordVBO);
glDeleteVertexArrays(1, &pyramidVAO);
glDeleteBuffers(1, &pyramidVBO);
glDeleteProgram(shaderProgram);
// 销毁窗口并终止GLFW
glfwDestroyWindow(window);
glfwTerminate();
}
/* 渲染函数 */
void GLWindow::render()
{
// 清空屏幕
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 使用着色器程序
glUseProgram(shaderProgram);
// 获取相机的视图矩阵和投影矩阵
const glm::mat4 &view = camera.getViewMatrix();
const glm::mat4 &projection = camera.getProjectionMatrix();
glm::mat4 model = glm::mat4(1.0f); // 模型矩阵(单位矩阵)
// 传递矩阵给着色器
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "model"), 1, GL_FALSE, &model[0][0]);
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "view"), 1, GL_FALSE, &view[0][0]);
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "projection"), 1, GL_FALSE, &projection[0][0]);
// -------------------------- 绘制XYZ坐标系 --------------------------
glBindVertexArray(coordVAO);
glDrawArrays(GL_LINES, 0, 6); // 6个顶点,绘制3条线段(GL_LINES模式:每2个顶点一条线)
// -------------------------- 绘制三棱锥 --------------------------
glBindVertexArray(pyramidVAO);
glDrawArrays(GL_TRIANGLES, 0, 18); // 18个顶点,绘制6个三角形(每个面2个三角形)
}
/* 更新函数 */
void GLWindow::update()
{
glfwSwapBuffers(window);
glfwPollEvents();
}
/* 检查窗口是否应该关闭 */
bool GLWindow::shouldClose()
{
return glfwWindowShouldClose(window);
}
/* 处理输入 */
void GLWindow::processInput()
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
/* 初始化GLFW */
bool GLWindow::initGLFW()
{
// 初始化GLFW
if (!glfwInit())
{
std::cerr << "GLFW初始化失败!" << std::endl;
return false;
}
// 设置OpenGL版本和配置文件
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 创建窗口
window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if (!window)
{
std::cerr << "窗口创建失败!" << std::endl;
glfwTerminate();
return false;
}
// 绑定窗口上下文将当前GLWindow对象指针存入窗口用户数据(供回调使用)
glfwMakeContextCurrent(window);
glfwSetWindowUserPointer(window, this);
// 设置事件回调函数
glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);
glfwSetMouseButtonCallback(window, mouseButtonCallback);
glfwSetCursorPosCallback(window, mouseMotionCallback);
glfwSetScrollCallback(window, scrollCallback);
return true;
}
/* 初始化GLAD*/
bool GLWindow::initGLAD()
{
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cerr << "Glad加载失败!" << std::endl;
return false;
}
// 设置视口大小(初始窗口大小)
glViewport(0, 0, width, height);
return true;
}
/* 初始化着色器程序 */
void GLWindow::initShaders()
{
glEnable(GL_DEPTH_TEST);
// 顶点着色器源码
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"uniform mat4 model;\n"
"uniform mat4 view;\n"
"uniform mat4 projection;\n"
"void main()\n"
"{\n"
" gl_Position = projection * view * model * vec4(aPos, 1.0);\n"
" ourColor = aColor;\n"
"}\0";
// 片段着色器源码
const char *fragmentShaderSource = "#version 330 core\n"
"in vec3 ourColor;\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor, 1.0);\n"
"}\0";
// 编译顶点着色器
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
// 检查顶点着色器错误
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
std::cerr << "顶点着色器编译失败:\n"
<< infoLog << std::endl;
}
// 编译片段着色器
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
// 检查片段着色器错误
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
std::cerr << "片段着色器编译失败:\n"
<< infoLog << std::endl;
}
// 链接着色器程序
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 检查链接错误
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
std::cerr << "着色器程序链接失败:\n"
<< infoLog << std::endl;
}
// 删除临时着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
/* 初始化缓冲区对象 */
void GLWindow::initBuffers()
{
// 格式:[位置(x,y,z), 颜色(r,g,b)],3条轴(X红、Y绿、Z蓝),每条轴2个顶点
float coordinateVertices[] = {
// X轴(红):原点(0,0,0) → (20,0,0)
0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
20.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
// Y轴(绿):原点(0,0,0) → (0,20,0)
0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 20.0f, 0.0f, 0.0f, 1.0f, 0.0f,
// Z轴(蓝):原点(0,0,0) → (0,0,20)
0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 20.0f, 0.0f, 0.0f, 1.0f};
// 创建VAO/VBO(坐标系)
glGenVertexArrays(1, &coordVAO);
glGenBuffers(1, &coordVBO);
glBindVertexArray(coordVAO);
glBindBuffer(GL_ARRAY_BUFFER, coordVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(coordinateVertices), coordinateVertices, GL_STATIC_DRAW);
// 位置属性(location=0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// 颜色属性(location=1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 顶点数据(位置+颜色)
float pyramidVertices[] = {
// -------------------------- 面1:底面(z=0平面,三角形(0,0,0)-(1,0,0)-(0,1,0),黄色)
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, // 顶点O(0,0,0)
1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, // 顶点A(1,0,0)
0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, // 顶点B(0,1,0)
// -------------------------- 面2:侧面(三角形(0,0,0)-(1,0,0)-(0,0,1),红色)
0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 顶点O(0,0,0)
1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 顶点A(1,0,0)
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, // 顶点C(0,0,1)
// -------------------------- 面3:侧面(三角形(0,0,0)-(0,1,0)-(0,0,1),绿色)
0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, // 顶点O(0,0,0)
0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // 顶点B(0,1,0)
0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 顶点C(0,0,1)
// -------------------------- 面4:侧面(三角形(1,0,0)-(0,1,0)-(0,0,1),蓝色)
1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, // 顶点A(1,0,0)
0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // 顶点B(0,1,0)
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f // 顶点C(0,0,1)
};
// 生成VAO和VBO
glGenVertexArrays(1, &pyramidVAO);
glGenBuffers(1, &pyramidVBO);
// 绑定VAO(必须先绑定VAO才能配置顶点属性)
glBindVertexArray(pyramidVAO);
// 绑定VBO并复制数据到GPU
glBindBuffer(GL_ARRAY_BUFFER, pyramidVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(pyramidVertices), pyramidVertices, GL_STATIC_DRAW);
// 配置位置属性(location=0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// 配置颜色属性(location=1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 解绑缓冲(可选,避免误操作)
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
// 窗口大小回调:转发给相机更新投影矩阵
void GLWindow::framebufferSizeCallback(GLFWwindow *window, int width, int height)
{
// 从窗口用户数据中获取GLWindow对象指针
GLWindow *glWindow = static_cast<GLWindow *>(glfwGetWindowUserPointer(window));
if (glWindow)
{
// 更新窗口大小并设置视口
glWindow->width = width;
glWindow->height = height;
glViewport(0, 0, width, height);
glWindow->camera.resize(width, height); // 更新相机投影矩阵
}
}
// 鼠标按键回调:转发给相机
void GLWindow::mouseButtonCallback(GLFWwindow *window, int button, int action, int mods)
{
GLWindow *glWindow = static_cast<GLWindow *>(glfwGetWindowUserPointer(window));
if (glWindow)
{
glWindow->camera.mouseButtonCallback(window, button, action, mods);
}
}
// 鼠标移动回调:转发给相机
void GLWindow::mouseMotionCallback(GLFWwindow *window, double xpos, double ypos)
{
GLWindow *glWindow = static_cast<GLWindow *>(glfwGetWindowUserPointer(window));
if (glWindow)
{
glWindow->camera.mouseMotionCallback(window, xpos, ypos);
}
}
// 鼠标滚轮回调:转发给相机
void GLWindow::scrollCallback(GLFWwindow *window, double xoffset, double yoffset)
{
GLWindow *glWindow = static_cast<GLWindow *>(glfwGetWindowUserPointer(window));
if (glWindow)
{
glWindow->camera.scrollCallback(xoffset, yoffset);
}
}
5. 运行效果图
bash
xmake run
