Do současnosti jsme programovali otáčející se kostku nebo pár hvězd. Máte (měli byste mít :-) základní pojem o 3D. Ale rotující krychle asi nejsou to nejlepší k tvorbě dobrých deathmatchových protivníků! Nečekejte a začněte s Quakem IV ještě dnes! Tyto dny potřebujete k velkému, komplikovanému a dynamickému 3D světu s pohybem do všech směrů, skvělými efekty zrcadel, portálů, deformacemi a třeba také vysokým frameratem. Tato lekce vám vysvětlí základní strukturu 3D světa a pohybu v něm.
#include <windows.h>// Hlavičkový soubor pro Windows
#include <stdio.h>// Hlavičkový soubor pro standardní vstup/výstup
#include <math.h>// Hlavičkový soubor pro matematickou knihovnu
#include <gl\gl.h>// Hlavičkový soubor pro OpenGL32 knihovnu
#include <gl\glu.h>// Hlavičkový soubor pro Glu32 knihovnu
#include <gl\glaux.h>// Hlavičkový soubor pro Glaux knihovnu
HDC hDC = NULL;// Privátní GDI Device Context
HGLRC hRC = NULL;// Trvalý Rendering Context
HWND hWnd = NULL;// Obsahuje Handle našeho okna
HINSTANCE hInstance;// Obsahuje instanci aplikace
bool keys[256];// Pole pro ukládání vstupu z klávesnice
bool active = TRUE;// Ponese informaci o tom, zda je okno aktivní
bool fullscreen = TRUE;// Ponese informaci o tom, zda je program ve fullscreenu
bool blend;// Blending ON/OFF
bool bp;// B stisknuto? (blending)
bool fp;// F stisknuto? (texturové filtry)
const float piover180 = 0.0174532925f;// Zjednoduší převod mezi stupni a radiány
float heading;// Pomocná pro přepočítávání xpos a zpos při pohybu
float xpos;// Určuje x-ové souřadnice na podlaze
float zpos;// Určuje z-ové souřadnice na podlaze
GLfloat yrot;// Y rotace (natočení scény doleva/doprava - směr pohledu)
GLfloat walkbias = 0;// Houpání scény při pohybu (simulace kroků)
GLfloat walkbiasangle = 0;// Pomocná pro vypočítání walkbias
GLfloat lookupdown = 0.0f;// Určuje úhel natočení pohledu nahoru/dolů
GLfloat z=0.0f;// Hloubka v obrazovce
GLuint filter;// Použitý texturový filtr
GLuint texture[3];// Ukládá textury
Během definování 3D světa stylem dlouhých sérií čísel se stává stále obtížnějším udržet složitý kód přehledný. Musíme třídit data do jednoduchého a především funkčního tvaru. Pro zpřehlednění vytvoříme celkem tři struktury.
Body obsahují skutečná data, která zajímají OGL. Každý bod definujeme pozicí v prostoru (x,y,z) a koordináty textury (u,v).
typedef struct tagVERTEX// Struktura bodu
{
float x, y, z;// Souřadnice v prostoru
float u, v;// Texturové koordináty
} VERTEX;
Všechno se skládá z ploch. Protože trojúhelníky jsou nejjednodušší, využijeme právě je.
typedef struct tagTRIANGLE// Struktura trojúhelníku
{
VERTEX vertex[3];// Pole tří bodů
} TRIANGLE;
Na počátku všeho je sektor. Každý 3D svět je v základě celý ze sektorů. Může jím být místnost, kostka či jakýkoli jiný větší útvar.
typedef struct tagSECTOR// Struktura sektoru
{
int numtriangles;// Počet trojúhelníků v sektoru
TRIANGLE* triangle;// Ukazatel na dynamické pole trojúhelníků
} SECTOR;
SECTOR sector1;// Bude obsahovat všechna data 3D světa
Abychom program ještě více zpřehlednili, ve zdrojovém kódu, který se kompiluje, nebudou žádné číselné souřadnice. K exe souboru - výsledku naší práce - přiložíme textový soubor. V něm nadefinujeme všechny body 3D prostoru a k nim odpovídající texturové koordináty. Z důvodu větší přehlednosti přidáme komentáře. Bez nich by byl totální zmatek. Obsah souboru se může kdykoli změnit. Hodit se to bude především při vytváření prostředí - metoda pokusů a omylů, kdy nemusíte pokaždé rekompilovat program. Upravovat může i uživatel a tím si vytvořit vlastní prostředí. Nemusíte mu poskytovat nic navíc, neřkuli zdrojové kódy. Tento soubor by přece stejně dostal. Ze začátku bude lepší používat textové soubory (snadná editace, méně kódu), binární odložíme na později.
První řádka NUMPOLLIES xx určuje celkový počet trojúhelníků. Text za zpětnými lomítky značí komentář. V každém následujícím řádku je definován jeden bod v prostoru a texturové koordináty. Tři řádky určí trojúhelník, celý soubor sektor.
NUMPOLLIES 36
// Floor 1
-3.0 0.0 -3.0 0.0 6.0
-3.0 0.0 3.0 0.0 0.0
3.0 0.0 3.0 6.0 0.0
-3.0 0.0 -3.0 0.0 6.0
3.0 0.0 -3.0 6.0 6.0
3.0 0.0 3.0 6.0 0.0
// Ceiling 1
-3.0 1.0 -3.0 0.0 6.0
-3.0 1.0 3.0 0.0 0.0
3.0 1.0 3.0 6.0 0.0
-3.0 1.0 -3.0 0.0 6.0
3.0 1.0 -3.0 6.0 6.0
3.0 1.0 3.0 6.0 0.0
... atd. Data jednoho trojúhelníku tedy obecně vypadají takto:
x1 y1 z1 u1 v1
x2 y2 z2 u2 v2
x3 y3 z3 u3 v3
Otázkou je, jak tyto data vyjmeme ze souboru. Vytvoříme funkci readstr(), která načte jeden použitelný řádek.
void readstr(FILE *f,char *string)// Načte jeden použitelný řádek ze souboru
{
do
{
fgets(string, 255, f);// Načti řádek
} while ((string[0] == '/') || (string[0] == '\n'));// Pokud není použitelný načti další
return;
}
Tuto funkci budeme volat v SetupWorld(). Nadefinujeme náš soubor jako filein a otevřeme ho pouze pro čtení. Na konci ho samozřejmě zavřeme.
void SetupWorld()// Načti 3D svět ze souboru
{
float x, y, z, u, v;// body v prostoru a koordináty textur
int numtriangles;// Počet trojúhelníků
FILE *filein;// Ukazatel na soubor
char oneline[255];// Znakový buffer
filein = fopen("data/world.txt", "rt");// Otevření souboru pro čtení
Přečteme data sektoru. Tato lekce bude počítat pouze s jedním sektorem, ale není těžké provést malou úpravu. Program potřebuje znát počet trojúhelníků v sektoru, aby věděl, kolik informací má přečíst. Tato hodnota může být definována jako konstanta přímo v programu, ale určitě uděláme lépe, když ji uložíme přímo do souboru (program se přizpůsobí).
readstr(filein,oneline);// Načtení prvního použitelného řádku
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);// Vyjmeme počet trojúhelníků
Alokujeme potřebnou paměť pro všechny trojúhelníky a uložíme jejich počet do položky struktury.
sector1.triangle = new TRIANGLE[numtriangles];// Alokace potřebné paměti
sector1.numtriangles = numtriangles;// Uložení počtu trojúhelníků
Po alokaci paměti můžeme přistoupit k inicializaci všech datových složek sektoru.
for (int loop = 0; loop < numtriangles; loop++)// Prochází trojúhelníky
{
for (int vert = 0; vert < 3; vert++)// Prochází vrcholy trojúhelníků
{
Načteme řádek, do pomocných proměnných uložíme jednotlivé hodnoty a ty znovu uložíme do položek struktury. S mezikrokem je kód mnohem přehlednější.
readstr(filein,oneline);// Načte řádek
sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);// Načtení do pomocných proměnných
// Inicializuje jednotlivé položky struktury
sector1.triangle[loop].vertex[vert].x = x;
sector1.triangle[loop].vertex[vert].y = y;
sector1.triangle[loop].vertex[vert].z = z;
sector1.triangle[loop].vertex[vert].u = u;
sector1.triangle[loop].vertex[vert].v = v;
}
}
fclose(filein);// Zavře soubor
return;
}
Právě napsanou funkci zavoláme při inicializaci programu.
int InitGL(GLvoid)// Všechna nastavení OpenGL
{
if (!LoadGLTextures())// Nahraje texturu
{
return FALSE;
}
glEnable(GL_TEXTURE_2D);// Zapne mapování textur
glBlendFunc(GL_SRC_ALPHA,GL_ONE);// Nastavení blendingu pro průhlednost
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);// Černé pozadí
glClearDepth(1.0);// Nastavení hloubkového bufferu
glDepthFunc(GL_LESS);// Typ hloubkového testování
glEnable(GL_DEPTH_TEST);// Zapne hloubkové testování
glShadeModel(GL_SMOOTH);// Povolíme jemné stínování
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Nejlepší perspektivní korekce
SetupWorld();// Loading 3D světa
return TRUE;
}
Teď když máme sektor načtený do paměti, potřebujeme ho zobrazit. Už dlouho známe nějaké ty rotace a pohyb, ale kamera vždy směřovala do středu (0,0,0). Každý dobrý 3D engine umožňuje chodit kolem a objevovat svět. Jedna možnost, jak k tomu dospět je točit kamerou a kreslit 3D prostředí relativně k pozici kamery - funkce gluLookAt(). Protože tohle ještě neznáme budeme kameru simulovat takto:
int DrawGLScene(GLvoid)// Vykreslování
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Vymaže obrazovku a hloubkový buffer
glLoadIdentity();// Reset matice
GLfloat x_m, y_m, z_m, u_m, v_m;// Pomocné souřadnice a koordináty textury
GLfloat xtrans = -xpos;// Pro pohyb na ose x
GLfloat ztrans = -zpos;// Pro pohyb na ose z
GLfloat ytrans = -walkbias-0.25f;// Poskakování kamery (simulace kroků)
GLfloat sceneroty = 360.0f - yrot;// Úhel směru pohledu
int numtriangles;// Počet trojúhelníků
glRotatef(lookupdown, 1.0f,0.0f,0.0f);// Rotace na ose x - pohled nahoru/dolů
glRotatef(sceneroty, 0.0f,1.0f,0.0f);// Rotace na ose y - otočení doleva/doprava
glTranslatef(xtrans, ytrans, ztrans);// Posun na pozici ve scéně
glBindTexture(GL_TEXTURE_2D, texture[filter]);// Výběr textury podle filtru
numtriangles = sector1.numtriangles;// Počet trojúhelníků - pro přehlednost
// Projde a vykreslí všechny trojúhelníky
for (int loop_m = 0; loop_m < numtriangles; loop_m++)
{
glBegin(GL_TRIANGLES);// Začátek kreslení trojúhelníků
glNormal3f(0.0f, 0.0f, 1.0f);// Normála ukazuje dopředu - světlo
x_m = sector1.triangle[loop_m].vertex[0].x;// První vrchol
y_m = sector1.triangle[loop_m].vertex[0].y;
z_m = sector1.triangle[loop_m].vertex[0].z;
u_m = sector1.triangle[loop_m].vertex[0].u;
v_m = sector1.triangle[loop_m].vertex[0].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);// Vykreslení
x_m = sector1.triangle[loop_m].vertex[1].x;// Druhý vrchol
y_m = sector1.triangle[loop_m].vertex[1].y;
z_m = sector1.triangle[loop_m].vertex[1].z;
u_m = sector1.triangle[loop_m].vertex[1].u;
v_m = sector1.triangle[loop_m].vertex[1].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);// Vykreslení
x_m = sector1.triangle[loop_m].vertex[2].x;// Třetí vrchol
y_m = sector1.triangle[loop_m].vertex[2].y;
z_m = sector1.triangle[loop_m].vertex[2].z;
u_m = sector1.triangle[loop_m].vertex[2].u;
v_m = sector1.triangle[loop_m].vertex[2].v;
glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);// Vykreslení
glEnd();// Konec kreslení trojúhelníků
}
return TRUE;
}
Přejdeme do funkce WinMain() na ovládání klávesnicí. Když je stisknuta šipka vlevo/vpravo, proměnná yrot je zvýšena/snížena, tudíž se natočí výhled. Když je stisknuta šipka dopředu/dozadu, spočítá se nová pozice pro kameru s použitím sinu a kosinu - vyžaduje trochu znalostí trigonometrie. Piover180 je pouze číslo pro konverzi mezi stupni a radiány. Walkbias je offset vytvářející houpání scény při simulaci kroků. Jednoduše upraví y pozici kamery podle sinové vlny. Jako jednoduchý pohyb vpřed a vzad nevypadá špatně.
// Funkce WinMain()
if (keys['B'] && !bp)// Klávesa B - zapne/vypne blending
{
bp=TRUE;
blend=!blend;
if (!blend)
{
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
else
{
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
}
}
if (!keys['B'])
{
bp=FALSE;
}
if (keys['F'] && !fp)// Klávesa F - cyklování mezi texturovými filtry
{
fp=TRUE;
filter+=1;
if (filter>2)
{
filter=0;
}
}
if (!keys['F'])
{
fp=FALSE;
}
if (keys[VK_UP])// Šipka nahoru - pohyb dopředu
{
xpos -= (float)sin(heading*piover180) * 0.05f;// Pohyb na ose x
zpos -= (float)cos(heading*piover180) * 0.05f;// Pohyb na ose z
if (walkbiasangle >= 359.0f)
{
walkbiasangle = 0.0f;
}
else
{
walkbiasangle+= 10;
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;// Simulace kroků
}
if (keys[VK_DOWN])// Šipka dolů - pohyb dozadu
{
xpos += (float)sin(heading*piover180) * 0.05f;// Pohyb na ose x
zpos += (float)cos(heading*piover180) * 0.05f;// Pohyb na ose z
if (walkbiasangle <= 1.0f)
{
walkbiasangle = 359.0f;
}
else
{
walkbiasangle-= 10;
}
walkbias = (float)sin(walkbiasangle * piover180)/20.0f;// Simulace kroků
}
if (keys[VK_RIGHT])// Šipka doprava
{
heading -= 1.0f;// Natočení scény
yrot = heading;
}
if (keys[VK_LEFT])// Šipka doleva
{
heading += 1.0f;// Natočení scény
yrot = heading;
}
if (keys[VK_PRIOR])// Page Up
{
lookupdown-= 1.0f;// Natočení scény
}
if (keys[VK_NEXT])// Page Down
{
lookupdown+= 1.0f;// Natočení scény
}
Vytvořili jsme první 3D svět. Nevypadá sice jako v Quake-ovi, ale my také nejsme Carmack nebo Abrash. Zkuste tlačítka F - texturový filtr a B - blending. PgUp/PgDown nachýlí kameru nahoru/dolů. Pohyb šipkami vás doufám napadne.
Teď asi přemýšlíte co dál. Možná použijete tento kód na plnohodnotný 3D engine, měli byste být schopni ho vytvořit. Pravděpodobně budete mít ve hře více než jeden sektor, zvláště při použití vchodů.
Tato implementace kódu umožňuje nahrávání mnohonásobných sektorů a má zpětné vykreslování /backface culling/ (nekreslí polygony od kamery). Hodně štěstí v dalších pokusech.
napsal: Jeff Molofee - NeHe <nehe (zavináč) connect.ab.ca>
přeložil: Jiří Rajský - RAJSOFT junior <predator.jr (zavináč) seznam.cz>
kompletně přepsal: Michal Turek - Woq <WOQ (zavináč) seznam.cz>
Pozn.: Tuto lekci nepsal NeHe, ale Lionel Brits. Jak sám autor uvádí, je to jeho první tutoriál - a bohužel bylo to vidět. Pokud se podíváte do anglické verze, tak zjistíte, že bez zdrojových kódů nemáte absolutní šanci něco pochopit. Někdy je dokonce velmi těžké identifikovat, která část kódu patří ke které funkci. Aby byl text kratší používal vynechávky (někdy i u hodně důležitého kódu - třeba načítání pozic ze souboru), ap. Překlad Jiřího Rajského byl, dá se říct, přesný a to v tomto případě, byla možná chyba. Proto jsem se rozhodl větší část lekce přepsat. Vím, že ani teď to není nijak zvlášť slavné, ale snažil jsem se. Kód jsem samozřejmě neupravoval (i když by si to také zasloužil).
Chyby v kódu: Když jsem přepisoval tuto lekci, musel jsem ji pochopit ze zdrojových kódů a při tom jsem našel několik chyb. Je mi to tak trochu blbý, protože bych kód asi sám nedokázal napsat, ale na druhou stranu byste o tom měli vědět.
Zbytečná deklarace proměnné z. Tuto proměnnou autor pravděpodobně používal ze začátku a pak ji nahradil jinou. Svědčí o tom i dvojité testování PageUp/PageDown (do lekce nevypisováno). Nikde jinde ji nenajdete.
Neuvolnění dynamicky alokované paměti. Ve funkci SetupWorld() jsme pomocí operátoru new alokovali paměť pro trojúhelníky. Nikdy v programu, ale není její uvolnění. I když by měl operační systém po skončení programu rušit všechny systémové zdroje, nelze se na to spoléhat. Tuto chybu odstraníte například takto:
// Přidat na konec funkce KillGLWindow()
delete [] sector1.triangle;// Uvolnění dynamicky alokované paměti