Lekce 10 - Vytvoření 3D světa a pohyb v něm

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

Zdrojové kódy

Lekce 10

<<< Lekce 9 | Lekce 11 >>>