Lekce 34 - Generování terénů a krajin za použití výškového mapování textur

Chtěli byste vytvořit věrnou simulaci krajiny, ale nevíte, jak na to? Bude nám stačit obyčejný 2D obrázek ve stupních šedi, pomocí kterého deformujeme rovinu do třetího rozměru. Na první pohled těžko řešitelné problémy bývají častokrát velice jednoduché.

Nyní byste už měli být opravdovými experty na OpenGL, ale možná nevíte, co to je výškové mapování (height mapping). Představte si rovinu, vytlačenou podle nějaké formy do 3D prostoru. Této formě se říká výšková mapa, kterou může být defakto jakýkoli typ dat. Obrázky, textové soubory nebo třeba datový proud zvuku - záleží jen na vás. My budeme používat .RAW obrázek ve stupních šedi.

Definujeme tři opravdu důležité symbolické konstanty. MAP_SIZE představuje rozměr mapy, v našem případě se jedná o šířku/výšku obrázku (1024x1024). Konstanta STEP_SIZE určuje velikost kroků při grabování hodnot z obrázku. V současné chvíli bereme v úvahu každý šestnáctý pixel. Zmenšením čísla přidáváme do výsledného povrchu polygony, takže vypadá méně hranatě, ale zároveň zvyšujeme náročnost na rendering. HEIGHT_RATIO slouží jako měřítko výšky na ose y. Malé číslo zredukuje vysoké hory s údolími na plochou rovinu.

#define MAP_SIZE 1024// Velikost .RAW obrázku výškové mapy

#define STEP_SIZE 16// Hustota grabování pixelů

#define HEIGHT_RATIO 1.5f// Zoom výšky terénu na ose y

Proměnná bRender představuje přepínač mezi pevnými polygony a drátěným modelem, scaleValue určuje zoom scény na všech třech osách.

bool bRender = TRUE;// Polygony - true, drátěný model - false

float scaleValue = 0.15f;// Měřítko velikosti terénu (všechny osy)

Deklarujeme jednorozměrné pole pro uložení všech dat výškové mapy. Používaný .RAW obrázek neobsahuje RGB složky barvy, ale každý pixel je tvořen jedním bytem, který specifikuje jeho odstín. Nicméně o barvu se starat nebudeme, jde nám především o hodnoty. Číslo 255 bude představovat nejvyšší možný bod povrchu a nula nejnižší.

BYTE g_HeightMap[MAP_SIZE * MAP_SIZE];// Ukládá data výškové mapy

Funkce LoadRawFile() nahrává RAW soubor s obrázkem. Nic komplexního! V parametrech se jí předává řetězec diskové cesty, velikost dat obrázku a ukazatel na paměť, do které se ukládá. Otevřeme soubor pro čtení v binárním módu a ošetříme situaci, kdy neexistuje.

void LoadRawFile(LPSTR strName, int nSize, BYTE* pHeightMap)// Nahraje .RAW soubor

{

FILE *pFile = NULL;// Handle souboru

pFile = fopen(strName, "rb");// Otevření souboru pro čtení v binárním módu

if (pFile == NULL)// Otevření v pořádku?

{

MessageBox(NULL, "Can't Find The Height Map!", "Error", MB_OK);

return;

}

Pomocí fread() načteme po jednom bytu ze souboru pFile data o velikosti nSize a uložíme je do paměti na lokaci pHeightMap. Vyskytne-li se chyba, vypíšeme varovnou zprávu.

fread(pHeightMap, 1, nSize, pFile);// Načte soubor do paměti

int result = ferror(pFile);// Výsledek načítání dat

if (result)// Nastala chyba?

{

MessageBox(NULL, "Failed To Get Data!", "Error", MB_OK);

}

Na konci zbývá už jenom zavřít soubor.

fclose(pFile);// Zavření souboru

}

Kód pro inicializaci OpenGL byste měli bez problémů pochopit sami.

int InitGL(GLvoid)// Inicializace OpenGL

