Kurz DirectX (45.)

Dnes budeme pokračovat s vertex shaderem a jak jsem minule slíbil, přepíšeme náš program do jazyka HLSL, který nám v mnoha směrech zjednoduší život. Na začátku lekce se podíváme na některé základy tohoto jazyka a poté si vše ukážeme na příkladu.

45.1. Základy jazyka HLSL

Verze DirectX 9.0 přinesla kromě jiného také jazyk HLSL (high level shading language), který hodně zjednodušuje práci člověka starajícího se o návrh materiálů. Tento člověk vlastně vůbec nemusí znát architekturu Vertex shaderu (nebo Pixel shaderu). Kromě toho, tento jazyk značně zrychluje vývoj programu. To, co jsme v minulé lekci psali (a snažili se pochopit) přes půl lekce, se dnes vejde na 10 naprosto průhledných řádků. A proč jsme se tedy assemblerem VS vůbec zabývali? Považuji za velmi užitečné vědět, co se uvnitř shaderů děje a zvláště, až budete někdy shader ladit, je dobré se podívat, jak byl váš program zkompilován.

V této části si povíme některé základy tohoto jazyka, který nemá příliš složitou syntaxi (ta je kromě toho dost podobná C++), ale jsou zde jistá specifika. Program budeme načítat z externího souboru (podobně jako minule vertex shader v assembleru).

45.1.1. Datové typy

V programu můžete použít tyto typy jako skalární proměnné:

Název
Popis
BOOL true nebo false
int 32-bitové celé číslo se znaménkem
half 16-bitové číslo s pohyblivou řádovou čárkou
float 32-bitové číslo s pohyblivou řádovou čárkou
double 64-bitové číslo s pohyblivou řádovou čárkou

Kromě toho z nich můžete vytvářet vektory a matice následujícím způsobem:

"Typ""Počet složek" "Jméno proměnné"

Takže například:

bool bVector; // skalární hodnota typu bool
half2 hVector; // vektor obsahující 2 složky typu half
float3 fVector; // tří složkový vektor

Počet složek může být v rozmezí 1 - 4. U matic platí podobné pravidlo:

"Typ""Počet řádků "x"Počet sloupců" "Jméno proměnné"

Například:

int1x1 iMatrix; // celočíselná matice 1x1
int1x4 iMatrix; // celočíselná matice 1x4
float2x2 fMatrix; // reálná matice 2x2

Kolem typů by se toho dalo povídat ještě mnoho, ale vše je dobře popsáno v dokumentaci DirectX. Nám bude prozatím stačit předchozí výčet.

Proměnné, které jsou definovány globálně (a nejsou označeny klíčovým slovem static) jsou přístupné z vnějšku shaderu přes konstantní registry (tedy budeme je nastavovat pomocí metody SetVertexShaderConstant() apod.).
Lokální proměnné definované uvnitř funkcí mají platnost pouze po dobu vykonávání funkce a nejsou přístupné z vnějšího prostředí.

Struktury a sémantické značky

Podobně jako v C můžete definovat v HLSL struktury. Syntaxe je vlastně úplně stejná. My struktury použijeme k definici výstupu z vertex shaderu:

// Vystup z vertex shaderu a vstup do pixel shaderu
struct VS_OUTPUT
{
   float4 Position : POSITION; // vertex position
   float2 TextureUV : TEXCOORD0; // vertex texture coords
   float3 Diffuse : COLOR0; // vertex diffuse color
};

Názvy za dvojtečkou označují sémantiku atributu. Tedy například atribut Position je označen jako POSITION a to znamená, že ve výsledném programu bude tato proměnná namapována na výstupní registr polohy (oPos). Tyto sémantické značky se používají i pro vstupní parametry shaderu. V následující tabulce jsou vypsány značky pro vstup do vertex shaderu:

Název
Význam
COLOR[n] Difusní a spekulární barva vertexu.
NORMAL[n] Normálový vektor
POSITION[n] Poloha vertexu (vektor).
POSITIONT Transformovaná poloha.
PSIZE[n] Velikost bodu.
TEXCOORD[n] Texturové koordináty.

A ještě nás budou zajímat značky pro výstup:

Název
Význam
COLOR[n] Difusní a spekulární barva vertexu.
FOG Normálový vektor
POSITION[n] Poloha vertexu (vektor).
PSIZE[n] Velikost bodu.
TEXCOORD[n] Texturové koordináty.

