扒幾個 3D 模型備用

前言

在上一篇中,我展示了 OpenGL 開發的基本過程,算是向 3D 世界邁出的一小步吧。對於簡單的 3D 物體,比如立方體、球體、圓環等等,我們只需要簡單的計算就可以得到他們的頂點的坐標。但是僅僅這樣,還不是太過癮,我們需要找一些複雜一點的 3D 模型,以便於我們體會 3D 世界的魅力。

在我學習 OpenGL 的過程中,我收集了不少的 3D 模型,主要是從 Free3D 下載的,都是 Obj 格式的文件,有的帶紋理貼圖,有的不帶紋理貼圖。比如,有一個小木屋的模型,帶紋理貼圖和法線貼圖,是我學習貼圖和光照的好素材。還有一個地球的模型,還有幾輛汽車的模型。還有我從著名的 OpenGL 網路教程 LearnOpenGL 中下載得有一套 nanosuit 的模型。對於這些有著規範格式的 3D 模型,我覺得使用 Assimp 庫載入是比較好的選擇,至於 Assimp 庫,以後再介紹。

另外,茶壺也是一個經典的模型,不過是以貝塞爾曲面的方式定義的。貝塞爾曲面其實不難,使用 16 個控制點可以描述一個曲面,並且可以根據我們需要的光滑程度選擇不同的細分級別,關於貝塞爾曲面的內容留待以後再專講,而且我覺得和曲面細分著色器一起學習效果更佳。那麼這個茶壺模型的數據在哪裡可以找到呢?FreeGlut 中有,可以在 github 中找到。除此之外,紅寶書的源程式碼中也有一個茶壺的數據。這裡不贅述。

我這裡要扒的幾個模型來自紅寶書的源程式碼,它們分別是 armadillo.vbm、 bunny.vbm 和 ninja.vbm。這裡,作者使用了他自創的 vbm 模型格式。作者還寫了從 obj 格式到 vbm 格式轉換的工具以及從 Maya 導出 vbm 格式的工具。但畢竟 vbm 格式不是標準的通用格式,我並不是很喜歡。但是為了把這三個模型顯示出來看看,我還是認真研究了作者的源程式碼。

VBM 模型文件的具體細節

我是通過閱讀紅寶書源程式碼中的 vbm.h 和 vbm.cpp 文件來了解 vbm 模型文件的細節的。這是一個二進位的模型文件,一開始是個 VBM_HEADER 結構,在作者的設計中,該文件分為新版和舊版,舊版的頭部結構為 VBM_HEADER_OLD,但是從我扒出的數據來看,根本就不需要考慮舊版。

在 VBM_HEADER 之後,是若干個 VBM_ATTRIB_HEADER 結構,該結構用來說明每個頂點包含哪些屬性,每個屬性又包含哪些分量。從我扒出的數據來看,以上三個模型,都是包含三個屬性的,分別是頂點坐標,包含 4 個 GLfloat 分量,頂點法向量,包含 3 個 GLfloat 分量,紋理貼圖坐標,包含兩個 GLfloat 分量。這和我上一篇中對頂點格式的設計簡直一模一樣。

在 VBM_ATTRIB_HEADER 之後,是若干個 VBM_FRAME_HEADER,看來該作者設計該格式是可以支援動畫的。不過以我扒出的數據來看,以上三個模型文件都只包含一幀。

在 VBM_FRAME_HEADER 之後就是頂點數據。從頭文件中可以得到頂點的個數,以及每個頂點包含哪些屬性,以及每個屬性包含幾個分量,就很容易算出頂點數據的長度。

頂點數據之後,就是索引數據。我讀源程式碼,同時還發現頂點數據之後是材質資訊。這兩組數據是有點混淆的。好在,以我扒出的數據來看,以上三個模型文件既沒有使用索引,也沒有包含任何材質,那倒是讓我省事了不少。

編寫我自己的 VbmObject 類

參考我之前寫的 Mesh 類,就很容易寫一個能在我的 App 框架中非常容易使用的 VbmObject 類。在 VbmObject 類中,寫一個 loadFromVBM() 方法,以從文件中載入頂點數據,同時獲取頂點個數的資訊。然後寫一個 setup() 方法,用來創建相應的 VAO 和 VBO,並向快取中存入數據,並啟用頂點屬性。這裡需要特別注意的是,該模型文件中的數據,是每一個屬性集中存放的,所以調用 glVertexAttribPointer() 方法時要特別注意。最後,寫一個 render() 方法進行渲染,render() 方法很簡單,就是調用 glDrawArrays(),當然,調用該方法之前需要綁定 VAO。

vbm.hpp 的完整程式碼如下:

#ifndef __VBM_H__
#define __VBM_H__

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <vector>
#include <string>
#include <string.h>
#include <GL/glew.h>
#include <iostream>

typedef struct VBM_HEADER_t
{
    unsigned int magic;
    unsigned int size;
    char name[64];
    unsigned int num_attribs;
    unsigned int num_frames;
    unsigned int num_vertices;
    unsigned int num_indices;
    unsigned int index_type;
    unsigned int num_materials;
    unsigned int flags;
} VBM_HEADER;

typedef struct VBM_ATTRIB_HEADER_t
{
    char name[64];
    unsigned int type;
    unsigned int components;
    unsigned int flags;
} VBM_ATTRIB_HEADER;

typedef struct VBM_FRAME_HEADER_t
{
    unsigned int first;
    unsigned int count;
    unsigned int flags;
} VBM_FRAME_HEADER;