{

glShadeModel(GL_SMOOTH);// Jemné stínování

glClearColor(0.0f, 0.0f, 0.0f, 0.5f);// Černé pozadí

glClearDepth(1.0f);// Nastavení hloubkového bufferu

glEnable(GL_DEPTH_TEST);// Zapne testování hloubky

glDepthFunc(GL_LEQUAL);// Typ testování hloubky

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Perspektivní korekce

Před vrácením true ještě do g_HeightMap nahrajeme .RAW obrázek.

LoadRawFile("Data/Terrain.raw", MAP_SIZE * MAP_SIZE, g_HeightMap);// Načtení dat výškové mapy

return TRUE;// Vše v pořádku

}

Máme zde jeden problém - uložili jsme dvourozměrný obrázek do jednorozměrného pole. Co s tím? Funkce Height() provede výpočet pro transformaci x, y souřadnic na index do tohoto pole a vrátí hodnotu, která je na něm uložená. Při práci s poli bychom se vždy měli start o možnost přetečení paměti. Jednoduchým trikem zmenšíme vysoké hodnoty tak, aby byly vždy platné. Pokud některá z hodnot přesáhne daný index, zbytek po dělení ji zmenší do rozmezí, které můžeme bez obav použít. Dále otestujeme, jestli se v poli opravdu nacházejí data.

int Height(BYTE *pHeightMap, int X, int Y)// Přepočítá 2D souřadnice na 1D a vrátí uloženou hodnotu

{

int x = X % MAP_SIZE;// Proti přetečení paměti

int y = Y % MAP_SIZE;

if(!pHeightMap)// Obsahuje paměť data?

{

return 0;

}

Aby se jednorozměrné pole chovalo jako dvojrozměrné, musíme zapojit trochu matematiky. Index do 1D pole na 2D souřadnicích získáme tak, že vynásobíme řádek (y) jeho šířkou (MAP_SIZE) a přičteme konkrétní pozici na řádku (x).

return pHeightMap[(y * MAP_SIZE) + x];// Vrátí hodnotu z pole

}

Na tomto místě nastavujeme barvu vertexu podle aktuální výšky nad height mapou. Získáme hodnotu na indexu pole a dělením 256.0f ji zmenšíme do rozmezí 0.0f až 1.0f. Abychom ji ještě trochu ztmavili, odečteme -0.15f. Výsledek předáme funkci glColor3f() jako modrou složku barvy.

void SetVertexColor(BYTE *pHeightMap, int x, int y)// Získá barvu v závislosti na výšce

{

if(!pHeightMap)// Obsahuje paměť data?

{

return;

}

// Získání hodnoty, přepočet do rozmezí 0.0f až 1.0f, ztmavení

float fColor = (Height(pHeightMap, x, y) / 256.0f) - 0.15f;

glColor3f(0, 0, fColor);// Odstíny modré barvy

}

Dostáváme se k nejpodstatnější části celého tutoriálu - renderování terénu. Proměnné X, Y slouží k procházejí výškové mapy a x, y, z jsou 3D souřadnicemi vertexu.

void RenderHeightMap(BYTE pHeightMap[])// Renderuje terén

