13 - OpenGL学习之模型加载

参考文章---learnopengl-cn --- 模型加载

在之前的文章中,我们用的都是箱子模型,但是现实世界中,有很多不同的模型,例如车子模型,机器人模型,但是这些模型通常都非常复杂,不太能够通过自己手写设置顶点,纹理,法线向量这些数据,然而,和箱子对象不同。实现加载模型的方法是 3D艺术家在Blender、3DS Max或者Maya这样的工具中制作3D模型。
我们所要做的就是将这些模型文件解析,从中提取所有需要的数据(例如 顶点,法线向量,纹理,贴图等),但是3D模型文件有几十种不同的格式,单纯手写解析数据的话无疑是个庞大的工作量,好在我们可以使用第三方库Assimp,Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。当Assimp加载完模型之后,我们就能够从Assimp的数据结构中提取我们所需的所有数据了。由于Assimp的数据结构保持不变,不论导入的是什么种类的文件格式,它都能够将我们从这些不同的文件格式中抽象出来,用同一种方式访问我们需要的数据。
当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:


截屏2021-12-30 下午2.43.25.png
  • 所有的场景/模型数据都包含在 Scene 对象中, Scene对象也包含了对场景根节点的引用。
  • 场景的 Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。
  • 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质
  • 一个网格包含了多个面。Face 代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的
  • 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。
    接下来,我们将使用这个库来加载模型,并且解析,绘制3D模型。

assimp编译iOS静态库

这里我们为了节省体积,只编译了arm64架构的静态库

1.首先去github上下载assimp 5.1.3-release版本(写文章时最新的版本),
解压后,进入到port文件夹,如下图所示:


WeChat8574b550c319295e73892e3c532b73cc.png

然后进入iOS文件下,修改build.sh文件如下图所示:


image.png

然后打开终端,cd到iOS文件夹,然后 ./build.sh,脚本就回执行编译工作,最后生成的静态库会在lib文件夹下,如下图:


image.png

生成libassimp.a导入项目就可以了,然后把include文件夹(包含头文件)导入项目,运行项目,会报错(大坑)找到原因是缺少两个静态库(libIrrXML-fat.a 和 libzlibstatic-fat.a)。

2.在assimp目录下下载 5.0.0版本的代码,然后按照上述步骤静态编译,这时候可以在lib文件夹中看到 libIrrXML-fat.a 和 libzlibstatic-fat.a,导入项目即可。

这里为啥不直接使用5.0.0版本呢?是因为5.0.0编译出来的 libassimp.a有几百兆那么大,具体还不知道原因,以后发现了再修改。

  1. 在项目中build settings 添加头文件路径


    image.png

代码实现:

首先我们定义两个类 Mesh和ModelLoader,Mesh类用来管理网格数据和具体的数据解析绘制过程,ModelLoader类用来处理加载模型文件,解析模型文件。

由于assimp库是基于C++,所以这两个类的后缀都要改为.mm。

ModelLoader

- (instancetype)initWithFilePath:(NSString *)filepath andContext:(nonnull ESContext *)eglContext {
    if (self = [super init]) {
        self.filePath = filepath;
        self.meshes = [NSMutableArray new];
        self.eglContext = eglContext;
        [self setup];
    }
    return self;
}

- (void)setup {
    Assimp::Importer importer;
    const aiScene *scene = importer.ReadFile(_filePath.cString, aiProcess_FlipUVs | aiProcess_Triangulate);
    if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
        NSLog(@"Error: Assimp failed to open obj file: %@",_filePath);
        return;
    }
    
    NSString *directory = [_filePath stringByDeletingLastPathComponent];
    self.filePath = directory;
    self.scene = scene;
    [self processNode:self.scene->mRootNode];
    
}
- (void)processNode:(aiNode *)node {
    for (unsigned int i = 0; i < node->mNumMeshes; i++) {
        aiMesh *mesh = self.scene->mMeshes[node->mMeshes[i]];
        Mesh *oneMesh = [self processMesh:mesh];
        if (oneMesh != nil) {
            [self.meshes addObject:oneMesh];
        }
    }
    
    //递归调用
    for (unsigned int i = 0; i < node->mNumChildren; i++) {
        [self processNode:node->mChildren[i]];
    }
}

