V této lekci se naučíte, jak nahrát souřadnice vrcholů z textového souboru a plynulou transformaci z jednoho objektu na druhý. Nezaměříme se ani tak na grafický výstup jako spíše na efekty a potřebnou matematiku okolo. Kód může být velice jednoduše modifikován k vykreslování linkami nebo polygony.
Poznamenejme, že každý objekt by měl být seskládán ze stejného počtu bodů jako všechny ostatní. Je to sice hodně omezující požadavek, ale co se dá dělat - chceme přece, aby změny vypadaly dobře. Začneme vložením hlavičkových souborů. Tentokrát nepoužíváme textury, takže se obejdeme bez glaux.
#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
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
Po deklarování všech standardních proměnných přidáme nové. Rot ukládají aktuální úhel rotace na jednotlivých souřadnicových osách, speed definují rychlost rotace. Poslední tři desetinné proměnné určují pozici ve scéně.
GLfloat xrot, yrot, zrot;// Rotace
GLfloat xspeed, yspeed, zspeed;// Rychlost rotace
GLfloat cx, cy, cz = -15;// Pozice
Aby se program zbytečně nezpomaloval při pokusech morfovat objekt sám na sebe, deklarujeme key, který označuje právě zobrazený objekt. Morph v programu indikuje, jestli právě provádíme transformaci objektů nebo ne. V ustáleném stavu má hodnotu FALSE.
int key = 1;// Právě zobrazený objekt
bool morph = FALSE;// Probíhá právě morfování?
Přiřazením 200 do steps určíme, že změna jednoho objektu na druhý bude trvat 200 překreslení. Čím větší číslo zadáme, tím budou přeměny plynulejší, ale zároveň méně pomalé. No a step definuje číslo právě prováděného kroku.
int steps = 200;// Počet kroků změny
int step = 0;// Aktuální krok
Struktura VERTEX obsahuje x, y, z složky pozice jednoho bodu ve 3D prostoru.
typedef struct// Struktura pro bod ve 3D
{
float x, y, z;// X, y, z složky pozice
} VERTEX;// Nazvaný VERTEX
Pokusíme se o vytvoření struktury objektu. co všechno budeme potřebovat? Tak určitě to bude nějaké pole pro uložení všech vrcholů. Abychom ho mohli v průběhu programu libovolně měnit, deklarujeme jej jako ukazatel do dynamické paměti. Celočíselná proměnná vert specifikuje maximální možný index tohoto pole a vlastně i počet bodů, ze kterých se skládá.
typedef struct// Struktura objektu
{
int verts;// Počet bodů, ze kterých se skládá
VERTEX* points;// Ukazatel do pole vertexů
} OBJECT;// Nazvaný OBJECT
Pokud bychom se nedrželi zásady, že všechny objekty musí mít stejný počet vrcholů, vznikly by komplikace. Dají se vyřešit proměnnou, která obsahuje číslo maximálního počtu souřadnic. Uveďme příklad: jeden objekt bude krychle s osmi vrcholy a druhý pyramida se čtyřmi. Do maxver tedy uložíme číslo osm. Nicméně stejně doporučuji, aby měly všechny objekty stejný počet bodů - vše bude jednodušší.
int maxver;// Eventuálně ukládá maximální počet bodů v jednom objektu
První tři instance struktury OBJECT ukládají data, která nahrajeme ze souborů. Do čtvrtého vygenerujeme náhodná čísla - body náhodně rozházené po obrazovce. Helper je objekt pro vykreslování. Obsahuje mezistavy získané kombinací objektů v určitém kroku morfingu. Poslední dvě proměnné jsou ukazatele na zdrojový a výsledný objekt, které chce uživatel zaměnit.
OBJECT morph1, morph2, morph3, morph4;// Koule, toroid, válec (trubka), náhodné body
OBJECT helper, *sour, *dest;// Pomocný, zdrojový a cílový objekt
Ve funkci objallocate() alokujeme paměť pro strukturu objektu, na který ukazuje pointer *k předaný parametrem. Celočíselné n definuje počet vrcholů objektu.
Funkci malloc(), která vrací ukazatel na dynamicky alokovanou paměť předáme její požadovanou velikost. Získáme ji operátorem sizeof() vynásobeným počtem vertexů. Protože malloc() vrací ukazatel na void, musíme ho přetypovat.
Pozn. překl.: Program by ještě měl otestovat jestli byla opravdu alokována. Kdyby se operace nezdařila, program by přistupoval k nezabrané paměti a aplikace by se zcela jistě zhroutila. Malloc() v případě neúspěchu vrací NULL.
void objallocate(OBJECT *k,int n)// Alokuje dynamickou paměť pro objekt
{
k->points = (VERTEX*) malloc(sizeof(VERTEX) * n);// Alokuje paměť
// Překladatel:
// if(k->points == NULL)
// {
// MessageBox(NULL,"Chyba při alokaci paměti pro objekt", "ERROR", MB_OK | MB_ICONSTOP);
// Ukončit program
// }
}
Po každé alokaci dynamické paměti musí VŽDY přijít její uvolnění. Funkci opět předáváme ukazatel na objekt.
void objfree(OBJECT *k)// Uvolní dynamickou paměť objektu
{
free(k->points);// Uvolní paměť
}
Funkce readstr() je velmi podobná (úplně stejná) jako v lekci 10. Načte jeden řádek ze souboru f a uloží ho do řetězce string. Abychom mohli udržet data souboru přehledná funkce přeskakuje prázdné řádky (\n) a c-éčkovské komentáře (řádky začínající //, respektive '/').
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;
}
Napíšeme funkci pro loading objektu z textového souboru. Name specifikuje diskovou cestu k souboru a k je ukazatel na objekt, do kterého uložíme výsledek.
void objload(char *name,OBJECT *k)// Nahraje objekt ze souboru
{
Začneme deklarací lokálních proměnných funkce. Do ver načteme počet vertexů, který určuje první řádka v souboru (více dále). Dá se říct, že rx, ry, rz jsou pouze pro zpřehlednění zdrojového kódu programu. Ze souboru do nich načteme jednotlivé složky bodu. Ukazatel filein ukazuje na soubor (po otevření). Oneline je znakový buffer. Vždy do něj načteme jednu řádku, analyzujeme ji a získáme informace, které potřebujeme.
int ver;// Počet bodů
float rx, ry, rz;// X, y, z pozice
FILE* filein;// Handle souboru
char oneline[255];// Znakový buffer
Pomocí funkce fopen() otevřeme soubor pro čtení. Pozn. překl.: Stejně jako u alokace paměti i zde chybí ošetření chyb.
filein = fopen(name, "rt");// Otevře soubor
// Překladatel:
// if(filein == NULL)
// {
// MessageBox(NULL,"Chyba při otevření souboru s daty", "ERROR", MB_OK | MB_ICONSTOP);
// Ukončit program
// }
Do znakového bufferu načteme první řádku. Měla by být ve tvaru: Vertices: x\n. Z řetězce tedy potřebujeme vydolovat číslo x, které udává počet vertexů definovaných v souboru. Tento počet uložíme do vnitřní proměnné struktury a potom alokujeme tolik paměti, aby se do ní všechny koordináty souřadnic vešly.
readstr(filein, oneline);// Načte první řádku ze souboru
sscanf(oneline, "Vertices: %d\n", &ver);// Počet vertexů
k->verts = ver;// Nastaví položku struktury na správnou hodnotu
objallocate(k, ver);// Alokace paměti pro objekt
Už tedy víme z kolikati bodů je objekt vytvořen a máme alokovánu potřebnou paměť. Nyní ještě musíme načíst jednotlivé hodnoty. Provedeme to cyklem for s řídící proměnnou i, která se každým průchodem inkrementuje. Postupně načteme všechny řádky do bufferu, ze kterého přes funkci sscanf() dostaneme číselné hodnoty složek vertexu pro všechny tři souřadnicové osy. Pomocné proměnné zkopírujeme do proměnných struktury. Po analýze celého souboru ho zavřeme.
Ještě musím upozornit, že je důležité, aby soubor obsahoval stejný počet bodů jako je definováno na začátku. Pokud by jich bylo více, tolik by to nevadilo - poslední by se prostě nenačetly. V žádném případě jich ale NESMÍ být méně! S největší pravděpodobností by to zhroutilo program. Vše, na co se pokouším upozornit by se dalo shrnout do věty: Jestliže soubor začíná "Vertices: 10", musí v něm být specifikováno 10 souřadnic (30 čísel - x, y, z).
for (int i = 0; i < ver; i++)// Postupně načítá body
{
readstr(filein, oneline);// Načte řádek ze souboru
sscanf(oneline, "%f %f %f", &rx, &ry, &rz);// Najde a uloží tři čísla
k->points[i].x = rx;// Nastaví vnitřní proměnnou struktury
k->points[i].y = ry;// Nastaví vnitřní proměnnou struktury
k->points[i].z = rz;// Nastaví vnitřní proměnnou struktury
}
fclose(filein);// Zavře soubor
Otestujeme, zda není proměnná ver (počet bodů aktuálního objektu) větší než maxver (v současnosti maximální známý počet vertexů v jednom objektu). Pokud ano přiřadíme ver do maxver.
if(ver > maxver)// Aktualizuje maximální počet vertexů
maxver = ver;
}
Na řadu přichází trochu méně pochopitelná funkce - zvlášť pro ty, kteří nemají v lásce matematiku. Bohužel morfing na ní staví. Co tedy dělá? Spočítá o klik máme posunout bod specifikovaný parametrem i. Na začátku deklarujeme pomocný vertex, podle vzorce spočítáme jeho jednotlivé x, y, z složky a v závěru ho vrátíme volající funkci.
Použitá matematika pracuje asi takto: od souřadnice i-tého bodu zdrojového objektu odečteme souřadnici bodu, do kterého morfujeme. Rozdíl vydělíme zamýšleným počtem kroků a konečný výsledek uložíme do a.
Řekněme, že x-ové souřadnice zdrojového objektu (sour) je rovna čtyřiceti a cílového objektu (dest) dvaceti. U deklarace globálních proměnných jsme steps přiřadili 200. Výpočtem a.x = (40-20)/200 = 20/200 = 0,1 zjistíme, že při přesunu ze 40 na 20 s krokem 200 potřebujeme každé překreslení pohnout na ose x bodem o desetinu jednotky. Nebo jinak: násobíme-li 200*0,1 dostaneme rozdíl pozic 20, což je také pravda (40-20=20). Mělo by to fungovat.
VERTEX calculate(int i)// Spočítá o kolik pohnout bodem při morfingu
{
VERTEX a;// Pomocný bod
a.x = (sour->points[i].x - dest->points[i].x) / steps;// Spočítá posun
a.y = (sour->points[i].y - dest->points[i].y) / steps;// Spočítá posun
a.z = (sour->points[i].z - dest->points[i].z) / steps;// Spočítá posun
return a;// Vrátí výsledek
}
Začátek inicializační funkce není žádnou novinkou, ale dále v kódu najdete změny.
int InitGL(GLvoid)// Všechno nastavení OpenGL
{
glBlendFunc(GL_SRC_ALPHA, GL_ONE);// Typ blendingu
// glEnable(GL_BLEND);// Zapne blending (překl.: autor asi zapomněl)
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);// Povolení testování hloubky
glShadeModel(GL_SMOOTH);// Jemné stínování
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Nejlepší perspektivní korekce
Protože ještě neznáme maximální počet bodů v jednom objektu, přiřadíme do maxver nulu. Poté pomocí funkce objload() načteme z disku data jednotlivých objektů (koule, toroid, válec). V prvním parametru předáváme cestu se jménem souboru, ve druhém adresu objektu, do kterého se mají data uložit.
maxver = 0;// Nulování maximálního počtu bodů
objload("data/sphere.txt", &morph1);//Načte kouli
objload("data/torus.txt", &morph2);// Načte toroid
objload("data/tube.txt", &morph3);// Načte válec
Čtvrtý objekt nenačítáme ze souboru. Budou jím po scéně rozházené body (přesně 486 bodů). Nejdříve musíme alokovat paměť pro jednotlivé vertexy a potom stačí v cyklu vygenerovat náhodné souřadnice. Budou v rozmezí od -7 do +7.
objallocate(& morph4, 486);// Alokace paměti pro 486 bodů
for(int i=0; i < 486; i++)// Cyklus generuje náhodné souřadnice
{
morph4.points[i].x = ((float)(rand() % 14000) / 1000) - 7;// Náhodná hodnota
morph4.points[i].y = ((float)(rand() % 14000) / 1000) - 7;// Náhodná hodnota
morph4.points[i].z = ((float)(rand() % 14000) / 1000) - 7;// Náhodná hodnota
}
Ze souborů jsme loadovali všechny objekty do struktur. Jejich data už nebudeme upravovat. Od teď jsou jen pro čtení. Potřebujeme tedy ještě jeden objekt, helper, který bude při morfingu ukládat jednotlivé mezistavy. Protože na začátku zobrazujeme morp1 (koule) načteme i do pomocného tento objekt.
objload("data/sphere.txt", &helper);// Načtení koule do pomocného objektu
Nastavíme ještě pointery pro zdrojový a cílový objekt, tak aby ukazovali na adresu morph1.
sour = dest = &morph1;// Inicializace ukazatelů na objekty
return TRUE;// Ukončí funkci
}
Vykreslování začneme klasicky smazáním obrazovky a hloubkového bufferu, resetem matice, posunem a rotacemi. Místo abychom všechny pohyby prováděli na konci funkce, tentokrát je umístíme na začátek. Poté deklarujeme pomocné proměnné. Do tx, ty, tz spočítáme souřadnice, které pak předáme funkci glVertex3f() kvůli nakreslení bodu. Q je pomocný bod pro výpočet.
void DrawGLScene(GLvoid)// Vykreslování
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže obrazovku a hloubkový buffer
glLoadIdentity();// Reset matice
glTranslatef(cx,cy,cz);// Přesun na pozici
glRotatef(xrot, 1,0,0);// Rotace na ose x
glRotatef(yrot, 0,1,0);// Rotace na ose y
glRotatef(zrot, 0,0,1);// Rotace na ose z
xrot += xspeed;// Zvětší úhly rotace
yrot += yspeed;
zrot += zspeed;
GLfloat tx, ty, tz;// Pomocné souřadnice
VERTEX q;// Pomocný bod pro výpočty
Přes glBegin(GL_POINTS) oznámíme OpenGL, že v blízké době budeme vykreslovat body. V cyklu for procházíme vertexy. Řídící proměnnou i bychom také mohli porovnávat s maxver, ale protože mají všechny objekty stejný počet souřadnic, můžeme s klidem použít počet vertexů prvního objektu - morph1.verts.
glBegin(GL_POINTS);// Začátek kreslení bodů
for(int i = 0; i < morph1.verts; i++)// Cyklus prochází vertexy
{
V případě morfingu spočítáme o kolik se má vykreslovaný bod posunout oproti pozici při minulém vykreslení. Takto vypočítané hodnoty odečteme od souřadnic pomocného objektu, do kterého každé překreslení ukládáme aktuální mezistav morfingu. Pokud se zrovna objekty mezi sebou netransformují odečítáme nulu, takže se souřadnice defakto nemění.
if(morph)// Pokud zrovna morfujeme
q = calculate(i);// Spočítáme hodnotu posunutí
else// Jinak
q.x = q.y = q.z = 0;// Budeme odečítat nulu, ale tím neposouváme
helper.points[i].x -= q.x;// Posunutí na ose x
helper.points[i].y -= q.y;// Posunutí na ose y
helper.points[i].z -= q.z;// Posunutí na ose z
Abychom si zpřehlednili program a také kvůli maličkému efektu, zkopírujeme právě získaná čísla do pomocných proměnných.
tx = helper.points[i].x;// Zpřehlednění + efekt
ty = helper.points[i].y;// Zpřehlednění + efekt
tz = helper.points[i].z;// Zpřehlednění + efekt
Všechno máme spočítáno, takže přejdeme k vykreslení. Nastavíme barvu na zelenomodrou a nakreslíme bod. Potom zvolíme trochu tmavší modrou barvu. Odečteme dvojnásobek souřadnic q od t a získáme umístění bodu při následujícím volání této funkce (ob jedno). Na této pozici znovu vykreslíme bod. Do třetice všeho dobrého znovu ztmavíme barvu a opět spočítáme další pozici, na které se vyskytne po čtyřech průchodech touto funkcí a opět ho vykreslíme.
Proč jsme krásně přehledný kód vlastně komplikovali? I když si to asi neuvědomujete, vytvořili jsme jednoduchý částicový systém. S použitím blendingu vytvoří perfektní efekt, který se ale bohužel projeví pouze při transformaci objektů z jednoho na druhý. Pokud zrovna nemorfujeme, v q souřadnicích jsou uloženy nuly, takže druhý a třetí bod kreslíme na stejné místo jako první.
glColor3f(0, 1, 1);// Zelenomodrá barva
glVertex3f(tx, ty, tz);// Vykreslí první bod
glColor3f(0, 0.5f, 1);// Modřejší zelenomodrá barva
tx -= 2*q.x;// Spočítání nových pozic
ty -= 2*q.y;
ty -= 2*q.y;
glVertex3f(tx, ty, tz);// Vykreslí druhý bod v nové pozici
glColor3f(0, 0, 1);// Modrá barva
tx -= 2*q.x;// Spočítání nových pozic
ty -= 2*q.y;
ty -= 2*q.y;
glVertex3f(tx, ty, tz);// Vykreslí třetí bod v nové pozici
Ukončíme tělo cyklu a glEnd() oznámí, že dále už nebudeme nic vykreslovat.
}
glEnd();// Ukončí kreslení
Jako poslední v této funkci zkontrolujeme jestli transformujeme objekty. Pokud ano a zároveň musí být aktuální krok morfingu menší než celkový počet kroků, inkrementujeme aktuální krok. Po dokončení morfingu ho vypneme. Protože jsme už došli k cílovému objektu, uděláme z něj zdrojový. Krok reinicializujeme na nulu.
if(morph && step <= steps)// Morfujeme a krok je menší než maximum
{
step++;// Příště pokračuj následujícím krokem
}
else// Nemorfujeme nebo byl právě ukončen
{
morph = FALSE;// Konec morfingu
sour = dest;// Cílový objekt je nyní zdrojový
step = 0;// První (nulový) krok morfingu
}
}
KillGLWindow upravíme jenom málo. Uvolníme pouze dynamicky alokovanou paměť.
GLvoid KillGLWindow(GLvoid)// Zavírání okna
{
objfree(&morph1);// Uvolní alokovanou paměť
objfree(&morph2);// Uvolní alokovanou paměť
objfree(&morph3);// Uvolní alokovanou paměť
objfree(&morph4);// Uvolní alokovanou paměť
objfree(&helper);// Uvolní alokovanou paměť
// Zbytek nezměněn
}
Ve funkci WinMain() upravíme kód testující stisk kláves. Následujícími šesti testy regulujeme rychlost rotace objektu.
// Funkce WinMain()
if(keys[VK_PRIOR])// PageUp?
zspeed += 0.01f;
if(keys[VK_NEXT])// PageDown?
zspeed -= 0.01f;
if(keys[VK_DOWN])// Šipka dolu?
xspeed += 0.01f;
if(keys[VK_UP])// Šipka nahoru?
xspeed -= 0.01f;
if(keys[VK_RIGHT])// Šipka doprava?
yspeed += 0.01f;
if(keys[VK_LEFT])// Šipka doleva?
yspeed -= 0.01f;
Dalších šest kláves pohybuje objektem po scéně.
if (keys['Q'])// Q?
cz -= 0.01f;// Dále
if (keys['Z'])// Z?
cz += 0.01f;// Blíže
if (keys['W'])// W?
cy += 0.01f;// Nahoru
if (keys['S'])// S?
cy -= 0.01f;// Dolu
if (keys['D'])// D?
cx += 0.01f;// Doprava
if (keys['A'])// A?
cx -= 0.01f;// Doleva
Teď ošetříme stisk kláves 1-4. Aby se kód provedl, nesmí být při stisku jedničky key roven jedné (nejde morfovat z prvního objektu na první) a také nesmíme právě morfovat (nevypadalo by to dobře). V takovém případě nastavíme pro příští průchod tímto místem key na jedna a morph na TRUE. Cílovým objektem bude objekt jedna. Klávesy 2, 3, 4 jsou analogické.
if (keys['1'] && (key!=1) && !morph)// Klávesa 1?
{
key = 1;// Proti dvojnásobnému stisku
morph = TRUE;// Začne morfovací proces
dest = &morph1;// Nastaví cílový objekt
}
if (keys['2'] && (key!=2) && !morph)// Klávesa 2?
{
key = 2;// Proti dvojnásobnému stisku
morph = TRUE;// Začne morfovací proces
dest = &morph2;// Nastaví cílový objekt
}
if (keys['3'] && (key!=3) && !morph)// Klávesa 3?
{
key = 3;// Proti dvojnásobnému stisku
morph = TRUE;// Začne morfovací proces
dest = &morph3;// Nastaví cílový objekt
}
if (keys['4'] && (key!=4) && !morph)// Klávesa 4?
{
key = 4;// Proti dvojnásobnému stisku
morph = TRUE;// Začne morfovací proces
dest = &morph4;// Nastaví cílový objekt
}
Doufám, že jste si tento tutoriál užili. Ačkoli výstup není až tak fantastický jako v některých jiných, naučili jste se spoustu věcí. Hraním si s kódem lze docílit skvělých efektů - třeba po scéně náhodně rozházené body měnící se ve slova. Zkuste použít polygony nebo linky namísto bodů, výsledek bude ještě lepší.
Před tím, než vznikla tato lekce bylo vytvořeno demo "Morph", které demonstruje mnohem pokročilejší verzi probíraného efektu. Lze ho najít na adrese http://homepage.ntlworld.com/fj.williams/PgSoftware.html.
napsal: Piotr Cieslak
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>