{

int X = 0, Y = 0;// Pro procházení polem

int x, y, z;// Souřadnice vertexů

if(!pHeightMap)// Obsahuje paměť data?

{

return;

}

Podle logické hodnoty bRender připínáme mezi vykreslováním obdélníků a linek.

if(bRender)// Co chce uživatel renderovat?

{

glBegin(GL_QUADS);// Polygony

}

else

{

glBegin(GL_LINES);// Drátěný model

}

Založíme dva vnořené cykly, které procházejí jednotlivé pixely výškové mapy. Vnější se stará o osu x a vnitřní o osu y, z čehož plyne, že vykreslujeme po sloupcích a ne po řádcích. Všimněte si, že po každém průchodu nezvětšujeme řídící proměnnou o jeden pixel, ale hned o několik najednou. Sice výsledný terén nebude tak hladký a přesný, ale díky menšímu počtu polygonů se rendering urychlí. Pokud by se STEP_SIZE rovnalo jedné, každému pixelu by se přiřadil jeden polygon. Myslím, že číslo šestnáct bude vyhovující, ale pokud zapnete světla, které zvýrazňují hranatost povrchu, měli byste ho snížit.

Překl.: Úplně nejlepší by bylo, kdyby se velikost kroku určovala před vstupem do cyklů podle aktuálního FPS. Následující ukázkový kód zavádí zpětnovazební regulační smyčku.

// Překl.: Regulace počtu polygonů

// if(FPS < 30)// Nižší hodnoty => viditelné trhání pohybů animace

// {

// if(STEP_SIZE > 1)// Dolní mez (1 pixel)

// {

// STEP_SIZE--;// Musí být proměnnou a ne symbolickou konstantou

// }

// }

// else

// {

// if(STEP_SIZE < MAP_SIZE-1)// Horní mez (velikost výškové mapy)

// {

// STEP_SIZE++;// Musí být proměnnou a ne symbolickou konstantou

// }

// }

for (X = 0; X < MAP_SIZE; X += STEP_SIZE)// Řádky výškové mapy

{

for (Y = 0; Y < MAP_SIZE; Y += STEP_SIZE)// Sloupce výškové mapy

{

Přepokládám, že to, jak určit pozici vertexu, jste už dávno vytušili. Hodnota na ose x odpovídá x-ové souřadnici výškové mapy a na ose z y-ové. Získali jsme umístění bodu na rovině, potřebujeme ho ještě vyzdvihnout do výšky, které v OpenGL odpovídá osa y. Tato výška je definována hodnotou uloženou na daném prvku pole (světlostí obrázku). Opravdu nic složitého...

// Souřadnice levého dolního vertexu

x = X;

y = Height(pHeightMap, X, Y );

z = Y;

Určíme barvu bodu podle výšky nad rovinou. Čím výše se nachází, tím bude světlejší. Potom pomocí funkce glVertex3i() předáme OpenGL souřadnice vertexu.

SetVertexColor(pHeightMap, x, z);// Barva vertexu

glVertex3i(x, y, z);// Definování vertexu

Druhý vertex určíme přičtením STEP_SIZE k ose z. Na tomto místě se budeme nacházet při příštím průchodu cyklem, takže se mezi jednotlivými polygony nebudou vyskytovat mezery. Analogicky získáme i další dva body obdélníku. Nyní mi už věříte, když jsem na začátku tutoriálu psal, že složitě vypadající věci bývají často velice jednoduché?

// Souřadnice levého horního vertexu

x = X;

y = Height(pHeightMap, X, Y + STEP_SIZE );

z = Y + STEP_SIZE ;

SetVertexColor(pHeightMap, x, z);// Barva vertexu

glVertex3i(x, y, z);// Definování vertexu

// Souřadnice pravého horního vertexu

x = X + STEP_SIZE;

y = Height(pHeightMap, X + STEP_SIZE, Y + STEP_SIZE );

z = Y + STEP_SIZE ;

SetVertexColor(pHeightMap, x, z);// Barva vertexu

glVertex3i(x, y, z);// Definování vertexu

// Souřadnice pravého dolního vertexu

x = X + STEP_SIZE;

y = Height(pHeightMap, X + STEP_SIZE, Y );

z = Y;

SetVertexColor(pHeightMap, x, z);// Barva vertexu

glVertex3i(x, y, z);// Definování vertexu

}

}

glEnd();// Konec kreslení

Po vykreslení terénu reinicializujeme barvu na bílou, abychom neměli starosti s barvou ostatních objektů ve scéně (netýká se tohoto dema).

glColor4f(1.0f, 1.0f, 1.0f, 1.0f);// Reset barvy

}

Na začátku DrawGLScene() začneme klasicky smazáním bufferů a resetem matice.

int DrawGLScene(GLvoid)// Vykreslení OpenGL scény

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Vymaže buffery

glLoadIdentity();// Reset matice

