摄像机
前面的教程中我们讨论了观察矩阵以及如何使用观察矩阵移动场景(我们向后移动了一点)。OpenGL本身没有**摄像机**(Camera)的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种**我们**在移动的感觉,而不是场景在移动。
本节我们将会讨论如何在OpenGL中配置一个摄像机,并且将会讨论FPS风格的摄像机,让你能够在3D场景中自由移动。我们也会讨论键盘和鼠标输入,最终完成一个自定义的摄像机类。
摄像机/观察空间
当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。

摄像机位置
获取摄像机位置很简单。摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。我们把摄像机位置设置为上一节中的那个相同的位置:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
!NOTE
不要忘记正z轴是从屏幕指向你的,如果我们希望摄像机向后移动,我们就沿着z轴的正方向移动。
摄像机方向
下一个需要的向量是摄像机的方向,这里指的是摄像机指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。还记得如果将两个矢量相减,我们就能得到这两个矢量的差吗?用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。由于我们知道摄像机指向z轴负方向,但我们希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果我们交换相减的顺序,我们就会获得一个指向摄像机正z轴方向的向量:
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
!NOTE
方向向量(Direction Vector)并不是最好的名字,因为它实际上指向从它到目标向量的相反方向(译注:注意看前面的那个图,蓝色的方向向量大概指向z轴的正方向,与摄像机实际指向的方向是正好相反的)。
右轴
我们需要的另一个向量是一个右向量(Right Vector),它代表摄像机空间的x轴的正方向。为获取右向量我们需要先使用一个小技巧:先定义一个上向量(Up Vector)。接下来把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此我们会得到指向x轴正方向的那个向量(如果我们交换两个向量叉乘的顺序就会得到相反的指向x轴负方向的向量):
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
上轴
现在我们已经有了x轴向量和z轴向量,获取一个指向摄像机的正y轴向量就相对简单了:我们把右向量和方向向量进行叉乘:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
在叉乘和一些小技巧的帮助下,我们创建了所有构成观察/摄像机空间的向量。对于想学到更多数学原理的读者,提示一下,在线性代数中这个处理叫做格拉姆—施密特正交化(Gram-Schmidt Process)。使用这些摄像机向量我们就可以创建一个LookAt矩阵了,它在创建摄像机的时候非常有用。
Look At
使用矩阵的好处之一是如果你使用3个相互垂直(或非线性)的轴定义了一个坐标空间,你可以用这3个轴外加一个平移向量来创建一个矩阵,并且你可以用这个矩阵乘以任何向量来将其变换到那个坐标空间。这正是LookAt矩阵所做的,现在我们有了3个相互垂直的轴和一个定义摄像机空间的位置坐标,我们可以创建我们自己的LookAt矩阵了:

