Sdílet prostřednictvím


Práce s shadery a prostředky shaderu

Je čas se naučit pracovat s shadery a prostředky shaderu při vývoji hry Microsoft DirectX pro Windows 8. Viděli jsme, jak nastavit grafické zařízení a prostředky a možná jste dokonce začali upravovat jeho kanál. Pojďme se tedy podívat na pixelové a vrcholové shadery.

Pokud neznáte jazyky shaderu, je rychlá diskuze v pořádku. Shadery jsou malé programy nízké úrovně, které jsou kompilovány a spouštěny v konkrétních fázích v grafickém kanálu. Jejich specialty jsou velmi rychlé matematické operace s plovoucí desetinou čárkou. Nejběžnější programy shaderu jsou:

  • Shader vrcholů – spouští se pro každý vrchol ve scéně. Tento shader pracuje s prvky vyrovnávací paměti vrcholů, které jí poskytuje volající aplikace, a minimálním výsledkem je vektor pozice 4 komponent, který bude rasterizován do pozice pixelu.
  • Pixel shader – spouští se pro každý pixel v cíli vykreslení. Tento shader přijímá rastrové souřadnice z předchozích fází shaderu (v nejjednodušších kanálech by to byl shader vrcholů) a vrátí barvu (nebo jinou hodnotu 4 komponent) pro danou pozici pixelu, která se pak zapíše do cíle vykreslení.

Tento příklad obsahuje velmi základní vrcholy a pixelové shadery, které vykresluje pouze geometrii, a složitější shadery, které přidávají základní výpočty osvětlení.

Programy Shader jsou napsané v jazyce HLSL (Microsoft High Level Shader Language). Syntaxe HLSL vypadá hodně jako C, ale bez ukazatelů. Shader programy musí být velmi kompaktní a efektivní. Pokud se shader zkompiluje na příliš mnoho instrukcí, nedá se spustit a vrátí se chyba. (Všimněte si, že přesný počet povolených instrukcí je součástí úrovně funkce Direct3D.)

V Direct3D nejsou shadery kompilovány za běhu; jsou zkompilovány při kompilaci zbytku programu. Při kompilaci aplikace pomocí sady Microsoft Visual Studio 2013 se soubory HLSL kompilují do souborů CSO (.cso), které musí aplikace načíst a umístit do paměti GPU před výkresem. Nezapomeňte do své aplikace zahrnout tyto soubory CSO, když je zabalíte; jsou prostředky stejně jako sítě a textury.

Principy sémantiky HLSL

Než budeme pokračovat, je důležité si chvilku probrat sémantiku HLSL, protože jsou pro nové vývojáře Direct3D často nejasné. Sémantika HLSL jsou řetězce, které identifikují hodnotu předanou mezi aplikací a programem shaderu. I když můžou být některé z různých možných řetězců, osvědčeným postupem je použít řetězec, jako POSITION je nebo COLOR který označuje použití. Tyto sémantiky přiřadíte při vytváření konstantní vyrovnávací paměti nebo vstupního rozložení. K sémantické sémantice můžete také připojit číslo od 0 do 7, abyste pro podobné hodnoty použili samostatné registry. Příklad: COLOR0, COLOR1, COLOR2...

Sémantika, která má předponu "SV_", jsou sémantika systémových hodnot , která jsou napsána vaším programem shaderu; samotná hra (spuštěná na procesoru) je nemůže upravit. Tyto sémantika obvykle obsahují hodnoty, které jsou vstupy nebo výstupy z jiné fáze shaderu v grafickém kanálu nebo které jsou generovány zcela GPU.

Sémantika má navíc různá chování, SV_ když se používají k určení vstupu nebo výstupu z fáze shaderu. Například SV_POSITION (výstup) obsahuje data vrcholu transformovaná během fáze shaderu vrcholů a SV_POSITION (vstup) obsahuje hodnoty pozice pixelů, které byly interpolovány GPU během fáze rasterizace.

Tady je několik běžných sémantiek HLSL:

  • POSITION n) pro data vyrovnávací paměti vrcholů. SV_POSITION poskytuje pixelovou pozici shaderu pixelů a nelze ji zapsat ve hře.
  • NORMAL n) pro normální údaje poskytnuté vyrovnávací pamětí vrcholů.
  • TEXCOORD n) pro údaje o souřadnici UV textury dodané do shaderu.
  • COLOR(n) pro barevná data RGBA dodávaná do shaderu. Všimněte si, že se zpracovává stejně jako ke koordinaci dat, včetně interpolace hodnoty během rasterizace; sémantický jednoduše pomáhá identifikovat, že se jedná o barevná data.
  • SV_Target[n] pro zápis z pixelového shaderu do cílové textury nebo jiné vyrovnávací paměti pixelů.

