阅读 103

OpenGL学习14——3D模型

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. 根据Debug或Release将相应的dll文件拷贝到程序执行目录。

    1. 在项目的【属性】-【链接器】-【输入】-【附加依赖项】根据是Debug或Release添加相应的静态库文件名(注意分号分隔)。

    1. 在项目的【属性】-【VC++目录】-【库目录】添加静态库文件的目录(头文件路径原先的已经包含了)。

    1. 将Assimp的静态库文件拷贝到我们的库目录下(如我的是D:\3Lib\Libs\Assimp),Debug和Release都拷贝进去。

    1. 将下载解压后的include文件和编译产生的include文件的头文件拷贝到我们项目头文件的包含路径下(如我的是D:\3Lib\Include\assimp)。

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 corelayout (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 coreout 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 corelayout (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 coreout 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



作者:蓬篙人
链接:https://www.jianshu.com/p/73ce30fd2b4f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