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í:
- 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/
- 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).
- 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.
-
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říkazmake
a případně ještě jako rootmake install; ldconfig
. Tím máte v Linuxu hotovo. - Ve Windows spusťte grafické rozhraní (viz první obrázek níže).
- 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/).
- 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.
- 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í.
- Předpokládám, že používáte Visual Studio. Pak tedy otevřete vygenerovaný projekt poklepáním na Assimp.sln).
-
Z menu vyberte
BUILD → Build Solution
a zajděte si na kafe.
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/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é:
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, 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
.
{
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):
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 …
- #version 330 core
- in vec3 FragPos;
- in vec3 Normal;
- in vec2 TexCoords;
- out vec4 color;
- uniform sampler2D texture_ambient1;
- uniform sampler2D texture_diffuse1;
- uniform sampler2D texture_specular1;
- uniform vec4 ambientColor;
- uniform vec4 diffuseColor;
- uniform vec4 specularColor;
- uniform vec4 emissiveColor;
- uniform float shininess;
- uniform int ambientTexCount;
- uniform int diffuseTexCount;
- uniform int specularTexCount;
- uniform vec4 light_ambient;
- uniform vec4 light_diffuse;
- uniform vec4 light_specular;
- uniform vec3 light_position;
- {
- // Ambient
- vec4 ambient = light_ambient * (texture(texture_ambient1, TexCoords) + ambientColor);
- // Diffuse
- vec4 diffuse;
- vec3 norm = normalize(Normal);
- vec3 lightDir = normalize(light_position - FragPos);
- diffuse = light_diffuse * diff * diffuseColor;
- }
- else {
- diffuse = light_diffuse * diff * texture(texture_diffuse1, TexCoords);
- }
- // Specular
- vec4 specular = vec4(0.0,0.0,0.0,0.0);
- vec3 viewDir = -normalize(FragPos);
- vec3 reflectDir = reflect(-lightDir, norm);
- specular = light_specular * spec * specularColor;
- }
- else {
- specular = light_specular * spec * texture(texture_specular1, TexCoords);
- }
- }
- color = diffuse + specular + emissiveColor;
- color += ambient;
- }
- }
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.
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í).
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.
Funkce constructModel()
dostane jako argument cestu
k modelu a postará se o vytvoření struktury Model
, včetně
všech meshů.
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.
- size_t i;
- model->program.Use(&model->program);
- GLuint uMVMatrixLoc = model->program.getUniformLocation(&model->program, "uMVMatrix");
- GLuint uPMatrixLoc = model->program.getUniformLocation(&model->program, "uPMatrix");
- vec4 lightAmbient = { .5f, .5f, .5f, .5f };
- vec4 lightDiffuse = { 1.0f, 1.0f, 1.0f, 1.0f };
- vec4 lightSpecular = { 1.0f, 1.0f, 1.0f, 1.0f };
- vec4 lightPosition = { 100.f, 100.f, 100.f, 1.0f };
- vec4 lightPositionV;
- mat4x4_mul_vec4(lightPositionV, view, lightPosition);
- glUniform4fv(model->program.getUniformLocation(&model->program, "light_ambient"), 1, (const GLfloat *) lightAmbient);
- glUniform4fv(model->program.getUniformLocation(&model->program, "light_diffuse"), 1, (const GLfloat *) lightDiffuse);
- glUniform4fv(model->program.getUniformLocation(&model->program, "light_specular"), 1, (const GLfloat *) lightSpecular);
- glUniform3fv(model->program.getUniformLocation(&model->program, "light_position"), 1, (const GLfloat *) lightPositionV);
- model->meshes[i].Draw(&(model->meshes[i]), model);
- }
- }
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.
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.
- glGenVertexArrays(1, &mesh->VAO);
- glGenBuffers(1, &mesh->VBO);
- glGenBuffers(1, &mesh->EBO);
- glBindVertexArray(mesh->VAO);
- glBindBuffer(GL_ARRAY_BUFFER, mesh->VBO);
- glBufferData(GL_ARRAY_BUFFER, mesh->numVertices * sizeof(Vertex), &mesh->vertices[0], GL_STATIC_DRAW);
- // Vertex Positions
- glEnableVertexAttribArray(0);
- // Vertex Normals
- glEnableVertexAttribArray(1);
- // Vertex Texture Coords
- glEnableVertexAttribArray(2);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh->EBO);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, mesh->numIndices * sizeof(GLuint), &mesh->indices[0], GL_STATIC_DRAW);
- glBindVertexArray(0);
- }
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()
.
- {
- Shader *program = &(model->program);
- GLuint texIx = 0;
- {
- {
- case aiTextureType_AMBIENT:
- glUniform1i(program->getUniformLocation(program, "texture_ambient1"), texIx);
- case aiTextureType_DIFFUSE:
- glUniform1i(program->getUniformLocation(program, "texture_diffuse1"), texIx);
- case aiTextureType_SPECULAR:
- glUniform1i(program->getUniformLocation(program, "texture_specular1"), texIx);
- }
- glActiveTexture(GL_TEXTURE0 + texIx);
- glBindTexture(GL_TEXTURE_2D, mesh->textures[i].id);
- texIx++;
- }
- glUniform4fv(program->getUniformLocation(program, "ambientColor"), 1, mesh->ambientColor);
- glUniform4fv(program->getUniformLocation(program, "diffuseColor"), 1, mesh->diffuseColor);
- glUniform4fv(program->getUniformLocation(program, "specularColor"), 1, mesh->specularColor);
- glUniform4fv(program->getUniformLocation(program, "emissiveColor"), 1, mesh->emissiveColor);
- glUniform1f(program->getUniformLocation(program, "shininess"), mesh->shininess);
- glUniform1i(program->getUniformLocation(program, "ambientTexCount"), mesh->ambientTexCount);
- glUniform1i(program->getUniformLocation(program, "diffuseTexCount"), mesh->diffuseTexCount);
- glUniform1i(program->getUniformLocation(program, "specularTexCount"), mesh->specularTexCount);
- glBindVertexArray(mesh->VAO);
- glDrawElements(GL_TRIANGLES, mesh->numIndices, GL_UNSIGNED_INT, 0);
- glBindVertexArray(0);
- }
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
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.
- {
- }
- model->numMeshes = getMeshesCount(scene->mRootNode);
- *(sep + 1) = '\0';
- meshIndex = 0;
- processNode(model, scene->mRootNode, scene);
- aiReleaseImport(scene);
- }
- {
- count += getMeshesCount(node->mChildren[i]);
- }
- return count;
- }
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.
- {
- Mesh mesh;
- {
- aiMesh = scene->mMeshes[node->mMeshes[i]];
- mesh = processMesh(model, aiMesh, scene);
- model->meshes[meshIndex++] = mesh;
- }
- processNode(model, node->mChildren[i], scene);
- }
- }
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í :-).
- {
- Mesh mesh = createMesh();
- // 1. vertices
- mesh.numVertices = aiMesh->mNumVertices;
- {
- Vertex vertex;
- vertex.Position[0] = aiMesh->mVertices[i].x;
- vertex.Position[1] = aiMesh->mVertices[i].y;
- vertex.Position[2] = aiMesh->mVertices[i].z;
- vertex.Normal[0] = aiMesh->mNormals[i].x;
- vertex.Normal[1] = aiMesh->mNormals[i].y;
- vertex.Normal[2] = aiMesh->mNormals[i].z;
- vertex.Normal[0] = 0.0;
- vertex.Normal[1] = 0.0;
- vertex.Normal[2] = 1.0;
- }
- {
- vertex.TexCoords[0] = aiMesh->mTextureCoords[0][i].x;
- vertex.TexCoords[1] = aiMesh->mTextureCoords[0][i].y;
- }
- mesh.vertices[i] = vertex;
- }
- // 2. indices
- mesh.numIndices = getIndicesCount(aiMesh);
- {
- mesh.indices[ix++] = face.mIndices[j];
- }
- }
- // 3. materilal
- // 3.1 colors
- //printf("ambient = { %.2f, %.2f, %.2f, %.2f }, ",_color.r,_color.g,_color.b,_color.a);
- }
- mesh.ambientColor[0] = _color.r;
- mesh.ambientColor[1] = _color.g;
- mesh.ambientColor[2] = _color.b;
- mesh.ambientColor[3] = _color.a;
- _color = _def_color;
- //printf("diffuse = { %.2f, %.2f, %.2f, %.2f }, ",_color.r,_color.g,_color.b,_color.a);
- }
- mesh.diffuseColor[0] = _color.r;
- mesh.diffuseColor[1] = _color.g;
- mesh.diffuseColor[2] = _color.b;
- mesh.diffuseColor[3] = _color.a;
- _color = _def_color;
- //printf("specular = { %.2f, %.2f, %.2f, %.2f }, ", _color.r, _color.g, _color.b, _color.a);
- }
- mesh.specularColor[0] = _color.r;
- mesh.specularColor[1] = _color.g;
- mesh.specularColor[2] = _color.b;
- mesh.specularColor[3] = _color.a;
- _color = _def_color;
- //printf("emissive = { %.2f, %.2f, %.2f, %.2f } ", _color.r, _color.g, _color.b, _color.a);
- }
- mesh.emissiveColor[0] = _color.r;
- mesh.emissiveColor[1] = _color.g;
- mesh.emissiveColor[2] = _color.b;
- mesh.emissiveColor[3] = _color.a;
- //printf("\n");
- float opacity;
- mesh.ambientColor[3] = opacity;
- mesh.diffuseColor[3] = opacity;
- mesh.specularColor[3] = opacity;
- }
- mesh.shininess = 0.0f;
- aiGetMaterialFloatArray(material, AI_MATKEY_SHININESS, &(mesh.shininess), NULL);
- // 3.2 textures
- mesh.ambientTexCount = aiGetMaterialTextureCount(material, aiTextureType_AMBIENT);
- mesh.diffuseTexCount = aiGetMaterialTextureCount(material, aiTextureType_DIFFUSE);
- mesh.specularTexCount = aiGetMaterialTextureCount(material, aiTextureType_SPECULAR);
- mesh.numTextures = 3;
- loadMaterialTextures(material, aiTextureType_AMBIENT, &(mesh.textures[0]));
- loadMaterialTextures(material, aiTextureType_DIFFUSE, &(mesh.textures[1]));
- loadMaterialTextures(material, aiTextureType_SPECULAR, &(mesh.textures[2]));
- setupMesh(&mesh);
- return mesh;
- }
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.
- {
- struct aiString str;
- Texture texture;
- texture.id = 0;
- texture.type = aiTextureType_NONE;
- texture.path = NULL;
- *textures = texture;
- }
- aiGetMaterialTexture(material, type, 0, &str, NULL, NULL, NULL, NULL, NULL, NULL);
- texture.type = type;
- texture.id = textureFromFile(path);
- *textures = texture;
- }
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:
- {
- GLuint textureID;
- glGenTextures(1, &textureID);
- glBindTexture(GL_TEXTURE_2D, textureID);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
- }
- else {
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
- }
- sx_stbi_image_free(image);
- // Parameters
- 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);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glBindTexture(GL_TEXTURE_2D, 0);
- return textureID;
- }
Poslední pomocná funkce prochází rekurzivně mFaces
, aby
dopředu zjistila, kolik bude potřeba alokovat paměti pro indexy vrcholů.
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.
- {
- }
- }
- }
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.