用GLSL 和 GLM 实现OpenGL trackball
以下是一个使用GLSL(OpenGL Shading Language)和GLM(OpenGL Mathematics)库来实现OpenGL trackball功能的示例。这个示例将展示如何通过鼠标操作来旋转场景中的物体,就像在操作一个虚拟的轨迹球一样。
1. 准备工作
- 包含必要的头文件和库:
- 需要包含GLFW头文件用于创建窗口和处理事件,GLAD头文件用于加载OpenGL函数指针,GLM头文件用于数学运算,以及
fstream
用于读取着色器文件(这里假设着色器代码存储在外部文件中)。
- 需要包含GLFW头文件用于创建窗口和处理事件,GLAD头文件用于加载OpenGL函数指针,GLM头文件用于数学运算,以及
#include <GLFW/glfw3.h>
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <fstream>
#include <sstream>
- 设置窗口相关参数:
- 定义窗口的宽度和高度,创建GLFW窗口,并初始化GLAD。
// 窗口大小
const int WIDTH = 800;
const int HEIGHT = 600;
int main(int argc, char** argv) {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "OpenGL Trackball with GLSL and GLM", nullptr, nullptr);
if (window == nullptr) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADLoadProc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
glfwTerminate();
return -1;
}
// 其他初始化步骤将在后续添加
//...
// 主循环
while (!glfwWindowShouldClose(window)) {
// 渲染和事件处理逻辑将在后续添加
//...
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
2. 加载着色器程序
- 读取着色器文件内容:
- 创建函数来读取顶点着色器和片段着色器文件的内容。
std::string readShaderFile(const std::string& filePath) {
std::ifstream shaderFile(filePath);
if (!shaderFile.is_open()) {
std::cout << "Failed to open shader file: " << filePath << std::endl;
return "";
}
std::stringstream buffer;
buffer << shaderFile.rdbuf();
shaderFile.close();
return buffer.str();
}
- 编译和链接着色器程序:
- 创建函数来编译顶点着色器和片段着色器,并将它们链接成一个完整的着色器程序。
unsigned int createShaderProgram(const std::string& vertexShaderPath, const std::string& fragmentShaderPath) {
// 读取顶点着色器文件内容
std::string vertexShaderCode = readShaderFile(vertexShaderPath);
const char* vertexShaderSource = vertexShaderCode.c_str();
// 编译顶点着色器
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::cout << "Failed to compile vertex shader:\n" << infoLog << std::endl;
return 0;
}
// 读取片段着色器文件内容
std::string fragmentShaderCode = readShaderFile(fragmentShaderPath);
const char* fragmentShaderSource = fragmentShaderCode.c_str();
// 编译片段着色器
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::cout << "Failed to compile fragment shader:\n" << infoLog << std::endl;
return 0;
}
// 链接着色器程序
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 检查着色器程序链接错误
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
std::cout << "Failed to link shader program:\n" << infoLog << std::endl;
return 0;
}
// 删除已编译的顶点着色器和片段着色器,因为它们已经链接到程序中
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return shaderProgram;
}
在main
函数中,可以调用createShaderProgram
函数来创建着色器程序,例如:
unsigned int shaderProgram = createShaderProgram("vertexShader.glsl", "fragmentShader.glsl");
这里假设顶点着色器文件名为vertexShader.glsl
,片段着色器文件名为fragmentShader.glsl
。
3. 设置顶点数据和缓冲
- 定义顶点数据结构:
- 这里简单定义一个立方体的顶点数据结构,包含位置和颜色信息。
struct Vertex {
glm::vec3 position;
glm::vec3 color;
};
- 设置立方体顶点数据:
- 创建一个数组来存储立方体的顶点数据。
Vertex vertices[] = {
// 前面
{glm::vec3(-0.5f, -0.5f, 0.5f), glm::vec3(1.0f, 0.0f, 0.0f)},
{glm::vec3( 0.5f, -0.5f, 0.5f), glm::vec3(0.0f, 1.0f, 0.0f)},
{glm::vec3( 0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f)},
{glm::vec3(-0.5f, 0.5f, 0.5f), glm::vec3(1.0f, 1.0f, 0.0f)},
// 后面
{glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(1.0f, 0.0f, 0.0f)},
{glm::vec3( 0.5f, -0.5f, -0.5f), glm::vec3(0.0f, 1.0f, 0.0f)},
{glm::vec3( 0.5f, 0.5f, -0.5f), glm::vec3(0.0f, 0.0f, 1.0f)},
{glm::vec3(-0.5f, 0.5f, -0.5f), glm::vec3(1.0f, 1.0f, 0.0f)},
// 左面
{glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(1.0f, 0.0f, 0.0f)},
{glm::vec3(-0.5f, -0.5f, 0.5f), glm::vec3(0.0f, 1.0f, 0.0f)},
{glm::vec3(-0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f)},
{glm::vec3(-0.5f, 0.5f, -0.5f), glm::vec3(1.0f, 1.0f, 0.0f)},
// 右面
{glm::vec3( 0.5f, -0.5f, -0.5f), glm::vec3(1.0f, 0.0f, 0.0f)},
{glm::vec3( 0.5f, -0.5f, 0.5f), glm::vec3(0.0f, 1.0f, 0.0f)},
{glm::vec3( 0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 0.0f, 1.0f)},
{glm::vec3( 0.5f, 0.5f, -0.5f), glm::vec3(1.0f, 1.0f, 0.0f)},
// 上面
{glm::vec3(-0.5f, 0.5f, 0.5f), glm::vec3(1.0f, 0.0f, 0.0f)},
{glm::vec3( 0.5f, 0.5f, 0.5f), glm::vec3(0.0f, 1.0f, 0.0f)},
{glm::vec3( 0.5f, 0.5f, -0.5f), glm::vec3(0.0f, 0.0f, 1.0f)},
{glm::vec3(-0.5f, 0.5f, -0.5f), glm::vec3(1.0f, 1.0f, 0.0f)},
// 下面
{glm::vec3(-0.5f, -0.5f, 0.5f), glm::vec3(1.0f, 0.0f, 0.0f)},
{glm::vec3( 0.5f, -0.5f, 0.5f), glm::0f, 1.0f, 0.0f)},
{glm::vec3( 0.5f, -0.5f, -0.5f), glm::vec3(0.0f, 0.0f, 1.0f)},
{glm::vec3(-0.5f, -0.5f, -0.5f), glm::vec3(1.0f, 1.0f, 0.0f)}
};
- 创建顶点缓冲对象(VBO)和顶点数组对象(VAO):
- 使用OpenGL函数创建VBO和VAO,并将顶点数据绑定到它们上面。
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_VERTEX_BUFFER, VBO);
glBufferData(GL_VERTEX_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 绑定顶点位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
glEnableVertexAttribArray(0);
// 绑定顶点颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, color));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
4. 处理鼠标事件和计算旋转矩阵
- 定义全局变量:
- 定义用于存储鼠标位置、旋转矩阵等的全局变量。
// 鼠标按下时的坐标
glm::vec2 mousePosInitial;
// 旋转矩阵
glm::mat4 rotationMatrix = glm::mat4(1.0f);
- 鼠标按下事件处理:
- 当鼠标按下时,记录鼠标的初始位置。
void mousePressed(GLFWwindow* window, int button, int state, int x, int y) {
if (button == GLFW_LEFT_BUTTON && state == GLFW_DOWN) {
mousePosInitial.x = x;
mousePosInitial.y = y;
}
}
- 鼠标移动事件处理:
- 当鼠标在按下状态下移动时,根据鼠标的初始位置和当前位置计算旋转轴和旋转角度,然后更新旋转矩阵。
void mouseMoved(GLFWwindow* window, int x, int y) {
if (glfwGetMouseButton(window, GLFW_LEFT_BUTTON) == GLFW_PRESS) {
glm::vec2 mousePosCurrent(x, y);
// 计算鼠标移动的偏移量
glm::vec2 mouseDelta = mousePosCurrent - mousePosInitial;
// 计算旋转角度
float angle = glm::length(mouseDelta) * 0.01f;
// 计算旋转轴
glm::vec3 axis = glm::vec3(mouseDelta.y, -mouseDelta.x, 0.0f);
// 规范化旋转轴向量
axis = glm::normalize(axis);
// 创建临时旋转矩阵
glm::mat4 tempMatrix = glm::rotate(rotationMatrix, angle, axis);
// 更新旋转矩阵
rotationMatrix = tempMatrix;
// 重新渲染场景以显示旋转后的效果
glfwPostRedraw(window);
}
}
5. 渲染场景
- 设置视图和投影矩阵:
- 在渲染场景之前,需要设置视图矩阵和投影矩阵。
void drawScene() {
// 清除颜色缓冲和深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 设置视图矩阵
glm::mat4 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::mat4 projection = glm::perspective(glm::radians(45.0f), (float)WIDTH / (float)HEIGHT, 0.1f, 100.0f);
// 获取着色器程序中的uniform变量位置
int viewLoc = glGetUniformLocation(shaderProgram, "view");
int projectionLoc = glGetUniformLocation(shaderProgram, "projection");
int rotationLoc = glGetUniformLocation(shaderProgram, "rotation");
// 上传视图矩阵到着色器程序
glUniformMatrix4f(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
// 上传投影矩阵到着色器程序
glUniformMatrix4f(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
// 上传旋转矩阵到着色器程序
glUniformMatrix4f(rotationLoc, 1, GL_FALSE, glm::value_ptr(rotationMatrix));
// 绑定顶点数组对象
glBindVertexArray(VAO);
// 绘制立方体
glDrawArrays(GL_TRIANGLES, 0, 36);
// 解除顶点数组对象的绑定
glBindVertexArray(0);
}
6. 主函数整合
- 在主函数中,需要注册鼠标事件回调函数,设置主循环来处理渲染和事件处理逻辑。
int main(int argc, char** argv) {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "OpenGL Trackball with GLSL and GLM", nullptr, nullptr);
if (window == nullptr) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADLoadProc)glfwGetProcAddress)) {
std::cout << "Failed to initialize GLAD" << std::endl;
glfwTerminate();
return -1;
}
unsigned int