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 glLookAt(). Proto₧e tohle jeÜt∞ neznßme budeme kameru simulovat takto:
1. U₧ivatel stiskne Üipku
2. Vlevo/vpravo - otoΦφme sv∞t okolo st°edu v opaΦnΘm sm∞ru ne₧ je rotace kamery - glRoratef()
3. Dop°edu/dozadu - posuneme sv∞t v opaΦnΘm sm∞ru ne₧ je pohyb kamery - glTranslatef()
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: Lionel Brits - ▀etelgeuse
p°elo₧il: Ji°φ Rajsk² - RAJSOFT junior
kompletn∞ p°epsal: Michal Turek - Woq
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