Při kontrole příkladu uvidíme několik příkladů sémantiky HLSL.

Čtení z konstantních vyrovnávacích pamětí

Jakýkoli shader může číst z konstantní vyrovnávací paměti, pokud je tato vyrovnávací paměť připojena k jeho fázi jako prostředek. V tomto příkladu je konstantní vyrovnávací paměti přiřazen pouze shader vrcholů.

Konstantní vyrovnávací paměť je deklarována na dvou místech: v kódu C++ a v odpovídajících souborech HLSL, které k němu přistupují.

Tady je postup, jak je struktura konstantní vyrovnávací paměti deklarována v kódu C++.

typedef struct _constantBufferStruct {
    DirectX::XMFLOAT4X4 world;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;

Při deklarování struktury pro konstantní vyrovnávací paměť v kódu C++, ujistěte se, že všechna data jsou správně zarovnaná podél 16bajtů hranic. Nejjednodušší způsob, jak to udělat, je použít typy DirectXMath , jako jsou XMFLOAT4 nebo XMFLOAT4X4, jak je vidět v ukázkovém kódu. Můžete také chránit proti nesprávně zarovnaným vyrovnávacím pamětím deklarováním statického kontrolního výrazu:

// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");

Tento řádek kódu způsobí chybu v době kompilace, pokud constantBufferStruct není zarovnaná 16 bajtů. Další informace o zarovnání a balení konstantní vyrovnávací paměti naleznete v tématu Pravidla balení pro konstantní proměnné.

Tady je postup, jak je konstantní vyrovnávací paměť deklarována v hlsl shaderu vrcholů.

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix mWorld;      // world matrix for object
    matrix View;        // view matrix
    matrix Projection;  // projection matrix
};

Všechny vyrovnávací paměti – konstantní, textura, vzorkovník nebo jiný – musí mít definovaný registr, aby k nim GPU měl přístup. Každá fáze shaderu umožňuje až 15 konstantních vyrovnávacích pamětí a každá vyrovnávací paměť může obsahovat až 4 096 konstantních proměnných. Syntaxe deklarace využití registru je následující:

  • b*#*: Registr pro konstantní vyrovnávací paměť (cbuffer).
  • t*#*: Registr vyrovnávací paměti textury (tbuffer).
  • s*#*: Registr pro sampler. (Sampler definuje chování vyhledávání pro texely v prostředku textury.)

Například HLSL pro pixel shader může jako vstup převzít texturu a vzorkovník jako vstup s deklarací, jako je tato.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

Je na vás, abyste přiřadili konstantní vyrovnávací paměti k registrům – při nastavování kanálu připojíte konstantní vyrovnávací paměť ke stejnému slotu, ke kterému jste ho přiřadili v souboru HLSL. Například v předchozím tématu volání VSSetConstantBuffers označuje 0 pro první parametr. To říká Direct3D, aby připojil prostředek konstantní vyrovnávací paměti k registraci 0, který odpovídá přiřazení vyrovnávací paměti k registraci (b0) v souboru HLSL.

Čtení z vyrovnávacích pamětí vrcholů

Vyrovnávací paměť vrcholů poskytuje data trojúhelníku pro objekty scény do shaderu vrcholů. Stejně jako u konstantní vyrovnávací paměti je struktura vyrovnávací paměti vrcholu deklarována v kódu C++ pomocí podobných pravidel balení.

typedef struct _vertexPositionColor
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 color;
} VertexPositionColor;

Pro data vrcholů v Direct3D 11 neexistuje standardní formát. Místo toho definujeme vlastní rozložení dat vrcholů pomocí popisovače; datová pole jsou definována pomocí pole D3D11_INPUT_ELEMENT_DESC struktur. Tady si ukážeme jednoduché rozložení vstupu, které popisuje stejný formát vrcholu jako předchozí struktura:

D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayout
    );

Pokud při úpravě ukázkového kódu přidáte data do formátu vrcholu, nezapomeňte také aktualizovat vstupní rozložení nebo ho shader nebude moct interpretovat. Rozložení vrcholů můžete upravit takto:

typedef struct _vertexPositionColorTangent
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;

V takovém případě byste definici vstupního rozložení upravili následujícím způsobem.

