美文网首页
OpenGL学习14——3D模型

OpenGL学习14——3D模型

作者: 蓬篙人 | 来源:发表于2021-06-28 10:28 被阅读0次

    1. Assimp类库

    • Assimp是一个流行的模型载入类库,全称为Open Asset Import Library。Assimp通过将模型数据载入Assimp的通用数据结构实现多种不同3D模型文件格式的数据载入和解析。

    • 当我们使用Assimp导入一个模型,Assimp将整个模型载入到一个场景(scene)对象。一个简单的Assimp结构如下所示:(图片取自书中

      Assimp数据结构
      • 场景/模型的所有数据都包含在Scene对象中。
      • 场景根节点可能包含子节点,并保存指向场景对象网格数组的索引。场景的mMeshes数组包含实际的网格对象,而节点的mMeshes数组只是包含索引。
      • 一个网格(Mesh) 对象本身包含渲染所需要的所有相关的数据,如顶点位置、法向量、纹理坐标、面片和物体材质。
      • 一个面片(face) 代表对象的一个渲染基元(如三角形,四方形和点)。一个面片包含组成基元图形的顶点的索引。
      • 最后,一个网格对象同时链接到一个材质对象,通过材质对象的函数可以检索一个物体的材质,如颜色和/或纹理图(扩散光和镜面光图)。
    • 使用Assimp类库载入3D模型的一般过程是:首先将模型数据载入到Scene对象,然后递归访问节点获取相应的网格对象,处理每个网格对象检索顶点数据,索引和相应的材质属性。结果就是我们获得了一个网格数据的集合。下面我们会将该数据集合包含到我们定义的Model对象当中。

    • 在OpenGL中,一个网格是绘制一个对象的最小单元。一个模型一般包含多个网格。

    • Assimp下载
      Assimp类库

    • 我们参照前面编译GLFW类库的过程对Assimp进行编译。我下载的是5.0.1版本,使用的是VS 2019,最终编译的Debug和Release内容如下:


      Assimp Debug编译内容
      Assimp Release编译内容
    • 配置Assimp类库

        1. 将下载解压后的include文件和编译产生的include文件的头文件拷贝到我们项目头文件的包含路径下(如我的是D:\3Lib\Include\assimp)。
        1. 将Assimp的静态库文件拷贝到我们的库目录下(如我的是D:\3Lib\Libs\Assimp),Debug和Release都拷贝进去。
        1. 在项目的【属性】-【VC++目录】-【库目录】添加静态库文件的目录(头文件路径原先的已经包含了)。
        1. 在项目的【属性】-【链接器】-【输入】-【附加依赖项】根据是Debug或Release添加相应的静态库文件名(注意分号分隔)。
        1. 根据Debug或Release将相应的dll文件拷贝到程序执行目录。

    2. 网格(Mesh)

    从上一小节我们知道,一个网格代表一个可绘制的实体,下面我们来定义一个自己的网格类。

    • 首先,我们定义一个代表顶点数据的结构。
    struct Vertex {
        glm::vec3 Position;
        glm::vec3 Normal;
        glm::vec2 TexCoords;
    };
    
    • 其次,我们定义一个代表纹理数据的结构。
    struct Texture {
        unsigned int id;  
        std::string type;
        std::string path;
    };
    
    • 接下来,我们定义网格类的结构。
    class Mesh {
    
    public:
        // 网格数据
        std::vector<Vertex> vertices;
        std::vector<unsigned int> indices;
        std::vector<Texture> textures;
    
        Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures);
        // 绘制网格
        void Draw(Shader& shader);
    private:
        // 渲染所需的OpenGL对象
        unsigned int VAO, VBO, EBO;
        // 网格初始化:初始化缓冲区
        void setupMesh();
    };
    
    • 网格类构造函数实现:设置所需成员变量,调用初始化函数。
    Mesh::Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures)
    {
        this->vertices = vertices;
        this->indices = indices;
        this->textures = textures;
    
        setupMesh();
    }
    
    • 实现网格类的初始化函数:设置合适的缓冲区对象,指定顶点数据的属性布局。
    void Mesh::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);
    }
    
    • 从前面章节我们知道,在绘制之前,我们需要激活相应的纹理单元并绑定纹理数据。但是,在网格对象绘制的时候,我们并不知道网格拥有多少纹理,纹理的类型是什么?要解决这个问题,我们设定一种命名约定:扩散光图纹理我们命名为texture_diffuseN,镜面光图纹理我们命名为texture_specularN,其中N代表从1到纹理取样器允许的最大值。假设我们的网格对象有3个扩散光图纹理和2个镜面光图纹理,那么着色器中的定义应类似下面这样:
    uniform sample2D texture_diffuse1;
    uniform sample2D texture_diffuse2;
    uniform sample2D texture_diffuse3;
    uniform sample2D texture_specular1;
    uniform sample2D texture_specular2;
    
    • 最终网格类绘制函数的实现。
    void Mesh::Draw(Shader& shader)
    {
        unsigned int diffuseNr = 1;
        unsigned int specularNr = 1;
        for (unsigned int i = 0; i < textures.size(); i++)
        {
            glActiveTexture(GL_TEXTURE0 + i);  // 激活纹理单元
    
            std::string number;
            std::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.setFloat(("material." + name + number).c_str(), i);
            glBindTexture(GL_TEXTURE_2D, textures[i].id);
        }
        // 这里默认启动一个纹理单元
        glActiveTexture(0);
    
        // 绘制网格
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
    }
    

    3. 模型(Model)

    下面我们来创建一个自定义的模型类来代表我们整个场景模型,并通过Assimp类库读入3D模型数据并解析出需要绘制的网格对象。

    • 首先,我们给出模型类的结构定义。
    // 注意引入Assimp相应的头文件
    #include <assimp/Importer.hpp>
    #include <assimp/scene.h>
    #include <assimp/postprocess.h>
    
    // 辅助函数,读取纹理文件
    unsigned int TextureFromFile(const char* path, const std::string& directory);
    
    class Model
    {
    public:
        Model(char* path)
        {
            loadModel(path);
        }
        void Draw(Shader& shader);
    private:
        // 模型数据
        std::vector<Mesh> meshes;
        std::string directory;
        std::vector<Texture> textures_loaded;   // 用于优化纹理载入
    
        void loadModel(std::string path);
        void processNode(aiNode* node, const aiScene* scene);
        Mesh processMesh(aiMesh* mesh, const aiScene* scene);
        std::vector<Texture> loadMaterialTextures(aiMaterial* mat,
            aiTextureType type, std::string typeName);
    }
    
    • 因为前面我们已经封装了网格类,因此对于模型的绘制函数,则只需调用所包含网格的绘制函数,循环将所有网格绘制出来即可。
    void Model::Draw(Shader& shader)
    {
        for (unsigned int i = 0; i < meshes.size(); i++)
        {
            meshes[i].Draw(shader);
        }
    }
    
    • 对于3D模型导入,Assimp抽象了加载所有不同格式文件的技术细节,我们只需调用Importer对象的ReadFile函数即可。
    Assimp::Importer import;
    const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
    
    1. 第一个参数是我们需要加载的3D模型的文件路径。
    2. 第二个参数是一些后处理选项。
        - aiProcess_GenNormals:如果模型没有法向量则为每个顶点创建法向量。
        - aiProcess_SplitLargeMeshes:将大网格分割成多个子网格,如果你的计算机只能处理一定数量的顶点,该选项十分有用。
        - aiProcess_OptimizeMeshes:将多个网格组合成一个大的网格,降低绘制调用次数,优化渲染。
    
    • 我们的模型载入函数实现如下。
    void Model::loadModel(std::string path)
    {
        Assimp::Importer import;
        const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
        if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
        {
            std::cout << "ERROR::ASSIMP::" << import.GetErrorString() << std::endl;
            return;
        }
        directory = path.substr(0, path.find_last_of('/'));
    
        processNode(scene->mRootNode, scene);
    }
    
    • 实现处理节点函数:递归处理所有节点,解析网格数据。
    void Model::processNode(aiNode* node, const aiScene* scene)
    {
        // 处理节点的所有网格
        for (unsigned int i = 0; i < node->mNumMeshes; i++)
        {
            aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
            meshes.push_back(processMesh(mesh, scene));
        }
        // 递归处理所有子节点
        for (unsigned int i = 0; i < node->mNumChildren; i++)
        {
            processNode(node->mChildren[i], scene);
        }
    }
    
    • 注意:由于节点保存指向网格数据的索引,所以根据节点父-子的关系,我们也可以为网格数据创建类似树形结构,但是这里我们只是简单遍历节点解析网格数据。对于实际开发,建议采用树形结构来对网格数据进行处理。
    • 处理网格数据函数的实现。
    Mesh Model::processMesh(aiMesh* mesh, const aiScene* scene)
    {
        std::vector<Vertex> vertices;
        std::vector<unsigned int> indices;
        std::vector<Texture> textures;
        // 处理顶点数据
        for (unsigned int i = 0; i < mesh->mNumVertices; i++)
        {
            Vertex vertex;
            glm::vec3 vector;
            // 顶点位置
            vector.x = mesh->mVertices[i].x;
            vector.y = mesh->mVertices[i].y;
            vector.z = mesh->mVertices[i].z;
            vertex.Position = vector;
            // 法向量(有些模型没有法向量,读取的时候记得设置生成法向量的选项)
            vector.x = mesh->mNormals[i].x;
            vector.y = mesh->mNormals[i].y;
            vector.z = mesh->mNormals[i].z;
            vertex.Normal = vector;
            // 纹理坐标
            if (mesh->mTextureCoords[0])
            {
                glm::vec2 vec;
                vec.x = mesh->mTextureCoords[0][i].x;
                vec.y = mesh->mTextureCoords[0][i].y;
                vertex.TexCoords = vec;
            }
            else
                vertex.TexCoords = glm::vec2(0.0f, 0.0f);
    
            vertices.push_back(vertex);
        }
        // 处理顶点索引
        for (unsigned int i = 0; i < mesh->mNumFaces; i++)
        {
            aiFace face = mesh->mFaces[i];
            for (unsigned int j = 0; j < face.mNumIndices; j++)
                indices.push_back(face.mIndices[j]);
        }
        // 处理物体材质
        if (mesh->mMaterialIndex >= 0)
        {
            aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
            std::vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
            textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
            std::vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
            textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
        }
    
        return Mesh(vertices, indices, textures);
    }
    
    • 载入物体材质纹理函数的实现。
    std::vector<Texture> Model::loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName)
    {
        std::vector<Texture> textures;
        for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
        {
            aiString str;
            mat->GetTexture(type, i, &str);
            // 判断纹理是否载入,防止载入重复的纹理数据
            bool skip = false;
            for (unsigned int j = 0; j < textures_loaded.size(); j++)
            {
                if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
                {
                    textures.push_back(textures_loaded[j]);
                    skip = true;
                    break;
                }
            }
            if (!skip)
            {
                Texture texture;
                texture.id = TextureFromFile(str.C_Str(), directory);
                texture.type = typeName;
                texture.path = str.C_Str();
                textures.push_back(texture);
                textures_loaded.push_back(texture);
            }
        }
    
        return textures;
    }
    
    • 实现载入纹理数据的辅助函数。
    unsigned int TextureFromFile(const char* path, const std::string& directory)
    {
        std::string filename = std::string(path);
        filename = directory + '/' + filename;
    
        unsigned int textureID;
        glGenTextures(1, &textureID);
    
        int width, height, nrComponents;
        unsigned char* data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
        if (data)
        {
            GLenum format;
            if (nrComponents == 1)
                format = GL_RED;
            else if (nrComponents == 3)
                format = GL_RGB;
            else if (nrComponents == 4)
                format = GL_RGBA;
    
            glBindTexture(GL_TEXTURE_2D, textureID);
            glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
            glGenerateMipmap(GL_TEXTURE_2D);
    
            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_MIPMAP_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
            stbi_image_free(data);
        }
        else
        {
            std::cout << "Texture failed to load at path: " << path << std::endl;
            stbi_image_free(data);
        }
    
        return textureID;
    }
    

    4. 模型渲染

    4.1 直接渲染模型

    • 编写顶点着色器
    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aNormal;
    layout (location = 2) in vec2 aTexCoords;
    
    out vec2 TexCoords;
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        TexCoords = aTexCoords;    
        gl_Position = projection * view * model * vec4(aPos, 1.0);
    }
    
    • 编写片元着色器:注意这里我们直接声明取样器的uniform变量,但是网格类绘制的时候我们默认指定了material.前缀,因为默认会启用一个纹理单元,所以是可行的。
    #version 330 core
    out vec4 FragColor;
    
    in vec2 TexCoords;
    
    uniform sampler2D texture_diffuse1;
    
    void main()
    {    
        FragColor = texture(texture_diffuse1, TexCoords);
    }
    
    • 声明着色器和模型(3D模型数据来源书中代码包中的资源)。
    // 翻转纹理图像
    stbi_set_flip_vertically_on_load(true);
    
    Shader objectShader("./VertexShader.vs", "./FragmentShader.fs");
    char path[] = "./backpack/backpack.obj";
    Model ourModel(path);
    
    • 渲染模型
    objectShader.use();
    glm::mat4 view = camera.GetViewMatrix();
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    objectShader.setMat4("view", view);
    objectShader.setMat4("projection", projection);
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); 
    model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f)); 
    objectShader.setMat4("model", model);
    
    ourModel.Draw(objectShader);
    
    • 渲染效果


      背包模型1
      背包模型2

    4.2 添加一个点光源

    • 编写顶点着色器:根据点光源计算需要的参数,我们将片元位置和法向量输出到片元着色器。
    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aNormal;
    layout (location = 2) in vec2 aTexCoords;
    
    out vec3 FragPos;
    out vec3 Normal;
    out vec2 TexCoords;
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        FragPos = vec3(model * vec4(aPos, 1.0));
        Normal = mat3(transpose(inverse(model))) * aNormal;
        TexCoords = aTexCoords;    
        gl_Position = projection * view * model * vec4(aPos, 1.0);
    }
    
    • 编写片元着色器:直接抽取多光源章节中点光源的计算函数。
    #version 330 core
    out vec4 FragColor;
    
    struct Material{
        sampler2D texture_diffuse1;
        sampler2D texture_specular1;
        float shininess;
    };
    
    struct PointLight
    {
        vec3 position;
    
        float constant;
        float linear;
        float quadratic;
    
        vec3 ambient;
        vec3 diffuse;
        vec3 specular;
    };
    
    in vec3 Normal;
    in vec3 FragPos;
    in vec2 TexCoords;
    
    uniform vec3 viewPos;
    uniform Material material;
    uniform PointLight pointLight;
    
    vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
    
    void main()
    {    
        vec3 norm = normalize(Normal);
        vec3 viewDir = normalize(viewPos - FragPos);
    
        vec3 result = CalcPointLight(pointLight, norm, FragPos, viewDir);
        FragColor = vec4(result, 1.0);
    }
    
    // 点光源计算
    vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
    {
        vec3 lightDir = normalize(light.position - fragPos);
        // diffuse
        float diff = max(dot(normal, lightDir), 0.0);
        // specular
        vec3 reflectDir = reflect(-lightDir, normal);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
        // attenuation
        float distance = length(light.position - fragPos);
        float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
        // combine
        vec3 ambient = light.ambient * vec3(texture(material.texture_diffuse1, TexCoords));
        vec3 diffuse = light.diffuse * diff * vec3(texture(material.texture_diffuse1, TexCoords));
        vec3 specular = light.specular * spec * vec3(texture(material.texture_specular1, TexCoords));
        ambient *= attenuation;
        diffuse *= attenuation;
        specular *= attenuation;
    
        return (ambient + diffuse + specular);
    }
    
    • 渲染循环中设置点光源的参数,这里直接将光源位置设置为相机的位置,光颜色偏绿。
    objectShader.setVec3("pointLight.position", camera.Position);
    objectShader.setVec3("viewPos", camera.Position);
    // 光属性
    objectShader.setVec3("pointLight.ambient", 0.1f, 0.1f, 0.1f);
    objectShader.setVec3("pointLight.diffuse", 0.5f, 0.8f, 0.6f);
    objectShader.setVec3("pointLight.specular", 1.0f, 1.0f, 1.0f);
    // 衰减参数
    objectShader.setFloat("pointLight.constant", 1.0f);
    objectShader.setFloat("pointLight.linear", 0.09f);
    objectShader.setFloat("pointLight.quadratic", 0.032f);
    // 光斑半径参数
    objectShader.setFloat("material.shininess", 32.0f);
    
    • 渲染效果


      背包模型+点光源1
      背包模型+点光源2

    相关文章

      网友评论

          本文标题:OpenGL学习14——3D模型

          本文链接:https://www.haomeiwen.com/subject/yfcqultx.html