Werken met shaders en shader-resources

Het is tijd om te leren werken met shaders en shader-resources bij het ontwikkelen van uw Microsoft DirectX-game voor Windows 8. We hebben gezien hoe u het grafische apparaat en de resources instelt en misschien bent u zelfs begonnen met het wijzigen van de pijplijn. Laten we nu eens kijken naar pixel- en hoekpunt-shaders.

Als u niet bekend bent met shader-talen, is een korte bespreking op zijn plaats. Shaders zijn kleine programma's op laag niveau die worden gecompileerd en uitgevoerd op specifieke fasen in de grafische pijplijn. Hun specialiteit is zeer snelle rekenkundige bewerkingen met drijvende komma. De meest voorkomende shader-programma's zijn:

  • vertex-shader: uitgevoerd voor elk hoekpunt in een scène. Deze shader werkt op hoekpuntbufferelementen die eraan worden geleverd door de aanroepende app en resulteert minimaal in een positievector met vier onderdelen die in een pixelpositie worden gerasterd.
  • Pixel-shader: uitgevoerd voor elke pixel in een renderdoel. Deze shader ontvangt rastercoördinaten van eerdere shaderfasen (in de eenvoudigste pijplijnen, dit zou de hoekpunt-shader zijn) en retourneert een kleur (of andere waarde van vier onderdelen) voor die pixelpositie, die vervolgens in een renderdoel wordt geschreven.

Dit voorbeeld bevat zeer eenvoudige hoekpunt- en pixel-shaders die alleen geometrie tekenen en complexere shaders die eenvoudige verlichtingsberekeningen toevoegen.

Shader-programma's worden geschreven in HlSL (High Level Shader Language). HLSL-syntaxis lijkt veel op C, maar zonder de aanwijzers. Shader-programma's moeten zeer compact en efficiënt zijn. Als uw shader naar teveel instructies wordt gecompileerd, kan deze niet worden uitgevoerd en wordt er een fout geretourneerd. (Houd er rekening mee dat het exacte aantal toegestane instructies deel uitmaakt van het Direct3D-functieniveau.)

In Direct3D worden shaders niet gecompileerd tijdens runtime; ze worden gecompileerd wanneer de rest van het programma wordt gecompileerd. Wanneer u uw app compileert met Microsoft Visual Studio 2013, worden de HLSL-bestanden gecompileerd naar CSO-bestanden (.cso) die uw app moet laden en plaatsen in GPU-geheugen voordat u gaat tekenen. Zorg ervoor dat u deze CSO-bestanden bij uw app opneemt wanneer u deze inpakt; ze zijn assets, net als meshes en texturen.

Meer informatie over HLSL-semantiek

Het is belangrijk om even te praten over HLSL-semantiek voordat we verdergaan, omdat ze vaak verwarrend zijn voor nieuwe Direct3D-ontwikkelaars. HLSL-semantiek zijn tekenreeksen die een waarde identificeren die wordt doorgegeven tussen de app en een shader-programma. Hoewel dit een van de mogelijke tekenreeksen kan zijn, kunt u het beste een tekenreeks zoals POSITION of COLOR gebruiken die het gebruik aangeeft. U wijst deze semantiek toe wanneer u een constante buffer of invoerindeling maakt. U kunt ook een getal tussen 0 en 7 toevoegen aan de semantische waarde, zodat u afzonderlijke registers voor vergelijkbare waarden gebruikt. Bijvoorbeeld: COLOR0, COLOR1, COLOR2...

Semantiek die worden voorafgegaan door 'SV_' zijn systeemwaarde semantiek waarnaar wordt geschreven door uw shader-programma; uw game zelf (uitgevoerd op de CPU) kan deze niet wijzigen. Deze semantiek bevat doorgaans waarden die invoer of uitvoer zijn van een andere shader-fase in de grafische pijplijn of die volledig worden gegenereerd door de GPU.