- (Mesh *)processMesh:(aiMesh *)mesh {
    Mesh *one = [Mesh new];
    one.eglContext  = self.eglContext;
    one.directory = string([self.filePath cStringUsingEncoding:NSUTF8StringEncoding]);
    [one parseWithMesh:mesh andAIScnene:self.scene];
    return one;
}

- (void)draw {
    for (Mesh *one in self.meshes) {
        [one draw];
    }
}

我们看一下具体步骤:

  • 1.初始化传入模型文件路径和ESContext对象(包含glprogram句柄等数据)。
    1. setup操作:通过Assimp加载模型文件,判断加载是否成功,然后就是加工节点
    1. 加工节点数据:遍历节点和字节点,取出aimesh对象,通过这个对象生成Mesh对象,保存在meshes数组中。
  • 4.加工网格数据: 这里就是Mesh对象通过aimesh对象,解析顶点,法线向量等数据
  • 5.绘制,遍历meshes数组,绘制其中每一个网格对象(Mesh).

Mesh

- (void)parseWithMesh:(aiMesh *)mesh andAIScnene:(const aiScene *)scene {
    for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
        Vertex vertex;
        Vector3 vector;
        
        //position
        vector.x = mesh->mVertices[i].x;
        vector.y = mesh->mVertices[i].y;
        vector.z = mesh->mVertices[i].z;
        vertex.position = vector;
        
        //normals
        if (mesh->HasNormals()) {
            vector.x = mesh->mNormals[i].x;
            vector.y = mesh->mNormals[i].y;
            vector.z = mesh->mNormals[i].z;
            vertex.normal = vector;
        }
        
        //texture coordinates
        if (mesh->mTextureCoords[0]) {
            Vector2 vec;
            vec.x = mesh->mTextureCoords[0][i].x;
            vec.y = mesh->mTextureCoords[0][i].y;
            vertex.textCoord = vec;
            
        }
        else {
            vertex.textCoord = {{0.0f,0.0f}};
        }
        vertices.push_back(vertex);
    }
    
    // now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices.
    for(unsigned int i = 0; i < mesh->mNumFaces; i++)
    {
        aiFace face = mesh->mFaces[i];
        // retrieve all indices of the face and store them in the indices vector
        for(unsigned int j = 0; j < face.mNumIndices; j++)
            indices.push_back(face.mIndices[j]);
    }
    // process materials
    aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
    // we assume a convention for sampler names in the shaders. Each diffuse texture should be named
    // as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER.
    // Same applies to other texture as the following list summarizes:
    // diffuse: texture_diffuseN
    // specular: texture_specularN
    // normal: texture_normalN

    // 1. diffuse maps
    vector diffuseMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_DIFFUSE andTypeName:"texture_diffuse"];
    
    textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
    
    
    // 2. specular maps
    vector specularMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_SPECULAR andTypeName:"texture_specular"];
    textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
    
    // 3. normal maps
    vector normalMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_NORMALS andTypeName:"texture_normal"];
    
    textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
    
    // 4. height maps
    std::vector heightMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_HEIGHT andTypeName:"texture_height"];
    textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
    [self setupMesh];
}

- (vector)loadMaterialTexturesWithMaterial:(aiMaterial *)mat andTextureType:(aiTextureType)type andTypeName:(string)typeName {
    {
        vector textures;
        for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
        {
            aiString str;
            mat->GetTexture(type, i, &str);
            // check if texture was loaded before and if so, continue to next iteration: skip loading a new texture
            bool skip = false;
            for(unsigned int j = 0; j < textures_loaded.size(); j++)
            {
                char *texturePath = textures_loaded[j].path.data();
                if(std::strcmp(texturePath, str.C_Str()) == 0)
                {
                    textures.push_back(textures_loaded[j]);
                    skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization)
                    break;
                }
            }
            if(!skip)
            {   // if texture hasn't been loaded already, load it
                Texture texture;
                texture.id = TextureFromFile(str.C_Str(), self.directory);
                texture.type = typeName;
                texture.path = str.C_Str();
                textures.push_back(texture);
                textures_loaded.push_back(texture);  // store it as texture loaded for entire model, to ensure we won't unnecesery load duplicate textures.
            }
        }
        return textures;
    }
}