V tabulkách nejsou uvedeny všechny značky, ale pro naše účely budou tyto stačit. Číslo n je od 0 a shora omezené počtem zdrojů příslušné kategorie (například texturových souřadnic může být až 8.).

42.1.2. Funkce

V programu můžete definovat funkce, minimálně zde musí být jedna taková, kterou označíte jako vstupní bod z vašeho projektu. Samozřejmě ale můžete definovat vlastní funkce, které lze pak volat z libovolného místa v programu.

Syntaxe hlavičky je opět velice podobná C++, jen jsou doplněny sémantické značky u vstupních parametrů. Tedy například:

VS_OUTPUT RenderSceneVS( float4 vPos : POSITION,
                         float3 vNormal : NORMAL,
                         float3 vDiffuse : COLOR0,
                         float2 vTexCoord0 : TEXCOORD0)
{
     // telo funkce
}

Takto definujeme vstupní bod (funkce main()). U ostatních funkcí nejsou sémantické značky potřeba.

Dále jazyk HLSL poskytuje desítky tzv. intristických funkcí, které pomáhají při běžných výpočtech. Když se na jejich výčet podíváte do MSDN dokumentace, zjistíte, že se jejich názvy často kryjí s názvy instrukcí shaderu. Některé z těchto funkcí můžete použít pouze ve vertex shaderu, jiné i v pixel shaderu a u každé je také napsána minimální verze.

Příklad:

Output.Position = mul(realPos, g_mWorldViewProjection);

Protože proměnná g_mWorldViewProjection je matice 4x4 a realPos vektor se 4-mi složkami, funkcí mul() docílíte násobení matice a vektoru čili transformaci vektoru maticí.

42.1.3. Překlad HLSL kódu

Nyní se ještě podívejme, jakým způsobem zpracujeme program z našeho projektu a jak nastavíme proměnné. Program je nejprve třeba přeložit do assembleru (spíše do binární podoby) cílové verze vertex shaderu a poté stejným způsobem předhodit funkci CreateVertexShader() jako v případě assemblerového kódu. Pro překlad slouží funkce D3DXCompileShaderFromFile(), která má řadu parametrů:

1) pSrcFile - jméno souboru, kde je uložen program
2) pDefines - podobně jako u funkce D3DXAssembleShaderFromFile() označuje tento parametr pole struktur D3DXMACRO
3) pInclude - a zde je ukazatel na rozhraní ID3DXInclude
4) pFunctionName - název funkce "main"
5) pProfile - řetězec, který určuje instrukční sadu (čili verzi shaderu) - může být například "vs_1_1" nebo "vs_2_0"
6) Flags - další příznaky nastavující optimalizace a další vlastnosti překladu
7) ppShader - ukazatel na rozhraní ID3DXBuffer, do kterého se uloží přeložený shader
8) ppErrorMsgs - ukazatel na rozhraní ID3DXBuffer, do kterého se v případě chyby uloží chybové hlášení
9) ppConstantTable - ukazatel na rozhraní ID3DXConstantTable, který použijeme při nastavení proměnných shaderu

Dále budeme chtít zkompilovaný kód převést zpět do podoby assembleru. K tomu použijeme funkci D3DXDisassembleShader(), jejímž výstupem je opět rozhraní typu ID3DXBuffer s uloženým kódem, který uložíme do souboru. Výstup je možný i ve formě HTML stránky, kde je vidět barevné vyznačení kódu.

Poznámka: Na závěr této části bych chtěl upozornit, že jazyk HLSL toho umožňuje daleko víc než jsem zde nastínil. Budeme se jím dále zabývat v dalších lekcích, až budeme poznávat pixel shader a efekty (tedy spojení vertex a pixel shaderu do jednoho souboru). Navíc si myslím, že je lepší některé vlastnosti jazyka ukázat přímo na příkladu, než se zde podrobně zabývat každou z nich.

45.2. Příklad

Dnešní příklad spočívá v tom, že přepíšeme příklad z minula do jazyka HLSL. Výsledek tedy bude zcela totožný, ale kód bude velice průhledný.

Přidáme podporu pro HLSL vertex shader do třídy XVertexShader. Vytvoříme druhou metodu LoadVertexShader(), která bude mít dva parametry navíc:

