Kurz DirectX (43.)

V dnešní lekci se podrobně podíváme na vertex shader. Poznáte jeho strukturu, jeho assembler a jak se s ním pracuje z prostředí C++ tzn. z našeho programu. Na závěr si uvedeme několik příkladů použití vertex shaderu a zamyslíme se, jak jej využít v našem projektu.

43.1. Vertex shader

Jak už jsme si řekli v minulé lekci, vertex shader představuje procesor, který zpracovává vertexy na vstupu a na výstup vyhazuje stejný počet nějak upravených (transformovaných a obarvených) vertexů.

Vertex shader má jako každý jiný procesor sadu registrů, výpočetní jednotku (ALU) a pochopitelně také instrukční sadu. Abyste byli schopni napsat smysluplný program je třeba, abyste znali popis registrů a instrukční sadu. Veškeré potřebné informace najdete v tomto článku. Předem chci upozornit, že se zde budu zabývat vertex shaderem verze 1.1, který je již obsažen v DirectX 8.1 (verzí 2.0 se budeme zabývat v budoucnu).

Nejprve se podíváme na blokové schéma vertex shaderu, které můžete najít v dokumentaci MSDN:

Každý registr ve vertex shaderu je reprezentován 128-bitovými hodnotami rozdělenými do čtyřech floatů (4*32 bitů):

Takové registry jsou velice výhodné, protože v drtivé většině pracujeme s vektory (poloha, normála, barva) nebo s maticemi (transformace). Matice 4x4 je pak uložena ve 4 registrech.

Vstupní registry v0 a v15 jsou pouze pro čtení a mohou obsahovat libovolná data z vertex streamu (my v našem projektu používáme pouze jeden vertex stream, ale většina grafických karet podporuje až 8 různých zdrojů). Data ve vstupních registrech jsou dána mapováním z našeho programu.

Další skupinou registrů jsou konstantní registry c0-c95, které jsou do vertex shaderu nahrány z programu před samotným kreslením (metodou SetVertexShaderConstant()). Do těchto registrů nelze zapisovat z vertex shaderu, zde jsou pouze pro čtení. Konstantní registry se hodí na předávání parametrů z programu (například poloha a směr světla, poloha kamery, interpolační koeficienty apod.). Omezením je, že lze použít pouze jeden konstantní registr na instrukci. Do konstantních registrů lze přistupovat také nepřímo pomocí registru a0 (v a0.x bude uložen index konstantního registru).

Dostáváme se k tzv. temporary registrům t0-t11 (nejvhodněji přeloženo jako odkládací registry). Tyto registry slouží k uložení mezivýsledků uvnitř vertex shaderu, lze do nich data zapisovat a následně je číst.

Poslední skupinou registrů jsou výstupní registry, kam vertex shader ukládá transformovaná data. Jedná se o tyto registry:

Název
Význam
oPos Poloha transformovaného vertexu. Tento vektor musí být vždy zapsán.
oD0 Difusní barva vertexu (složky RGB, případně alpha).

oD1 Speculární barva vertexu.
oPts Velikost bodu (při používání point sprites).
oFog Tento registr bude obsahovat tzv. faktor mlhy, podle kterého se posléze počítá per-vertex mlha.
oTn Texturové souřadnice (těchto registrů je 8, pro každý stupeň souřadnic jeden registr).

Všechny výstupní registry jsou pouze pro zápis! Pro naše jednoduché účely si vystačíme s polohou, barvou a texturovými souřadnicemi.

Nejjednodušší vertex shader by měl provést transformaci vertexu světovou, pohledovou a projekční maticí. V praxi se pak do vertex shaderu předává jedna matice jako výsledek násobení těchto tří matic (čili není potřeba uvnitř vertex shaderu provádět složité násobení matic, ještě navíc pro každý vertex znovu).

43.2. Assembler vertex shaderu verze 1.1

Na začátku jsem zmínil, že se zde budu zabývat vertex shaderem 1.1. Jeho instrukční sada je velice omezená. Většina instrukcí, které známe z běžných procesorů zde schází. Naopak jsou zde speciální instrukce, které se hodí pro výpočet osvětlení apod. Délka programu vertex shaderu je omezena podle použité grafické karty nejčastěji na 128 instrukcí.

Vertex shader verze 2.0, ke kterému se doufám časem dostaneme, je o mnoho instrukcí rozšířen. Umožňuje například skoky, podmíněné příkazy nebo smyčky, takže program ve vertex shaderu se může libovolně větvit (což u základní verze dost dobře nejde). Jediné omezení které tu platí, je délka programu (zde už grafické karty dovolují okolo 256 instrukcí). Takže vše co napíšeme, se bude odvíjet od verze vertex shaderu!