Daarnaast heeft SV_-semantiek verschillend gedrag wanneer ze worden gebruikt om invoer naar of uitvoer van een shaderfase op te geven. Zo bevat SV_POSITION (uitvoer) de hoekpuntgegevens die zijn getransformeerd tijdens de fase van de vertex-shader, en bevat SV_POSITION (invoer) de pixelpositiewaarden die door de GPU zijn geïnterpoleerd tijdens de rasterisatiefase.

Hier volgen enkele algemene HLSL-semantiek:

  • POSITION(n) voor hoekpuntbuffergegevens. SV_POSITION voorziet de pixel shader van een pixelpositie en kan niet door uw spel worden beschreven.
  • NORMAL(n) voor normale gegevens die door de hoekpuntbuffer worden verstrekt.
  • TEXCOORD(n) voor textuur-UV-coördinaatgegevens die aan een shader worden geleverd.
  • COLOR(n) voor RGBA-kleurgegevens geleverd aan een shader. Houd er rekening mee dat het identiek wordt behandeld als coördinatiegegevens, inclusief het interpoleren van de waarde tijdens rastervorming; de semantiek helpt u eenvoudig te identificeren dat het om kleurgegevens gaat.
  • SV_Target[n] voor het schrijven vanuit een pixel shader naar een doeltextuur of een andere pixelbuffer.

We zien enkele voorbeelden van HLSL-semantiek terwijl we het voorbeeld bekijken.

Lezen uit de constante buffers

Elke shader kan uit een constante buffer lezen indien die buffer als een hulpbron aan zijn fase is gekoppeld. In dit voorbeeld wordt alleen aan de hoekpunt-shader een constante buffer toegewezen.

De constante buffer wordt op twee plaatsen gedeclareerd: in de C++-code en in de bijbehorende HLSL-bestanden die er toegang toe hebben.

Zo wordt de constante bufferstruct gedeclareerd in de C++-code.

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

Wanneer u de structuur voor de constante buffer in uw C++-code declareert, moet u ervoor zorgen dat alle gegevens correct zijn uitgelijnd langs grenzen van 16 bytes. De eenvoudigste manier om dit te doen, is door DirectXMath--typen te gebruiken, zoals XMFLOAT4 of XMFLOAT4X4, zoals te zien is in de voorbeeldcode. U kunt ook bescherming bieden tegen verkeerd uitgelijnde buffers door een statische assertie te declareren:

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

Deze coderegel veroorzaakt een fout tijdens het compileren als ConstantBufferStruct- niet is uitgelijnd op 16 byte. Zie Verpakkingsregels voor constante variabelenvoor meer informatie over het uitlijnen en inpakken van een constante buffer.

Hier ziet u hoe de constante buffer wordt gedeclareerd in de hoekpunt-shader HLSL.

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

Alle buffers ( constant, textuur, sampler of andere) moeten een register hebben gedefinieerd, zodat de GPU er toegang toe heeft. Elke arceringsfase biedt maximaal 15 constante buffers en elke buffer kan maximaal 4.096 constante variabelen bevatten. De syntaxis van de declaratie voor registergebruik is als volgt:

  • b*#*: een register voor een constante buffer (cbuffer).
  • t*#*: een register voor een textuurbuffer (tbuffer).
  • s*#*: een register voor een sampler. (Een sampler definieert het opzoekgedrag voor texels in de structuurresource.)

De HLSL voor een pixel-shader kan bijvoorbeeld een patroon en een sampler als invoer met een declaratie als volgt aannemen.

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

Het is aan u om constante buffers toe te wijzen aan registers. Wanneer u de pijplijn instelt, koppelt u een constante buffer aan dezelfde site waaraan u deze in het HLSL-bestand hebt toegewezen. In het vorige onderwerp geeft de aanroep naar VSSetConstantBuffers bijvoorbeeld '0' aan voor de eerste parameter. Dat vertelt Direct3D om de constante-bufferresource te koppelen aan register 0, wat overeenkomt met de toewijzing van de buffer aan register(b0) in het HLSL-bestand.

