Cel-Shading je druh vykreslování, při kterém výsledné modely vypadají jako ručně kreslené karikatury z komiksů (cartoons). Rozličné efekty mohou být dosaženy miniaturní modifikací zdrojového kódu. Cel-Shading je velmi úspěšným druhem renderingu, který dokáže kompletně změnit duch hry. Ne ale vždy... musí se umět a použít s rozmyslem.
Článek o teorii Cel-Shadingu napsal Sami "MENTAL" Hamlaoui a umístil ho na GameDev.net. Poté, co byl jeho článek publikován, zavalili čtenáři Samiho emaily, ve kterých se dotazovali po zdrojovém kódu. Napsal tedy další článek, tentokrát pro NeHe, který už ale popisuje pouze zdrojový kód. Tato česká verze je složena z obou článků - teoretického i praktického.
Předtím než půjdete dál, měli byste mít dostatečné znalosti z následujících oblastí:
Pokud něčemu z těchto čtyř položek nerozumíte, neznamená to, že byste nutně neporozuměli Cel-Shadingu, ale určitě budete mít obrovské potíže s psaním vlastních programů.
Začneme opravdu jednoduchými věcmi. Žádná světla, žádné obrysy, pouze ploché cartoon modely. Budeme potřebovat jenom dva druhy dat - pozici a barvu každého vertexu. Před kreslením vždy vypneme osvětlení a blending. Co by se stalo? Při zapnutých světlech by objekty vypadaly normálně. Nedosáhli bychom plochého cartoon efektu. Blending vypínáme, aby se jednotlivé vertexy nesmíchaly s ostatními.
Každý vertex bude potřebovat i další data. Kromě původní pozice a barvy budeme používat i normálový vektor a intenzitu osvětlení (jedna float hodnota). Tyto nové proměnné použijeme pro renderování se základním osvětlením.
Nechci vás poplést, pod lightmapami si nepředstavujte simulaci světel na objektech typu Quake 1 a Quake 2. Podívejte se na stěny, abyste pochopili, co mám na mysli. Nepředstavujte si oblasti, které jsou osvětleny/ztmaveny specifickými místy map. To, co budeme používat zde, je kompletně novou formou lightmap - 1D textury.
Zkuste si najít nějakou animaci (Cartoon Network je vždy dobrým zdrojem) a podívejte se na osvětlení postav. Všimli jste si, že nejsou hladké jako v reálném životě? Světlo se rozděluje do jednotlivých plošek. Nikdy jsem neslyšel žádný termín nebo pojmenování pro tento efekt, takže mu budeme říkat Sharp lighting. Abychom ho vytvořili potřebujeme definovat 1D texturu, která bude ukládat požadované hodnoty.
Toto je textura 1x16 pixelů (velmi zvětšená). Používáme hodnoty stupňů šedi, protože budou zkombinovány s barvou vertexu. Můžete si všimnout, že v lightmapě jsou pouze 3 barvy, které mají podobnou intenzitu, jaká se používá v animovaných filmech. Díky tomu, že používáme velikost právě 16 pixelů, můžeme snadno modifikovat hodnoty, abychom vytvořili rozličné efekty. Pokud chcete, můžete také použít obyčejnou černobílou texturu, ale nedoporučuje se to. Nikdy byste neměli použít 100% černou, protože tato barva vytváří vyzdvižení a okraje, které vypadají dost špatně.
Jakmile máte vytvořenu svou texturu, nahrajte ji do API, které používáte (OpenGL, DX, software). Vrátíme se k ní za chvíli.
Teď přijdou vhod znalosti ohledně softwarového osvětlení. Pokusím se vše vysvětlit jednoduchým jazykem. Vždy se ujistěte, že máte normalizovaný směrový vektor světla! Vše, co potřebujeme udělat, je spočítání skalárního součinu vektoru světla s normálou vertexu.
Skalární součin je matematická funkce, která spočítá úhel mezi dvěma vektory a vrátí ho jako kosinus úhlu. Invertujeme-li kosinus získáme úhel. Namísto kosinu však považujte číslo za texturovací koordinátu. Texturovací koordináty jsou čísla od nuly do jedné. Kosinus je sice v rozmezí -1 až 1, ale pokud bude číslo záporné můžeme mu přiřadit nulu. Skalární součin vektoru světla a normály vertexu můžeme tedy považovat za texturovací koordináty!
Nyní máme texturovací koordináty každého vertexu, je čas vykreslit objekt. Stejně jako minule vypneme světla i blending, ale zapneme 1D texturování. Vykreslíme objekt stejně jako minule, ale před tím, než umístíme vertex, specifikujeme texturové koordináty (simulace světla).
Tato metoda je pouhou modifikací minulého postupu. Umístitelné světlo nabízí mnohem více flexibility než směrové osvětlení, protože může být libovolně posunováno po scéně. Dynamicky osvětlované polygony jsou více realistické, ale použitá matematika je delší. Ne komplikovanější, pouze delší.
U směrového osvětlení jsme potřebovali získat skalární součin směrového vektoru světla s normálou vertexu. Nyní, protože umístitelná světla nemají směrový vektor (emitují světlo do všech směrů), bude mít každý vertex svůj paprsek, který září skrz něj. Nejdříve potřebujeme určit vektor směřující z pozice světelného zdroje k pozici vertexu. Normalizujeme ho, takže bude mít jednotkovou délku. tím jsme získali směr světla k vertexu. Vypočítáme skalární součin mezi vektorem světla a normálou vertexu. Vše opakujeme pro každý vertex ve scéně. Těmito nadbytečnými výpočty se však sníží FPS. Pojďme se podívat na rychlejší metodu, která redukuje celkový počet osvětlených vertexů.
Ke snížení počtu osvětlených vertexů přiřadíme každému světlu poloměr kam paprsky dosahují. Před počítáním hodnot osvětlení (viz. výše) zkontrolujeme, jestli je vertex v kouli, určené poloměrem světla. Pokud ano, aplikujeme na něj světla. Je to základní detekce kolizí s koulí, na kterou existují spousty článků a tutoriálů.
Objekt vykreslíme stejně jako u směrového osvětlení. Specifikujeme barvu, texturovací koordináty a pozici.
Obrysy a zvýraznění jsou tenké černé linky reprezentující tahy tužkou, které zdůrazňují okraje. Můžou znepokojit, ale jejich vytvoření je mnohem jednodušší než si myslíte.
Pravidlo je jednoduché: vykreslit linku na okraji, který má jeden přivrácený a jeden odvrácený polygon. Zní to hloupě, ale zkuste se podívat například na klávesnici. Všimli jste si, že nemůžete vidět zadní části kláves? To proto, že jsou odvrácené. Na rozhraní vykreslíme čáru, abychom zvýraznili, že tam je okraj.
Možná, že si to ani neuvědomujete, ale nikde jsem se nezmínil o našem vlastním cullingu polygonů. To proto, že vše za nás udělá API, ve kterém programujeme.
Klasicky vykreslíme objekt a pak nastavíme šířku čáry na dva až tři pixely. Můžeme také zapnout antialiasing. Změníme mód cullingu, aby odstraňoval přivrácené polygony. Přepneme do drátěného modelu, takže se budou vykreslovat pouze okrajové hrany polygonů. Vykreslíme je, ale nepotřebujeme specifikovat barvu a texturovací koordináty. Tím vykreslíme drátěný model objektu z relativně širokých linek. Nicméně... cullingem jsou linky přivrácených polygonů odstraněny a depth bufferem se vyřadí všechny linky, které jsou hlouběji než přivrácené (tedy ty zadní). Zdálo by se, že tedy nevykreslíme nic. Ale díky šířce čáry zasahují linky okrajových polygonů až za okraje objektu. Právě ty se vykreslí. Z toho plyne, že tato metoda nebude pracovat při tloušťce čáry nastavené na jeden pixel.
To je z teorie asi všechno. Nyní se ji pokusíme převést do praxe.
Na začátku bych se chtěl omluvit za volbu použitého modelu, ale v poslední době si hodně hraji s Quake 2...
#include <windows.h>// Hlavičkový soubor pro Windows
#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
#include <math.h>// Hlavičkový soubor pro matematickou knihovnu
#include <stdio.h>// Hlavičkový soubor pro standardní vstup/výstup
#include "NeHeGL.h"// Hlavičkový soubor pro NeHeGL
Nadefinujeme pár struktur, které nám pomohou při ukládání dat. První z těchto struktur je tagMATRIX. Pokud se na ni podíváte, zjistíte, že ukládá matici jako jednorozměrné pole 16ti floatů místo toho, aby to bylo dvourozměrné pole 4x4. To je proto, že OpenGL pracuje taky s jednorozměrným polem. Pokud bychom použili 4x4, hodnoty by byly ve špatném pořadí.
typedef struct tagMATRIX// Ukládá OpenGL matici
{
float Data[16];// Matice ve formátu OpenGL
}
MATRIX;
Další strukturou je vektor, který ukládá jednotlivé x, y, z složky na osách.
typedef struct tagVECTOR// Struktura vektoru
{
float X, Y, Z;// Složky vektoru
}
VECTOR;
Třetí je vertexová struktura. Každý vertex se bude skládat z pozice a normály (žádné texturovací koordináty). Složky struktury musí být v uvedeném pořadí, jinak se při loadování stane něco OPRAVDU strašného (sám jsem kvůli tomu rozsekal celý tento kód, abych našel chybu).
typedef struct tagVERTEX// Struktura vertexu
{
VECTOR Nor;// Normála vertexu
VECTOR Pos;// Pozice vertexu
}
VERTEX;
Nakonec struktura polygonu. Vím, že toto není nejlepší způsob, jak ukládat vertexy, ale pro jednoduchost to stačí. Normálně bych použil pole vertexů a pole polygonů obsahujících indexy vertexů tvořících polygon, ale my to uděláme jinak - vše pro jednoduchost.
typedef struct tagPOLYGON// Struktura polygonu
{
VERTEX Verts[3];// Pole tří vertexů
}
POLYGON;
Nádherně jednoduchá část kódu. Přečtěte si komentář ke každé proměnné a budete vědět, proč jsme ji deklarovali.
bool outlineDraw = true;// Flag pro vykreslování obrysu
bool outlineSmooth = false;// Flag pro vyhlazování čar
float outlineColor[3] = { 0.0f, 0.0f, 0.0f };// Barva čar
float outlineWidth = 3.0f;// Tloušťka čar
VECTOR lightAngle;// Směr světla
bool lightRotate = false;// Flag oznamující zda rotujeme světlem
float modelAngle = 0.0f;// Úhel natočení objektu na ose y
bool modelRotate = false;// Flag na otáčení modelem
POLYGON* polyData = NULL;// Data polygonů
int polyNum = 0;// Počet polygonů
GLuint shaderTexture[1];// Místo pro jednu texturu
Model je uložen úplně nejjednodušším způsobem. Prvních pár bajtů obsahuje počet polygonů tvořících objekt a zbytek souboru je pole tagPOLYGON struktur. Proto může následující funkce data přímo načíst bez jakéhokoliv dalšího upravování.
BOOL ReadMesh()// Načte obsah souboru model.txt
{
FILE *In = fopen("Data\\model.txt", "rb");// Otevře soubor
if (!In)// Kontrola chyby otevření
return FALSE;
fread(&polyNum, sizeof(int), 1, In);// Načte hlavičku souboru (počet vertexů)
polyData = new POLYGON[polyNum];// Alokace paměti
fread(&polyData[0], sizeof(POLYGON) * polyNum, 1, In);// Načte všechna data
fclose(In);// Zavře soubor
return TRUE;// Loading objektu úspěšný
}
Funkce DotProduct() spočítá úhel mezi dvěma vektory nebo rovinami. Funkce Magnitude() spočítá délku vektoru a funkce Normalize() upraví vektor na jednotkovou délku.
inline float DotProduct(VECTOR &V1, VECTOR &V2)// Spočítá odchylku dvou vektorů
{
return V1.X * V2.X + V1.Y * V2.Y + V1.Z * V2.Z;// Vrátí úhel
}
inline float Magnitude(VECTOR &V)// Spočítá délku vektoru
{
return sqrtf(V.X * V.X + V.Y * V.Y + V.Z * V.Z);// Vrátí délku vektoru
}
void Normalize(VECTOR &V)// Vytvoří jednotkový vektor
{
float M = Magnitude(V);// Spočítá aktuální délku vektoru
if (M != 0.0f)// Proti dělení nulou
{
V.X /= M;// Normalizování jednotlivých složek
V.Y /= M;
V.Z /= M;
}
}
Funkce RotateVector() pootočí vektor podle zadané matice. Všimněte si, že vektor pouze otočí, ale už nic nedělá s jeho pozicí. Funkce se používá pro otáčení normál, aby zajistila, že normály budou při počítání osvětlení ukazovat správným směrem.
void RotateVector(MATRIX &M, VECTOR &V, VECTOR &D)// Rotace vektoru podle zadané matice
{
D.X = (M.Data[0] * V.X) + (M.Data[4] * V.Y) + (M.Data[8] * V.Z);// Otočení na x
D.Y = (M.Data[1] * V.X) + (M.Data[5] * V.Y) + (M.Data[9] * V.Z);// Otočení na y
D.Z = (M.Data[2] * V.X) + (M.Data[6] * V.Y) + (M.Data[10] * V.Z);// Otočení na z
}
První významnější funkcí tohoto enginu je Initialize(), která provádí to, co je z jejího názvu zjevné - inicializaci.
BOOL Initialize (GL_Window* window, Keys* keys)// Uživatelská a OpenGL inicializace
{
int i;// Řídící proměnná cyklů
Následující 3 proměnné jsou použity pro načtení shader souboru. Line obsahuje jeden řádek řetězce a pole shaderData ukládá hodnoty pro shading. Používáme 96 hodnot namísto 32, protože potřebujeme převést stupně šedi na hodnoty RGB, aby s nimi mohlo OpenGL pracovat. Můžeme sice hodnoty uložit jako stupně šedi, ale bude jednodušší když při nahrávání textury použijeme stejné hodnoty pro jednotlivé složky RGB.
char Line[255];// Pole 255 znaků
float shaderData[32][3];// Pole 96 shader hodnot
FILE *In = NULL;// Ukazatel na soubor
Klasické nastavení enginu a OpenGL...
g_window = window;
g_keys = keys;
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Perspektivní korekce
glClearColor(0.7f, 0.7f, 0.7f, 0.0f);// Světle šedé pozadí
glClearDepth(1.0f);// Nastavení hloubkového bufferu
glEnable(GL_DEPTH_TEST);// Zapne testování hloubky
glDepthFunc(GL_LESS);// Typ testování hloubky
Při vykreslování čar chceme, aby byly pěkně vyhlazené. Implicitně je tato funkce vypnuta, ale stiskem klávesy 2 ji můžeme zapínat a vypínat podle libosti.
glShadeModel(GL_SMOOTH);// Jemné stínování
glDisable(GL_LINE_SMOOTH);// Vypne vyhlazování čar
Zapneme ořezávání vnitřních stěn objektu, které stejně nejsou vidět a vypneme OpenGL světla, protože potřebné výpočty provedeme po svém.
glEnable(GL_CULL_FACE);// Zapne face culling (ořezávání stěn)
glDisable(GL_LIGHTING);// Vypne světla
V další části kódu načteme shader soubor. Obsahuje pouze 32 desetinných čísel uložených, pro jednoduchou modifikaci, v ASCII formátu, každé na samostatném řádku.
In = fopen("Data\\shader.txt", "r");// Otevření shader souboru
if (In)// Kontrola, zda je soubor otevřen
{
for (i = 0; i < 32; i++)// Projde všech 32 hodnot ve stupních šedi
{
if (feof(In))// Kontrola konce souboru
break;
fgets(Line, 255, In);// Získání aktuálního řádku
Přeměníme načtené stupně šedi na RGB, jak jsme si popsali výše.
// Zkopíruje danou hodnotu do všech složek barvy
shaderData[i][0] = shaderData[i][1] = shaderData[i][2] = float(atof(Line));
}
fclose(In);// Zavře soubor
}
else
{
return FALSE;// Neúspěch
}
Nahrajeme texturu přesně tak, jak je. Bez použití filtrování, jinak by výsledek vypadal opravdu hnusně, přinejmenším. Použijeme GL_TEXTURE_1D, protože jde o jednorozměrné pole hodnot.
glGenTextures(1, &shaderTexture[0]);// Získání ID textury
glBindTexture(GL_TEXTURE_1D, shaderTexture[0]);// Přiřazení textury; od teď je 1D texturou
// Nikdy nepoužívejte bi-/trilinearní filtrování!
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB, 32, 0, GL_RGB , GL_FLOAT, shaderData);// Upload dat
Nastavíme směr dopadání světla na objekt ze směru kladné části osy z. Ve svém důsledku to znamená, že světlo bude zepředu svítit na model.
lightAngle.X = 0.0f;// Nastavení směru x
lightAngle.Y = 0.0f;// Nastavení směru y
lightAngle.Z = 1.0f;// Nastavení směru z
Normalize(lightAngle);// Normalizování vektoru světla
Načtení tvaru ze souboru (funkce popsána výše).
return ReadMesh();// Vrátí návratovou hodnotu funkce ReadMesh()
}
Funkce Deinitialize() je pravým opakem předchozí funkce. Smaže texturu a data polygonů nahraná pomocí funkcí Initialize() a ReadMesh().
void Deinitialize(void)// Deinicializace
{
glDeleteTextures(1, &shaderTexture[0]);// Smaže shader texturu
delete [] polyData;// Uvolní data polygonů
}
Funkce Update() se periodicky volá v hlavní smyčce tohoto dema. Jedinou její funkcí je zpracování vstupu z klávesnice.
void Update(DWORD milliseconds)// Aktualizace scény (objektu)
{
if (g_keys->keyDown [VK_ESCAPE] == TRUE)// Klávesa ESC
{
TerminateApplication (g_window);// Ukončení programu
}
if (g_keys->keyDown [VK_F1] == TRUE)// Klávesa F1
{
ToggleFullscreen(g_window);// Přepnutí módů fullscreen/okno
}
if (g_keys->keyDown [' '] == TRUE)// Mezerník
{
modelRotate = !modelRotate;// Zapne/vypne rotaci objektu
g_keys->keyDown [' '] = FALSE;
}
if (g_keys->keyDown ['1'] == TRUE)// Klávesa čísla 1
{
outlineDraw = !outlineDraw;// Zapne/vypne vykreslování obrysu
g_keys->keyDown ['1'] = FALSE;
}
if (g_keys->keyDown ['2'] == TRUE)// Klávesa číslo 2
{
outlineSmooth = !outlineSmooth;// Zapne/vypne anti-aliasing
g_keys->keyDown ['2'] = FALSE;
}
if (g_keys->keyDown [VK_UP] == TRUE)// Šipka nahoru
{
outlineWidth++;// Zvětší tloušťku čáry
g_keys->keyDown [VK_UP] = FALSE;
}
if (g_keys->keyDown [VK_DOWN] == TRUE)// Šipka dolů
{
outlineWidth--;// Zmenší tloušťku čáry
g_keys->keyDown [VK_DOWN] = FALSE;
}
if (modelRotate)// Je rotace zapnutá
modelAngle += (float)(milliseconds) / 10.0f;// Aktualizace úhlu natočení v závislosti na FPS
}
Funkce, na kterou už určitě netrpělivě čekáte. Draw() provádí většinu nejdůležitější práce v tomto tutoriálu - počítá hodnoty stínu, renderuje daný tvar a renderuje obrys.
void Draw(void)// Vykreslování
{
int i, j;// Řídící proměnné cyklů
Proměnná TmpShade se použije na uložení dočasné hodnoty stínu pro aktuální vertex. Všechna data týkajícího se jednoho vertexu jsou spočítána ve stejném čase, což znamená, že můžeme použít jen jednu proměnnou, kterou postupně použijeme pro všechny vertexy. Struktury TmpMatrix, TmpVector a TmpNormal jsou také použity pro spočítání dat jednoho vertexu. TmpMatrix se nastaví vždy jednou při startu funkce Draw() a nezmění se až do jejího dalšího startu. TmpVector a TmpNormal se liší vertex od vertexu.
float TmpShade;// Dočasná hodnota stínu
MATRIX TmpMatrix;// Dočasná MATRIX struktura
VECTOR TmpVector, TmpNormal;// Dočasné VECTOR struktury
Po deklaraci proměnných vymažeme buffery a data OpenGL matice.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Vymaže buffery
glLoadIdentity();// Reset matice
Nejdříve zkontrolujeme zda chceme obrys vyhlazený. Když ano, zapneme anti-aliasing. Když ne, tak ho vypneme. Jak jednoduché...
if (outlineSmooth)// Chce uživatel vyhlazené čáry?
{
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);// Použije nejkvalitnější výpočty
glEnable(GL_LINE_SMOOTH);// Zapne anti-aliasing
}
else// Nechce
{
glDisable(GL_LINE_SMOOTH);// Vypne anti-aliasing
}
Posunutím kamery o 2 jednotky dozadu nastavíme pohled, potom model pootočíme o daný úhel. Poznámka: protože jsme nejdříve pohnuli s kamerou, model se bude točit na místě. Pokud bychom to udělali opačně, model by rotoval kolem kamery.
glTranslatef(0.0f, 0.0f, -2.0f);// Posun do hloubky
glRotatef(modelAngle, 0.0f, 1.0f, 0.0f);// Rotace objektem na ose y
Získáme nově vytvořenou OpenGL matici a uložíme ji do TmpMatrix.
glGetFloatv(GL_MODELVIEW_MATRIX, TmpMatrix.Data);// Získání matice
Kouzla začínají... Povolíme 1D texturování a použijeme texturu stínu. Potom nastavíme barvu modelu. Vybral jsem bílou, protože na ní jde lépe vidět světlo a stín než na ostatních barvách. Nejméně vhodná je zcela určitě černá.
// Kód Cel-Shadingu
glEnable(GL_TEXTURE_1D);// Zapne 1D texturování
glBindTexture(GL_TEXTURE_1D, shaderTexture[0]);// Zvolí texturu
glColor3f(1.0f, 1.0f, 1.0f);// Nastavení barvy modelu (bílá)
Začneme s kreslením trojúhelníků. Projdeme všechny polygony v poli a všechny vertexy každého z těchto polygonů. Nejdříve zkopírujeme normálu do dočasné struktury. Díky tomu můžeme hodnotami normály otáčet bez toho, že bychom ztratili původní data (bez průběžné degradace).
glBegin(GL_TRIANGLES);// Začátek kreslení trojúhelníků
for (i = 0; i < polyNum; i++)// Prochází jednotlivé polygony
{
for (j = 0; j < 3; j++)// Prochází jednotlivé vertexy
{
// Zkopírování aktuální normály do dočasné struktury
TmpNormal.X = polyData[i].Verts[j].Nor.X;
TmpNormal.Y = polyData[i].Verts[j].Nor.Y;
TmpNormal.Z = polyData[i].Verts[j].Nor.Z;
Otočíme vektor o matici, kterou jsme získali od OpenGL a normalizujeme ho.
RotateVector(TmpMatrix, TmpNormal, TmpVector);// Otočí vektor podle matice
Normalize(TmpVector);// Normalizace normály
Spočítáme odchylku pootočené normály a směru světla. Potom hodnotu dáme do rozmezí 0-1 (z původního -1 až 1).
TmpShade = DotProduct(TmpVector, lightAngle);// Spočítání hodnoty stínu
if (TmpShade < 0.0f)// Pokud je TmpShade menší než nula bude se rovnat nule
TmpShade = 0.0f;
Předáme tuto hodnotu OpenGL jako texturovací souřadnici. Potom předáme pozici vertexu a opakujeme. A opakujeme. A opakujeme. Myslím, že podstatu už chápete.
glTexCoord1f(TmpShade);// Nastavení texturovací souřadnice na hodnotu stínu
glVertex3fv(&polyData[i].Verts[j].Pos.X);// Pošle pozici vertexu
}
}
glEnd();// Konec kreslení
glDisable(GL_TEXTURE_1D);// Vypne 1D texturování
Přesuneme se k obrysům. Obrys můžeme definovat jako hranu, kde je jeden polygon přivrácen směrem k nám a druhý od nás. Použijeme pro OpenGL běžné testování hloubky - méně nebo stejně (GL_LEQUAL) a také nastavíme vyřazování všech polygonů otočených k nám. Také použijeme blending, aby to trochu vypadalo.
Nastavíme OpenGL tak, aby polygony čelem od nás vyrenderoval jako čáry. Vyřadíme všechny polygony čelem k nám a nastavíme testování hloubky na menší nebo stejné na aktuální ose Z. Potom ještě nastavíme barvu čar, projdeme všechny polygony a vykreslíme jejich rohy. Stačí zadat pozici. Nemusíme zadávat normálu a stíny, protože chceme jenom obrys.
// Kód pro vykreslení obrysů
if (outlineDraw)// Chceme vůbec kreslit obrys?
{
glEnable(GL_BLEND);// Zapne blending
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);// Mód blendingu
glPolygonMode(GL_BACK, GL_LINE);// Odvrácené polygony se stanout pouze obrysovými čarami
glLineWidth(outlineWidth);// Nastavení šířky čáry
glCullFace(GL_FRONT);// Nerenderovat přivrácené polygony
glDepthFunc(GL_LEQUAL);// Mód testování hloubky
glColor3fv(&outlineColor[0]);// Barva obrysu (černá)
glBegin(GL_TRIANGLES);// Začátek kreslení trojúhelníků
for (i = 0; i < polyNum; i++)// Prochází jednotlivé polygony
{
for (j = 0; j < 3; j++)// Prochází jednotlivé vertexy
{
glVertex3fv(&polyData[i].Verts[j].Pos.X);// Pošle pozici vertexu
}
}
glEnd();// Konec kreslení
Na konci už jenom vrátíme nastavení do původního stavu a ukončíme funkci i tutoriál.
glDepthFunc(GL_LESS);// Testování hloubky na původní nastavení
glCullFace(GL_BACK);// Nastavení ořezávání na původní hodnotu
glPolygonMode(GL_BACK, GL_FILL);// Normální vykreslování
glDisable(GL_BLEND);// Vypne blending
}
}
napsal: Sami "MENTAL" Hamlaoui <disk_disaster (zavináč) hotmail.com>
teoretickou část přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>
praktickou část přeložil: Václav Slováček - Wessan <horizont (zavináč) host.sk>