OpenGL - Assimp

Vytvoření vertex dat pro krychli nebo jehlan není nejjednodušší práce. Cokoliv složitějšího je už skoro nadlidský úkol. Proto vznikly programy pro modelování 3D objektů, jako je například Blender. Knihovna Assimp pak dokáže takový model načíst a vytvořit z něj datovou strukturu, ze které můžete získat data potřebná pro vykreslení modelu.

O projektu Assimp

Assimp (Open Asset Import Library) je free opensource projekt. Dokákže si poradit s celou řadou formátů. Není ale úplně dokonalý.

Nejlépe si asi poradí s formátem .obj. Dle mé zkušenosti, když se pokusím importovat formát .blend (nativní formát pro Blender), nedopadne to úplně nejlépe (model je rozsypaný). Když v Blenderu model vyexportuji do .obj, s výsledkem si assimp poradí mnohem lépe.

Instalace

Assimp není součástí zdrojových kódů ke stažení. Budete si ho muset nainstalovat a nastavit k němu správné cesty v projektu příkladu.

Pokud používáte Linux a máte štěstí, tak najdete assimp v distribučních v balíčcích. V Debianu se balíček jmenuje libassimp-dev. Pokud vaše Linuxová distribuce takový balíček nemá (což je pravděpodobné), nebo obsahuje starší verzi (jako v Debianu v době pasní této kapitoly), nebo používáte Windows, čtěte dále.

Na webu projektu assimpu si můžete stáhnout zdrojové kódy, které si musíte sami přeložit. K tomu vám pomůže cmake.

Program cmake je taková vylepšená verze make. Místo překladu má na starosti vytvoření projektu, který pak můžete přeložit. Cmake dokáže vytvořit projekt pro různé překladače a vývojová prostředí. V Linuxu obvykle vygeneruje soubory pro Makefile. Ve Windows vám vytvoří např. projekt pro Visual Studio.

Postup instalace je následující:

  1. Stáhněte si a nainstalujte cmake. V Linuxu budete mít pro svůj systém oficiální balíček, do Windows si stáhněte a spusťte instalátor z http://www.cmake.org/
  2. Ve Windows stáhněte zdrojové kódy assimpu a rozbalte je (např. do adresáře assimp-4.X.X/). Pozor! Ve verzi 4.0.1 je bug, který znemožňuje překlad s Visual Studiem (v Linuxu je to OK). Stáhněte si novější verzi, pokud už vyšla, nebo si stáněte zdrojáky přímo z masteru (Clone or download → Download ZIP).
  3. Od verze 4.0.0 je součástí assimpu assimp-viewer. Pokud ho chcete přeložit, nainstalujte si v Linuxu balíčky qtbase5-dev a libdevil-dev. Překlad tohoto programu pro Windows popisovat nebudu.
  4. V Linuxu dále pokračujte v příkazové řádce. Přejděte do rozbaleného adresáře a spusťte příkaz cmake . Pak příkaz make a případně ještě jako root make install; ldconfig. Tím máte v Linuxu hotovo.
  5. Ve Windows spusťte grafické rozhraní (viz první obrázek níže).
  6. Vyberte adresář s assimpem jako source code. Do Where to build the binaries: vyberte adresář, kam se vygeneruje projekt pro generování assimpu (vytvořte libovolný adresář, např. assimp-4.X.X-build/).
  7. Klikněte na Configure. Dále vyberte Specifi the generator for this project dle toho, jaké používáte vývojové prostředí (např. Visual Studio 15 2017). Klikněte na Finish.
  8. Po chvilce, kdy se zdá, že se nic neděje, uvidíte to, co je na druhém obrázku dole. Volby jsou červeně označené proto, protože jste je zatím nepotvrdili. Klikněte znovu na Configure a pak na Generate. Tím je vytvořen projekt, který můžete přeložit ve vybraném vývojovém prostředí.
  9. Předpokládám, že používáte Visual Studio. Pak tedy otevřete vygenerovaný projekt poklepáním na Assimp.sln).
  10. Z menu vyberte BUILD → Build Solution a zajděte si na kafe.
CMake CMake

CMake

Překlad OpenGL projektu

Dalším krokem je nastavení projektu, který využívá assimp.

Pokud překládáte ve Windows, tak v adresáři assimp-4.X.X-build/code/Debug/ najdete soubor assimp-vc140-mt.dll (assimp-vc-120-mt.dll u Visual Studia 2013). Ten zkopírujte do adresáře s vaším OpenGL projektem. Dále tam najdete assimp-vc140-mt.lib. Ten musíte přidat do nastavení projektu (podobně, jako jste to už dělali například pro glfw nebo glew knihovny …).

Otevřete ve Visual Studiu vlastnosti projektu (menu PROJECT → ... Properties). Dále, do Linker → General → Additional Library Directories přidejte adresář assimp-4.X.X-build/code/Debug/.

Do Linker → Input → Additional Dependencies přidejte assimp-vc140-mt.lib.

Musíte také přidat cestu k include/ v původním, i vygenerovaném adresáři. Do C/C++ → Additional Include Directories přidejte cestu k adresáři assimp-4.X.X/include/ i assimp-4.X.X-build/include/.

V Linuxu přeložíte svůj projekt s assimpem, glfw a glew nějak takto:

clang++ `pkg-config --cflags glfw3` -o main main.cpp `pkg-config --libs glfw3` -lm -ldl -std=c++11 `pkg-config --cflags --libs glew`\
-I../assimp-4.X.X/include -L../assimp-4.X.X/lib -lassimp

Při spuštění musíte programu říct, kde najde assimp knihovnu:

LD_LIBRARY_PATH=../assimp-4.X.X/lib/ ./main 