HRESULT XVertexShader::LoadVertexShader(char* szFile,
                                       char *szFunction,
                                       char* szVSProfile,
                                       DWORD dwFlags)
{
   IDisplay *pDis;
   CreateDisplayObject(DISIID_IDisplay, (void**) &pDis);

   ID3DXBuffer* ppShader;
   ID3DXBuffer* ppErrors;

   TRACE("Creation vertex shader from HLSL file: %s", szFile);

   char szFilePath[MAX_PATH];
   cmnGetDataFilePath(szFilePath, "effects\\");
   strcat(szFilePath, szFile);

   D3DXCompileShaderFromFile(szFilePath, NULL, NULL, szFunction,
                             szVSProfile, dwFlags, &ppShader,
                             &ppErrors, &m_constTable);

   if(ppErrors)
   {
      TRACE(" %s", (char*)ppErrors->GetBufferPointer());
      SAFE_RELEASE(ppErrors);
      SAFE_RELEASE(pDis);
      return S_FALSE;
   }
   if(ppShader)
   {
      if(D3D_OK == pDis->GetDevice()->CreateVertexShader(
            (DWORD*)ppShader->GetBufferPointer(), &m_pVertexShader))
      {
         TRACE(" VertexShader has been created.");
      }

      // Shader se a v assembleru ulozi do souboru .o
      D3DXDisassembleShader((DWORD*)ppShader->GetBufferPointer(),
                            false, NULL, &ppErrors);
      cmnGetDataFilePath(szFilePath, szFile);
      // Nahrazeni pripony
      char * dot = strrchr(szFilePath, '.');
      *(dot+1) = 'o';
      *(dot+2) = 0;
      // Ulozeni
      FILE* f = fopen(szFilePath, "w");
      fwrite(ppErrors->GetBufferPointer(), 1,
             ppErrors->GetBufferSize(), f);
      fclose(f);

      m_pDevice = pDis->GetDevice();
      SAFE_RELEASE(ppShader);
      SAFE_RELEASE(pDis);
      return S_OK;
   }
   return S_FALSE;
}

Obě varianty metody LoadVertexShader() jsou velice podobné, jen jsem nahradil funkci D3DXAssemblyShaderFromFile() funkcí D3DXCompileShaderFromFile() a přidal jsem ukládání assembleru do souboru odvozeného od jména vstupního souboru. Ještě se zmíním o členské proměnné typu ID3DXConstantTable, kterou použijeme při nastavení globálních proměnných shaderu (konstantní registry). Nezapomeneme samozřejmě ani na inline metodu GetConstantTable(), která tento objekt vrací. Dva nové parametry funkce označují vstupní bod programu a verzi, do které se bude shader překládat.

Nyní se konečně podívejme na přepsaný vertex shader z minulé lekce do jazyka HLSL. Kód je uložený v souboru "terrain.fx":

// Globalni promenne, ktere se nastavuji zvenku
//

float4x4 g_mWorldViewProjection; // World * View * Projection matice
float3 g_vecLight; // Smer svetla
float3 g_colAmbient; // Ambientni slozka svetla
float3 g_colDiffuse; // Difuzni slozka svetla

float g_fSeaLevel; // Hladina more
float g_zdelta[4];

// Vystup z vertex shaderu a vstup do pixel shaderu
struct VS_OUTPUT
{
  float4 Position : POSITION; // vertex position
  float2 TextureUV : TEXCOORD0; // vertex texture coords
  float3 Diffuse : COLOR0; // vertex diffuse color
};

VS_OUTPUT RenderSceneVS( float4 vPos : POSITION,
                         float3 vNormal : NORMAL,
                         float3 vDiffuse : COLOR0,
                         float2 vTexCoord0 : TEXCOORD0)
{
   VS_OUTPUT Output;
   float adr = vNormal.x;
   float4 realPos = vPos;

   // Pokud je vertex pod vodou
   if(vPos.z > 0 && vPos.z < g_fSeaLevel)
   {
      if(adr < 0) adr += 1; // pokud je adresa zaporna prictu jednicku
      adr = adr * 4; // vypocteni adresy (0-3)

      // mam adresu, sahnu do konstatniho registru podle adresy
      realPos.z += g_zdelta[adr];
   }

   // Transform the position from object space to homogeneous projection space
   Output.Position = mul(realPos, g_mWorldViewProjection);

   // Vypocet osvetleni
  
 Output.Diffuse = (dot(vNormal, -g_vecLight) * g_colDiffuse) *
                     vDiffuse + g_colAmbient;

   // Texturove koordinaty se pouze zkopiruji na vystup
   Output.TextureUV = vTexCoord0;

   return Output;
}

