Lekce 31 - Nahrávání a renderování modelů

Další skvělý tutoriál! Naučíte se, jak nahrát a zobrazit otexturovaný Milkshape3D model. Nezdá se to, ale asi nejvíce se budou hodit znalosti o práci s dynamickou pamětí a jejím kopírování z jednoho místa na druhé.

Zdrojový kód tohoto projektu byl vyjmut z PortaLib3D, knihovny, kterou jsem napsal, abych lidem umožnil zobrazovat modely za použití velmi malého množství dalšího kódu. Abyste se na ni mohli opravdu spolehnout musíte nejdříve vědět, co dělá a jak pracuje.

Část PortaLib3D, uvedená zde, si stále zachovává můj copyright. To neznamená, že ji nesmíte používat, ale že při vložení kódu do svého projektu musíte uvést náležitý credit. To je vše - žádné velké nároky. Pokud byste chtěli číst, pochopit a re-implementovat celý kód (žádné kopírovat vložit!), budete uvolněni ze své povinnosti. Pak je to váš výtvor. Pojďme se ale podívat na něco zajímavějšího.

Model, který používáme v tomto projektu, pochází z Milkshape3D. Je to opravdu kvalitní balík pro modelování, který zahrnuje vlastní file-formát. Mým dalším plánem je implementovat Anim8or (http://www.anim8or.com/), souborový reader. Je free a umí číst samozřejmě i 3DS. Nicméně formát souboru není tím hlavním pro loading modelů. Nejdříve se musí vytvořit vlastní struktury, které jsou schopny pojmout data.

První ze všeho deklarujeme obecnou třídu Model, která je kontejnerem pro všechna data.

class Model// Obecné úložiště dat (abstraktní třída)

{

public:

Ze všeho nejdůležitější jsou samozřejmě vertexy. Pole tří desetinných hodnot m_location reprezentuje jednotlivé x, y, z souřadnice. Proměnnou m_boneID budeme v tomto tutoriálu ignorovat. Její čas přijde až v dalším při kosterní animaci.

struct Vertex// Struktura vertexu

{

float m_location[3];// X, y, z souřadnice

char m_boneID;// Pro skeletální animaci

};

Všechny vertexy potřebujeme seskupit do trojúhelníků. Pole m_vertexIndices obsahuje tři indexy do pole vertexů. Touto cestou bude každý vertex uložen v paměti pouze jednou. V polích m_s a m_t jsou texturové koordináty každého vrcholu. Poslední atribut definuje tři normálové vektory pro světlo.

struct Triangle// Struktura trojúhelníku

{

int m_vertexIndices[3];// Tři indexy do pole vertexů

float m_s[3], m_t[3];// Texturové koordináty

float m_vertexNormals[3][3];// Tři normálové vektory

};

Další struktura popisuje mesh modelu. Mesh je skupina trojúhelníků, na které je aplikován stejný materiál a textura. Skupiny meshů dohromady tvoří celý model. Stejně jako trojúhelníky obsahovaly pouze indexy na vertexy, budou i meshe obsahovat pouze indexy na trojúhelníky. Protože neznáme jejich přesný počet, musí být pole dynamické. Třetí proměnná je opět indexem, tentokrát do materiálů (textura, osvětlení).

struct Mesh//Mesh modelu

{

int *m_pTriangleIndices;// Indexy do trojúhelníků

int m_numTriangles;// Počet trojúhelníků

int m_materialIndex;// Index do materiálů

};

Ve struktuře Material jsou uložené standardní koeficienty světla, ve stejném formátu jako používá OpenGL: okolní (ambient), rozptýlené (diffuse), odražené (specular), vyzařující (emissive) a lesklost (shininess). dále obsahuje objekt textury a souborovou cestu k textuře, aby mohla být znovu nahrána, když je ukončen kontext OpenGL.

struct Material// Vlastnosti materiálů

{

float m_ambient[4], m_diffuse[4], m_specular[4], m_emissive[4];// Reakce materiálu na světlo

float m_shininess;// Lesk materiálu

GLuint m_texture;// Textura

char *m_pTextureFilename;// Souborová cesta k textuře

};

Vytvoříme proměnné právě napsaných struktur ve formě ukazatelů na dynamická pole, jejichž paměť alokuje funkce pro loading objektů. Musíme samozřejmě ukládat i velikost polí.

protected:

int m_numVertices;// Počet vertexů

Vertex *m_pVertices;// Dynamické pole vertexů

int m_numTriangles;// Počet trojúhelníků

Triangle *m_pTriangles;// Dynamické pole trojúhelníků

int m_numMeshes;// Počet meshů

Mesh *m_pMeshes;// Dynamické pole meshů

int m_numMaterials;// Počet materiálů

Material *m_pMaterials;// Dynamické pole materiálů

A konečně metody třídy. Virtuální členská funkce loadModelData() má za úkol nahrát data ze souboru. Přiřadíme jí nulu, aby nemohl být vytvořen objekt třídy (abstraktní třída). Tato třída je zamýšlena pouze jako úložiště dat. Všechny operace pro nahrávání mají na starosti odvozené třídy, kdy každá z nich umí svůj vlastní formát souboru. Celá hierarchie je více obecná.

public:

Model();// Konstruktor

virtual ~Model();// Destruktor

virtual bool loadModelData(const char *filename) = 0;// Loading objektu ze souboru

Metoda reloadTextures() slouží pro loading textur a jejich znovunahrávání, když se ztratí kontext OpenGL (např. při přepnutí z/do fullscreenu). Draw() vykresluje objekt. Tato funkce nemusí být virtuální, protože defakto známe všechny potřebné informace o struktuře objektu (vertexy, trojúhelníky...).

void reloadTextures();// Znovunahrání textur

void draw();// Vykreslení objektu

};

Od třídy Model podědíme třídu MilkshapeModel. Přepíšeme v ní metodu loadModelData().

class MilkshapeModel : public Model

{

public:

MilkshapeModel();// Konstruktor

virtual ~MilkshapeModel();// Destruktor

virtual bool loadModelData(const char *filename);// Loading objektu ze souboru

};

Nyní nahrávání objektů. Přepíšeme virtuální funkci loadModelData() abstraktní třídy Model tak, aby ve třídě MilkShapeModel nahrávala data ze souboru ve formátu Milkshape3D. Předáváme jí řetězec se jménem souboru. Pokud vše proběhne v pořádku, funkce nastaví datové struktury a vrátí true.

bool MilkshapeModel::loadModelData(const char *filename)

{

Soubor otevřeme jako vstupní (ios::in), binární (ios::binary) a nebudeme ho vytvářet (ios::nocreate). Pokud nebyl nalezen vrátí funkce false, aby indikovala error.

ifstream inputFile(filename, ios::in | ios::binary | ios::nocreate);// Otevření souboru

if (inputFile.fail())// Podařilo se ho otevřít?

return false;

Zjistíme velikost souboru v bytech a potom ho celý načteme do pomocného bufferu pBuffer.

// Velikost souboru

inputFile.seekg(0, ios::end);

long fileSize = inputFile.tellg();

inputFile.seekg(0, ios::beg);

byte *pBuffer = new byte[fileSize];// Alokace paměti pro kopii souboru

inputFile.read(pBuffer, fileSize);// Vytvoření paměťové kopie souboru

inputFile.close();// Zavření souboru

Deklarujeme pomocný ukazatel pPtr, který ihned inicializujeme tak, aby ukazoval na stejné místo jako pBuffer, tedy na začátek paměti. Do hlavičky souboru pHeader uložíme adresu hlavičky a zvětšíme adresu v pPtr o velikost hlavičky.

Pozn.: Strukturu hlavičky a jí podobné jsem na začátku tutoriálu neuváděl, protože je budeme používat jenom zde, v této funkci. Pokud vás přeci zajímají, stáhněte si zdrojový kód. Jsou deklarované nahoře v souboru MilkshapeModel.cpp.

const byte *pPtr = pBuffer;// Pomocný ukazatel na kopii souboru

MS3DHeader *pHeader = (MS3DHeader*)pPtr;// Ukazatel na hlavičku

pPtr += sizeof(MS3DHeader);// Posun za hlavičku

Hlavička přímo specifikuje formát souboru. Ujistíme se, že se jedná o platný formát, který umíme nahrát.

// Není Milkshape3D souborem

if (strncmp(pHeader->m_ID, "MS3D000000", 10) != 0)

{

delete [] pBuffer;// Překl.: Smaže kopii souboru !!!!!

return false;

}

// Špatná verze souboru, třída podporuje pouze verze 1.3 a 1.4

if (pHeader->m_version < 3 || pHeader->m_version > 4)

{

delete [] pBuffer;// Překl.: Smaže kopii souboru !!!!!

return false;

}

Načteme všechny vertexy. Nejdříve zjistíme jejich počet, alokujeme potřebnou paměť a přesuneme pPtr na další pozici. V cyklu procházíme jednotlivé vertexy. Nastavíme ukazatel pVertex na přetypovaný pPtr a definujeme m_boneID. Nakonec zavoláme memcpy() pro zkopírování hodnot a zvětšíme pPtr.

int nVertices = *(word*)pPtr;// Počet vertexů

m_numVertices = nVertices;// Nastaví atribut třídy

m_pVertices = new Vertex[nVertices];// Alokace paměti pro vertexy

pPtr += sizeof(word);// Posun za počet vertexů

int i;//Pomocná proměnná

for (i = 0; i < nVertices; i++)// Nahrává vertexy

{

MS3DVertex *pVertex = (MS3DVertex*)pPtr;// Ukazatel na vertex

// Načtení vertexu

m_pVertices[i].m_boneID = pVertex->m_boneID;

memcpy(m_pVertices[i].m_location, pVertex->m_vertex, sizeof(float) * 3);

pPtr += sizeof(MS3DVertex);// Posun za tento vertex

}

Stejně jako u vertexů, tak i trojúhelníků nejdříve provedeme potřebné operace pro alokaci paměti. V cyklu procházíme jednotlivé trojúhelníky a inicializujeme je. Všimněte si, že v souboru jsou indexy vertexů uloženy v poli word hodnot, ale v modelu kvůli konzistentnosti a jednoduchosti používáme datový typ int. Číslo se implicitně přetypuje.

int nTriangles = *(word*)pPtr;// Počet trojúhelníků

m_numTriangles = nTriangles;// Nastaví atribut třídy

m_pTriangles = new Triangle[nTriangles];// Alokace paměti pro trojúhelníky

pPtr += sizeof(word);// Posun za počet trojúhelníků

for (i = 0; i < nTriangles; i++)// Načítá trojúhelníky

{

MS3DTriangle *pTriangle = (MS3DTriangle*)pPtr;// Ukazatel na trojúhelník

// Načtení trojúhelníku

int vertexIndices[3] = { pTriangle->m_vertexIndices[0], pTriangle->m_vertexIndices[1], pTriangle->m_vertexIndices[2] };

Všechna čísla v poli t jsou nastavena na 1.0 mínus originál. To proto, že OpenGL používá počátek texturovacího souřadnicového systému vlevo dole, narozdíl od Milkshape, které ho má vlevo nahoře. Odečtením od jedničky, y souřadnici invertujeme. Vše ostatní by mělo být bez problémů.

float t[3] = { 1.0f-pTriangle->m_t[0], 1.0f-pTriangle->m_t[1], 1.0f-pTriangle->m_t[2] };

memcpy(m_pTriangles[i].m_vertexNormals, pTriangle->m_vertexNormals, sizeof(float)*3*3);

memcpy(m_pTriangles[i].m_s, pTriangle->m_s, sizeof(float)*3);

memcpy(m_pTriangles[i].m_t, t, sizeof(float)*3);

memcpy(m_pTriangles[i].m_vertexIndices, vertexIndices, sizeof(int)*3);

pPtr += sizeof(MS3DTriangle);// Posun za tento trojúhelník

}

Nahrajeme struktury mesh. V Milkshape3D jsou také nazývány groups - skupiny. V každé se liší počet trojúhelníků, takže nemůžeme načíst žádnou standardní strukturu. Namísto toho budeme dynamicky alokovat paměť pro indexy trojúhelníků a v každém průchodu je načítat.

int nGroups = *(word*)pPtr;// Počet meshů

m_numMeshes = nGroups;// Nastaví atribut třídy

m_pMeshes = new Mesh[nGroups];// Alokace paměti pro meshe

pPtr += sizeof(word);// Posun za počet meshů

for (i = 0; i < nGroups; i++)// Načítá meshe

{

pPtr += sizeof(byte);// Posun za flagy

pPtr += 32;// Posun za jméno

word nTriangles = *(word*)pPtr;// Počet trojúhelníků v meshi

pPtr += sizeof(word);// Posun za počet trojúhelníků

int *pTriangleIndices = new int[nTriangles];// Alokace paměti pro indexy trojúhelníků

for (int j = 0; j < nTriangles; j++)// Načítá indexy trojúhelníků

{

pTriangleIndices[j] = *(word*)pPtr;// Přiřadí index trojúhelníku

pPtr += sizeof(word);// Posun za index trojúhelníku

}

char materialIndex = *(char*)pPtr;// Načte index materiálu

pPtr += sizeof(char);// Posun za index materiálu

m_pMeshes[i].m_materialIndex = materialIndex;// Index materiálu

m_pMeshes[i].m_numTriangles = nTriangles;// Počet trojúhelníků

m_pMeshes[i].m_pTriangleIndices = pTriangleIndices;// Indexy trojúhelníků

}

Poslední, co načítáme jsou informace o materiálech.

int nMaterials = *(word*)pPtr;// Počet materiálů

m_numMaterials = nMaterials;// Nastaví atribut třídy

m_pMaterials = new Material[nMaterials];// Alokace paměti pro materiály

pPtr += sizeof(word);// Posun za počet materiálů

for (i = 0; i < nMaterials; i++)// Prochází materiály

{

MS3DMaterial *pMaterial = (MS3DMaterial*)pPtr;// Ukazatel na materiál

// Načte materiál

memcpy(m_pMaterials[i].m_ambient, pMaterial->m_ambient, sizeof(float)*4);

memcpy(m_pMaterials[i].m_diffuse, pMaterial->m_diffuse, sizeof(float)*4);

memcpy(m_pMaterials[i].m_specular, pMaterial->m_specular, sizeof(float)*4);

memcpy(m_pMaterials[i].m_emissive, pMaterial->m_emissive, sizeof(float)*4);

m_pMaterials[i].m_shininess = pMaterial->m_shininess;

Alokujeme paměť pro řetězec jména souboru textury a zkopírujeme ho.

// Alokace pro jméno souboru textury

m_pMaterials[i].m_pTextureFilename = new char[strlen(pMaterial->m_texture)+1];

// Zkopírování jména souboru

strcpy(m_pMaterials[i].m_pTextureFilename, pMaterial->m_texture);

// Posun za materiál

pPtr += sizeof(MS3DMaterial);

}

Nakonec loadujeme textury objektu, uvolníme paměť kopie souboru a vrátíme true, abychom oznámili úspěch celé akce.

reloadTextures();// Nahraje textury

delete [] pBuffer;// Smaže kopii souboru

return true;// Model byl nahrán

}

Nyní jsou členské proměnné třídy Model vyplněné. Zbývá ještě nahrát textury. V cyklu procházíme všechny materiály a testujeme, jestli je řetězec se jménem textury delší než nula. Pokud ano nahrajeme texturu pomocí standardní NeHe funkce. Pokud ne přiřadíme textuře nulu jako indikaci, že neexistuje.

void Model::reloadTextures()// Nahrání textur

{

for (int i = 0; i < m_numMaterials; i++)// Jednotlivé materiály

{

if (strlen(m_pMaterials[i].m_pTextureFilename) > 0)// Existuje řetězec s cestou

{

// Nahraje texturu

m_pMaterials[i].m_texture = LoadGLTexture(m_pMaterials[i].m_pTextureFilename);

}

else

{

// Nulou indikuje, že materiál nemá texturu

m_pMaterials[i].m_texture = 0;

}

}

}

Můžeme začít vykreslovat model. Díky uspořádání do struktur to není nic složitého. Ze všeho nejdříve uložíme atribut, jestli je zapnuté nebo vypnuté texturování. Na konci funkce ho budeme moci obnovit.

void Model::draw()

{

GLboolean texEnabled = glIsEnabled(GL_TEXTURE_2D);// Uloží atribut

Každý mesh renderujeme samostatně, protože mesh seskupuje všechny trojúhelníky se stejnými vlastnostmi. Stačí jedno hromadné nastavení OpenGL pro velkou skupinu polygonů, namísto mnohem méně efektivnímu: nastavit vlastnosti pro trojúhelník - vykreslit trojúhelník. S meshi postupujeme takto: nastavit vlastnosti - vykreslit všechny trojúhelníky s těmito vlastnostmi.

for (int i = 0; i < m_numMeshes; i++)// Meshe

{

M_pMeshes[i] použijeme jako referenci na aktuální mesh. Každý z nich má vlastní materiálové vlastnosti, podle kterých nastavíme OpenGL. Pokud se materialIndex rovná -1, znamená to, že mesh není definován. V takovém případě zůstaneme u implicitních nastavení OpenGL. Texturu zvolíme a zapneme pouze tehdy, pokud je větší než nula. Při jejím loadingu jsme nadefinovali, že pokud neexistuje nastavíme ji na nulu. Vypnutí texturingu je tedy logickým krokem. Pokud materiál meshe neexistuje, texturování také vypneme, protože nemáme kde vzít texturu.

int materialIndex = m_pMeshes[i].m_materialIndex;// Index

if (materialIndex >= 0)// Obsahuje mesh index materiálu?

{

// Nastaví OpenGL

glMaterialfv(GL_FRONT, GL_AMBIENT, m_pMaterials[materialIndex].m_ambient);

glMaterialfv(GL_FRONT, GL_DIFFUSE, m_pMaterials[materialIndex].m_diffuse);

glMaterialfv(GL_FRONT, GL_SPECULAR, m_pMaterials[materialIndex].m_specular);

glMaterialfv(GL_FRONT, GL_EMISSION, m_pMaterials[materialIndex].m_emissive);

glMaterialf(GL_FRONT, GL_SHININESS, m_pMaterials[materialIndex].m_shininess);

if (m_pMaterials[materialIndex].m_texture > 0)// Obsahuje materiál texturu?

{

glBindTexture(GL_TEXTURE_2D, m_pMaterials[materialIndex].m_texture);

glEnable(GL_TEXTURE_2D);

}

else// Bez textury

{

glDisable(GL_TEXTURE_2D);

}

}

else// Bez materiálu nemůže být ani textura

{

glDisable(GL_TEXTURE_2D);

}

Při vykreslování procházíme nejdříve všechny trojúhelníky meshe a potom každý z jeho vrcholů. Specifikujeme normálový vektor a texturové koordináty.

glBegin(GL_TRIANGLES);// Začátek trojúhelníků

{

for (int j = 0; j < m_pMeshes[i].m_numTriangles; j++)// Trojúhelníky v meshi

{

int triangleIndex = m_pMeshes[i].m_pTriangleIndices[j];// Index

const Triangle* pTri = &m_pTriangles[triangleIndex];// Trojúhelník

for (int k = 0; k < 3; k++)// Vertexy v trojúhelníku

{

int index = pTri->m_vertexIndices[k];// Index vertexu

glNormal3fv(pTri->m_vertexNormals[k]);// Normála

glTexCoord2f(pTri->m_s[k], pTri->m_t[k]);// Texturovací souřadnice

glVertex3fv(m_pVertices[index].m_location);// Souřadnice vertexu

}

}

}

glEnd();// Konec kreslení

}

Obnovíme atribut OpenGL.

// Obnovení nastavení OpenGL

if (texEnabled)

{

glEnable(GL_TEXTURE_2D);

}

else

{

glDisable(GL_TEXTURE_2D);

}

}

Jediným dalším kódem ve třídě Model, který stojí za pozornost je konstruktor a destruktor. Konstruktor inicializuje všechny členské proměnné na nulu nebo v případě ukazatelů na NULL. Mějte na paměti, že pokud zavoláte funkci loadModelData() dvakrát pro jeden objekt, nastanou úniky paměti! Paměť se totiž uvolňuje až v destruktoru.

Model::Model()// Konstruktor

{

m_numMeshes = 0;

m_pMeshes = NULL;

m_numMaterials = 0;

m_pMaterials = NULL;

m_numTriangles = 0;

m_pTriangles = NULL;

m_numVertices = 0;

m_pVertices = NULL;

}

Model::~Model()// Destruktor

{

int i;

for (i = 0; i < m_numMeshes; i++)

{

delete[] m_pMeshes[i].m_pTriangleIndices;

}

for (i = 0; i < m_numMaterials; i++)

{

delete[] m_pMaterials[i].m_pTextureFilename;

}

m_numMeshes = 0;

if (m_pMeshes != NULL)

{

delete[] m_pMeshes;

m_pMeshes = NULL;

}

m_numMaterials = 0;

if (m_pMaterials != NULL)

{

delete[] m_pMaterials;

m_pMaterials = NULL;

}

m_numTriangles = 0;

if (m_pTriangles != NULL)

{

delete[] m_pTriangles;

m_pTriangles = NULL;

}

m_numVertices = 0;

if (m_pVertices != NULL)

{

delete[] m_pVertices;

m_pVertices = NULL;

}

}

Vysvětlili jsme si třídu Model, zbytek už bude velice jednoduchý. Nahoře v souboru Lesson32.cpp deklarujeme ukazatel na model a inicializujeme ho na NULL.

Model *pModel = NULL;// Ukazatel na model

Jeho data nahrajeme až ve funkci WinMain(). Loading NIKDY nevkládejte do InitGL(), protože se volá vždycky, když uživatel změní mód fullscreen/okno. Při této akci se ztrácí a znovu vytváří OpenGL kontext, ale data modelu se nemusí (a kvůli únikům paměti dokonce nesmí) reloadovat. Zůstávají nedotčená. Stačí znovu nahrát textury, které jsou na OpenGL závislé. Je-li ve scéně více modelů, musí se reloadTextures() volat zvlášť pro každý objekt třídy. Pokud se stane, že budou modely najednou bílé, znamená to, že se textury nenahrály správně.

// Začátek funkce WinMain()

pModel = new MilkshapeModel();// Alokace paměti pro model

if (pModel->loadModelData("data/model.ms3d") == false)// Pokusí se nahrát model

{

MessageBox(NULL, "Couldn't load the model data\\model.ms3d", "Error", MB_OK | MB_ICONERROR);

return 0;// Model se nepodařilo nahrát - program se ukončí

}

// Začátek funkce InitGL()

pModel->reloadTextures();// Nahrání textur modelu

Poslední, co popíšeme je DrawGLScene(). Namísto klasických glTranslatef() a glRotatef() použijeme funkci gluLookAt(). Prvními třemi parametry umísťuje kameru na pozici, prostřední tři souřadnice určují střed scény a poslední tři definují vektor směřující vzhůru. V našem případě se díváme z bodu (75, 75, 75) na bod (0, 0, 0). Model tedy bude vykreslen kolem souřadnic (0, 0, 0), pokud před kreslením neprovedeme translaci. Osa y směřuje vzhůru. Aby se gluLookAt() chovala tímto způsobem, musí být volána jako první po glLoadIdentity().

int DrawGLScene(GLvoid)// Rendering scény

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže buffery

glLoadIdentity();// Reset matice

gluLookAt(75,75,75, 0,0,0, 0,1,0);// Přesun kamery

Aby byl výsledek trochu zajímavější rotujeme modelem kolem osy y.

glRotatef(yrot, 0.0f, 1.0f, 0.0f);// Rotace na ose y

Pro rendering modelu použijeme jeho vlastní funkce. Vykreslí se vycentrovaný okolo středu, ale pouze tehdy, že i v Milkshape 3D byl modelován okolo středu. Pokus s ním budete chtít rotovat, posunovat nebo měnit velikost, zavolejte odpovídající OpenGL funkce. Pro otestování si zkuste vytvořit vlastní model a nahrajte ho do programu. Funguje?

pModel->draw();// Rendering modelu

yrot += 1.0f;// Otáčení scény

return TRUE;

}

A co dál? Plánuji další tutoriál pro NeHe, ve kterém rozšíříme třídu tak, aby umožňovala animaci objektu pomocí jeho kostry (skeletal animation). Možná také naprogramuji další třídy loaderů - program bude schopen nahrát více různých formátů. Krok ke skeletální animaci není až zase tak velký, jak se může zdát, ačkoli matematika bude o stupeň složitější. Pokud ještě nerozumíte maticím a vektorům, je čas se na ně trochu podívat.

napsal: Brett Porter
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Informace o autorovi

Brett Porter se narodil v Austrálii, studoval na Wollogongské Univerzitě. Nedávno absolvoval na BCompSc A BMath (BSc - bakalář přírodních věd). Programovat začal před dvanácti lety v Basicu na "klonu" Commodore 64 zvaném VZ300, ale brzy přešel na Pascal, Intel Assembler, C++ a Javu. Před několika lety začal používat OpenGL.

Zdrojové kódy

Lekce 31

<<< Lekce 30 | Lekce 32 >>>