Lekce 47 - CG vertex shader

Používání vertex a fragment (pixel) shaderů ke "špinavé práci" při renderingu může mít nespočet výhod. Nejvíce je vidět např. pohyb objektů do teď výhradně závislý na CPU, který neběží na CPU, ale na GPU. Pro psaní velice kvalitních shaderů poskytuje CG (přiměřeně) snadné rozhraní. Tento tutoriál vám ukáže jednoduchý vertex shader, který sice něco dělá, ale nebude předvádět ne nezbytné osvětlení a podobné složitější nadstavby. Tak jako tak je především určen pro začátečníky, kteří už mají nějaké zkušenosti s OpenGL a zajímají se o CG.

Hned na začátku uvedu dvě internetové adresy, které by se vám mohli hodit. Jedná se o http://developer.nvidia.com/ a http://www.cgshaders.org/.

Překl.: Perfektní článek o vertex a pixel shaderech vyšel v časopise CHIP 01/2004: Hardwarový Fotorealismus - Možnosti moderních 3D grafických akcelerátorů (str. 96 - 100).

Poznámka: účelem tohoto tutoriálu není naučit úplně všechno o psaní vertex shaderů používajících CG. Má v úmyslu vysvětlit, jak úspěšně nahrát a spustit vertex shader v OpenGL.

Nastavení

První krok spočívá v downloadu CG kompilátoru od nVidie. Protože existují rozdíly mezi verzemi 1.0 a 1.1, dbejte na to, abyste si stáhli ten novější. Kód přeložený pro jeden nemusí pracovat i s druhým. Rozdíly jsou např. v rozdílně pojmenovaných proměnných, nahrazených funkcích a podobně.

Dále musíme nahrát hlavičkové a knihovní soubory CG na místo, kde je může Visual Studio najít. Protože ze zásady nedůvěřuji instalátorům, které povětšinou pracují jinak, než se očekává, osobně dávám přednost ručnímu kopírování knihovních souborů

z: C:\Program Files\NVIDIA Corporation\Cg\lib

do: C:\Program Files\Microsoft Visual Studio\VC98\Lib

a hlavičkových souborů

z: C:\Program Files\NVIDIA Corporation\Cg\include

do: C:\Program Files\Microsoft Visual Studio\VC98\Include

CG Tutoriál