Pokud jste assimp nainstalovali příkazem make install, tak volbu -I../assimp-4.X.X/linclude ani nastavení LD_LIBRARY_PATH nebudete potřebovat. Include soubory i .so knihovna se najdou ve standardních adresářích. (Ale začne to fungovat asi až po restartování počítače.)

Projekt 10assimp/assimp/ z zdrojových kódů ke stažení neobsahuje model. Stáhněte si i model Nanosuit a rozbalte ho do adresáre 10assimp/assimp/models/nanosuit/.

Popis struktur Assimpu

Použití assimpu je jednoduché. Stačí zavolat funkci aiImportFile(), která dostává jako první argument cestu k souboru s modelem a jako druhý argument přiznaky, kterými můžete assimpu říct, jak má model před vrácením ještě zpracovat.

Příznak aiProcess_FlipUVs přinutí assimp otočit souřadnice u textur, aby byly se znaménkem, jaké očekává OpenGL.

Příznak aiProcess_Triangulate přimněje assimp vrátit strukturu, která se bude skládat jen z trojúhelníků. Pokud autor modelu použil jiná vykreslovací primitiva, jako např. bod, čáru nebo čtverec, assimp vše zkonvertuje na trojúhelníky. To nám zjednoduší práci. (Ale pro assimp je to práce navíc, takže bude načítat model pomaleji.)

#include <assimp/cimport.h>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
//...
const struct aiScene* scene = aiImportFile(path, aiProcess_FlipUVs | aiProcess_Triangulate);
if (!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
    fprintf(stderr, "aiImportFile error: %s\n", aiGetErrorString());
    model->success = false;
    return;
}
//...

Tímto získáte odkaz na strukturu aiScene, jejíž zjednodušený model můžete vidět na obrázku níže.

Ať už budete nahrávat model z jakéhokoliv formátu, struktura vrácená assimpem bude vypadat stejně. Její kompletní popis najdete v dokumentaci. Dokumentace je sice pro C++ verzi, ale struktury pro C vypadají téměř stejně (jen se místo metod pro přístup k vlastnostem struktury používají staré dobré funkce, viz dále).
Já budu v příkladu používat jen atributy a funkce zobrazené v následujícím UML diagramu:

struct aiScene

Hlavním atributem je zde mRootNode, což je odkaz na strukturu aiNode (který může být NULL, pokud se načtení modelu nezdaří). Každý model je tvořen jako stromová struktura, kde mRootNode je kořenem této struktury a každý aiNode obsahuje mNumChildren potomků v poli mChildren.

Například model člověka se může skládat z hlavy, těla, rukou a nohou. Ruka se pak skládá z předloktí, zápěstí, prstů. Prsty z jednotlivých článků atd.

Takto poskládaná struktura se hodí například pro případ, kdy chcete s částí objektu pohybovat – když pohnete rukou v rameni, pohnete se všemi „dětmi“ ruky …

V příkladu níže převedu struktury Assimpu na jednodušší ne-stromovou strukturu, která se bude snadněji vykreslovat. Ale to teď trochu předbíhám.

struct aiNode

Struktura aiNode obsahuje pole s dětmi, jak jsem vysvětlil výše a také pole mMeshes. Jedná se o pole indexů do pole mMeshes ve struktuře aiScene (jednotlivé meshe může využívat více aiNode instancí). Struktura aiNode také obsahuje počet prvků jednotlivých polí.

struct aiMesh a struct aiFace

Struktura aiMesh obsahuje data potřebná pro vykreslení (části) modelu. Najdete tam počet vertexů, pole s vertexy, normály, souřadnice textur a index do pole mMaterials struktury aiScene.

Struktura aiMesh také obsahuje pole mFaces, což jsou struktury, které popisují OpenGL primitiva. Protože jsem nechal přegenerovat všechny primitiva jako trojúhelníky (viz aiProcess_Triangulate), bude aiFace.mNumIndexes vždy rovno 3 a aiFace.mIndices tříprvkové pole obsahující indexy vertexů.

struct aiMaterial

Tato struktura obsahuje informace o barvě, textuře, lesklosti povrchu atp.

K těmto informacím se nepřistupuje pomocí atributů struktury, ale pomocí funkcí, jako je třeba aiGetMaterialColor(), která vrací přes třetí argument barvu pro ambientní, specular, nebo diffuse barvu – podle toho, o co si zažádáte druhým argumentem.

Model

Struktury assimpu převedu na jiné, jednodušší struktury, které budu používat pro vykreslování modelu. Hlavní změnou bude, ža ze stromové struktury nódů assimpu udělám jednorozměrné pole struktur nového typu Mesh (viz Struktura meshe dále).

Hlavní práci na převodu modelu odvede struktura Model, která vytvoří z každého uzlu (aiNode) strukturu Mesh a uloží si odkaz na ni do svého pole.

Model je také zodpovědný za vytvoření OpenGL programu (z vertex a fragment shaderů). Vykreslování modelu je pak pouze otázkou použití tohoto OpenGL programu, nastavení některých GL vlastností a zavolání funkce draw() pro každý Mesh.

Použití struktury Model bude takto jednoduché:

Model model;
//...
    model = constructModel("models/nanosuit/nanosuit.obj");
    //...
    if (!program.Program || !model.success) {
        return -1;
    }
    //...
    // use model
    //....
    model.destroy(&model);

Funkce constructModel() požádá assimp o vytvoření aiScene z modelu, jehož cestu dostane jako svůj argument. Vytvoří a inicializuje strukturu Model, která bude obsahovat pole struktur Mesh.

Při vytváření struktury Model bude potřeba alokovat dynamicky paměť, proto je na struktuře ještě odkaz na funkci destory(), která všechnu, již nepotřebnou, paměť uvolní.

