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>
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.