Lezen uit de hoekpuntbuffers

De hoekpuntbuffer levert de driehoekgegevens voor de scèneobjecten aan de vertexshader(s). Net als bij de constante buffer wordt de hoekpuntbufferstruct gedeclareerd in de C++-code, met behulp van vergelijkbare verpakkingsregels.

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

Er is geen standaardindeling voor hoekpuntgegevens in Direct3D 11. In plaats daarvan definiëren we onze eigen indeling voor hoekpuntgegevens met behulp van een descriptor; de gegevensvelden worden gedefinieerd met behulp van een matrix van D3D11_INPUT_ELEMENT_DESC structuren. Hier laten we een eenvoudige invoerindeling zien waarin dezelfde hoekpuntindeling wordt beschreven als in de voorgaande struct:

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
    );

Als u gegevens toevoegt aan het vertexformaat bij het wijzigen van de voorbeeldcode, moet u de invoerindeling ook bijwerken, anders kan de shader deze niet interpreteren. U kunt de indeling van het hoekpunt als volgt wijzigen:

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

In dat geval wijzigt u de definitie van de invoerindeling als volgt.

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
    );

Elk van de definities van invoerindelingselementen wordt voorafgegaan door een tekenreeks, zoals 'POSITION' of 'NORMAL', dat is de semantische die we eerder in dit onderwerp hebben besproken. Het is net een handvat waarmee de GPU dat element kan identificeren bij het verwerken van de vertex. Kies algemene, betekenisvolle namen voor uw hoekpuntelementen.

Net als bij de constante buffer heeft de hoekpunt-shader een bijbehorende bufferdefinitie voor binnenkomende hoekpuntelementen. (Daarom hebben we bij het maken van de invoerindeling een verwijzing naar de vertex-shader-resource opgegeven. Direct3D valideert de gegevensindeling per hoekpunt met de invoerstruct van de shader.) Let op hoe de semantiek overeenkomt met de definitie van de invoerindeling en deze HLSL-bufferdeclaratie. Aan COLOR is echter een '0' toegevoegd. Het is niet nodig om de 0 toe te voegen als u slechts één COLOR element in de indeling hebt gedeclareerd, maar het is een goede gewoonte om het toe te voegen als u ervoor kiest om in de toekomst meer kleurelementen toe te voegen.

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

Gegevens doorgeven tussen shaders

Shaders nemen invoertypen op en retourneren uitvoertypen van hun hoofdfuncties bij uitvoering. Voor de hoekpunt-shader die in de vorige sectie is gedefinieerd, was het invoertype de VS_INPUT structuur en hebben we een overeenkomende invoerindeling en C++-struct gedefinieerd. Een matrix van deze struct wordt gebruikt om een hoekpuntbuffer te maken in de methode CreateCube.

De hoekpunt-shader retourneert een PS_INPUT structuur, die minimaal de uiteindelijke hoekpuntpositie van 4 onderdelen (float4) moet bevatten. Deze positiewaarde moet de semantische systeemwaarde, SV_POSITION, hebben gedeclareerd, zodat de GPU beschikt over de gegevens die nodig zijn om de volgende tekenstap uit te voeren. U ziet dat er geen correspondentie van 1:1 is tussen de uitvoer van hoekpunt-shader en pixel-shader-invoer; de hoekpunt-shader retourneert één structuur voor elk hoekpunt dat het krijgt, maar de pixel-shader wordt eenmaal voor elke pixel uitgevoerd. Dat komt doordat de gegevens per hoekpunt eerst door de rastervormingsfase gaan. In deze fase wordt bepaald welke pixels de geometrie 'bedekken' die u tekent, worden geïnterpoleerde gegevens per hoekpunt voor elke pixel berekend en wordt de pixel-shader eenmaal voor elk van deze pixels aanroepen. Interpolatie is het standaardgedrag bij het rasteren van uitvoerwaarden en is met name essentieel voor de juiste verwerking van uitvoervectorgegevens (lichtvectoren, normale hoekpunten en tangens, enzovoort).

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