Pro vykreslení modelu jsem si vytvořil pomocnou funkci drawModel(). Vypadá hodně podobně, jako funkce drawCube() z minulé kapitoly. Volá se ve funkci draw() (v mém příkladu ji volám 3x, protože chci model 3x vykreslit).

        drawModel(ratio, 10.0, 0.0, -20.0, -100.0, &model);
        drawModel(ratio, 10.0, 100.0, -20.0, -100.0, &model);
        drawModel(ratio, 10.0, -100.0, -20.0, -100.0, &model);

Funkce drawModel() je jednoduchá. Vypočte příslušné transformační matice a zavolá funkci draw() ze struktury Model.

 static void drawModel(GLfloat ratio, GLfloat scale, GLfloat x, GLfloat y, GLfloat z, Model *model)
{
        mat4x4 uMVMatrix, uPMatrix, view;
        mat4x4_identity(uMVMatrix);
        mat4x4_identity(uPMatrix);
        mat4x4_perspective(uPMatrix, degToRad(camera.Zoom), ratio, 1.0, 20000.0);
        mat4x4_translate(uMVMatrix, x, y, z);
        mat4x4_scale_aniso(uMVMatrix, uMVMatrix, scale, scale, scale);

        // Create camera transformation
        camera.GetViewMatrix(&camera, view, NULL);
        mat4x4_mul(uMVMatrix, view, uMVMatrix);

        model->draw(model, uMVMatrix, view, uPMatrix);
}

Funkce umožňuje nastavit velikost a pozici modelu. Za domácí úkol ji můžete rozšířit tak, aby umožňovala model i pootočit.

Toto jsou všechny změny potřebné k vykreslování modelů oproti příkladu z minulé kapitoly. Takže můžete vidět modely vykreslené uvnitř cubemap, mezi spoustou malých černých krychliček.

OpenGL program pro model

Model bude přirozeně používat jiný OpenGL program, než se používá pro vykreslení krychliček.

Takto vypadá vertex shader (soubor model.vs):

#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 texCoords;

out vec3 Normal;
out vec3 FragPos;
out vec2 TexCoords;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

void main()
{
        gl_Position = uPMatrix * uMVMatrix * vec4(position, 1.0f);
        FragPos = vec3(uMVMatrix * vec4(position, 1.0f));
        Normal = mat3(transpose(inverse(uMVMatrix))) * normal;
        TexCoords = texCoords;
}

Na něm asi není nic k vysvětlování, nedělá nic, co byste už neviděli v předchozích kapitolách.

Fragment shader se ale trochu rozrostl. Je to tím, že se v něm používají tři textury, několik barev a různá světla. Není zde ale zase nic, co by už nebylo vysvětleno v předchozích kapitolách.

Za zmínku snad jen stojí podmínka if(shininess > 1.0) {. Ta je tam proto, protože některé modely vracejí pro AI_MATKEY_SHININESS 0 (viz načítání modelů dále). A protože se shininess používá jako exponent a cokoliv na nultou je 1, rozzářilo by to model do běla. Hodota snininess má naopak za úkol specular složku světla exponenciálně snižovat, proto, cokoliv menší než 1 nedává smysl …

  1. #version 330 core
  2.  
  3. in vec3 FragPos;
  4. in vec3 Normal;
  5. in vec2 TexCoords;
  6.  
  7. out vec4 color;
  8.  
  9. uniform sampler2D texture_ambient1;
  10. uniform sampler2D texture_diffuse1;
  11. uniform sampler2D texture_specular1;
  12.  
  13. uniform vec4 ambientColor;
  14. uniform vec4 diffuseColor;
  15. uniform vec4 specularColor;
  16. uniform vec4 emissiveColor;
  17. uniform float shininess;
  18. uniform int ambientTexCount;
  19. uniform int diffuseTexCount;
  20. uniform int specularTexCount;
  21.  
  22. uniform vec4 light_ambient;
  23. uniform vec4 light_diffuse;
  24. uniform vec4 light_specular;
  25. uniform vec3 light_position;
  26.  
  27. void main()
  28. {
  29.         // Ambient
  30.         vec4 ambient = light_ambient * (texture(texture_ambient1, TexCoords) + ambientColor);
  31.  
  32.         // Diffuse
  33.         vec4 diffuse;
  34.         vec3 norm = normalize(Normal);
  35.         vec3 lightDir = normalize(light_position - FragPos);
  36.         float diff = max(dot(norm, lightDir), 0.05);
  37.         if (diffuseTexCount == 0) {
  38.                 diffuse =  light_diffuse * diff * diffuseColor;
  39.         }
  40.         else {
  41.                 diffuse =  light_diffuse * diff * texture(texture_diffuse1, TexCoords);
  42.         }
  43.        
  44.         // Specular
  45.         vec4 specular = vec4(0.0,0.0,0.0,0.0);
  46.         vec3 viewDir = -normalize(FragPos);
  47.         vec3 reflectDir = reflect(-lightDir, norm);
  48.         if(shininess > 1.0) {
  49.                 float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);
  50.                 if (specularTexCount == 0) {
  51.                         specular = light_specular * spec * specularColor;
  52.                 }
  53.                 else {
  54.                         specular = light_specular * spec * texture(texture_specular1, TexCoords);
  55.                 }
  56.         }
  57.  
  58.         color = diffuse + specular + emissiveColor;
  59.         if (diffuseTexCount == 0) {
  60.                 color += ambient;
  61.         }
  62. }

