用GLSL 和 GLM 实现OpenGL trackball

以下是一个使用GLSL(OpenGL Shading Language)和GLM(OpenGL Mathematics)库来实现OpenGL trackball功能的示例。这个示例将展示如何通过鼠标操作来旋转场景中的物体,就像在操作一个虚拟的轨迹球一样。

1. 准备工作

  • 包含必要的头文件和库
    • 需要包含GLFW头文件用于创建窗口和处理事件,GLAD头文件用于加载OpenGL函数指针,GLM头文件用于数学运算,以及fstream用于读取着色器文件(这里假设着色器代码存储在外部文件中)。
#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
posted @ 2024-11-09 18:31  MarsCactus  阅读(27)  评论(0编辑  收藏  举报