Informace o CG uvedené v tomto tutoriálu byly většinou získány z CG uživatelského manuálu (CG Toolkit User's Manual).

Existuje několik podstatných bodů, které byste si měli provždy zapamatovat. První a nejdůležitější je, že se vertex program provede na KAŽDÉM vertexu, který předáte grafické kartě. Jediná možnost, jak ho spustit nad několika zvolenými vertexy je buď ho nahrávat/mazat individuálně pro každý vertex nebo posílat vertexy do proudu, ve kterém budou ovlivněny a do proudu, kde nebudou. Výstup vertex programu je předán fragment (pixel) shaderu. To platí pouze tehdy, pokud je implementován a zapnut. Za poslední si zapamatujte, že se vertex program provede nad vertexy předtím, než se vytvoří primitiva. Fragment shader je na rozdíl od toho vykonán až po rasterizaci.

Pojďme se konečně podívat na tutoriál. Vytvoříme prázdný textový soubor a pojmenujeme ho wave.cg. Do něj budeme psát veškerý CG kód. Nejdříve vytvoříme datové struktury, které budou obsahovat všechny proměnné a informace potřebné pro shader.

Každá ze všech tří proměnných struktury (pozice, barva a hodnota vlny) je následována předdefinovaným jménem (POSITION, COLOR0, COLOR1). Tato předdefinovaná jména se vztahují k sémantice jazyka. Specifikují mapování vstupů do přesně určených hardwarových registrů. Mimochodem, jedinou opravdu požadovanou vstupní proměnnou do vertex programu je position.

struct appdata

{

float4 position : POSITION;

float4 color: COLOR0;

float3 wave: COLOR1;

};

Dále vytvoříme strukturu vfconn. Ta bude obsahovat výstup vertex programu, který se po rasterizace předá fragment shaderu. Stejně jako vstupy mají i výstupy předdefinovaná jména. HPos reprezentuje pozici transformovanou do homogenního souřadnicového systému a Col0 určuje barvu vertexu změněnou v programu.

struct vfconn

{

float4 HPos: POSITION;

float4 Col0: COLOR0;

};

Zbývá nám pouze napsat vertex program. Funkce se definuje stejně jako v jazyce C. Má návratový typ (struktura vfconn), jméno (main, ale může jím být i jakékoli jiné) a parametry. V našem příkladě ze vstupu převezmeme strukturu appdata, která obsahuje pozici vertexu, jeho barvu a hodnotu výšky pro vytvoření sinusových vln. Dostaneme také uniformní parametr, kterým je aktuální modelview matice. Potřebujeme ji pro transformaci pozice do homogenního souřadnicového systému.

vfconn main(appdata IN, uniform float4x4 ModelViewProj)

{

Do proměnné OUT uložíme modifikované vstupní parametry a na konci programu ji vrátíme.

vfconn OUT;// Výstup z vertex shaderu (posílá se na fragment shader, pokud je dostupný)

Vypočítáme pozici na ose y v závislosti na x a z pozici vertexu. X i z vydělíme pěti (respektive čtyřmi), přechody budou jemnější. Změňte hodnoty na 1.0, abyste viděli, co myslím. Proměnná IN.wave specifikovaná hlavním programem obsahuje stále se zvětšující hodnotu, která způsobí, že se sinusová vlna rozpohybuje přes celý mesh. Y pozici spočítáme z pozice v meshi jako sinus hodnoty vlny plus aktuální x nebo z pozice. Aby byla výsledná vlna vyšší, vynásobíme ještě výsledek číslem 2,5.

// Změna y pozice v závislosti na sinusové vlně

IN.position.y = (sin(IN.wave.x + (IN.position.x / 5.0)) + sin(IN.wave.x + (IN.position.z / 4.0))) * 2.5f;

Nastavíme výstupní proměnné našeho vertex programu. Nejdříve transformujeme novou pozici vertexu do homogenního souřadnicového systému a potom přiřadíme výstupní barvě hodnotu vstupní. Pomocí return předáme vše fragment shaderu (pokud je zapnutý).

OUT.HPos = mul(ModelViewProj, IN.position);// Transformace pozice na homogenní souřadnice

OUT.Col0.xyz = IN.color.xyz;// Nastavení barvy

return OUT;

}

OpenGL Tutoriál

V tuto chvíli máme vertex program běžící na grafické kartě hotov. Můžeme se pustit do hlavního programu. Vytvoříme v něm rovinný mesh poskládaný z trojúhelníků (triangle stripů), které budeme posílat na grafickou kartu. Na ní se ovlivní y pozice každého vertexu tak, aby ve výsledku vznikly pohybující se sinusové vlny.

V první řadě inkludujeme hlavičkové soubory, které v OpenGL umožní spustit CG shader. Musíme také říct Visual Studiu, aby přilinkovalo potřebné knihovní soubory.

#include <windows.h>// Windows

#include <gl\gl.h>// OpenGL

#include <gl\glu.h>// GLU

#include <cg\cg.h>// CG hlavičky

#include <cg\cggl.h>// CG hlavičky specifické pro OpenGL

#include "NeHeGL.h"// NeHe OpenGL

#pragma comment(lib, "opengl32.lib")// Přilinkování OpenGL

#pragma comment(lib, "glu32.lib")// Přilinkování GLU

#pragma comment(lib, "cg.lib")// Přilinkování CG

#pragma comment(lib, "cggl.lib")// Přilinkování OpenGL CG

#define TWO_PI 6.2831853071// PI * 2

GL_Window* g_window;// Struktura okna

Keys* g_keys;// Klávesnice

Symbolická konstanta SIZE určuje velikost meshe na osách x a z. Dále vytvoříme proměnnou cg_enable, která bude oznamovat, jestli má být vertex program zapnutý nebo vypnutý. Pole mesh slouží pro uložení dat meshe a wave_movement pro vytvoření sinusové vlny.

#define SIZE 64// Velikost meshe

bool cg_enable = TRUE, sp;// Flag spuštění CG

GLfloat mesh[SIZE][SIZE][3];// Data meshe

GLfloat wave_movement = 0.0f;// Pro vytvoření sinusové vlny

Následují proměnné pro CG. CGcontext slouží jako kontejner pro několik CG programů. Obecně stačí pouze jeden CGcontext bez ohledu na počet vertex a fragment programů, které využíváme. Z jednoho kontextu můžete pomocí funkcí cgGetFirstProgram() a cgGetNextProgram() zvolit libovolný program. CG profile definuje profil vertexů. CG parametry zprostředkovávají vazbu mezi hlavním programem a CG programem běžícím na grafické kartě. Každý CG parameter je handle na korespondující proměnnou v shaderu.

CGcontext cgContext;// CG kontext

CGprogram cgProgram;// CG vertex program

CGprofile cgVertexProfile;// CG profil

CGparameter position, color, modelViewMatrix, wave;// Parametry pro shader

Deklaraci globálních proměnných máme za sebou, pojďme se podívat na inicializační funkci. Po obvyklých nastaveních zapneme vykreslování drátěných modelů. Používáme je z důvodu, že vyplněné polygony nevypadají bez světel dobře. Pomocí dvou vnořených cyklů inicializujeme pole mesh tak, aby se střed roviny nacházel v počátku souřadnicového systému. Pozici na ose y nastavíme u všech bodů na 0.0f, sinusovou deformaci má na starosti CG program.

BOOL Initialize(GL_Window* window, Keys* keys)// Inicializace

{

g_window = window;// Okno

g_keys = keys;// Klávesnice

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

glClearDepth(1.0f);// Mazání hloubky

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

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

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

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Nastavení perspektivy

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);// Drátěný model

for (int x = 0; x < SIZE; x++)// Inicializace meshe

{

for (int z = 0; z < SIZE; z++)

{

mesh[x][z][0] = (float) (SIZE / 2) - x;// Vycentrování na ose x

mesh[x][z][1] = 0.0f;// Plochá rovina

mesh[x][z][2] = (float) (SIZE / 2) - z;// Vycentrování na ose z

}

}

Musíme také inicializovat CG, jako první vytvoříme kontext. Pokud funkce vrátí NULL, něco selhalo, chyby většinou nastávají kvůli nepovedené alokaci paměti. Zobrazíme chybovou zprávu a vrátíme false, čímž ukončíme i celý program.

cgContext = cgCreateContext();// Vytvoření CG kontextu

if (cgContext == NULL)// OK?

{

MessageBox(NULL, "Failed To Create Cg Context", "Error", MB_OK);

return FALSE;

}

Pomocí cgGLGetLatestProfile() určíme minulý profil vertexů, za typ profilu předáme CG_GL_VERTEX. Kdybychom vytvářeli fragment shader, předávali bychom CG_GL_FRAGMENT. Pokud není žádný vhodný profil dostupný, vrátí funkce CG_PROFILE_UNKNOWN. S validním profilem můžeme zavolat cgGLSetOptimalOptions(). Tato funkce se používá pokaždé, když se překládá nový CG program, protože podstatně optimalizuje kompilaci shaderu v závislosti na aktuálním grafickém hardwaru a jeho ovladačích.

cgVertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX);// Získání minulého profilu vertexů