其中R是右向量,U是上向量,D是方向向量P是摄像机位置向量。注意,位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向。把这个LookAt矩阵作为观察矩阵可以很高效地把所有世界坐标变换到刚刚定义的观察空间。LookAt矩阵就像它的名字表达的那样:它会创建一个看着(Look at)给定目标的观察矩阵。
!WARNING
注意,这里有个错误,应该是 Ux那一行是 相机正在注视的世界坐标系点,应该是0 0 0 。然后Dx那一行才是上向量,用于告诉opengel屏幕的垂直方向对应世界Y轴。
幸运的是,GLM已经提供了这些支持。我们要做的只是定义一个摄像机位置,一个目标位置和一个表示世界空间中的上向量的向量(我们计算右向量使用的那个上向量)。接着GLM就会创建一个LookAt矩阵,我们可以把它当作我们的观察矩阵:
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
glm::LookAt函数需要一个位置、目标和上向量。它会创建一个和在上一节使用的一样的观察矩阵。
在讨论用户输入之前,我们先来做些有意思的事,把我们的摄像机在场景中旋转。我们会将摄像机的注视点保持在(0, 0, 0)。
我们需要用到一点三角学的知识来在每一帧创建一个x和z坐标,它会代表圆上的一点,我们将会使用它作为摄像机的位置。通过重新计算x和y坐标,我们会遍历圆上的所有点,这样摄像机就会绕着场景旋转了。我们预先定义这个圆的半径radius,在每次渲染迭代中使用GLFW的glfwGetTime函数重新创建观察矩阵,来扩大这个圆。
float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
自由移动
让摄像机绕着场景转的确很有趣,但是让我们自己移动摄像机会更有趣!首先我们必须设置一个摄像机系统,所以在我们的程序前面定义一些摄像机变量很有用:
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);
LookAt函数现在成了:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
我们首先将摄像机位置设置为之前定义的cameraPos。方向是当前的位置加上我们刚刚定义的方向向量。这样能保证无论我们怎么移动,摄像机都会注视着目标方向。让我们摆弄一下这些向量,在按下某些按钮时更新cameraPos向量。
我们已经为GLFW的键盘输入定义过一个processInput函数了,我们来新添加几个需要检查的按键命令:
void processInput(GLFWwindow *window)
{
...
float cameraSpeed = 0.05f; // adjust accordingly
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
当我们按下WASD键的任意一个,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向左右移动,我们使用叉乘来创建一个右向量(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的横移(Strafe)效果。
!NOTE
注意,我们对右向量进行了标准化。如果我们没对这个向量进行标准化,最后的叉乘结果会根据cameraFront变量返回大小不同的向量。如果我们不对向量进行标准化,我们就得根据摄像机的朝向不同加速或减速移动了,但如果进行了标准化移动就是匀速的。
现在你就应该能够移动摄像机了,虽然移动速度和系统有关,你可能会需要调整一下cameraSpeed。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <opencv4/opencv2/opencv.hpp>
#include <iostream>
#include <cmath>
#include <filesystem>
#include "utils/shader.h"
#include "utils/glmCompute.h"
#include "utils/logger.h"
using namespace cv;
using namespace glm;
// 窗口大小改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
std::string getCurrentWorkingDirectory() {
namespace fs = std::filesystem;
try {
// current_path() 返回当前工作目录
return fs::current_path().string();
} catch (const fs::filesystem_error& e) {
std::cerr << "Filesystem Error: " << e.what() << std::endl;
return "";
}
}
// 相机的位置和方向
vec3 cameraPos = vec3(0.0f, 0.0f, 3.0f);
vec3 cameraFront = vec3(0.0f, 0.0f, -1.0f); // 相机朝向
vec3 cameraUp = vec3(0.0f, 1.0f, 0.0f); // 上向量
// 鼠标控制变量
float yawAngle = -90.0f; // 偏航角
float pitchAngle = 0.0f; // 俯仰角
float lastX = 400.0f; // 鼠标上次X位置
float lastY = 300.0f; // 鼠标上次Y位置
bool firstMouse = true; // 是否是第一次鼠标移动
bool mouseCaptured = true; // 鼠标是否被捕获
// 鼠标回调函数
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
// 只有在鼠标被捕获时才处理鼠标移动
if (!mouseCaptured) return;
if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 注意这里是相反的,因为y坐标是从底部往顶部递增的
lastX = xpos;
lastY = ypos;
float sensitivity = 0.1f;
xoffset *= sensitivity;
yoffset *= sensitivity;
yawAngle += xoffset;
pitchAngle += yoffset;
// 限制俯仰角,避免翻转
if (pitchAngle > 89.0f)
pitchAngle = 89.0f;
if (pitchAngle < -89.0f)
pitchAngle = -89.0f;
// 计算新的相机方向
vec3 front;
front.x = cos(radians(yawAngle)) * cos(radians(pitchAngle));
front.y = sin(radians(pitchAngle));
front.z = sin(radians(yawAngle)) * cos(radians(pitchAngle));
cameraFront = normalize(front);
}
void processInput(GLFWwindow *window){
// 检查 Ctrl+Alt 组合键来切换鼠标捕获状态
bool ctrlPressed = glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS;
bool altPressed = glfwGetKey(window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS ||
glfwGetKey(window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS;
if (ctrlPressed && altPressed) {
if (mouseCaptured) {
// 释放鼠标捕获
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
mouseCaptured = false;
firstMouse = true; // 重置鼠标状态
std::cout << "鼠标捕获已释放 - 按 Ctrl+Alt 重新捕获" << std::endl;
} else {
// 重新捕获鼠标
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
mouseCaptured = true;
firstMouse = true; // 重置鼠标状态
std::cout << "鼠标捕获已启用" << std::endl;
}
}
// 只有在鼠标被捕获时才处理WASD移动
if (!mouseCaptured) return;
float cameraSpeed = 0.05f;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= normalize(cross(cameraFront, cameraUp)) * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += normalize(cross(cameraFront, cameraUp)) * cameraSpeed;
}
int main() {
// 模型矩阵
mat4 model = mat4(1.0f);
model = rotate(model, radians(-55.0f), vec3(1.0f, 0.0f, 0.0f));
// lookat矩阵
mat4 view = mat4(1.0f);
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
// 投影矩阵
mat4 projection = perspective(radians(45.0f), (float)800 / (float)600, 0.1f, 100.0f);
// 立方体数组
vec3 cubePositions[] = {
vec3(0.0f, 0.0f, 0.0f),
vec3(2.0f, 5.0f, -15.0f),
vec3(-1.5f, -2.2f, -2.5f),
vec3(-3.8f, -2.0f, -12.3f),
vec3(2.4f, -0.4f, -3.5f),
};
auto path = getCurrentWorkingDirectory().append("/Assets/image.png");
auto path2 = getCurrentWorkingDirectory().append("/Assets/image2.png");
cv::Mat image = cv::imread(path);
cv::Mat image2 = cv::imread(path2);
cv::flip(image2, image2, 0);
cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
cv::cvtColor(image2, image2, cv::COLOR_BGR2RGB);
// 转换为字节数组
std::vector<uchar> imageData;
std::vector<uchar> imageData2;
if (image.empty()) {
std::cerr << "Could not read the image: " << path << std::endl;
return 1;
}
if (image2.empty()) {
std::cerr << "Could not read the image2: " << path2 << std::endl;
return 1;
}
std::cout << "Image 1 loaded: " << image.cols << "x" << image.rows << std::endl;
std::cout << "Image 2 loaded: " << image2.cols << "x" << image2.rows << std::endl;
cv::imencode(".png", image, imageData);
cv::imencode(".png", image2, imageData2);
// 初始化GLFW
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Window", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, mouse_callback);
// 隐藏鼠标光标并捕获鼠标
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// 初始化GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 启用深度测试
glEnable(GL_DEPTH_TEST);
float vertices[] = {
// 位置(3) + 纹理坐标(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,
-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,
-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,
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,
-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,
-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
};
// indices数组未使用,已删除
// 纹理
unsigned int texture;
unsigned int texture2;
glGenTextures(1, &texture); // 生成纹理
glGenTextures(1, &texture2); // 生成纹理
// 配置第一个纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture); // 绑定纹理
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); // 设置纹理过滤方式
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image.cols, image.rows, 0, GL_RGB, GL_UNSIGNED_BYTE, image.data); // 设置纹理数据
glGenerateMipmap(GL_TEXTURE_2D); // 生成mipmap
// 配置第二个纹理
glActiveTexture(GL_TEXTURE1);
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); // 设置纹理过滤方式
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image2.cols, image2.rows, 0, GL_RGB, GL_UNSIGNED_BYTE, image2.data); // 设置纹理数据
glGenerateMipmap(GL_TEXTURE_2D); // 生成mipmap
// 创建VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO); // 绑定VAO
// 创建VBO
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 启用顶点属性
// 纹理属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(2);
// 构建并编译着色器程序
Shader ourShader("shader/custom.vert", "shader/fragment.vert");
// 渲染循环
while (!glfwWindowShouldClose(window)) {
// 处理输入
processInput(window);
glfwPollEvents();
// 更新view矩阵
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 使用着色器程序
ourShader.use();
// 设置矩阵uniform
ourShader.setMat4("model", model);
ourShader.setMat4("view", view);
ourShader.setMat4("projection", projection);
ourShader.setInt("ourTexture", 0);
ourShader.setInt("ourTexture2", 1);
glBindVertexArray(VAO);
// 绘制多个立方体
for(unsigned int i = 0; i < 10; i++) {
glm::mat4 modelMatrix = glm::mat4(1.0f);
modelMatrix = glm::translate(modelMatrix, cubePositions[i]);
float angle = 20.0f * i;
modelMatrix = glm::rotate(modelMatrix, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", modelMatrix);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
// 交换缓冲区和检查IO事件
glfwSwapBuffers(window);
}
// 清理资源
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteTextures(1, &texture);
glDeleteTextures(1, &texture2);
glfwTerminate();
return 0;
}
移动速度
目前我们的移动速度是个常量。理论上没什么问题,但是实际情况下根据处理器的能力不同,有些人可能会比其他人每秒绘制更多帧,也就是以更高的频率调用processInput函数。结果就是,根据配置的不同,有些人可能移动很快,而有些人会移动很慢。当你发布你的程序的时候,你必须确保它在所有硬件上移动速度都一样。
图形程序和游戏通常会跟踪一个时间差(Deltatime)变量,它储存了渲染上一帧所用的时间。我们把所有速度都去乘以deltaTime值。结果就是,如果我们的deltaTime很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间。使用这种方法时,无论你的电脑快还是慢,摄像机的速度都会相应平衡,这样每个用户的体验就都一样了。
win11上dpi不稳定的解决方案: dpi稳定器:
#pragma once
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>
#include <cmath>
#include <string>
#include <algorithm>
/**
* @brief DPI 稳定器类 - 解决 Windows 11 等系统上的鼠标 DPI 漂移问题
*
* 功能特性:
* - 异常偏移检测和过滤
* - 动态敏感度调整
* - DPI 感知设置
* - 平滑的鼠标输入处理
*/
class DPIStabilizer {
private:
// 鼠标状态变量
float yawAngle;
float pitchAngle;
float lastX, lastY;
bool firstMouse;
bool mouseCaptured;
// DPI 稳定性参数
float mouseSensitivity;
float maxOffset; // 最大允许偏移量
int frameCount; // 帧计数器
float smoothingFactor; // 平滑因子
// 相机相关
glm::vec3 cameraPos;
glm::vec3 cameraFront;
glm::vec3 cameraUp;
// 统计信息
int totalMoves;
int filteredMoves;
float averageOffset;
public:
/**
* @brief 构造函数
* @param sensitivity 初始鼠标敏感度 (默认 0.1f)
* @param maxOffset 最大允许偏移量 (默认 50.0f)
* @param smoothing 平滑因子 (默认 0.8f)
*/
DPIStabilizer(float sensitivity = 0.1f, float maxOffset = 50.0f, float smoothing = 0.8f)
: yawAngle(-90.0f), pitchAngle(0.0f), lastX(400.0f), lastY(300.0f)
, firstMouse(true), mouseCaptured(true)
, mouseSensitivity(sensitivity), maxOffset(maxOffset), frameCount(0)
, smoothingFactor(smoothing), totalMoves(0), filteredMoves(0), averageOffset(0.0f)
, cameraPos(0.0f, 0.0f, 3.0f), cameraFront(0.0f, 0.0f, -1.0f), cameraUp(0.0f, 1.0f, 0.0f) {
}
/**
* @brief 设置 DPI 感知提示(在创建窗口前调用)
*/
static void setDPIHint() {
glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
}
/**
* @brief 初始化 DPI 稳定器(在窗口创建后调用)
* @param window GLFW 窗口指针
*/
void initialize(GLFWwindow* window) {
// 设置用户指针,用于静态回调函数
glfwSetWindowUserPointer(window, this);
// 设置鼠标回调
glfwSetCursorPosCallback(window, mouseCallback);
// 捕获鼠标
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
std::cout << "DPI 稳定器已初始化" << std::endl;
}
/**
* @brief 鼠标回调函数(静态,供 GLFW 调用)
*/
static void mouseCallback(GLFWwindow* window, double xpos, double ypos) {
// 获取实例指针(通过用户指针存储)
DPIStabilizer* stabilizer = static_cast<DPIStabilizer*>(glfwGetWindowUserPointer(window));
if (stabilizer) {
stabilizer->processMouseInput(xpos, ypos);
}
}
/**
* @brief 处理鼠标输入
* @param xpos 鼠标 X 坐标
* @param ypos 鼠标 Y 坐标
*/
void processMouseInput(double xpos, double ypos) {
if (!mouseCaptured) return;
// 第一次鼠标移动
if (firstMouse) {
lastX = xpos;
lastY = ypos;
firstMouse = false;
return;
}
// 计算偏移量
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // Y 轴反向
lastX = xpos;
lastY = ypos;
// 统计信息
totalMoves++;
float currentOffset = std::sqrt(xoffset * xoffset + yoffset * yoffset);
averageOffset = (averageOffset * (totalMoves - 1) + currentOffset) / totalMoves;
// DPI 漂移检测
if (currentOffset > maxOffset) {
filteredMoves++;
std::cout << "检测到异常偏移: " << currentOffset << " 像素,已过滤" << std::endl;
return;
}
// 应用敏感度
xoffset *= mouseSensitivity;
yoffset *= mouseSensitivity;
// 平滑处理
xoffset *= smoothingFactor;
yoffset *= smoothingFactor;
// 更新角度
yawAngle += xoffset;
pitchAngle += yoffset;
// 限制俯仰角
if (pitchAngle > 89.0f) pitchAngle = 89.0f;
if (pitchAngle < -89.0f) pitchAngle = -89.0f;
// 计算新的相机方向
updateCameraDirection();
}
/**
* @brief 更新相机方向
*/
void updateCameraDirection() {
glm::vec3 front;
front.x = cos(glm::radians(yawAngle)) * cos(glm::radians(pitchAngle));
front.y = sin(glm::radians(pitchAngle));
front.z = sin(glm::radians(yawAngle)) * cos(glm::radians(pitchAngle));
cameraFront = glm::normalize(front);
}
/**
* @brief 处理键盘输入
* @param window GLFW 窗口指针
*/
void processKeyboardInput(GLFWwindow* window) {
// 动态调整敏感度
if (glfwGetKey(window, GLFW_KEY_EQUAL) == GLFW_PRESS) {
mouseSensitivity += 0.01f;
if (mouseSensitivity > 0.5f) mouseSensitivity = 0.5f;
std::cout << "鼠标敏感度: " << mouseSensitivity << std::endl;
}
if (glfwGetKey(window, GLFW_KEY_MINUS) == GLFW_PRESS) {
mouseSensitivity -= 0.01f;
if (mouseSensitivity < 0.01f) mouseSensitivity = 0.01f;
std::cout << "鼠标敏感度: " << mouseSensitivity << std::endl;
}
// Ctrl+Alt 切换鼠标捕获
bool ctrlPressed = glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS;
bool altPressed = glfwGetKey(window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS ||
glfwGetKey(window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS;
if (ctrlPressed && altPressed) {
toggleMouseCapture(window);
}
// WASD 移动(仅在捕获模式下)
if (mouseCaptured) {
float cameraSpeed = 0.05f;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
}
/**
* @brief 切换鼠标捕获状态
* @param window GLFW 窗口指针
*/
void toggleMouseCapture(GLFWwindow* window) {
if (mouseCaptured) {
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
mouseCaptured = false;
firstMouse = true;
std::cout << "鼠标捕获已释放 - 按 Ctrl+Alt 重新捕获" << std::endl;
} else {
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
mouseCaptured = true;
firstMouse = true;
std::cout << "鼠标捕获已启用" << std::endl;
}
}
/**
* @brief 获取 View 矩阵
* @return glm::mat4 View 矩阵
*/
glm::mat4 getViewMatrix() const {
return glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
}
/**
* @brief 获取相机位置
* @return glm::vec3 相机位置
*/
glm::vec3 getCameraPosition() const {
return cameraPos;
}
/**
* @brief 获取相机朝向
* @return glm::vec3 相机朝向
*/
glm::vec3 getCameraFront() const {
return cameraFront;
}
/**
* @brief 设置相机位置
* @param pos 新的相机位置
*/
void setCameraPosition(const glm::vec3& pos) {
cameraPos = pos;
}
/**
* @brief 设置鼠标敏感度
* @param sensitivity 新的敏感度值
*/
void setSensitivity(float sensitivity) {
mouseSensitivity = std::max(0.01f, std::min(0.5f, sensitivity));
}
/**
* @brief 获取当前敏感度
* @return float 当前敏感度
*/
float getSensitivity() const {
return mouseSensitivity;
}
/**
* @brief 获取统计信息
* @return std::string 统计信息字符串
*/
std::string getStats() const {
std::string stats = "DPI 稳定器统计:\n";
stats += "总移动次数: " + std::to_string(totalMoves) + "\n";
stats += "过滤次数: " + std::to_string(filteredMoves) + "\n";
float filterRate = (totalMoves > 0) ? (float)filteredMoves / totalMoves * 100.0f : 0.0f;
stats += "过滤率: " + std::to_string(filterRate) + "%\n";
stats += "平均偏移: " + std::to_string(averageOffset) + " 像素\n";
stats += "当前敏感度: " + std::to_string(mouseSensitivity) + "\n";
return stats;
}
/**
* @brief 重置统计信息
*/
void resetStats() {
totalMoves = 0;
filteredMoves = 0;
averageOffset = 0.0f;
}
/**
* @brief 设置最大偏移量
* @param maxOffset 新的最大偏移量
*/
void setMaxOffset(float maxOffset) {
this->maxOffset = maxOffset;
}
/**
* @brief 获取是否被捕获
* @return bool 是否被捕获
*/
bool isMouseCaptured() const {
return mouseCaptured;
}
};
DPI 稳定器使用说明
🎯 功能特性
✅ 已集成的功能:
- 异常偏移检测:自动过滤 DPI 漂移
- 动态敏感度调整:实时调节鼠标响应
- 平滑处理:减少鼠标抖动
- 统计信息:监控过滤效果
- 鼠标捕获切换:Ctrl+Alt 释放/捕获鼠标
🎮 控制方式
| 按键 | 功能 |
|---|---|
WASD | 相机移动(仅在捕获模式下) |
鼠标移动 | 控制相机视角 |
Ctrl+Alt | 切换鼠标捕获状态 |
+ | 增加鼠标敏感度 |
- | 减少鼠标敏感度 |
T | 显示统计信息 |
🔧 技术实现
1. DPI 漂移检测
// 检测异常大的偏移量
if (currentOffset > maxOffset) {
// 过滤异常输入
return;
}
2. 平滑处理
// 应用平滑因子
xoffset *= smoothingFactor;
yoffset *= smoothingFactor;
3. 统计监控
- 总移动次数
- 过滤次数和过滤率
- 平均偏移量
- 当前敏感度
📊 参数说明
构造函数参数:
DPIStabilizer(敏感度, 最大偏移量, 平滑因子)
DPIStabilizer(0.1f, 50.0f, 0.8f)
- 敏感度:0.01f - 0.5f(默认 0.1f)
- 最大偏移量:像素阈值(默认 50.0f)
- 平滑因子:0.0f - 1.0f(默认 0.8f)
🚀 使用方法
1. 基本使用
// 创建实例
DPIStabilizer dpiStabilizer(0.1f, 50.0f, 0.8f);
// 在窗口创建前设置 DPI 提示
DPIStabilizer::setDPIHint();
// 在窗口创建后初始化
dpiStabilizer.initialize(window);
// 在渲染循环中处理输入
dpiStabilizer.processKeyboardInput(window);
// 获取 view 矩阵
mat4 view = dpiStabilizer.getViewMatrix();
2. 高级功能
// 获取统计信息
std::string stats = dpiStabilizer.getStats();
// 设置敏感度
dpiStabilizer.setSensitivity(0.15f);
// 重置统计
dpiStabilizer.resetStats();
// 检查捕获状态
bool captured = dpiStabilizer.isMouseCaptured();
🎯 优势
- 模块化设计:易于集成和维护
- 自动过滤:无需手动处理 DPI 漂移
- 实时调整:运行时动态调节参数
- 统计监控:了解过滤效果
- 跨平台兼容:支持不同操作系统
🔍 调试技巧
- 按 T 键查看统计信息
- 调整敏感度找到最佳设置
- 监控过滤率了解 DPI 漂移情况
- 使用 Ctrl+Alt释放鼠标进行调试
📈 性能优化
- 使用静态回调函数减少开销
- 智能过滤减少不必要的计算
- 平滑处理提升用户体验
- 统计信息帮助性能调优
显示imgui窗口的完整代码:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <opencv4/opencv2/opencv.hpp>
#include <iostream>
#include <cmath>
#include <filesystem>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include "utils/shader.h"
#include "utils/glmCompute.h"
#include "utils/logger.h"
#include "include/dpi_stabilizer.h"
#include "include/imgui_manager.h"
using namespace cv;
using namespace glm;
// 窗口大小改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
std::string getCurrentWorkingDirectory() {
namespace fs = std::filesystem;
try {
// current_path() 返回当前工作目录
return fs::current_path().string();
} catch (const fs::filesystem_error& e) {
std::cerr << "Filesystem Error: " << e.what() << std::endl;
return "";
}
}
// 全局变量
float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间
// DPI 稳定器实例
DPIStabilizer dpiStabilizer(0.1f, 50.0f, 0.8f);
// ImGui 管理器实例
ImGuiManager imguiManager;
// 简化的输入处理函数
void processInput(GLFWwindow *window){
// 使用 DPI 稳定器处理所有输入
dpiStabilizer.processKeyboardInput(window);
// 窗口控制快捷键
if (glfwGetKey(window, GLFW_KEY_F1) == GLFW_PRESS) {
imguiManager.toggleWindow("Hello ImGui!");
}
if (glfwGetKey(window, GLFW_KEY_F2) == GLFW_PRESS) {
imguiManager.toggleWindow("控制面板");
}
if (glfwGetKey(window, GLFW_KEY_F3) == GLFW_PRESS) {
imguiManager.toggleWindow("性能监控");
}
if (glfwGetKey(window, GLFW_KEY_F4) == GLFW_PRESS) {
imguiManager.toggleWindow("调试信息");
}
// 显示统计信息 (按 T 键)
if (glfwGetKey(window, GLFW_KEY_T) == GLFW_PRESS) {
std::cout << dpiStabilizer.getStats() << std::endl;
}
}
int main() {
// 模型矩阵
mat4 model = mat4(1.0f);
model = rotate(model, radians(-55.0f), vec3(1.0f, 0.0f, 0.0f));
// 投影矩阵
mat4 projection = perspective(radians(45.0f), (float)800 / (float)600, 0.1f, 100.0f);
// 立方体数组
vec3 cubePositions[] = {
vec3(0.0f, 0.0f, 0.0f),
vec3(2.0f, 5.0f, -15.0f),
vec3(-1.5f, -2.2f, -2.5f),
vec3(-3.8f, -2.0f, -12.3f),
vec3(2.4f, -0.4f, -3.5f),
};
auto path = getCurrentWorkingDirectory().append("/Assets/image.png");
auto path2 = getCurrentWorkingDirectory().append("/Assets/image2.png");
cv::Mat image = cv::imread(path);
cv::Mat image2 = cv::imread(path2);
cv::flip(image2, image2, 0);
cv::cvtColor(image, image, cv::COLOR_BGR2RGB);
cv::cvtColor(image2, image2, cv::COLOR_BGR2RGB);
// 转换为字节数组
std::vector<uchar> imageData;
std::vector<uchar> imageData2;
if (image.empty()) {
std::cerr << "Could not read the image: " << path << std::endl;
return 1;
}
if (image2.empty()) {
std::cerr << "Could not read the image2: " << path2 << std::endl;
return 1;
}
std::cout << "Image 1 loaded: " << image.cols << "x" << image.rows << std::endl;
std::cout << "Image 2 loaded: " << image2.cols << "x" << image2.rows << std::endl;
cv::imencode(".png", image, imageData);
cv::imencode(".png", image2, imageData2);
// 初始化GLFW
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 设置 DPI 感知提示
DPIStabilizer::setDPIHint();
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Window", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 初始化 DPI 稳定器
dpiStabilizer.initialize(window);
// 初始化GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 初始化 ImGui 管理器
imguiManager.initialize(window);
// 注册窗口
imguiManager.registerWindow("Hello ImGui!", []() {
ImGui::Text("Hello, World!");
ImGui::Text("DPI 稳定器状态: %s", dpiStabilizer.isMouseCaptured() ? "已捕获" : "已释放");
ImGui::Text("鼠标敏感度: %.3f", dpiStabilizer.getSensitivity());
if (ImGui::Button("重置统计")) {
dpiStabilizer.resetStats();
}
float sensitivity = dpiStabilizer.getSensitivity();
if (ImGui::SliderFloat("鼠标敏感度", &sensitivity, 0.01f, 0.5f)) {
dpiStabilizer.setSensitivity(sensitivity);
}
});
imguiManager.registerWindow("控制面板", []() {
ImGuiIO& io = ImGui::GetIO();
ImGui::Text("FPS: %.1f", io.Framerate);
ImGui::Text("帧时间: %.3f ms", 1000.0f / io.Framerate);
});
// 启用深度测试
glEnable(GL_DEPTH_TEST);
float vertices[] = {
// 位置(3) + 纹理坐标(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,
-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,
-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,
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,
-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,
-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
};
// indices数组未使用,已删除
// 纹理
unsigned int texture;
unsigned int texture2;
glGenTextures(1, &texture); // 生成纹理
glGenTextures(1, &texture2); // 生成纹理
// 配置第一个纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture); // 绑定纹理
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); // 设置纹理过滤方式
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image.cols, image.rows, 0, GL_RGB, GL_UNSIGNED_BYTE, image.data); // 设置纹理数据
glGenerateMipmap(GL_TEXTURE_2D); // 生成mipmap
// 配置第二个纹理
glActiveTexture(GL_TEXTURE1);
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); // 设置纹理过滤方式
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image2.cols, image2.rows, 0, GL_RGB, GL_UNSIGNED_BYTE, image2.data); // 设置纹理数据
glGenerateMipmap(GL_TEXTURE_2D); // 生成mipmap
// 创建VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO); // 绑定VAO
// 创建VBO
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 启用顶点属性
// 纹理属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(2);
// 构建并编译着色器程序
Shader ourShader("shader/custom.vert", "shader/fragment.vert");
// 渲染循环
while (!glfwWindowShouldClose(window)) {
// 处理输入
processInput(window);
glfwPollEvents();
// 从 DPI 稳定器获取 view 矩阵
mat4 view = dpiStabilizer.getViewMatrix();
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 使用着色器程序
ourShader.use();
// 设置矩阵uniform
ourShader.setMat4("model", model);
ourShader.setMat4("view", view);
ourShader.setMat4("projection", projection);
ourShader.setInt("ourTexture", 0);
ourShader.setInt("ourTexture2", 1);
glBindVertexArray(VAO);
// 绘制多个立方体
for(unsigned int i = 0; i < 10; i++) {
glm::mat4 modelMatrix = glm::mat4(1.0f);
modelMatrix = glm::translate(modelMatrix, cubePositions[i]);
float angle = 20.0f * i;
modelMatrix = glm::rotate(modelMatrix, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", modelMatrix);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
// 使用 ImGui 管理器渲染所有窗口
imguiManager.render();
// 交换缓冲区和检查IO事件
glfwSwapBuffers(window);
}
// 清理 ImGui 管理器
imguiManager.shutdown();
// 清理资源
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteTextures(1, &texture);
glDeleteTextures(1, &texture2);
glfwTerminate();
return 0;
}
#pragma once
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <GLFW/glfw3.h>
#include <string>
#include <functional>
#include <unordered_map>
#include <iostream>
/**
* @brief ImGui 窗口管理器类 - 封装 ImGui 的初始化和窗口管理
*
* 功能特性:
* - 自动初始化和清理 ImGui
* - 窗口注册和管理
* - 统一的渲染循环
* - 可扩展的窗口系统
*/
class ImGuiManager {
private:
GLFWwindow* window;
bool initialized;
// 窗口注册系统
struct WindowInfo {
std::string name;
std::function<void()> renderFunc;
bool visible;
};
std::unordered_map<std::string, WindowInfo> windows;
// 字体管理
std::unordered_map<std::string, ImFont*> fonts;
std::string defaultFont;
public:
/**
* @brief 构造函数
*/
ImGuiManager() : window(nullptr), initialized(false) {}
/**
* @brief 析构函数
*/
~ImGuiManager() {
shutdown();
}
/**
* @brief 初始化 ImGui 管理器
* @param glfwWindow GLFW 窗口指针
* @return 是否初始化成功
*/
bool initialize(GLFWwindow* glfwWindow) {
if (initialized) return true;
window = glfwWindow;
// 初始化 ImGui
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
// 设置 ImGui 样式
ImGui::StyleColorsDark();
// 初始化 ImGui 平台/渲染器绑定
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 330");
// 加载默认字体
loadDefaultFonts();
initialized = true;
std::cout << "ImGui 管理器已初始化" << std::endl;
return true;
}
/**
* @brief 注册一个窗口
* @param name 窗口名称
* @param renderFunc 渲染函数
* @param visible 是否默认可见
*/
void registerWindow(const std::string& name, std::function<void()> renderFunc, bool visible = true) {
windows[name] = {name, renderFunc, visible};
}
/**
* @brief 切换窗口可见性
* @param name 窗口名称
*/
void toggleWindow(const std::string& name) {
auto it = windows.find(name);
if (it != windows.end()) {
it->second.visible = !it->second.visible;
}
}
/**
* @brief 设置窗口可见性
* @param name 窗口名称
* @param visible 是否可见
*/
void setWindowVisible(const std::string& name, bool visible) {
auto it = windows.find(name);
if (it != windows.end()) {
it->second.visible = visible;
}
}
/**
* @brief 开始新帧
*/
void beginFrame() {
if (!initialized) return;
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
}
/**
* @brief 渲染所有注册的窗口
*/
void renderWindows() {
if (!initialized) return;
for (const auto& [name, windowInfo] : windows) {
if (windowInfo.visible) {
if (ImGui::Begin(name.c_str())) {
windowInfo.renderFunc();
}
ImGui::End();
}
}
}
/**
* @brief 结束帧并渲染
*/
void endFrame() {
if (!initialized) return;
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
/**
* @brief 完整的渲染循环(推荐使用)
*/
void render() {
beginFrame();
renderWindows();
endFrame();
}
/**
* @brief 获取 ImGui IO
* @return ImGuiIO 引用
*/
ImGuiIO& getIO() {
return ImGui::GetIO();
}
/**
* @brief 检查窗口是否存在
* @param name 窗口名称
* @return 是否存在
*/
bool hasWindow(const std::string& name) const {
return windows.find(name) != windows.end();
}
/**
* @brief 加载字体
* @param fontPath 字体文件路径
* @param fontName 字体名称
* @param fontSize 字体大小
* @param glyphRanges 字符范围
* @return 是否加载成功
*/
bool loadFont(const std::string& fontPath, const std::string& fontName,
float fontSize = 16.0f, const ImWchar* glyphRanges = nullptr) {
if (!initialized) return false;
ImGuiIO& io = ImGui::GetIO();
ImFont* font = io.Fonts->AddFontFromFileTTF(fontPath.c_str(), fontSize, nullptr, glyphRanges);
if (font) {
fonts[fontName] = font;
if (defaultFont.empty()) {
defaultFont = fontName;
}
// 重新构建字体纹理
io.Fonts->Build();
ImGui_ImplOpenGL3_CreateFontsTexture();
std::cout << "字体加载成功: " << fontName << " (" << fontPath << ")" << std::endl;
return true;
} else {
std::cerr << "字体加载失败: " << fontPath << std::endl;
return false;
}
}
/**
* @brief 使用指定字体
* @param fontName 字体名称
* @return 是否成功使用字体
*/
bool useFont(const std::string& fontName) {
auto it = fonts.find(fontName);
if (it != fonts.end() && it->second) {
ImGui::PushFont(it->second);
return true;
}
return false;
}
/**
* @brief 弹出当前字体
* @return 是否成功弹出字体
*/
bool popFont() {
ImGuiIO& io = ImGui::GetIO();
if (io.Fonts->Fonts.Size > 0) {
ImGui::PopFont();
return true;
}
return false;
}
/**
* @brief 设置默认字体
* @param fontName 字体名称
*/
void setDefaultFont(const std::string& fontName) {
if (fonts.find(fontName) != fonts.end()) {
defaultFont = fontName;
}
}
/**
* @brief 获取字体
* @param fontName 字体名称
* @return 字体指针
*/
ImFont* getFont(const std::string& fontName) {
auto it = fonts.find(fontName);
return (it != fonts.end()) ? it->second : nullptr;
}
/**
* @brief 检查字体是否存在
* @param fontName 字体名称
* @return 是否存在
*/
bool hasFont(const std::string& fontName) const {
return fonts.find(fontName) != fonts.end();
}
/**
* @brief 关闭 ImGui 管理器
*/
void shutdown() {
if (!initialized) return;
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
initialized = false;
windows.clear();
fonts.clear();
defaultFont.clear();
std::cout << "ImGui 管理器已关闭" << std::endl;
}
private:
/**
* @brief 加载默认字体
*/
void loadDefaultFonts() {
// 加载 Inter 字体
loadFont("Assets/Inter.ttc", "Inter", 16.0f, ImGui::GetIO().Fonts->GetGlyphRangesChineseFull());
loadFont("Assets/Inter.ttc", "Inter-Large", 20.0f, ImGui::GetIO().Fonts->GetGlyphRangesChineseFull());
loadFont("Assets/Inter.ttc", "Inter-Small", 14.0f, ImGui::GetIO().Fonts->GetGlyphRangesChineseFull());
// 加载 Windows 系统黑体字体
loadFont("C:\\Windows\\Fonts\\simhei.ttf", "SimHei", 18.0f, ImGui::GetIO().Fonts->GetGlyphRangesChineseFull());
loadFont("C:\\Windows\\Fonts\\simhei.ttf", "SimHei-Large", 22.0f, ImGui::GetIO().Fonts->GetGlyphRangesChineseFull());
loadFont("C:\\Windows\\Fonts\\simhei.ttf", "SimHei-Small", 16.0f, ImGui::GetIO().Fonts->GetGlyphRangesChineseFull());
// 设置默认字体为黑体
setDefaultFont("SimHei");
}
};
/**
* @brief 预定义的窗口创建器 - 提供常用的窗口模板
*/
class ImGuiWindowTemplates {
public:
/**
* @brief 创建信息显示窗口
* @param title 窗口标题
* @param infoFunc 信息获取函数
* @return 窗口渲染函数
*/
static std::function<void()> createInfoWindow(
const std::string& title,
std::function<std::string()> infoFunc
) {
return [title, infoFunc]() {
ImGui::Text("=== %s ===", title.c_str());
ImGui::Separator();
ImGui::Text("%s", infoFunc().c_str());
};
}
/**
* @brief 创建控制面板窗口
* @param controls 控制函数列表
* @return 窗口渲染函数
*/
static std::function<void()> createControlPanel(
std::vector<std::function<void()>> controls
) {
return [controls]() {
for (const auto& control : controls) {
control();
}
};
}
/**
* @brief 创建性能监控窗口
* @return 窗口渲染函数
*/
static std::function<void()> createPerformanceWindow() {
return []() {
ImGuiIO& io = ImGui::GetIO();
ImGui::Text("性能监控");
ImGui::Separator();
ImGui::Text("FPS: %.1f", io.Framerate);
ImGui::Text("帧时间: %.3f ms", 1000.0f / io.Framerate);
ImGui::Text("绘制调用: %d", io.MetricsRenderVertices);
};
}
/**
* @brief 创建调试信息窗口
* @param debugInfo 调试信息获取函数
* @return 窗口渲染函数
*/
static std::function<void()> createDebugWindow(
std::function<std::string()> debugInfo
) {
return [debugInfo]() {
ImGui::Text("调试信息");
ImGui::Separator();
ImGui::TextWrapped("%s", debugInfo().c_str());
};
}
};