Dela via


Arbeta med shader och shader-resurser

Det är dags att lära dig hur du arbetar med skuggnings- och skuggningsresurser i utvecklingen av ditt Microsoft DirectX-spel för Windows 8. Vi har sett hur du konfigurerar grafikenheten och -resurserna, och du kanske till och med har börjat ändra dess pipeline. Nu ska vi titta på pixel- och hörnskuggor.

Om du inte är bekant med skuggningsspråk är en snabb diskussion i ordning. Skuggningar är små lågnivåprogram som kompileras och körs i specifika steg i grafikpipelinen. Deras specialitet är mycket snabba flyttalsberäkningar. De vanligaste skuggningsprogrammen är:

  • Vertexshader– Körs för varje vertex i en scen. Den här skuggningen fungerar på hörnbuffertelement som tillhandahålls av den anropande appen, och resulterar minimalt i en positionsvektor med 4 komponenter som rastreras till en pixelposition.
  • Pixel shader– Körs för varje pixel i ett återgivningsmål. Den här skuggningen tar emot rastrerade koordinater från tidigare skuggningssteg (i de enklaste pipelinesna skulle detta vara hörnskuggaren) och returnerar en färg (eller ett annat 4-komponentvärde) för pixelpositionen, som sedan skrivs till ett återgivningsmål.

Det här exemplet innehåller mycket grundläggande hörn- och pixelskuggare som bara ritar geometri och mer komplexa skuggor som lägger till grundläggande ljusberäkningar.

Skuggningsprogram skrivs i Microsoft High Level Shader Language (HLSL). HLSL-syntaxen ser mycket ut som C, men utan pekare. Skuggningsprogram måste vara mycket kompakta och effektiva. Om skuggningen kompileras till för många instruktioner kan den inte köras och ett fel returneras. (Observera att det exakta antalet instruktioner som tillåts är en del av Direct3D-funktionsnivå.)

I Direct3D kompileras inte shaders vid körning, utan de kompileras när resten av programmet kompileras. När du kompilerar din app med Microsoft Visual Studio 2013 kompileras HLSL-filerna till CSO-filer (.cso) som appen måste läsa in och placera i GPU-minnet före ritningen. Se till att du inkluderar dessa CSO-filer med din app när du paketerar den; de är tillgångar precis som mesh och texturer.

Förstå HLSL-semantik

Det är viktigt att ta en stund att diskutera HLSL-semantik innan vi fortsätter, eftersom de ofta är en förvirringspunkt för nya Direct3D-utvecklare. HLSL-semantik är strängar som identifierar ett värde som skickas mellan appen och ett skuggningsprogram. Även om de kan vara en mängd olika möjliga strängar är bästa praxis att använda en sträng som POSITION eller COLOR som anger användningen. Du tilldelar dessa semantik när du skapar en konstant buffert- eller indatalayout. Du kan också lägga till ett tal mellan 0 och 7 i semantiken så att du använder separata register för liknande värden. Till exempel: COLOR0, COLOR1, COLOR2...

Semantik som har prefixet "SV_" är systemvärde semantik som skrivs till av skuggningsprogrammet, och själva spelet (körs på processorn) kan inte ändra dem. Dessa semantik innehåller vanligtvis värden som är indata eller utdata från en annan skuggningsfas i grafikpipelinen, eller som genereras helt av GPU:n.

Dessutom har SV_ semantik olika beteenden när de används för att ange indata till eller utdata från en skuggningsfas. Till exempel innehåller SV_POSITION (utdata) hörndata som transformerats under hörnskuggningssteget, och SV_POSITION (indata) innehåller pixelpositionsvärdena som interpolerades av GPU:n under rastreringssteget.

Här är några vanliga HLSL-semantik:

  • POSITION(n) för hörnbuffertdata. SV_POSITION ger pixelpositionen till pixelskuggaren och kan inte skrivas av ditt spel.
  • NORMAL(n) för normala data som tillhandahålls av hörnbufferten.
  • TEXCOORD(n) för UV-koordinatdata för texturer som levereras till en shader.
  • COLOR(n) för RGBA-färginformation som skickas till en shader. Observera att det behandlas identiskt som koordinatdata, inklusive att interpolera värdet under rastrering; semantiken hjälper dig helt enkelt att identifiera att det är färgdata.
  • SV_Target[n] för att skriva från en pixelskuggning till en målstruktur eller annan pixelbuffert.