V shaderu najdete tři textury, pro ambient, diffuse a a specular složku barvy fragmentu. Najdete tam i barvy pro tyto složky barev. Pokud textura pro danou složku barvy existuje (proměnná *TexCount je větší než 0), použiji barvu z textury, jinak použiji barevnou složku. Jen ambientColor rovnou přičítám k texture_ambient1 a výslednou barvu přičtu jen v případě existence textury. Popravdě, těžko říct, jestli je to ten správný způsob výpočtu výsledné barvy. Obvykle, pokud má mesh nějakou texturu, textura by měla barvu zakrýt. Kombinace textury a barvy nemusí být to, co autor modelu zamýšlel.

Mesh může mít pro každý typ barvy (ambient, duffuse, specular) více než jednu texturu. Moje implementace ale pracuje jen s jednou texturou od každého druhu a další textury, v zájmu zjednodušení příkladu, ignoruje.

V shaderu si také můžete všimnout nového druhu barvy – emissiveColor. Jedná se o barvu, která jakoby vydává sama světlo. Nekombinuje s žádnou složkou světla, takže, bez ohledu na osvětlení, tato (obvykle jasná, zářící) barva se k barvě fragmentu přičte jak je. Ideální pro zářící oči Terminátora :-).

Model, který v příkladu používám (nanosuit), nemá ambient texturu. Proto počítám diff tak, aby bylo nejméně 0.05 a tak i v místech, kam zdroj světla nezasvítí, nebyla jen černočerná tma. Přiznávám, tento fragment shader je tak trochu šitý na míru modelům, na kterých jsem jej testoval.

Sturktura meshe

Níže můžete vidět strukturu Mesh a další dvě pomocné struktury.

Struktura Vertex obsahuje pozici, směr normály a souřadnice textury pro každý vrchol. Jde tedy o vertex data.

Struktura Texture obsahuje id bufferu, do kterého je textura nahraná, dále označení typu textury (aiTextureType_AMBIENT, aiTextureType_DIFFUSE, aiTextureType_SPECULAR) a cestu v souborovém systému k textuře (tato informace je užitečná jen pro ladění).

typedef struct Vertex {
        vec3 Position;
        vec3 Normal;
        vec2 TexCoords;
} Vertex;

typedef struct Texture {
        GLuint id;
        enum aiTextureType type;
        char * path;
} Texture;

typedef struct Mesh {
        GLuint VAO, VBO, EBO;
        Vertex *vertices;
        GLuint *indices;
        Texture *textures;
        size_t numVertices;
        size_t numIndices;
        size_t numTextures;
        bool hasTextures;
        vec4 ambientColor;
        vec4 diffuseColor;
        vec4 specularColor;
        vec4 emissiveColor;
        float shininess;
        unsigned int ambientTexCount;
        unsigned int diffuseTexCount;
        unsigned int specularTexCount;
        void (*Draw)(Mesh *mesh, Model * model);
} Mesh;

Mesh createMesh();
void setupMesh(Mesh * mesh);

Vlastní struktura Mesh obsahuje atributy, které ve většině korespondují s uniform proměnnými z fragment shaderu. Takže, pokud chcete vědět, k čemu se jednotlivé atributy používají, podívejte se na shader.

Struktura Mesh také obsahuje odkaz na metodu Draw. Ta se postará o inicializaci uniform proměnných a zavolání OpenGL funkcí pro vykreslení.

Poslední, co můžete v úryvku kódu vidět, jsou funkce createMesh() a setupMesh(). Ta první vrací strukturu Mesh inicializovanou nějakými defaultními hodnotami. Druhá funkce funguje podobně jako stará známá funkce initBuffers() z předchozích kapitol. Inicialzuje VAO, buffery, nahraje vertex data na grafickou kartu atp.

Sturktura modelu

Struktura Model obsahuje boolean attribut success, do kterého ukládám informaci o (ne)úspěchu nahrání modelu. Dále shader program (sestavený z model.vs a model.fs, viz výše), pole odkazů na meshe a jejich počet. Dále odkaz na funkci, která meshe vykreslí a ještě funkci destroy, která se postará o uvolnění veškeré paměti, která se alokuje během konstrukce modelu.

typedef struct Model {
        bool success;
        Shader program;
        Mesh * meshes;
        size_t numMeshes;
        void(*draw)(Model * model, mat4x4 uMVMatrix, mat4x4 view, mat4x4 uPMatrix);
        void(*destroy)(Model *model);
} Model;

Model constructModel(char *path);

Funkce constructModel() dostane jako argument cestu k modelu a postará se o vytvoření struktury Model, včetně všech meshů.

  1. static size_t meshIndex;
  2. static char directory[MAXLEN];
  3.  
  4. Model constructModel(char * path)
  5. {
  6.         Model model;
  7.         model.success = true;
  8.         model.draw = drawModel;
  9.         model.destroy = destroyModel;
  10.         model.program = shader("model.vs", "model.fs");
  11.         if (!model.program.Program) {
  12.                 model.success = false;
  13.                 return model;
  14.         }
  15.         loadModel(&model, path);
  16.         return model;
  17. }

Jak můžete vidět, hlavní práci odvede funkce loadModel(). Vrátím se k ní za chvilku.

Vykreslení modelu