unsigned int TextureFromFile(const char *path, const string &directory)
{
    string filename = string(path);
    filename = directory + '/' + filename;
    
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;

    UIImage *img = [UIImage imageWithContentsOfFile:[[NSString alloc] initWithCString:filename.data() encoding:NSUTF8StringEncoding]];
    CGImageRef imageref = [img CGImage];

     width = CGImageGetWidth(imageref);
     height = CGImageGetHeight(imageref);

    GLubyte *textureData = (GLubyte *)malloc(width * height * 4);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    //每个像素点四个字节RGBA
    NSUInteger bytesperPixel = 4;
    NSUInteger bytesperRow = bytesperPixel * width;
    NSUInteger bitsperComponent = 8;

    CGContextRef context = CGBitmapContextCreate(textureData, width, height, bitsperComponent, bytesperRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);



    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageref);
    CGColorSpaceRelease(colorSpace);
    CGContextRelease(context);
    NSData  *_imageData = [NSData dataWithBytes:textureData length:(width * height * 4)];
    
    if (_imageData)
    {

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (unsigned char *)_imageData.bytes);
        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);

    }
    else
    {
        printf("Texture failed to load at path: %s",path);
    }

    return textureID;
}

- (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);
    /**
     layout (location = 0) in vec3 aPos;
     layout (location = 1) in vec3 aNormal;
     layout (location = 2) in vec2 aTexCoords;
     */
    
    GLuint positionIndex = glGetAttribLocation(_eglContext->program, "aPos");
    GLuint texCoordIndex = glGetAttribLocation(_eglContext->program, "aTexCoords");
    GLuint normalIndex = glGetAttribLocation(_eglContext->program, "aNormal");
   
    
    // vertex Positions
    glEnableVertexAttribArray(positionIndex);
    glVertexAttribPointer(positionIndex, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
    // vertex normals
    glEnableVertexAttribArray(normalIndex);
    glVertexAttribPointer(normalIndex, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
    // vertex texture coords
    glEnableVertexAttribArray(texCoordIndex);
    glVertexAttribPointer(texCoordIndex, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, textCoord));

    glBindVertexArray(0);

}

-(void)draw {
    unsigned int diffuseNr = 1;
    unsigned int specularNr = 1;
    unsigned int normalNr = 1;
    unsigned int heightNr = 1;
    for (unsigned int i = 0; i < textures.size(); i++)
    {
        glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding
        // retrieve texture number (the N in diffuse_textureN)
        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++); // transfer unsigned int to stream
        else if (name == "texture_normal")
            number = std::to_string(normalNr++); // transfer unsigned int to stream
        else if (name == "texture_height")
            number = std::to_string(heightNr++); // transfer unsigned int to stream

        const char * one = (name + number).c_str();
        GLuint index = glGetUniformLocation(_eglContext->program, one);
        glUniform1i(index, i);
        glBindTexture(GL_TEXTURE_2D, textures[i].id);
    }
    glActiveTexture(GL_TEXTURE0);

    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);
}

mesh类的主要操作如上图代码,就是绑定数据,加载贴图等。
这里我们着重注意loadMaterialTexturesWithMaterial这个函数,这里相当于做了一个优化,因为在每一次绘制中,有可能有的纹理已经生成加载过了,这时候我们可以通过vector保存纹理对象,加载纹理的时候先判断是否已经加载过,如果加载过就不用重新加载,直接取出,没有加载过就加载,这样可以提高性能。

最后看一下实现的效果,如下图:


模型加载

代码已上传至github.这里添加了一个点光源照明,有兴趣的读者可以结合上篇文章投光物的知识,添加聚光灯等照明效果。

你可能感兴趣的:(13 - OpenGL学习之模型加载)