Dříve než nějaký vertex shaderový program napíšeme, udělám rychlý přehled instrukcí možných ve verzi 1.1:

Název
Parametry
Použití a význam
add dest, src1, src2

Sečte registr src1 a src2 a výsledek uloží do registru dest. Registry jsou zpracovány po složkách:

dest.x = src1.x + src2.x
dest.y = src1.y + src2.y
dest.z = src1.z + src2.z
dest.w = src1.w + src2.w

dp3 dest, src1, src2

Třísložkový skalární součin dvou vektorů:

dest.x = dest.y = dest.z = dest.w = (src1.x * src2.x) + (src1.y * src2.y) + (src1.z * src2.z)

dp4 dest, src1, src2

Čtyřsložkový skalární součin dvou vektorů:

dest.w = (src1.x * src2.x) + (src1.y * src2.y) + (src1.z * src2.z) + (src1.w * src2.w);
dest.x = dest.y = dest.z = nepoužity

dst dest, src1, src2

Instrukce dst pracuje následujícím způsobem: src1 se bere jako vektor (-, d*d, d*d, -), src2 jako vektor (-, 1/d, - , 1/d). Výsledný registr dest pak vypadá následovně:

dest.x = 1
dest.y = src1.y * src2.y
dest.z = src1.z
dest.w = src2.w

Pomocí této instrukce se počítá útlum bodového světla.

expp dest, src.w

Exponenciální funkce s 10-bitovou přesností

Výsledný registr dest po této instrukci vypadá takto:

dest.x = 2 **(int) src.w ; hodnota funkce 2src.w celé části vstupu
dest.y = mantisa(src.w)  ; zlomková část vstupní hodnoty
dest.z = expp(src.w)     ; méně přesná hodnota funkce 2src.w
dest.w = 1.0

logp dest, src.w

Logaritmus s 10-bitovou přesností

Výsledný registr dest po této instrukci vypadá takto:

dest.x = exponent((int)src.w) ; hodnota funkce log2( src.w ) celé části vstupu
dest.y = mantisa(src.w)       ; zlomková část vstupní hodnoty
dest.z = log2(src.w)          ; méně přesná hodnota funkce log2( src.w )
dest.w = 1.0

lit dest, src.w

Tato instrukce počítá koeficienty difusního a spekulárního osvětlení. Vstupní registr musí být nastaven před použitím instrukce následujícím způsobem:

src.x = N*L ; Skalární součin normálového vektoru a směru světla.
src.y = N*H ; Skalární součin normálového vektoru a half-vektoru spekulárního světla. Vektor H je určen takto: H = (L + V) / 2, kde L je vektor světla a V vektor pozorovatele (obojí vůči vertexu).
src.z = tato hodnota je na vstupu ignorována
src.w = intenzita spekulárního světla. Hodnota musí být v rozmezí mezi -128 a +128.

Výsledek instrukce:

src.x = 1.0
src.y = max (src.x, 0.0)
if (src.x > 0.0 && src.w == 0.0)
   dest.z = 1.0;
else if (src.x > 0.0 && src.y > 0.0)
   dest.z = (src.y)src.w
dest.w = 1.0;

Obě tyto hodnoty se běžně používají pro transformaci barvy vertexu (difusní či spekulární). Ve složce dest.y je tedy koeficient pro difusní světlo a v dest.z pro spekulární světlo.

mad dest, src1, src2, src3 dest = (src1 * src2) + src3
max dest, src1, src2 dest = (src1 >= src2) ? src1 : src2
min dest, src1, src2 dest = (src1 < src2) ? src1 : src2
mov dest, src dest <= src
mul dest, src1, src2 dest = src1 * src2
nop - prázdná instrukce
rcp dest, src.w

if(src.w == 1.0f)
{
   dest.x = dest.y = dest.z = dest.w = 1.0f;
}
else if(src.w == 0)
{
   dest.x = dest.y = dest.z = dest.w = PLUS_INFINITY();
}
else
{
   dest.x = dest.y = dest.z = m_dest.w = 1.0f/src.w;
}

Tato instrukce lze využít na dělení:

; scalar r0.x = r1.x/r2.x
RCP r0.x, r2.x
MUL r0.x, r1.x, r0.x

rsq dest, src float v = ABSF(src.w);

if(v == 1.0f)
{
   dest.x = dest.y = dest.z = dest.w = 1.0f;
}
else if(v == 0)
{
   dest.x = dest.y = dest.z = dest.w = PLUS_INFINITY();
}
else
{
   v = (float)(1.0f / sqrt(v)); dest.x = dest.y = dest.z = dest.w = v;
}

Tato instrukce lze použít, pokud chceme vypočítat druhou odmocninu:

; scalar r0.x = sqrt(r1.x)
RSQ r0.x, r1.x
MUL r0.x, r0.x, r1.x

sge dest, src1, src2 dest = (src1 >= src2) ? 1 : 0
slt dest, src1, src2 dest = (src1 < src2) ? 1 : 0

Všechny tyto instrukce zaberou ve vertex shaderu jeden slot, což znamená, že můžete použít až 128 těchto instrukcí v jednom programu. Většina z nich je provedena v jednom taktu hodin grafického procesoru, kromě rsq a rcp. Ty mohou trvat dva takty a proto je vhodné nepoužít výsledek těchto instrukcí hned v následující instrukci jako operand. Jinak totiž dojde k jevu nazvanému register stall, což znamená, že program ve vertex shaderu se na jeden takt zastaví a čeká se na výsledek instrukce rcp nebo rsq.

Kromě těchto rychlých instrukcí, je definováno několik "maker", čili o něco málo složitějších instrukcí, které mohou některé operace zjednodušit. Je ale třeba dát pozor na to, že tyto instrukce zaberou více jak jeden slot a trvají mnohem více hodinových taktů, takže snadno překročíte limit 128 instrukcí nebo bude váš program příliš časově náročný. V následující tabulce se na tyto speciální instrukce podíváme:

Název Parametry Význam Počet hodinových cyklů
exp dest, src1 exponenciální funkce s přesností na 20 bitů 12
log dest, src1 logaritmus s přesností na 20 bitů 12
frc dest, src1 zlomková část vstupního registru (pro každou složku) 3
m3x2 dest, src1 , src2 transformace vstupního vektoru maticí 3x2 2
m3x3 dest, src1 , src2 transformace vstupního vektoru maticí 3x3 3
m3x4 dest, src1 , src2 transformace vstupního vektoru maticí 3x4 4
m4x3 dest, src1 , src2 transformace vstupního vektoru maticí 4x3 3
m4x4 dest, src1 , src2 transformace vstupního vektoru maticí 4x4 4

Může se zdát, že instrukční sada vertex shaderu (1.1) je hodně omezená. 17 instrukcí není mnoho. Přesto s touto sadou je možno napsat téměř libovolný výpočet. Už jsme si uvedli dělení pomocí instrukce rcp a mul nebo odmocniny instrukcí rsq a mul.

Dále si povíme o dalších několika modifikátorech, které rozšiřují funkčnost vertex shaderu. Například odčítání můžete napsat instrukcí add a modifikátorem - (unární minus). Další důležitou vlastností je tzv. swizzling a masking.

Jistě jste si už všimli, že abychom se dostali ke komponentě x určitého registru, použijeme zápis reg.x nebo třeba reg.z pro složku z (reg je název registru). Tento přístup potřebujeme zejména proto, že některé instrukce pracují pouze se skalárními hodnotami. Modifikátor swizzling (o překlad se raději pokoušet nebudu) vám navíc umožní změnit pořadí složek zdrojového vektoru. Například mějme instrukci:

Z obrázku vidíme, že komponenta x výsledku je tvořena součtem z-ových složek zdrojových registrů. Dále y složka je tvořena součtem y složky v0 a z složky v1 atd. Pomocí swizzlingu tedy můžeme prohazovat (případ registru v0) nebo rozšiřovat (registr v1) operandy.

Druhý často používaný modifikátor je maskování. Narozdíl od swizllingu, který lze použít pouze u zdrojových registrů, masking lze aplikovat pouze na cílové registry (oba modifikátory lze libovolně kombinovat). Příklad maskingu:

add r0.xy, v0, v1

Tato instrukce sečte komponenty x a y registrů v0 a v1 a výsledky uloží do komponenty x a y registru r0 (ostatní složky se ignorují). Další příklad:

mov r0.wz, v0.xx

Tato instrukce uloží skalární hodnotu x z registru v0 do komponenty z a w registru r0.

Na závěr ještě jeden praktický příklad vektorového součinu dvou vektorů:

; r0 = r1 x r2 (3-vector cross-product)
mul r0, r1.yzxw, r2.zxyw
mad r0, -r2.yzxw, r1.zxyw, r0

43.3. Závěr

A je tu opět konec lekce. Dnes jsme si pověděli o instrukční sadě vertex shaderu. V příští lekci nabyté informace využijeme v našem projektu. Chtěl bych vytvořit jednoduchou iluzi refrakce vodní hladiny tak, že se vertexy terénu, které jsou pod hladinou budou pohybovat směrem nahoru a dolu. Zde bude potíž, jak uvnitř vertex shaderu rozeznat, zda-li je vertex pod hladinou či nad. Ale o tom až v příští lekci.

Jiří Formánek