Funkce drawModel(), která se přiřazuje do struktury Model, nejdříve aktivuje svůj OpenGL program. Pak nastavuje uniform proměnné pro transofrmační matice (které dostane jako své argumenty), jednotlivé složky světla a polohu světla. Poloha světla se nejdříve musí transformovat pomocí view matice, aby neputovalo společně s hráčem.

  1. static void drawModel(Model * model, mat4x4 uMVMatrix, mat4x4 view, mat4x4 uPMatrix) {
  2.         size_t i;
  3.         model->program.Use(&model->program);
  4.         GLuint uMVMatrixLoc = model->program.getUniformLocation(&model->program, "uMVMatrix");
  5.         GLuint uPMatrixLoc = model->program.getUniformLocation(&model->program, "uPMatrix");
  6.         glUniformMatrix4fv(uMVMatrixLoc, 1, GL_FALSE, (const GLfloat *)uMVMatrix);
  7.         glUniformMatrix4fv(uPMatrixLoc, 1, GL_FALSE, (const GLfloat *)uPMatrix);
  8.  
  9.         vec4 lightAmbient = { .5f, .5f, .5f, .5f };
  10.         vec4 lightDiffuse = { 1.0f, 1.0f, 1.0f, 1.0f };
  11.         vec4 lightSpecular = { 1.0f, 1.0f, 1.0f, 1.0f };
  12.         vec4 lightPosition = { 100.f, 100.f, 100.f, 1.0f };
  13.         vec4 lightPositionV;
  14.         mat4x4_mul_vec4(lightPositionV, view, lightPosition);
  15.  
  16.         glUniform4fv(model->program.getUniformLocation(&model->program, "light_ambient"), 1, (const GLfloat *) lightAmbient);
  17.         glUniform4fv(model->program.getUniformLocation(&model->program, "light_diffuse"), 1, (const GLfloat *) lightDiffuse);
  18.         glUniform4fv(model->program.getUniformLocation(&model->program, "light_specular"), 1, (const GLfloat *) lightSpecular);
  19.         glUniform3fv(model->program.getUniformLocation(&model->program, "light_position"), 1, (const GLfloat *) lightPositionV);
  20.  
  21.         for (i = 0; i < model->numMeshes; i++) {
  22.                 model->meshes[i].Draw(&(model->meshes[i]), model);
  23.         }
  24. }

Posledním krokem je jen procyklení pole meshes a zavolání funkce Draw. Tím je model vykreslen.

Konstrukce a vykreslení meshe

O funkci createMesh() jsem se již zmiňoval. Tady ji máte v celé kráse.

  1. Mesh createMesh() {
  2.         Mesh mesh;
  3.         mesh.hasTextures = false;
  4.         mesh.vertices = NULL;
  5.         mesh.indices = NULL;
  6.         mesh.textures = NULL;
  7.         mesh.Draw = drawMesh;
  8.         return mesh;
  9. }

Důležité je nezapomenout zavolat setupMesh() (po inicializaci všech proměnných), který dělá podobné věci, jako initBuffers() v předchozích kapitolách.

  1. void setupMesh(Mesh * mesh) {
  2.         glGenVertexArrays(1, &mesh->VAO);
  3.         glGenBuffers(1, &mesh->VBO);
  4.         glGenBuffers(1, &mesh->EBO);
  5.  
  6.         glBindVertexArray(mesh->VAO);
  7.         glBindBuffer(GL_ARRAY_BUFFER, mesh->VBO);
  8.         glBufferData(GL_ARRAY_BUFFER, mesh->numVertices * sizeof(Vertex), &mesh->vertices[0], GL_STATIC_DRAW);
  9.  
  10.         // Vertex Positions
  11.         glEnableVertexAttribArray(0);
  12.         glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)0);
  13.         // Vertex Normals
  14.         glEnableVertexAttribArray(1);
  15.         glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, Normal));
  16.         // Vertex Texture Coords
  17.         glEnableVertexAttribArray(2);
  18.         glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, TexCoords));
  19.  
  20.         glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh->EBO);
  21.         glBufferData(GL_ELEMENT_ARRAY_BUFFER, mesh->numIndices * sizeof(GLuint), &mesh->indices[0], GL_STATIC_DRAW);
  22.  
  23.         glBindVertexArray(0);
  24. }

Za zmínku zde stojí použití makra offsetof. Jak už víte, funkce glVertexAttribPointer() má jako poslední argument informaci o tom, o kolik se má v bufferu posunout ukazatel při čtení dat pro další vertex v řadě. Do bufferu VBO je nahrané pole struktur typu Vertex. Makro offsetof dostává jako první argument jméno struktury a jako druhý argument jméno atributu této struktury. Jako výsledek vrací o kolik bajtů je atribut posunut od začátku struktury. Díky tomu, i kdybyste přidali do struktury Vertex nějaké další položky, nebudete muset v tomto kódu nic měnit. Makro offset vždy vrátí správný počet bajtů předcházející daný atribut.

Na vykreslení meshe také není nic záhadného. Nastaví se uniform proměnné, přibinduje se VAO (inicializované v setupMesh()) a zavolá se glDrawElemenets().

  1. void drawMesh(Mesh *mesh, Model *model)
  2. {
  3.         size_t i = 0;
  4.         Shader *program = &(model->program);
  5.  
  6.         GLuint texIx = 0;
  7.         for (i = 0; i < mesh->numTextures; i++)
  8.         {
  9.                 switch (mesh->textures[i].type)
  10.                 {
  11.                 case aiTextureType_AMBIENT:
  12.                         glUniform1i(program->getUniformLocation(program, "texture_ambient1"), texIx);
  13.                         break;
  14.                 case aiTextureType_DIFFUSE:
  15.                         glUniform1i(program->getUniformLocation(program, "texture_diffuse1"), texIx);
  16.                         break;
  17.                 case aiTextureType_SPECULAR:
  18.                         glUniform1i(program->getUniformLocation(program, "texture_specular1"), texIx);
  19.                         break;
  20.                 default:
  21.                         continue;
  22.                 }
  23.                 glActiveTexture(GL_TEXTURE0 + texIx);
  24.                 glBindTexture(GL_TEXTURE_2D, mesh->textures[i].id);
  25.                 texIx++;
  26.         }
  27.  
  28.         glUniform4fv(program->getUniformLocation(program, "ambientColor"), 1, mesh->ambientColor);
  29.         glUniform4fv(program->getUniformLocation(program, "diffuseColor"), 1, mesh->diffuseColor);
  30.         glUniform4fv(program->getUniformLocation(program, "specularColor"), 1, mesh->specularColor);
  31.         glUniform4fv(program->getUniformLocation(program, "emissiveColor"), 1, mesh->emissiveColor);
  32.         glUniform1f(program->getUniformLocation(program, "shininess"), mesh->shininess);
  33.         glUniform1i(program->getUniformLocation(program, "ambientTexCount"), mesh->ambientTexCount);
  34.         glUniform1i(program->getUniformLocation(program, "diffuseTexCount"), mesh->diffuseTexCount);
  35.         glUniform1i(program->getUniformLocation(program, "specularTexCount"), mesh->specularTexCount);
  36.  
  37.         glBindVertexArray(mesh->VAO);
  38.         glDrawElements(GL_TRIANGLES, mesh->numIndices, GL_UNSIGNED_INT, 0);
  39.         glBindVertexArray(0);
  40. }