D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayoutExtended
    );

Každá definice elementu rozložení vstupu má předponu řetězce, například "POSITION" nebo "NORMAL", což je sémantika, kterou jsme probrali dříve v tomto tématu. Je to jako popisovač, který gpu pomáhá identifikovat tento prvek při zpracování vrcholu. Zvolte běžné a smysluplné názvy prvků vrcholu.

Stejně jako u konstantní vyrovnávací paměti má shader vrcholu odpovídající definici vyrovnávací paměti pro příchozí prvky vrcholu. (Proto jsme při vytváření vstupního rozložení poskytli odkaz na prostředek shaderu vrcholů – Direct3D ověří rozložení dat jednotlivých vrcholů pomocí vstupní struktury shaderu.) Všimněte si, jak se sémantika shoduje mezi definicí rozložení vstupu a touto deklarací vyrovnávací paměti HLSL. COLOR Má však k němu připojenou hodnotu 0. Není nutné přidat hodnotu 0, pokud máte v rozložení deklarovaný jenom jeden COLOR prvek, ale je vhodné ho připojit pro případ, že v budoucnu zvolíte přidání dalších barevných prvků.

struct VS_INPUT
{
    float3 vPos   : POSITION;
    float3 vColor : COLOR0;
};

Předávání dat mezi shadery

Shadery při provádění přebírají vstupní typy a vracejí výstupní typy z jejich hlavních funkcí. Pro vrchol shader definovaný v předchozí části byl vstupní typ VS_INPUT struktura a definovali jsme odpovídající rozložení vstupu a strukturu jazyka C++. Pole této struktury se používá k vytvoření vyrovnávací paměti vrcholu v CreateCube metoda.

Shader vrcholů vrátí PS_INPUT strukturu, která musí minimálně obsahovat konečnou pozici vrcholu 4 komponenty (float4). Tato hodnota pozice musí mít sémantickou systémovou hodnotu , deklarována pro ni, SV_POSITIONtakže GPU má data, která potřebuje k provedení dalšího kroku výkresu. Všimněte si, že mezi výstupem shaderu vrcholů a vstupem shaderu pixelů neexistuje korespondence 1:1; shader vrcholů vrátí jednu strukturu pro každý vrchol, který je zadaný, ale shader pixelů se spustí jednou pro každý pixel. Je to proto, že data jednotlivých vrcholů nejprve procházejí fází rasterizace. Tato fáze rozhoduje, které pixely "pokrývají" geometrii, kterou kreslíte, vypočítá interpolovaná data jednotlivých vrcholů pro každý pixel a potom jednou zavolá shader pixelů pro každý z těchto pixelů. Interpolace je výchozím chováním při rasterizaci výstupních hodnot a je nezbytné zejména pro správné zpracování výstupních vektorových dat (světelných vektorů, normálních hodnot vrcholů a tangens a dalších).

struct PS_INPUT
{
    float4 Position : SV_POSITION;  // interpolated vertex position (system value)
    float4 Color    : COLOR0;       // interpolated diffuse color
};

Kontrola shaderu vrcholů

Ukázkový shader vrcholů je velmi jednoduchý: vezměte vrchol (pozici a barvu), transformujte pozici ze souřadnic modelu na perspektivní předpokládané souřadnice a vraťte ho (spolu s barvou) do rastrovače. Všimněte si, že hodnota barvy je interpolovaná přímo spolu s daty o poloze a poskytuje jinou hodnotu pro každý pixel, i když shader vrcholů neprovádí žádné výpočty s hodnotou barvy.

VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
    VS_OUTPUT Output;

    float4 pos = float4(input.vPos, 1.0f);

    // Transform the position from object space to homogeneous projection space
    pos = mul(pos, mWorld);
    pos = mul(pos, View);
    pos = mul(pos, Projection);
    Output.Position = pos;

    // Just pass through the color data
    Output.Color = float4(input.vColor, 1.0f);

    return Output;
}

Složitější shader vrcholů, například ten, který nastavuje vrcholy objektu pro stínování Phong, může vypadat podobněji. V tomto případě využíváme skutečnost, že vektory a normální hodnoty jsou interpolované tak, aby se přibližný povrch s hladkým vzhledem.

// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
};

cbuffer LightConstantBuffer : register(b1)
{
    float4 lightPos;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 normal : NORMAL;
};

// Per-pixel color data passed through the pixel shader.