Pomocí funkce gluLookAt() umístíme a natočíme kameru tak, aby byl renderovaný terén v záběru. První tři parametry určují její pozici vzhledem k počátku souřadnicového systému, další tři body reprezentují místo, kam je natočená a poslední tři představují vektor vzhůru. V našem případě se nacházíme nad sledovaným terénem a díváme se na něj trochu dolů (55 je menší než 60) spíše doleva (186 je menší než 212). Hodnota 171 představuje vzdálenost od kamery na ose z. Protože se hory zvedají od zdola nahoru, nastavíme u vektoru vzhůru jedničku na ose y. Ostatní dvě hodnoty zůstanou na nule.

Při prvním použití může být gluLookAt() trochu odstrašující, asi jste zmateni. Nejlepší radou je pohrát si se všemi hodnotami, abyste viděli, jak se pohled na scénu postupně mění. Pokud byste například přepsal pozici z 60 na 120, viděli byste terén spíše seshora než z boku, protože se stále díváte na souřadnice 55.

Praktický příklad: Řekněme, že jste vysoký kolem 1,8 m. Oči, které reprezentují kameru, jsou trochu níže - 1,7 m. Stojíte před stěnou, která je vysoká pouze 1 m, takže bez problémů vidíte její horní stranu. Pokud ale zedníci dostaví stěnu do výšky tří metrů, budete se muset dívat VZHŮRU, ale její vrch už NEUVIDÍTE. Výhled se změnil podle toho, jestli se díváte dolů nebo vzhůru (respektive jestli jste nad nebo pod objektem).

// Umístění a natočení kamery

gluLookAt(212,60,194, 186,55,171, 0,1,0);// Pozice, směr, vektor vzhůru

Aby byl výsledný terén poněkud menší, změníme měřítko souřadnicových os. Protože navíc násobíme y-ovou hodnotu, budou se hory jevit vyšší. Mohli bychom také použít translace a rotace, ale to už nechám na vás.

glScalef(scaleValue, scaleValue * HEIGHT_RATIO, scaleValue);// Zoom terénu

Pomocí dříve napsané funkce vyrenderujeme terén.

RenderHeightMap(g_HeightMap);// Renderování terénu

return TRUE;// Vše v pořádku

}

Kliknutím levého tlačítka myši může uživatel přepnout mezi renderováním polygonů a linek (drátěný model).

// Funkce WndProc()

case WM_LBUTTONDOWN:// Levé tlačítko myši

{

bRender = !bRender;// Přepne mezi polygony a drátěným modelem

return 0;// Konec funkce

}

Šipkami nahoru a dolů zvětšujeme/zmenšujeme měřítko scény a tím i velikost terénu.

// Funkce WinMain()

if (keys[VK_UP])// Šipka nahoru

{

scaleValue += 0.001f;// Vyvýší hory

}

if (keys[VK_DOWN])// Šipka dolů

{

scaleValue -= 0.001f;// Sníží hory

}

Tak to je všechno, výškovým mapováním textur jsme naprogramovali nádherou krajinu, která je ale zabarvená do modra. Zkuste si nakreslit texturu (letecký pohled), která reprezentuje zasněžené vrcholy hor, louky, jezera a podobně a namapujte ji na terén. Texturovací koordináty získáte vydělením pozice na rovině rozměrem obrázku (zmenšení hodnot do rozsahu 0.0f až 1.0f). Plazmovými efekty a rolováním se může krajina dynamicky měnit. Déšť a sníh zajistí částicové systémy, které už také znáte. Vložíte-li krajinu do skyboxu, nikdo nepozná, že se jedná o počítačový model a ne o video animaci.

Nebo můžete vytvořit mořskou hladinu s vlnami, na kterých se pohupuje uplavaný míč (výšku nad mořským dnem přece znáte - hodnota na indexu v poli). Nechte uživatele, ať ho může ovládat. Možnosti jsou bez hranic...

napsal: Ben Humphrey - DigiBen
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Zdrojové kódy

Lekce 34

<<< Lekce 33 | Lekce 35 >>>