13 - OpenGL學(xué)習(xí)之模型加載

參考文章---learnopengl-cn --- 模型加載

在之前的文章中,我們用的都是箱子模型寄猩,但是現(xiàn)實(shí)世界中找岖,有很多不同的模型,例如車子模型敛滋,機(jī)器人模型许布,但是這些模型通常都非常復(fù)雜,不太能夠通過自己手寫設(shè)置頂點(diǎn)绎晃,紋理蜜唾,法線向量這些數(shù)據(jù),然而庶艾,和箱子對象不同袁余。實(shí)現(xiàn)加載模型的方法是 3D藝術(shù)家在Blender、3DS Max或者M(jìn)aya這樣的工具中制作3D模型咱揍。
我們所要做的就是將這些模型文件解析颖榜,從中提取所有需要的數(shù)據(jù)(例如 頂點(diǎn),法線向量煤裙,紋理掩完,貼圖等),但是3D模型文件有幾十種不同的格式硼砰,單純手寫解析數(shù)據(jù)的話無疑是個龐大的工作量且蓬,好在我們可以使用第三方庫Assimp,Assimp能夠?qū)牒芏喾N不同的模型文件格式(并也能夠?qū)С霾糠值母袷剑鼤⑺械哪P蛿?shù)據(jù)加載至Assimp的通用數(shù)據(jù)結(jié)構(gòu)中题翰。當(dāng)Assimp加載完模型之后恶阴,我們就能夠從Assimp的數(shù)據(jù)結(jié)構(gòu)中提取我們所需的所有數(shù)據(jù)了。由于Assimp的數(shù)據(jù)結(jié)構(gòu)保持不變豹障,不論導(dǎo)入的是什么種類的文件格式冯事,它都能夠?qū)⑽覀儚倪@些不同的文件格式中抽象出來,用同一種方式訪問我們需要的數(shù)據(jù)血公。
當(dāng)使用Assimp導(dǎo)入一個模型的時候桅咆,它通常會將整個模型加載進(jìn)一個場景(Scene)對象,它會包含導(dǎo)入的模型/場景中的所有數(shù)據(jù)坞笙。Assimp會將場景載入為一系列的節(jié)點(diǎn)(Node)岩饼,每個節(jié)點(diǎn)包含了場景對象中所儲存數(shù)據(jù)的索引,每個節(jié)點(diǎn)都可以有任意數(shù)量的子節(jié)點(diǎn)薛夜。Assimp數(shù)據(jù)結(jié)構(gòu)的(簡化)模型如下:


截屏2021-12-30 下午2.43.25.png
  • 所有的場景/模型數(shù)據(jù)都包含在 Scene 對象中籍茧, Scene對象也包含了對場景根節(jié)點(diǎn)的引用。
  • 場景的 Root node(根節(jié)點(diǎn))可能包含子節(jié)點(diǎn)(和其它的節(jié)點(diǎn)一樣)梯澜,它會有一系列指向場景對象中mMeshes數(shù)組中儲存的網(wǎng)格數(shù)據(jù)的索引寞冯。Scene下的mMeshes數(shù)組儲存了真正的Mesh對象,節(jié)點(diǎn)中的mMeshes數(shù)組保存的只是場景中網(wǎng)格數(shù)組的索引。
  • 一個Mesh對象本身包含了渲染所需要的所有相關(guān)數(shù)據(jù)吮龄,像是頂點(diǎn)位置俭茧、法向量、紋理坐標(biāo)漓帚、面(Face)和物體的材質(zhì)
  • 一個網(wǎng)格包含了多個面母债。Face 代表的是物體的渲染圖元(Primitive)(三角形、方形尝抖、點(diǎn))毡们。一個面包含了組成圖元的頂點(diǎn)的索引。由于頂點(diǎn)和索引是分開的昧辽,使用一個索引緩沖來渲染是非常簡單的
  • 最后衙熔,一個網(wǎng)格也包含了一個Material對象,它包含了一些函數(shù)能讓我們獲取物體的材質(zhì)屬性搅荞,比如說顏色和紋理貼圖(比如漫反射和鏡面光貼圖)红氯。
    接下來,我們將使用這個庫來加載模型咕痛,并且解析脖隶,繪制3D模型。

assimp編譯iOS靜態(tài)庫

這里我們?yōu)榱斯?jié)省體積暇检,只編譯了arm64架構(gòu)的靜態(tài)庫

1.首先去github上下載assimp 5.1.3-release版本(寫文章時最新的版本),
解壓后产阱,進(jìn)入到port文件夾,如下圖所示:


WeChat8574b550c319295e73892e3c532b73cc.png

然后進(jìn)入iOS文件下块仆,修改build.sh文件如下圖所示:


image.png

