LearnOpenGL-模型加载-2.网格与自定义Mesh类

本人刚学OpenGL不久且自学,文中定有代码、术语等错误,欢迎指正

我写的项目地址:https://github.com/liujianjie/LearnOpenGLProject

网格

  • 自定义网格类

    通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。

    为了渲染这个模型/物体,我们需要将这些数据转换为OpenGL能够理解的格式,所以需要自定义网格类来读取存储在Assimp数据结构中的模型数据。

  • 思考自定义网格类的属性

    • 一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个向量和一个纹理坐标向量
    • 一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。
    struct Vertex {
        glm::vec3 Position;
        glm::vec3 Normal;
        glm::vec2 TexCoords;
    };
    struct Texture {
        unsigned int id;
        string type;
    };
    

自定义Mesh类重点代码讲解

初始化函数

  • 代码

    void setupMesh()
    {
        glGenVertexArrays(1, &VAO);
        glGenBuffers(1, &VBO);
        glGenBuffers(1, &EBO);
    
        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
    
        glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);  
    
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), 
                     &indices[0], GL_STATIC_DRAW);
    
        // 顶点位置
        glEnableVertexAttribArray(0);   
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
        // 顶点法线
        glEnableVertexAttribArray(1);   
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
        // 顶点纹理坐标
        glEnableVertexAttribArray(2);   
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
    
        glBindVertexArray(0);
    }  
    
  • 说明

    • sizeof(Vertex);能得到结构体的大小

      因为

      C++结构体有一个很棒的特性,它们的内存布局是连续的(Sequential)。

      将结构体作为一个数据数组使用,那么它将会以顺序排列结构体的变量,这将会直接转换为我们在数组缓冲中所需要的float(实际上是字节)数组

      Vertex vertex;
      vertex.Position  = glm::vec3(0.2f, 0.4f, 0.6f);
      vertex.Normal    = glm::vec3(0.0f, 1.0f, 0.0f);
      vertex.TexCoords = glm::vec2(1.0f, 0.0f);
      // = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];
      

      sizeof(Vertex); 32字节(8个float * 每个4字节)

    • 结构体的另外一个很好的用途是它的预处理指令offsetof(s, m)

      • 它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)

      • 这正好可以用在定义glVertexAttribPointer函数中的偏移参数:

        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal)); 
        

        偏移量现在是使用offsetof来定义

        这里它会将法向量的字节偏移量设置为结构体中法向量的偏移量,也就是3个float,即12字节。

渲染函数

  • 问题引入

    真正渲染这个网格之前,我们需要在调用glDrawElements函数之前先绑定相应的纹理

    我们一开始并不知道这个网格(如果有的话)有多少纹理、纹理是什么类型的。

  • 如何解决

    设定一个shader纹理采样命名标准:

    每个漫反射纹理被命名为texture_diffuseN,每个镜面光纹理应该被命名为texture_specularN,其中N的范围是1到纹理采样器最大允许的数字

    uniform sampler2D texture_diffuse1;
    uniform sampler2D texture_diffuse2;
    uniform sampler2D texture_diffuse3;
    uniform sampler2D texture_specular1;
    uniform sampler2D texture_specular2;
    

    需要预先在shader中定义那么多纹理采样名称,可能会有所浪费或者不够的问题

    • 浪费

      网格只有个纹理,而shader预定义了4个uniform名称

    • 不够(这个问题可能不会出现)

      网格有34个纹理,而shader只能根据opengl最大预定义32

  • 代码

    void Draw(Shader shader) 
    {
        unsigned int diffuseNr = 1;
        unsigned int specularNr = 1;
        for(unsigned int i = 0; i < textures.size(); i++)
        {
            glActiveTexture(GL_TEXTURE0 + i); // 在绑定之前激活相应的纹理单元
            // 获取纹理序号(diffuse_textureN 中的 N)
            string number;
            string name = textures[i].type;
            if(name == "texture_diffuse")
                number = std::to_string(diffuseNr++);
            else if(name == "texture_specular")
                number = std::to_string(specularNr++);
    
            shader.setInt(("material." + name + number).c_str(), i);
            glBindTexture(GL_TEXTURE_2D, textures[i].id);
        }
        glActiveTexture(GL_TEXTURE0);
    
        // 绘制网格
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
    }
    
### OpenGL 实现三维场景的代码示例 为了创建一个基本的三维场景,在初始化阶段设置好着色器程序、顶点缓冲对象(VBOs),以及索引缓冲对象(EBOs)是非常重要的[^1]。 ```cpp #include <glad/glad.h> #include <GLFW/glfw3.h> #include "stb_image.h" #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp> #include <learnopengl/shader_m.h> // 自定义Shader,用于简化着色器管理 #include <learnopengl/camera.h> // Camera 处理相机移动逻辑 #include <learnopengl/model.h> // Model 封装了Assimp库的功能来加载模型文件 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); // settings const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; // camera Camera camera(glm::vec3(0.0f, 0.0f, 3.0f)); float lastX = SCR_WIDTH / 2.0f; float lastY = SCR_HEIGHT / 2.0f; bool firstMouse = true; // timing float deltaTime = 0.0f; // 时间间隔 (当前帧上一帧之间的时间差) float lastFrame = 0.0f; int main() { // glfw: 初始化和配置 glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X #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(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); glfwSetCursorPosCallback(window, mouse_callback); glfwSetScrollCallback(window, scroll_callback); // 告诉GLFW我们要捕获我们的鼠标 glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // glad: 加载所有OpenGL调用指针 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // 配置全局opengl状态 glEnable(GL_DEPTH_TEST); // 构建并编译着色器程序... Shader ourShader("shaders/vertex_shader.glsl", "shaders/fragment_shader.glsl"); // 加载模型 Model ourModel("resources/models/nanosuit/nanosuit.obj"); while(!glfwWindowShouldClose(window)) { float currentFrame = static_cast<float>(glfwGetTime()); deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; processInput(window); glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 不断更新变换矩阵传入着色器中 glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); glm::mat4 view = camera.GetViewMatrix(); ourShader.setMat4("projection", projection); ourShader.setMat4("view", view); // 绘制物体时记得激活对应的着色器 ourShader.use(); // 获得实体的位置向量,并将其转换为平移matrix glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); model = glm::rotate(model, (GLfloat)glfwGetTime() * glm::radians(-50.0f), glm::vec3(1.0f, 0.3f, 0.5f)); // 将model matrix发送到着色器uniform处 ourShader.setMat4("model", model); // 渲染网格列表中的每一个mesh ourModel.Draw(ourShader); // 交换颜色缓存并轮询事件 glfwSwapBuffers(window); glfwPollEvents(); } // 可选: 销毁之前分配的所有资源. glfwDestroyWindow(window); glfwTerminate(); return 0; } ``` 上述代码展示了如何利用`assimp`库通过自定义的`Model`加载`.obj`格式的3D模型文件,并使用简单的光照效果渲染该模型。这段代码还包含了基础的摄像机控制功能,允许用户通过键盘输入和平板电脑滚轮交互调整视角。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘建杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值