De vertex-shader controleren

De voorbeeld-hoekpunt-shader is heel eenvoudig: neem een hoekpunt (positie en kleur) op, transformeer de positie van modelcoördinaten in perspectief geprojecteerde coördinaten en retourneer deze (samen met de kleur) naar de rasterizer. U ziet dat de kleurwaarde direct wordt geïnterpoleerd, samen met de positiegegevens, waardoor voor elke pixel een andere waarde wordt opgegeven, ook al heeft de hoekpunt-shader geen berekeningen uitgevoerd op de kleurwaarde.

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;
}

Een complexere vertexshader, zoals een die de hoekpunten van een object instelt voor Phong-arcering, kan er als volgt uitzien. In dit geval maken we gebruik van het feit dat de vectoren en normalen worden geïnterpoleerd om een vloeiend uitziend oppervlak te benaderen.

// 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;
}

De pixel-shader controleren

Deze pixel-shader in dit voorbeeld is mogelijk de absolute minimale hoeveelheid code die u in een pixel-shader kunt hebben. Hiervoor worden de geïnterpoleerde pixelkleurgegevens gebruikt die tijdens rasterisatie worden gegenereerd en geretourneerd als uitvoer, waar deze naar een renderdoel worden geschreven. Hoe saai!

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

Het belangrijkste onderdeel is de semantiek van de systeemwaarde SV_TARGET van de retourwaarde. Hiermee wordt aangegeven dat de uitvoer naar het primaire renderdoel moet worden geschreven, wat de textuurbuffer is die aan de swap chain voor weergave wordt geleverd. Dit is vereist voor pixel-shaders- zonder de kleurgegevens van de pixel-shader zou Direct3D niets te zien hebben.

Een voorbeeld van een complexere pixelshader voor het uitvoeren van Phong-arcering kan er als volgt uitzien. Omdat de vectoren en normalen zijn geïnterpoleerd, hoeven we ze niet per pixel te berekenen. We moeten ze echter wel opnieuw normaliseren vanwege de werking van interpolatie; conceptueel moeten we de vector geleidelijk "draaien" van richting op hoekpunt A naar richting bij hoekpunt B, waarbij de lengte behouden blijft, terwijl interpolatie in plaats daarvan over een rechte lijn tussen de twee vectoreindpunten snijdt.

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;
}

In een ander voorbeeld gebruikt de pixel-shader zijn eigen constante buffers die licht- en materiaalinformatie bevatten. De invoerindeling in de vertex-shader zou worden uitgebreid om normaalgegevens op te nemen en de uitvoer van die vertex-shader zou naar verwachting getransformeerde vectoren bevatten voor het hoekpunt, het licht en de hoekpuntsnormaal in het coördinatensysteem van de weergave.

Als u texturebuffers en samplers hebt met toegewezen registers (t en s, respectievelijk), kunt u deze ook openen in de pixelshader.

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;
}

Shaders zijn zeer krachtige hulpprogramma's die kunnen worden gebruikt om procedurele middelen te genereren, zoals schaduwkaarten of ruistexturen. In feite vereisen geavanceerde technieken dat je texturen op een meer abstracte manier benadert, niet als visuele elementen, maar als buffers. Ze bevatten gegevens zoals hoogte-informatie of andere gegevens die kunnen worden bemonsterd in de laatste pixel-shader pass of in dat specifieke frame als onderdeel van een meerfasen-effectenpass. Multisampling is een krachtig hulpmiddel en de backbone van veel moderne visuele effecten.

Volgende stappen

Hopelijk bent u vertrouwd met DirectX 11at dit punt en bent u klaar om aan uw project te gaan werken. Hier volgen enkele koppelingen om andere vragen te beantwoorden die u mogelijk hebt over ontwikkeling met DirectX en C++:

Werken met DirectX-apparaatbronnen

inzicht in de Direct3D 11-renderingpijplijn