Kurz DirectX (46.)

Minule jsem slíbil, že se podíváme na další možnosti jazyka HLSL. Rozšíříme tedy práci s proměnnými a povíme si jaký je rozdíl mezi standardním násobením (*) a intristickou funkcí mul(), když pracujeme s vektory nebo maticemi. V druhé části nakousneme problematiku pixel shaderu. Ten se na první pohled může zdát složitější než vertex shader, ale užitím jazyka HLSL si práci výrazně zjednodušíme.

46.1. Proměnné v HLSL

V minulé lekci jsme ve zkratce probrali jak definovat skalární proměnnou, vektor a matici. Dnes si vše zopakujeme a o něco málo rozšíříme.

Základní typy

Pokud chceme deklarovat proměnnou, vždy musíme použít některý ze základních typů (anebo jak uvidíme později definovat vlastní strukturu). Základní typy jsou: bool, int, half, float a double. Z těchto typů se budou skládat i struktury (podobně jako v jazyku C).

Typ bool představuje standardní Boolean hodnotu, která může nabývat hodnot true nebo false. Typ int je 32-bitový celočíselný typ tak jak jej známe z ostatních programovacích jazyků. Na některých systémech je tento typ emulován pomocí reálného typu. V tomto případě předpoklad, že výpočet urychlíme celočíselnou aritmetikou, bude mylný. Jistě si pamatujete na registry vertex shaderu, které byly všechny reálné (kromě adresového registru).
Typy half, float a double jsou reálné. Liší se tedy pouze velikostí nebo chcete-li rozsahem. Half je 16-bitový, float má 32 bitů a poslední double dokonce 64-bitů. Opět je zde důležitá poznámka, že grafické karty podporují většinou pouze 32-bitové registry (například všechny registry vertex shaderu jsou 32-bitové) čili ostatní typy half a double se emulují pomocí floatu. Z toho plyne, že i když použijete typ half v domnění, že ušetříte paměť, ve výsledném programu se použije cely jeden registr, který má 32-bitů. Je tu ovšem možnost, že se do takového registru vejdou dvě proměnné typu half.

Vektor

Vektorový typ můžete definovat hned dvěma způsoby. Jeden jsme si ukázali v minulé lekci:

"typ""počet komponent" "jméno proměnné";

Tedy například třísložkový vektor, kde každá složka bude typu float:

float3 prikladVektor;

Uvědomte si, že vektor je vždy homogenní (podobně jako pole v C++) tj. všechny složky budou mít stejný typ narozdíl od struktury.

Nyní si ukážeme druhý způsob:

vector <typ, počet komponent> "jméno proměnné";

Stejný příklad, tedy třísložkový vektor typu float:

vector <float, 3> prikladVektor;

Obě deklarace jsou zcela ekvivalentní takže záleží na vás, který způsob se vám více zamlouvá. Proměnnou můžete v deklaraci rovnou inicializovat. V případě skalární proměnné se jedná o triviální případ, takže si rovnou ukážeme, jak taková inicializace vypadá u vektoru:

float3 fVector = { 0.2f, 0.3f, 0.4f };

nebo

vector <float, 3> fVector = { 0.2f, 0.3f, 0.4f };

Je to vlastně opět stejné jako při inicializaci pole v C++. Ke složkám vektoru můžeme přistupovat přímo podobně jako v assembleru a to hned dvěma způsoby. Můžete použít složky x, y, z, w nebo r, g, b, a. Oba způsoby však nejdou mixovat.

Příklad:

float4 vektor;

vektor.x = 9;
vektor.yz = 0;

vektor.a = 5;
vektor.rg = 0;

ale nelze:

vektor.ay = 0;

Jistě si pamatuje na swizzling a masking z vertex shaderu. I zde se pomocí složek vektoru můžeme při čtení použít swizzling, tedy číst jen určité složky anebo masking čili zápis jen určitých složek. Vše si ukážeme na příkladu.

Swizzling:

float4 vektor4d;
float2 vektor2d;

vektor2d = vektor4d.xz;
vektor2d = vektor4d.ar;

A masking:

vektor4d.xy = vektor2d;
vektor4d.ab = vektor2d;

Ale nelze zapsat jedna složka vícekrát:

vektor4d.rr = vektor2d;

Na závěr této části se podíváme, jak funguje násobení v případě vektorů. Pokud použijete standardní násobení pomocí hvězdičky, výsledkem bude opět vektor, kde každá složka vznikne násobením příslušných složek zdrojových vektorů:

float4 v = a*b;

v.x = a.x*b.x;
v.y = a.y*b.y;
v.z = a.z*b.z;
v.w = a.w*a.w;

Často ovšem potřebujeme implementovat skalární součin vektorů, který vypadá následovně:

v = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;

V tomto případě použijeme intristickou funkci mul(). Tato funkce vrací pro dva vektory, které musí mít stejný počet komponent, jejich skalární součin.

Matice

Podívejme se nyní na matice. Zjistíme, že pravidla uvedená u vektorů platí i pro matice. Tedy i matici můžete deklarovat dvěma způsoby a rovněž ji můžete inicializovat při deklaraci.

Podívejme se rovnou na příklad. Deklarujeme matici 4x4 typu float (to může být například transformační matice):

float4x4 prikladMatice;

nebo

matrix <float, 4, 4> prikladMatice;