if (cgVertexProfile == CG_PROFILE_UNKNOWN)// OK?

{

MessageBox(NULL, "Invalid profile type", "Error", MB_OK);

return FALSE;

}

cgGLSetOptimalOptions(cgVertexProfile);// Nastavení profilu

Zavoláme funkci cgCreateprogramFromFile(), čímž načteme a zkompilujeme CG program. První parametr specifikuje CG kontext, ke kterému bude program připojen. Druhý parametr určuje, že soubor obsahuje zdrojový kód (CG_SOURCE) a ne objektový kód předkompilovaného programu (CG_OBJECT). Jako třetí položka se předává cesta k souboru, čtvrtý je minulým profilem pro konkrétní typ programu (vertex profil pro vertex program, fragment profil pro fragment program). Pátý parametr specifikuje vstupní funkci do programu, její jméno může být libovolné, ne pouze main(). Poslední parametr slouží pro předání přídavných argumentů kompilátoru. Většinou se dává NULL.

Pokud z nějakého důvodu funkce selže, získáme pomocí cgGetError() typ chyby. Do řetězcové podoby ho můžeme převést prostřednictvím cgGetErrorString().

// Nahraje a zkompiluje vertex shader

cgProgram = cgCreateProgramFromFile(cgContext, CG_SOURCE, "CG/Wave.cg", cgVertexProfile, "main", 0);

if (cgProgram == NULL)// OK?

{

CGerror Error = cgGetError();// Typ chyby

MessageBox(NULL, cgGetErrorString(Error), "Error", MB_OK);

return FALSE;

}

Nahrajeme zkompilovaný program a připravíme ho pro zvolení (binding).

cgGLLoadProgram(cgProgram);// Nahraje program do grafické karty

Jako poslední krok inicializace získáme handle na proměnné, se kterými bude CG program manipulovat. Pokud daná proměnná neexistuje, cgGetNamedParameter() vrátí NULL. Neznáme-li jména parametrů, můžeme použít dvojici funkcí cgGetFirstParameter() a cgGetNextParameter().

// Handle na proměnné

position = cgGetNamedParameter(cgProgram, "IN.position");

color = cgGetNamedParameter(cgProgram, "IN.color");