Model, který se snaží naše struktura Model načíst, může obsahovat několik druhů textur. A co víc, pro každý druh textury může mít několik textur. Příklad, který jsem napsal, počítá ale s tím, že pro každý druh textury existuje jen jedna textura.

Pokud byste chtěli rozšířit příklad tak, aby pracoval s více texturami, museli byste upravit fragment shader tak, abyste měli více uniform proměnných pro textury ("texture_ambient1", "texture_ambient2", …). Dále byste musetli upravit předchozí funkci tak, aby tyto uniform proměnné správně inicializovala. A museli byste upravit i logiku fragment shaderu, aby s více texturami pracoval. K tomu vám mohou pomoci uniform proměnné, které obsahují informaci o počtu textur (amibentTexCount, diffuseTexCount, specularTexCount).

Nanosuit

Nanosuit - drátový model

Drátový model zapnete zavoláním glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);.

Načtení modelu

Teď se dostáváme k nejdůležitější části této kapitoly, která se stará o vytvoření a parsování assimp struktury. Podívejte se nejdříve na funkci loadModel().

O vytvoření struktury aiScene se postará funkce aiImportFile(), kterou jsem již popisoval výše. Funkce getMeshesCount() projde rekurzivně celou strukturu (od mRootNode přes všechny mChildren) a spočítá, kolik obsahuje scéna meshů. Následně se pro meshe alokuje paměť. Dále se do globální proměnné directory uloží adresář, ve kterém je model (bude se hodit při načítání obrázků textur). Pak už se jen zavolá processNode(), která znovu projde rekurzivně všechny nódy (počínaje mRootNode) a zkonstruuje meshe. Nakonec se ještě uvolní paměť alokovaná pro scénu scene, protože všechny potřebné informace budou již uložené v novém modelu.

  1. static void loadModel(Model * model, char *path)
  2. {
  3.         const struct aiScene* scene = aiImportFile(path, aiProcess_FlipUVs | aiProcess_Triangulate);
  4.         if (!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
  5.                 fprintf(stderr, "aiImportFile error: %s\n", aiGetErrorString());
  6.                 model->success = false;
  7.                 return;
  8.         }
  9.         model->numMeshes = getMeshesCount(scene->mRootNode);
  10.         model->meshes = malloc(sizeof(Mesh) * model->numMeshes);
  11.  
  12.         strncpy(directory, path, MAXLEN);
  13.         char * sep = strrchr(directory, '/');
  14.         *(sep + 1) = '\0';
  15.         printf("Directory:\t\t\t%s\n", directory);
  16.  
  17.         meshIndex = 0;
  18.         printf("Count of meshes/lights:\t\t%lu %i\n", model->numMeshes, scene->mNumLights);
  19.         printf("Node processing:\t\tstart\n");
  20.         processNode(model, scene->mRootNode, scene);
  21.         printf("Node processing:\t\tdone\n");
  22.  
  23.         aiReleaseImport(scene);
  24. }
  25.  
  26. size_t getMeshesCount(const struct aiNode const *  node) {
  27.         size_t count = node->mNumMeshes;
  28.         for (GLuint i = 0; i < node->mNumChildren; i++)
  29.         {
  30.                 count += getMeshesCount(node->mChildren[i]);
  31.         }
  32.         return count;
  33. }

Struktura aiNode obsahuje pole indexů do atributu mMeshes struktury aiScene v atributu mMeshes. Funkce processNode() prochází tyto indexy a na příslužné meshe zavolá funkci processMesh(), která z assimp struktury aiMesh vytvoří naší strutkuru Mesh.

Následně aplikuje rekurzivně sebe sama na všechny potomky zpracovávaného uzlu.

  1. static void processNode(Model *model, struct aiNode* node, const struct aiScene* scene)
  2. {
  3.         const struct aiMesh* aiMesh;
  4.         static unsigned mashCounter = 0;
  5.         Mesh mesh;
  6.  
  7.         for (GLuint i = 0; i < node->mNumMeshes; i++)
  8.         {
  9.                 aiMesh = scene->mMeshes[node->mMeshes[i]];
  10.                 printf("Processing mesh num.:\t\t%u\n", mashCounter++);
  11.                 mesh = processMesh(model, aiMesh, scene);
  12.                 model->meshes[meshIndex++] = mesh;
  13.         }
  14.         for (GLuint i = 0; i < node->mNumChildren; i++)         {
  15.                 processNode(model, node->mChildren[i], scene);
  16.         }
  17. }