struct PixelShaderInput
{
    float4 position : SV_POSITION; 
    float3 outVec : POSITION0;
    float3 outNormal : NORMAL0;
    float3 outLightVec : POSITION1;
};

PixelShaderInput main(VertexShaderInput input)
{
    // Inefficient -- doing this only for instruction. Normally, you would
 // premultiply them on the CPU and place them in the cbuffer.
    matrix mvMatrix = mul(model, view);
    matrix mvpMatrix = mul(mvMatrix, projection);

    PixelShaderInput output;

    float4 pos = float4(input.pos, 1.0f);
    float4 normal = float4(input.normal, 1.0f);
    float4 light = float4(lightPos.xyz, 1.0f);

    // 
    float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);

    // Transform the vertex position into projected space.
    output.gl_Position = mul(pos, mvpMatrix);
    output.outNormal = mul(normal, mvMatrix).xyz;
    output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
    output.outLightVec = mul(light, mvMatrix).xyz;

    return output;
}

Kontrola shaderu pixelů

Tento pixel shader v tomto příkladu je docela možná absolutní minimální množství kódu, který můžete mít ve shaderu pixelů. Vezme interpolovaná pixelová barevná data vygenerovaná během rasterizace a vrátí je jako výstup, kde se zapíše do cíle vykreslení. Jak nudný!

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

Důležitou součástí je SV_TARGET sémantika systémové hodnoty pro návratovou hodnotu. Označuje, že výstup se zapisuje do primárního cíle vykreslení, což je vyrovnávací paměť textury zadaná do řetězce prohození pro zobrazení. To se vyžaduje pro shadery pixelů – bez barevných dat z pixelového shaderu by Direct3D neměl nic k zobrazení!

Příklad složitějšího pixelového shaderu pro provedení phong shadingu může vypadat takto. Vzhledem k tomu, že vektory a normální hodnoty byly interpolované, nemusíme je vypočítat na základě pixelů. Musíme je ale znovu normalizovat z důvodu toho, jak interpolace funguje; Koncepčně musíme vektor postupně "otočit" ze směru vrcholu A na směr vrcholu B a zachovat jeho délku – zatímco interpolace místo toho protíná přímku mezi dvěma koncovými body vektoru.

cbuffer MaterialConstantBuffer : register(b2)
{
    float4 lightColor;
    float4 Ka;
    float4 Kd;
    float4 Ks;
    float4 shininess;
};

struct PixelShaderInput
{
    float4 position : SV_POSITION;
    float3 outVec : POSITION0;
    float3 normal : NORMAL0;
    float3 light : POSITION1;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 L = normalize(input.light);
    float3 V = normalize(input.outVec);
    float3 R = normalize(reflect(L, input.normal));

    float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
    diffuse = saturate(diffuse);

    float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
    specular = saturate(specular);

    float4 finalColor = diffuse + specular;

    return finalColor;
}

V jiném příkladu používá shader pixelů vlastní konstantní vyrovnávací paměti, které obsahují informace o světle a materiálech. Vstupní rozložení ve shaderu vrcholů by se rozšířilo tak, aby zahrnovalo normální data a očekává se, že výstup z tohoto shaderu vrcholů bude obsahovat transformované vektory pro vrchol, světlo a normální vrchol v souřadnicovém systému zobrazení.

Pokud máte vyrovnávací paměti textury a vzorkovače s přiřazenými registry (t a s), můžete k nim přistupovat také v shaderu pixelů.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
    float3 lightDirection = normalize(float3(1, -1, 0));
    float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
    float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
    return texelColor * lightMagnitude;
}

Shadery jsou velmi výkonné nástroje, které lze použít ke generování procedurálních prostředků, jako jsou stínové mapy nebo textury šumu. Ve skutečnosti pokročilé techniky vyžadují, abyste si textury pomysleli abstraktivněji, ne jako vizuální prvky, ale jako vyrovnávací paměti. Uchovávají data, jako jsou informace o výšce nebo jiná data, která se dají vzorkovat v konečném pixelovém shaderu nebo v daném snímku jako součást průchodu vícefázových efektů. Více vzorkování je výkonný nástroj a páteř mnoha moderních vizuálních efektů.

Další kroky

Doufejme, že jste obeznámeni s DirectX 11at tento bod a jste připraveni začít pracovat na projektu. Tady jsou některé odkazy, které vám pomůžou zodpovědět další otázky týkající se vývoje pomocí DirectX a C++:

Práce s prostředky zařízení DirectX

Vysvětlení kanálu vykreslování Direct3D 11