wave = cgGetNamedParameter(cgProgram, "IN.wave");

modelViewMatrix = cgGetNamedParameter(cgProgram, "ModelViewProj");

return TRUE;

}

Pomocí deinicializační funkce po sobě uklidíme. Jednoduše zavoláme cgDestroyContext() pro každý CGcontext proměnnou. Také bychom mohli smazat jednotlivé CG programy, k tomu slouží funkce cgDestroyProgram(), nicméně cgDestroyContext() je smaže automaticky.

void Deinitialize(void)// Deinicializace

{

cgDestroyContext(cgContext);// Smaže CG kontext

}

Do aktualizační funkce přidáme kód pro ošetření stisku mezerníku, který zapíná/vypíná CG program běžící na grafické kartě.

void Update(float milliseconds)// Aktualizace

{

if (g_keys->keyDown[VK_ESCAPE])// Stisk Esc

{

TerminateApplication(g_window);// Konec programu

}

if (g_keys->keyDown[VK_F1])// Stisk F1

{

ToggleFullscreen(g_window);// Přepnutí do/z fullscreenu

}

if (g_keys->keyDown[' '] && !sp)// Stisk mezerníku

{

sp = TRUE;

cg_enable = !cg_enable;// Zapne/vypne CG program

}

if (!g_keys->keyDown[' '])

{

sp = FALSE;

}

}

A jako poslední vykreslování. Kamerou se přesuneme o 45 jednotek před počátek souřadnicového systému a nahoru o 25 jednotek.

void Draw(void)// Vykreslování

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže obrazovku

glLoadIdentity();// Reset matice

gluLookAt(0.0f, 25.0f, -45.0f, 0.0f, 0.0f, 0.0f, 0, 1, 0);// Pozice kamery

Modelview matici vertex shaderu nastavíme na aktuální OpenGL matici. Bez toho bychom nemohli přepočítávat pozici vertexů do homogenních souřadnic.

// Nastavení modelview matice v shaderu

cgGLSetStateMatrixParameter(modelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);

Pokud je flag cg_enable v true, voláním cgGLEnableProfile() aktivujeme předaný profil. Funkce cgGLBindProgram() zvolí náš program a dokud ho nevypneme, provede se nad každým vertexem poslaným na grafickou kartu. Také musíme poslat barvu vertexů.

if (cg_enable)// Zapnout CG shader?

{

cgGLEnableProfile(cgVertexProfile);// Zapne profil

cgGLBindProgram(cgProgram);// Zvolí program

cgGLSetParameter4f(color, 0.5f, 1.0f, 0.5f, 1.0f);// Nastaví barvu (světle zelená)

}

Tak teď jsme konečně připraveni na rendering meshe. Pro každou hodnotu souřadnice x v cyklu vykreslíme proužek roviny seskládaný triangle stripem.

for (int x = 0; x < SIZE - 1; x++)// Vykreslení meshe

{

glBegin(GL_TRIANGLE_STRIP);// Každý proužek jedním triangle stripem

for (int z = 0; z < SIZE - 1; z++)

{

Současně s renderovanými vertexy dynamicky předáme i hodnotu wave parametru, díky kterému bude moci CG program z roviny vygenerovat sinusové vlny. Jakmile grafická karta dostane všechna data, automaticky spustí CG program. Všimněte si, že do triangle stripu posíláme dva body, to má za následek, že se nevykreslí pouze trojúhelník, ale rovnou celý čtverec.

cgGLSetParameter3f(wave, wave_movement, 1.0f, 1.0f);// Parametr vlny

glVertex3f(mesh[x][z][0], mesh[x][z][1], mesh[x][z][2]);// Vertex

glVertex3f(mesh[x+1][z][0], mesh[x+1][z][1], mesh[x+1][z][2]);// Vertex

wave_movement += 0.00001f;// Inkrementace parametru vlny

if (wave_movement > TWO_PI)// Větší než dvě pí (6,28)?

{

wave_movement = 0.0f;// Vynulovat

}

}

glEnd();// Konec triangle stripu

}

Po dokončení renderingu otestujeme, jestli je cg_enable rovno true a pokud ano, vypneme vertex profil. Dále můžeme kreslit cokoli chceme, aniž by to bylo ovlivněno CG programem.

if (cg_enable)// Zapnutý CG shader?

{

cgGLDisableProfile(cgVertexProfile);// Vypne profil

}

glFlush();// Vyprázdnění renderovací pipeline

}

napsal: Owen Bourne <o.bourne (zavináč) griffith.edu.au>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Zdrojové kódy

Lekce 47

<<< Lekce 46 | Lekce 48 >>>