Vi ser några exempel på HLSL-semantik när vi granskar exemplet.

Läsa från konstantbuffertar

Alla shaders kan läsa från en konstant buffert om bufferten är kopplad till dess steg som en resurs. I det här exemplet tilldelas endast hörnskuggningen en konstant buffert.

Konstantbufferten deklareras på två platser: i C++-koden och i motsvarande HLSL-filer som kommer åt den.

Så här deklareras den konstanta bufferten i C++-koden.

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

När du deklarerar strukturen för den konstanta bufferten i C++-koden kontrollerar du att alla data är korrekt justerade längs 16 bytes gränser. Det enklaste sättet att göra detta är att använda DirectXMath typer, till exempel XMFLOAT4 eller XMFLOAT4X4, enligt exempelkoden. Du kan också skydda dig mot felanpassade buffertar genom att deklarera en statisk assert.

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

Den här kodraden orsakar ett fel vid kompilering om ConstantBufferStruct inte är justerat till 16 byte. Mer information om konstant buffertjustering och packning finns i Packregler för konstanta variabler.

Så här deklareras konstantbufferten i hörnskuggnings-HLSL.

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

Alla buffertar – konstant, struktur, provtagare eller annat – måste ha ett register definierat så att GPU:n kan komma åt dem. Varje skuggningssteg tillåter upp till 15 konstanta buffertar och varje buffert kan innehålla upp till 4 096 konstanta variabler. Syntaxen för registeranvändningsdeklarationen är följande:

  • b*#*: Ett register för en konstant buffert (cbuffer).
  • t*#*: Ett register för en strukturbuffert (tbuffer).
  • s*#*: Ett register för en provtagare. En sampler definierar uppslagsbeteendet för texlar i texturresursen.

Till exempel kan HLSL för en pixelskuggare ta en struktur och en sampler som indata med en deklaration som denna.

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

Det är upp till dig att tilldela konstanta buffertar till register – när du konfigurerar pipelinen kopplar du en konstant buffert till samma fack som du tilldelade den till i HLSL-filen. I föregående avsnitt anger till exempel anropet till VSSetConstantBuffers "0" för den första parametern. Det talar om för Direct3D att koppla den konstanta buffertresursen till register 0, vilket matchar buffertens tilldelning till register(b0) i HLSL-filen.

Läsa från hörnbuffertar

Vertexbufferten tillhandahåller triangeldata för scenobjekten till vertexshadern. Precis som med den konstanta bufferten deklareras hörnbuffertens struct i C++-koden med liknande förpackningsregler.

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

Det finns inget standardformat för hörndata i Direct3D 11. I stället definierar vi vår egen vertexdatalayout med hjälp av en deskriptor; datafälten definieras med hjälp av en matris med D3D11_INPUT_ELEMENT_DESC strukturer. Här visar vi en enkel indatalayout som beskriver samma vertexformat som föregående struktur.

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

Om du lägger till data i vertexformatet när du ändrar exempelkoden måste du uppdatera indatalayouten också, annars kommer shadern inte att kunna tolka den. Du kan ändra hörnlayouten så här:

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

I så fall ändrar du definitionen för inmatningslayout på följande sätt.

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

Var och en av elementdefinitionerna för indatalayout är prefixade med en sträng, till exempel "POSITION" eller "NORMAL", det är den semantik som vi diskuterade tidigare i det här avsnittet. Det är som ett handtag som hjälper GPU:n att identifiera elementet vid bearbetning av brytpunkten. Välj vanliga, meningsfulla namn för hörnelementen.

Precis som med den konstanta bufferten har hörnskuggningen en motsvarande buffertdefinition för inkommande hörnelement. (Därför angav vi en referens till vertexshaderresursen när vi skapade indatalayouten – Direct3D validerar datalayouten per vertex med shaderns inputstruktur.) Observera hur semantiken matchar mellan indatalayoutdefinition och den här HLSL-buffertdeklarationen. Men COLOR har en "0" tillagd. Det är inte nödvändigt att lägga till 0 om du bara har ett COLOR element som deklarerats i layouten, men det är en bra idé att lägga till det om du väljer att lägga till fler färgelement i framtiden.

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

Skicka data mellan skuggningar

Shaderprogram tar in datatyper och returnerar utdatatyper från sina huvudfunktioner vid exekvering. För vertexshadern som definierades i föregående avsnitt var indatatypen strukturen VS_INPUT, och vi definierade en matchande indatalayout och en C++-struktur. En matris med den här structen används för att skapa en hörnbuffert i metoden CreateCube.