然后打開終端,cd到iOS文件夾构蹬,然后 ./build.sh,腳本就回執(zhí)行編譯工作,最后生成的靜態(tài)庫會在lib文件夾下悔据,如下圖:


image.png

生成libassimp.a導(dǎo)入項(xiàng)目就可以了庄敛,然后把include文件夾(包含頭文件)導(dǎo)入項(xiàng)目,運(yùn)行項(xiàng)目科汗,會報錯(大坑)找到原因是缺少兩個靜態(tài)庫(libIrrXML-fat.a 和 libzlibstatic-fat.a)藻烤。

2.在assimp目錄下下載 5.0.0版本的代碼,然后按照上述步驟靜態(tài)編譯头滔,這時候可以在lib文件夾中看到 libIrrXML-fat.a 和 libzlibstatic-fat.a,導(dǎo)入項(xiàng)目即可怖亭。

這里為啥不直接使用5.0.0版本呢?是因?yàn)?.0.0編譯出來的 libassimp.a有幾百兆那么大坤检,具體還不知道原因兴猩,以后發(fā)現(xiàn)了再修改。

  1. 在項(xiàng)目中build settings 添加頭文件路徑


    image.png

代碼實(shí)現(xiàn):

首先我們定義兩個類 Mesh和ModelLoader,Mesh類用來管理網(wǎng)格數(shù)據(jù)和具體的數(shù)據(jù)解析繪制過程早歇,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];
        }
    }
    
    //遞歸調(diào)用
    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句柄等數(shù)據(jù))潭千。
    1. setup操作:通過Assimp加載模型文件,判斷加載是否成功借尿,然后就是加工節(jié)點(diǎn)
    1. 加工節(jié)點(diǎn)數(shù)據(jù):遍歷節(jié)點(diǎn)和字節(jié)點(diǎn)刨晴,取出aimesh對象,通過這個對象生成Mesh對象垛玻,保存在meshes數(shù)組中。
  • 4.加工網(wǎng)格數(shù)據(jù): 這里就是Mesh對象通過aimesh對象奶躯,解析頂點(diǎn)帚桩,法線向量等數(shù)據(jù)
  • 5.繪制,遍歷meshes數(shù)組嘹黔,繪制其中每一個網(wǎng)格對象(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();

    //每個像素點(diǎn)四個字節(jié)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類的主要操作如上圖代碼,就是綁定數(shù)據(jù),加載貼圖等熄驼。
這里我們著重注意loadMaterialTexturesWithMaterial這個函數(shù)蛤售,這里相當(dāng)于做了一個優(yōu)化,因?yàn)樵诿恳淮卫L制中喂江,有可能有的紋理已經(jīng)生成加載過了召锈,這時候我們可以通過vector保存紋理對象,加載紋理的時候先判斷是否已經(jīng)加載過获询,如果加載過就不用重新加載涨岁,直接取出,沒有加載過就加載吉嚣,這樣可以提高性能梢薪。

最后看一下實(shí)現(xiàn)的效果,如下圖:


模型加載

代碼已上傳至github.這里添加了一個點(diǎn)光源照明尝哆,有興趣的讀者可以結(jié)合上篇文章投光物的知識秉撇,添加聚光燈等照明效果。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秋泄,一起剝皮案震驚了整個濱河市琐馆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恒序,老刑警劉巖啡捶,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異奸焙,居然都是意外死亡瞎暑,警方通過查閱死者的電腦和手機(jī)彤敛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來了赌,“玉大人墨榄,你說我怎么就攤上這事∥鹚” “怎么了袄秩?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長逢并。 經(jīng)常有香客問我之剧,道長,這世上最難降的妖魔是什么砍聊? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任背稼,我火速辦了婚禮,結(jié)果婚禮上玻蝌,老公的妹妹穿的比我還像新娘蟹肘。我一直安慰自己,他們只是感情好俯树,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布帘腹。 她就那樣靜靜地躺著,像睡著了一般许饿。 火紅的嫁衣襯著肌膚如雪阳欲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天陋率,我揣著相機(jī)與錄音胸完,去河邊找鬼。 笑死翘贮,一個胖子當(dāng)著我的面吹牛赊窥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播狸页,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼锨能,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了芍耘?” 一聲冷哼從身側(cè)響起址遇,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎斋竞,沒想到半個月后倔约,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坝初,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年浸剩,在試婚紗的時候發(fā)現(xiàn)自己被綠了钾军。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡绢要,死狀恐怖吏恭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情重罪,我是刑警寧澤樱哼,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站剿配,受9級特大地震影響搅幅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呼胚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一茄唐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧砸讳,春花似錦琢融、人聲如沸界牡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宿亡。三九已至常遂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挽荠,已是汗流浹背克胳。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留圈匆,地道東北人漠另。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像跃赚,于是被迫代替她去往敵國和親笆搓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容