class VbmObject{
    protected:
        unsigned char* file_data;
        unsigned char* vertex_data;
        unsigned int vertex_num;
        GLuint VAO, VBO;
               
    public:
        bool loadFromVBM(const char * filename){
            std::cout << "File name: " << filename << std::endl;
            FILE * f = NULL;
            f = fopen(filename, "rb");
            if(f == NULL)
                return false;

            fseek(f, 0, SEEK_END);
            size_t filesize = ftell(f);
            fseek(f, 0, SEEK_SET);

            file_data = new unsigned char [filesize];
            fread(file_data, filesize, 1, f);
            fclose(f);

            VBM_HEADER * header = (VBM_HEADER *)file_data;
            vertex_data = file_data + header->size + header->num_attribs * sizeof(VBM_ATTRIB_HEADER) + header->num_frames * sizeof(VBM_FRAME_HEADER);
            vertex_num = header->num_vertices;
            std::cout << "Num of Vertices: " << vertex_num << std::endl;

            return true;
        }

        void setup(){
            glCreateVertexArrays(1, &VAO);
            glBindVertexArray(VAO);
            glCreateBuffers(1, &VBO);
            glBindBuffer(GL_ARRAY_BUFFER, VBO);
            glNamedBufferStorage(VBO, 9*sizeof(GLfloat)*vertex_num, vertex_data, 0);
            glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, (void*)0);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, (void*)(sizeof(GLfloat)*vertex_num*4));
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, (void*)(sizeof(GLfloat)*vertex_num*3));
            glEnableVertexAttribArray(2);
        }

        void render(){
            glBindVertexArray(VAO);
            glDrawArrays(GL_TRIANGLES, 0, vertex_num);
        }

        ~VbmObject(){
            if(file_data != NULL){
                delete file_data;
            }
        }
};

#endif

主程式文件是 DumpVbm.cpp,其框架結構還是和前面的差不多,先是繼承 App 類,在 init() 方法中初始化數據,比如調用 VbmObject 對象的 loadFromVBM() 方法,調用 setup() 方法,同時創建 shader。然後在 display() 中準備模型、視圖、投影矩陣,向 shader 中傳遞這些矩陣數據,然後調用 VbmObject 對象的 render() 方法。

DumpVbm.cpp 的完整內容如下:

#include "../include/app.hpp"
#include "../include/shader.hpp"
#include "../include/vbm.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

class MyApp : public App {
    private:
        const GLfloat clearColor[4] = {0.2f, 0.3f, 0.3f, 1.0f};
        VbmObject armadillo;
        VbmObject bunny;
        VbmObject ninja;
        Shader* shaderDumpVbm;

    public:
        void init(){
            
            ShaderInfo shaders[] = {
                {GL_VERTEX_SHADER, "dumpvbm.vert"},
                {GL_FRAGMENT_SHADER, "dumpvbm.frag"},
                {GL_NONE, ""}
            };
            shaderDumpVbm = new Shader(shaders);
            armadillo.loadFromVBM("armadillo.vbm");
            armadillo.setup();

            bunny.loadFromVBM("bunny.vbm");
            bunny.setup();

            ninja.loadFromVBM("ninja.vbm");
            ninja.setup();
            
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_LEQUAL);

            glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
        }

        void display(){
            glClearBufferfv(GL_COLOR, 0, clearColor);
            glClear(GL_DEPTH_BUFFER_BIT);

            glm::mat4 I(1.0f);
            glm::vec3 X(1.0f, 0.0f, 0.0f);
            glm::vec3 Y(0.0f, 1.0f, 0.0f);
            glm::vec3 Z(0.0f, 0.0f, 1.0f);
            float t = (float)glfwGetTime();

            glm::mat4 view_matrix = glm::translate(I, glm::vec3(0.0f, 0.0f, -5.0f))
                                        * glm::rotate(I, t, Y);

            glm::mat4 projection_matrix = glm::perspective(glm::radians(45.0f), aspect, 1.0f, 100.0f);

            glm::mat4 armadillo_model_matrix = glm::translate(I, glm::vec3(-2.0f, 0.0f, 0.0f)) * glm::scale(I, glm::vec3(0.015f, 0.015f, 0.015f)) * glm::rotate(I, glm::radians(180.0f), Y);
            
            shaderDumpVbm->setModelMatrix(armadillo_model_matrix);
            shaderDumpVbm->setViewMatrix(view_matrix);
            shaderDumpVbm->setProjectionMatrix(projection_matrix);
            shaderDumpVbm->setCurrent();
            armadillo.render();

            glm::mat4 bunny_model_matrix =  glm::scale(I, glm::vec3(10.0f, 10.0f, 10.0f));
            shaderDumpVbm->setModelMatrix(bunny_model_matrix);
            bunny.render();

            glm::mat4 ninja_model_matrix = glm::translate(I, glm::vec3(2.0f, -1.0f, 0.0f)) * glm::scale(I, glm::vec3(0.015f, 0.015f, 0.015f));
            shaderDumpVbm->setModelMatrix(ninja_model_matrix);
            ninja.render();
        }

        ~MyApp(){
            if(shaderDumpVbm != NULL){
                delete shaderDumpVbm;
            }
        }

};


DECLARE_MAIN(MyApp)

shader 文件和之前沒有區別。編譯運行,命令如下:

g++ DumpVbm.cpp -o DumpVbm -lGL -lglfw -lGLEW
./DumpVbm

就可以看到效果了。如下:

版權申明

該隨筆由京山遊俠在2021年02月23日發佈於部落格園,引用請註明出處,轉載或出版請聯繫部落客。QQ郵箱:[email protected]