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<Texture> diffuseMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_DIFFUSE andTypeName:"texture_diffuse"];
    
    textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
    
    
    // 2. specular maps
    vector<Texture> specularMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_SPECULAR andTypeName:"texture_specular"];
    textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
    
    // 3. normal maps
    vector<Texture> normalMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_NORMALS andTypeName:"texture_normal"];
    
    textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
    
    // 4. height maps
    std::vector<Texture> heightMaps = [self loadMaterialTexturesWithMaterial:material andTextureType:aiTextureType_HEIGHT andTypeName:"texture_height"];
    textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
    [self setupMesh];
}

- (vector<Texture>)loadMaterialTexturesWithMaterial:(aiMaterial *)mat andTextureType:(aiTextureType)type andTypeName:(string)typeName {
    {
        vector<Texture> 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.这里添加了一个点光源照明,有兴趣的读者可以结合上篇文章投光物的知识,添加聚光灯等照明效果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容