První číslo v deklaraci znamená počet řádků, druhé pak počet sloupců matice. Pokud tedy deklarujete matici:

float1x4 matice;

jedná se vlastně o řádkový vektor. Kdybychom čísla prohodili, definovali bychom sloupcový vektor (což je vlastně transpozice).

Inicializaci provedeme nasledovně:

float2x2 fMatrix = { 0.0f, 0.1, // row 1
                     2.1f, 2.2f // row 2
                   };

Podobně jako u vektorů můžeme přistupovat k jednotlivým složkám matice dvěma způsoby:

a) číslování od nuly
_m00, _m01, _m02, _m03
_m10, _m11, _m12, _m13
_m20, _m21, _m22, _m23
_m30, _m31, _m32, _m33

b) číslování od jedničky
_11, _12, _13, _14
_21, _22, _23, _24
_31, _32, _33, _34
_41, _42, _43, _44

V obou případech první číslo značí číslo řádku a druhé číslo sloupce.

Například:

float4x4 matice;

matice._11 = 1;
matice._m00_m11_m22_m33 = 1;
// slozky se daji takto skladat podobne jako u vektoru

Vzhledem k tomu, že lze přistupovat k jednotlivým složkám matice, můžeme použít důvěrně známý swizzling a masking. Další způsob přístupu do matice dovoluje číst celé řádky:

float2x2 fMatrix = { 1.0f, 1.1f, // row 1
                     2.0f, 2.1f
// row 2
                   };
float temp;

temp = fMatrix[0][0];
// cteni slozky 0, 0
temp = fMatrix[0][1];
// cteni slozky 0, 1

float2 temp2;
temp2 = fMatrix[0];
// do vektoru temp2 se ulozi cely prvni radek matice fMatrix

Tento způsob důvěrně připomíná pole C++.

46.2. Pixel shader

Stejně jako jsme v případě vertexu shaderu pracovali s vrcholy objektů, pixel shader pracuje na úrovni pixelů. Hlavní činností vertex shaderu je transformace a obarvení vrcholů. Výstupem je tedy seznam vrcholů, které mají souřadnice projektované do 2D frame bufferu. Již jsme si řekli, že počet vertexů nelze ve vertex shaderu zvyšovat ani snižovat (to platí u verze 2.0, ale dá se v budoucnu čekat, že toto omezení padne).

Dříve než se dostaneme k samotnému pixel shaderu, povíme si něco o dalším zpracování vertexů, které vystupují z vertex shaderu. Připomeňme si celou situaci na schematickém obrázku Direct3D pipeline:

Nyní jsme tedy na úrovní bloku Clipping, Back Face Culling, Attribute Evaluation a Rasterization. Clipping je technika, která ořeže hrany ležící mimo frame buffer. Pro ilustraci clippingu se podívejme na další obrázek:

Metod clippingu je celá řada a pokud se grafikou chcete zabývat podrobněji, není špatné si o nich něco přečíst (například algoritmy Cohen-Sutherland, Cyrus-Beck, Sutherland-Hodgman nebo Weiler-Atherton). Všechny tyto algoritmy najdete v knize Moderní počítačová grafika (druhé vydání 2004, autoři: Jiří Žára, Bedřich Beneš, Jiří Sochor a Petr Felkel).

Dalším zjednodušení scény přinese technika Back face culling, čili odstranění trojúhelníků, které mají normálu odkloněnou od pozorovatele. Tyto polygony zpravidla považujeme za neviditelné. Přesto i toto přirozené chování můžeme v Direct3D ovlivnit nastavením metodou SetRenderState() s příznakem D3DRS_CULLMODE.

Rasterizace je kapitola sama pro sebe. Zahrnuje převod vrcholů na pixely trojůhelníku, je tedy potřeba rasterizace úsečky (například Bresenhamův algoritmus) a vyplňovaní polygonu (například řádkovým rozkladem). Navíc je třeba brát v úvahu barvu vertexů, protože barva mezilehlých pixelů se interpoluje (pochopitelně záleží na zvolené stínovací technice). Opět zde silně doporučuji výše uvedenou knihu, kde je rasterizace popsána velice podrobně.

Vraťme se však zpět k pixel shaderu. Program PS je spuštěn pro každý pixel renderovaného trojúhelníku. Zpravidla výstup vertex shaderu je vstupem pixel shaderu. Výstupem je barva daného pixelu. Takže například do funkce PS přijde poloha, barva a texturová souřadnice pixelu (jedná se vlastně o interpolované hodnoty vertexů). My tyto informace použijeme pro výpočet nové barvy, kterou vrátíme. Jestli si pamatujete, jak jsme prováděli nanášení více textur a jejich kombinací, v pixel shaderu si tento výpočet můžete naprogramovat zcela po svém. To umožňuje vytvářet různé masky apod. Již zde je vidět obrovská síla pixel shaderu.

Strukturu pixel shaderu si probereme až v příští lekci hlavně z toho důvodu, že jednotlivé verze se výrazně liší.

46.3. Závěr

Příště se budeme dále zabývat pixel shaderem. Podrobně proberu strukturu pixelu shaderu pro verze 1.0-1.3, 1.4 a 2.0. Ty se hlavně liší v adresování textur a to je důležité dobře chápat. Na druhou stranu je pravda, že až budeme používat jazyk HLSL i pro pixel shader, překladač si s rozdíly mezi verzemi poradí sám.

Jiří Formánek