Na začátku definujeme globální proměnné, které jsou přístupné z vnějšího prostředí. Dále definujeme strukturu pro výstup z vertex shaderu VS_OUTPUT. A pak následuje samotná funkce shaderu.

Na závěr upravíme inicializaci a použití vertex shaderu pro terén. Vertex shader vytvoříme následujícím voláním:

m_pvsTerrain->LoadVertexShader("terrain.fx", "RenderSceneVS",
                                   "vs_1_1", 0);

Můžete použít samozřejmě verzi 2.0, pokud to vaše grafická karta umožňuje, ale v tomto konrétním případě bude výstup praktický stejný. Nyní nastavíme konstantní registry, což bude o poznání průhlednější. K tomu použijeme objekt ID3DXConstantTable:

if(m_pvsTerrain && m_pvsTerrain->Loaded())
{
   pDis->GetCamera()->GetWorldViewProjMatrix(&mat);
   D3DXMatrixTranspose(&mat2, &mat);
   m_pvsTerrain->GetConstantTable()->SetMatrixTranspose(
        pDis->GetDevice(), "g_mWorldViewProjection", &mat2);
   m_pvsTerrain->GetConstantTable()->SetFloat(
        pDis->GetDevice(), "g_fSeaLevel", WATER_SURFACE-1);

   float randn[4];
   randn[0] = m_zmod0;
   randn[1] = -m_zmod1;
   randn[2] = m_zmod2;
   randn[3] = -m_zmod3;
   m_pvsTerrain->GetConstantTable()->SetFloatArray(
               pDis->GetDevice(), "g_zdelta", randn, 4);

   D3DXVECTOR4 lit[3];
   D3DXVec4Normalize(&lit[0], &D3DXVECTOR4(-2,-1,-5, 0.0));
   // light direction
   lit[1].x = 1.0f;lit[1].y = 1.0f;lit[1].z = 1.0f;
   // diffuse light
   lit[2].x = 0.3f;lit[2].y = 0.4f;lit[2].z = 0.3f;
   // ambient light

   m_pvsTerrain->GetConstantTable()->SetVector(
                    pDis->GetDevice(), "g_vecLight", &lit[0]);
   m_pvsTerrain->GetConstantTable()->SetVector(
                    pDis->GetDevice(), "g_colDiffuse", &lit[1]);
   m_pvsTerrain->GetConstantTable()->SetVector(
                    pDis->GetDevice(), "g_colAmbient", &lit[2]);

   UpdateZMod(m_zmod0, m_zdir0, fFactor, 0.3f, -0.3f);
   UpdateZMod(m_zmod1, m_zdir1, fFactor, 0.6f, -0.2f);
   UpdateZMod(m_zmod2, m_zdir2, fFactor, 0.1f, -0.8f);
   UpdateZMod(m_zmod3, m_zdir3, fFactor, 0.5f, -0.4f);

   m_pvsTerrain->SetVertexShader();
}
else
{
   pDis->GetDevice()->SetFVF(VERTEXFORMAT);
}

Všimněte si, jakým způsobem nastavujeme matice, reálné hodnoty a také pole pomocí objektu IConstantTable. Druhý parametr příslušné funkce je typu D3DXHANDLE a zde použijeme jméno proměnné ze shaderu. Toto přiřazování má mnoho výhod: nemusíme přetypovávat parametry, jasně je zde vidět, kterou proměnnou nastavujeme, nemusíme pro každý float zabrat celý konstantní registr (tzn. 4 floaty).

Zdrojové kódy projektu D3DEngine2 s dnešními úpravami naleznete zde.

45.3. Závěr

V příští lekci budeme dále rozvíjet možnosti jazyka HLSL a také se podíváme na strukturu pixel shaderu. Ten začneme rovněž probírat od hardwaru a samozřejmě skončíme opět u HLSL. Ale to je látka na několik příštích lekcí.

Jiří Formánek