Nejvíce práce odvede funkce processMesh(). Z aiMesh získává souřadnice vrcholů, normály (pokud existují), souřadnice textur, jednotlivé složky barev a textury. Funkce využívá atributy, které jsou zobrazené na UML diagramu na začátku kapitoly. A také funkce pro získání dat z struktury aiMaterial (přesněji řečeno to nejsou funkce, ale jde o makra).

Když jsme u toho materiálu, pokud se podíváte na oficiální dokumentaci k Material System, uvidíte, že z materiálu se dá dostat mnohem více informací, než o které se zajímá processMesh(). Existuje tu tak ještě velký prostor pro vylepšování :-).

  1. static Mesh processMesh(Model * model, const struct aiMesh* aiMesh, const struct aiScene* scene)
  2. {
  3.         unsigned int i;
  4.         Mesh mesh = createMesh();
  5.         // 1. vertices
  6.         mesh.vertices = malloc(sizeof(Vertex)* aiMesh->mNumVertices);
  7.         mesh.numVertices = aiMesh->mNumVertices;
  8.  
  9.         for (i = 0; i < aiMesh->mNumVertices; i++)
  10.         {
  11.                 Vertex vertex;
  12.                 vertex.Position[0] = aiMesh->mVertices[i].x;
  13.                 vertex.Position[1] = aiMesh->mVertices[i].y;
  14.                 vertex.Position[2] = aiMesh->mVertices[i].z;
  15.                 if (aiMesh->mNormals != NULL) {
  16.                         vertex.Normal[0] = aiMesh->mNormals[i].x;
  17.                         vertex.Normal[1] = aiMesh->mNormals[i].y;
  18.                         vertex.Normal[2] = aiMesh->mNormals[i].z;
  19.                 } else {
  20.                         fprintf(stderr,"No normals found!\n");
  21.                         vertex.Normal[0] = 0.0;
  22.                         vertex.Normal[1] = 0.0;
  23.                         vertex.Normal[2] = 1.0;
  24.                 }
  25.  
  26.                 if (aiMesh->mTextureCoords[0])
  27.                 {
  28.                         vertex.TexCoords[0] = aiMesh->mTextureCoords[0][i].x;
  29.                         vertex.TexCoords[1] = aiMesh->mTextureCoords[0][i].y;
  30.                 }
  31.                 mesh.vertices[i] = vertex;
  32.         }
  33.         // 2. indices
  34.         mesh.numIndices = getIndicesCount(aiMesh);
  35.         mesh.indices = malloc(sizeof(GLuint) * mesh.numIndices);
  36.         size_t ix = 0;
  37.         for (i = 0; i < aiMesh->mNumFaces; i++)
  38.         {
  39.                 struct aiFace face = aiMesh->mFaces[i];
  40.                 for (GLuint j = 0; j < face.mNumIndices; j++) {
  41.                         mesh.indices[ix++] = face.mIndices[j];
  42.                 }
  43.         }
  44.  
  45.         // 3. materilal
  46.         struct aiMaterial * material = scene->mMaterials[aiMesh->mMaterialIndex];
  47.  
  48.         // 3.1 colors
  49.         const struct aiColor4D _def_color = { 0.0, 0.0, 0.0, 0.0 };
  50.         struct aiColor4D _color = { 0.0, 0.0, 0.0, 1.0 };
  51.         if (AI_SUCCESS == aiGetMaterialColor(material, AI_MATKEY_COLOR_AMBIENT, &_color)) {
  52.                 //printf("ambient = { %.2f, %.2f, %.2f, %.2f }, ",_color.r,_color.g,_color.b,_color.a);
  53.         }
  54.         mesh.ambientColor[0] = _color.r;
  55.         mesh.ambientColor[1] = _color.g;
  56.         mesh.ambientColor[2] = _color.b;
  57.         mesh.ambientColor[3] = _color.a;
  58.  
  59.         _color = _def_color;
  60.         if (AI_SUCCESS == aiGetMaterialColor(material, AI_MATKEY_COLOR_DIFFUSE, &_color)) {
  61.                 //printf("diffuse = { %.2f, %.2f, %.2f, %.2f }, ",_color.r,_color.g,_color.b,_color.a);
  62.         }
  63.         mesh.diffuseColor[0] = _color.r;
  64.         mesh.diffuseColor[1] = _color.g;
  65.         mesh.diffuseColor[2] = _color.b;
  66.         mesh.diffuseColor[3] = _color.a;
  67.  
  68.         _color = _def_color;
  69.         if (AI_SUCCESS == aiGetMaterialColor(material, AI_MATKEY_COLOR_SPECULAR, &_color)) {
  70.                 //printf("specular = { %.2f, %.2f, %.2f, %.2f }, ", _color.r, _color.g, _color.b, _color.a);
  71.         }
  72.         mesh.specularColor[0] = _color.r;
  73.         mesh.specularColor[1] = _color.g;
  74.         mesh.specularColor[2] = _color.b;
  75.         mesh.specularColor[3] = _color.a;
  76.  
  77.         _color = _def_color;
  78.         if (AI_SUCCESS == aiGetMaterialColor(material, AI_MATKEY_COLOR_EMISSIVE, &_color)) {
  79.                 //printf("emissive = { %.2f, %.2f, %.2f, %.2f } ", _color.r, _color.g, _color.b, _color.a);
  80.         }
  81.         mesh.emissiveColor[0] = _color.r;
  82.         mesh.emissiveColor[1] = _color.g;
  83.         mesh.emissiveColor[2] = _color.b;
  84.         mesh.emissiveColor[3] = _color.a;
  85.         //printf("\n");
  86.  
  87.         float opacity;
  88.         if (AI_SUCCESS == aiGetMaterialFloatArray(material, AI_MATKEY_OPACITY, &opacity, NULL)) {
  89.                 mesh.ambientColor[3] = opacity;
  90.                 mesh.diffuseColor[3] = opacity;
  91.                 mesh.specularColor[3] = opacity;
  92.         }
  93.         mesh.shininess = 0.0f;
  94.         aiGetMaterialFloatArray(material, AI_MATKEY_SHININESS, &(mesh.shininess), NULL);
  95.  
  96.         // 3.2 textures
  97.         mesh.ambientTexCount = aiGetMaterialTextureCount(material, aiTextureType_AMBIENT);
  98.         mesh.diffuseTexCount = aiGetMaterialTextureCount(material, aiTextureType_DIFFUSE);
  99.         mesh.specularTexCount = aiGetMaterialTextureCount(material, aiTextureType_SPECULAR);
  100.         mesh.numTextures = 3;
  101.         mesh.textures = malloc(sizeof(Texture) * mesh.numTextures);
  102.  
  103.         loadMaterialTextures(material, aiTextureType_AMBIENT, &(mesh.textures[0]));
  104.         loadMaterialTextures(material, aiTextureType_DIFFUSE, &(mesh.textures[1]));
  105.         loadMaterialTextures(material, aiTextureType_SPECULAR, &(mesh.textures[2]));
  106.  
  107.         setupMesh(&mesh);
  108.         return mesh;
  109. }

