Lekce 25 - Morfování objektů a jejich nahrávání z textového souboru

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>

Zdrojové kódy

Lekce 25

<<< Lekce 24 | Lekce 26 >>>