Hörnskuggningen returnerar en PS_INPUT struktur, som minimalt måste innehålla den sista hörnpositionen med 4 komponenter (float4). Det här positionsvärdet måste ha systemvärdet semantiskt, SV_POSITION, deklarerat för det så att GPU:n har de data som krävs för att utföra nästa ritningssteg. Observera att det inte finns någon 1:1-korrespondens mellan hörnskuggningsutdata och pixelskuggningsindata. hörnskuggningen returnerar en struktur för varje hörn som anges, men pixelskuggningen körs en gång för varje pixel. Det beror på att data per hörn först passerar rasteriseringssteget. Det här steget bestämmer vilka pixlar som "täcker" geometrin som du ritar, beräknar interpolerade data per hörn för varje pixel och anropar sedan pixelskuggaren en gång för var och en av dessa pixlar. Interpolering är standardbeteendet vid rastrering av utdatavärden och är särskilt viktigt för korrekt bearbetning av utdatavektordata (ljusvektorer, normalvärden per hörn och tangenter och andra).

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

Granska hörnskuggningen

Exempelvertex-shadern är mycket enkel: ta in en vertex (position och färg), transformera positionen från modellkoordinater till perspektivprojekterade koordinater och returnera den (tillsammans med färgen) till rasterizatorn. Observera att färgvärdet interpoleras direkt tillsammans med positionsinformationen, vilket ger ett annat värde för varje pixel trots att hörnskuggningen inte utförde några beräkningar på färgvärdet.

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

En mer komplex hörnskuggning, till exempel en som konfigurerar ett objekts hörn för Phong-skuggning, kan se mer ut så här. I det här fallet drar vi nytta av det faktum att vektorerna och normalerna interpoleras för att approximeras till en jämn yta.

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

Granska pixelskuggningen

Den här pixelskuggningen i det här exemplet är förmodligen den absolut minsta mängd kod som du kan ha i en pixelskuggning. Den tar de interpolerade pixelfärgdata som genereras under rastreringen och returnerar dem som utdata, där de skrivs till ett återgivningsmål. Så tråkigt!

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

Den viktiga delen är SV_TARGET systemvärdessemantik för returvärdet. Det anger att utdata ska skrivas till det primära återgivningsmålet, vilket är den strukturbuffert som levereras till växlingskedjan för visning. Detta krävs för pixelskuggare – utan färgdata från pixelskuggningen skulle Direct3D inte ha något att visa!

Ett exempel på en mer komplex pixelshader för att göra Phong-skuggning kan se ut så här. Eftersom vektorerna och normalerna interpolerades behöver vi inte beräkna dem per bildpunkt. Vi måste dock normalisera dem igen på grund av hur interpoleringen fungerar. konceptuellt måste vi gradvis "snurra" vektorn från riktningen vid hörn A till riktning vid hörn B och behålla dess längd – wheras interpolation skär istället över en rak linje mellan de två vektorslutpunkterna.

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

I ett annat exempel tar pixelskuggaren sina egna konstanta buffertar som innehåller ljus- och materialinformation. Indatalayouten i hörnskuggningen skulle utökas till att omfatta normala data, och utdata från hörnskuggningen förväntas innehålla transformerade vektorer för hörn, ljus och hörn som är normala i visningskoordinatsystemet.

Om du har strukturbuffertar och exempel med tilldelade register (t respektive s) kan du även komma åt dem i pixelskuggningen.

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

Skuggningar är mycket kraftfulla verktyg som kan användas för att generera processuella resurser som skuggkartor eller brusstrukturer. I själva verket kräver avancerade tekniker att du tänker på texturer mer abstrakt, inte som visuella element utan som buffertar. De innehåller data som höjdinformation eller andra data som kan samplas i det sista pixelskuggningspasset eller i den specifika ramen som en del av ett multistegseffektpass. Multi-sampling är ett kraftfullt verktyg och ryggraden i många moderna visuella effekter.

Nästa steg

Förhoppningsvis är du bekväm med DirectX 11 på den här punkten och är redo att börja arbeta med ditt projekt. Här följer några länkar som hjälper dig att besvara andra frågor som du kan ha om utveckling med DirectX och C++:

Arbeta med DirectX-enhetsresurser

Förstå renderingspipeline för Direct3D 11