Funkce processMesh() využívá následující funkci pro získání textury. Všimněte si, jak využívá globální proměnné directory pro sestavení cesty k obrázku s texturou.

  1. static void loadMaterialTextures(struct aiMaterial * material, enum aiTextureType type, Texture *textures)
  2. {
  3.         struct aiString str;
  4.         char path[MAXLEN];
  5.         Texture texture;
  6.         unsigned int texCount = aiGetMaterialTextureCount(material, type);
  7.         if (!texCount) {
  8.                 texture.id = 0;
  9.                 texture.type = aiTextureType_NONE;
  10.                 texture.path = NULL;
  11.                 *textures = texture;
  12.                 return;
  13.         }
  14.         aiGetMaterialTexture(material, type, 0, &str, NULL, NULL, NULL, NULL, NULL, NULL);
  15.         strncpy(path, directory, MAXLEN);
  16.         strncat(path, str.data, MAXLEN);
  17.         printf("Load texture:\t\t\t%s\n", path);
  18.         texture.type = type;
  19.         texture.path = malloc(sizeof(char) * (strlen(path) + 1));
  20.         strcpy(texture.path, path);
  21.         texture.id = textureFromFile(path);
  22.         *textures = texture;
  23. }

Tady je třeba říct, že některé modely mají uloženou cestu k obrázkům absélutně (tedy špatně). U .obj formátu se to dá opravit snadno. Otevřete si v nějakém textovém editoru soubor nazev-modelu.mtl a změňte cestu k textuře na relativní (odstrantě z cesty vše kromě jména obrázku).

Další pomocná funkce vytváří buffer pro texturu a nahrává do něj obrázek:

  1. static GLuint textureFromFile(char * path)
  2. {
  3.         GLuint textureID;
  4.         glGenTextures(1, &textureID);
  5.         int width, height, n;
  6.  
  7.         unsigned char * image = sx_stbi_load(path, &width, &height, &n, 0);
  8.         glBindTexture(GL_TEXTURE_2D, textureID);
  9.         if (n == 4) {
  10.                 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
  11.         }
  12.         else {
  13.                 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
  14.         }
  15.         sx_stbi_image_free(image);
  16.  
  17.  
  18.         // Parameters
  19.         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  20.         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  21.         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  22.         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  23.         glBindTexture(GL_TEXTURE_2D, 0);
  24.  
  25.         return textureID;
  26. }

Poslední pomocná funkce prochází rekurzivně mFaces, aby dopředu zjistila, kolik bude potřeba alokovat paměti pro indexy vrcholů.

  1. static size_t getIndicesCount(const struct aiMesh * aiMesh)
  2. {
  3.         unsigned int i;
  4.         unsigned int num = aiMesh->mNumFaces;
  5.         size_t count = 0;
  6.         for (i = 0; i < num; i++)
  7.         {
  8.                 struct aiFace face = aiMesh->mFaces[i];
  9.                 count += face.mNumIndices;
  10.         }
  11.         return count;
  12. }

Na závěr je tu ještě funkce, která se stará o uvolnění veškeré alokované paměti během nahrávání modelu.

  1. static void destroyModel(Model * model)
  2. {
  3.         for (unsigned int i = 0; i < model->numMeshes; i++) {
  4.                 free(model->meshes[i].vertices);
  5.                 free(model->meshes[i].indices);
  6.                 if (model->meshes[i].textures != NULL) {
  7.                         free(model->meshes[i].textures[0].path);
  8.                         free(model->meshes[i].textures[1].path);
  9.                         free(model->meshes[i].textures[2].path);
  10.                 }
  11.                 free(model->meshes[i].textures);
  12.         }
  13.         free(model->meshes);
  14. }

Závěr

Assimp není knihovna zaměřená na rychlost. Počítejte s tím, že větší modely se načítají řádově v půlminutách. A pokud spouštíte program s assimpem z Visual Studia, které si do kódu kvůli ladění přidává spoustu dalších instrukcí, počítejte s tím, že nějaký model můžete nahrávat i 10 minut (nebojte, pokud spustíte přeložený program bez Visual Stuida, bude několikanásobně rychlejší).

Pokud jste to vydrželi pročíst celé až sem, tak vám gratuluji k trpělivosti. Získali jste jednoduchý nástroj pro zobrazování modelů, který byste měli být schopni rozšiřovat a přizpůsobit si k obrazu svému.

Komentář Hlášení chyby
Created: 24.12.2016
Last updated: 29.11.2017
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..