Condividi tramite


Usare shader e risorse shader

È il momento di imparare a usare shader e risorse shader per sviluppare il gioco Microsoft DirectX per Windows 8. È stato illustrato come configurare il dispositivo grafico e le risorse, e forse è stato anche iniziato a modificare la pipeline. Ora esaminiamo pixel e vertex shader.

Se non si ha familiarità con le lingue shader, è in ordine una discussione rapida. Gli shader sono programmi di basso livello di piccole dimensioni compilati ed eseguiti in fasi specifiche della pipeline grafica. La loro specialità è operazioni matematiche a virgola mobile molto veloci. I programmi shader più comuni sono:

  • Vertex shader: eseguito per ogni vertice in una scena. Questo shader opera sugli elementi del buffer dei vertici forniti dall'app chiamante e produce un vettore di posizione a 4 componenti che verrà rasterizzato in una posizione in pixel.
  • Pixel shader: eseguito per ogni pixel in una destinazione di rendering. Questo shader riceve le coordinate rasterizzate dalle fasi dello shader precedenti (nelle pipeline più semplici, si tratta del vertex shader) e restituisce un colore (o un altro valore di 4 componenti) per tale posizione del pixel, che viene quindi scritto in una destinazione di rendering.

Questo esempio include vertex shader e pixel shader di base che disegnano solo geometria e shader più complessi che aggiungono calcoli di illuminazione di base.

I programmi shader sono scritti in Microsoft High Level Shader Language (HLSL). La sintassi HLSL è molto simile a C, ma senza i puntatori. I programmi shader devono essere molto compatti ed efficienti. Se lo shader compila troppe istruzioni, non può essere eseguito e viene restituito un errore. Si noti che il numero esatto di istruzioni consentite fa parte del Livello di funzionalità Direct3D.

In Direct3D gli shader non vengono compilati in fase di esecuzione; vengono compilati quando il resto del programma viene compilato. Quando si compila l'app con Microsoft Visual Studio 2013, i file HLSL vengono compilati in file CSO (con estensione cso) che l'app deve caricare e inserire nella memoria GPU prima del disegno. Assicurarsi di includere questi file CSO con l'app al momento del pacchetto; sono asset come mesh e trame.

Informazioni sulla semantica HLSL

È importante dedicare un momento a discutere della semantica HLSL prima di continuare, perché spesso sono un punto di confusione per i nuovi sviluppatori Direct3D. La semantica HLSL sono stringhe che identificano un valore passato tra l'app e un programma shader. Anche se possono essere una qualsiasi delle stringhe possibili, la procedura consigliata consiste nell'usare una stringa come POSITION o COLOR che indica l'utilizzo. Queste semantiche vengono assegnate quando si crea un buffer costante o un layout di input. È anche possibile aggiungere un numero compreso tra 0 e 7 alla semantica in modo da usare registri separati per valori simili. Ad esempio: COLOR0, COLOR1, COLOR2...

La semantica preceduta da "SV_" è una semantica dei valori di sistema scritta dal programma shader. Il gioco stesso (in esecuzione nella CPU) non può modificarle. In genere, queste semantiche contengono valori che sono input o output da un'altra fase dello shader nella pipeline grafica o generati interamente dalla GPU.

Inoltre, SV_ la semantica presenta comportamenti diversi quando vengono usati per specificare l'input o l'output da una fase dello shader. Ad esempio, SV_POSITION (output) contiene i dati dei vertici trasformati durante la fase del vertex shader e SV_POSITION (input) contiene i valori di posizione pixel interpolati dalla GPU durante la fase di rasterizzazione.

Ecco alcune semantiche HLSL comuni:

  • POSITION(n) per i dati del buffer dei vertici. SV_POSITION fornisce una posizione pixel al pixel shader e non può essere scritta dal gioco.
  • NORMAL(n) per i dati normali forniti dal vertex buffer.
  • TEXCOORD(n) per i dati delle coordinate UV della trama forniti a uno shader.
  • COLOR(n) per i dati di colore RGBA forniti a uno shader. Si noti che viene trattato in modo identico per coordinare i dati, inclusa l'interpolazione del valore durante la rasterizzazione; la semantica consente semplicemente di identificare che si tratta di dati a colori.
  • SV_Target[n] per la scrittura da un pixel shader in una trama di destinazione o in un altro buffer di pixel.

Verranno visualizzati alcuni esempi di semantica HLSL durante la revisione dell'esempio.

Leggere dai buffer costanti

Qualsiasi shader può leggere da un buffer costante se tale buffer è collegato alla fase come risorsa. In questo esempio viene assegnato solo il vertex shader a un buffer costante.

Il buffer costante viene dichiarato in due posizioni: nel codice C++ e nei file HLSL corrispondenti a cui accederà.

Ecco come viene dichiarato lo struct del buffer costante nel codice C++.

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

Quando si dichiara la struttura per il buffer costante nel codice C++, assicurarsi che tutti i dati siano allineati correttamente lungo i limiti di 16 byte. Il modo più semplice per eseguire questa operazione consiste nell'usare i tipi DirectXMath, ad esempio XMFLOAT4 o XMFLOAT4X4, come illustrato nel codice di esempio. È anche possibile proteggersi da buffer non allineati dichiarando un'asserzione statica:

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

Questa riga di codice genererà un errore in fase di compilazione se ConstantBufferStruct non è allineato a 16 byte. Per altre informazioni sull'allineamento e la compressione costanti del buffer, vedere Regole di compressione per le variabili costanti.

Ecco ora come viene dichiarato il buffer costante nel vertex shader HLSL.

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

Tutti i buffer, costanti, trame, campionatori o altri, devono avere un registro definito in modo che la GPU possa accedervi. Ogni fase dello shader consente fino a 15 buffer costanti e ogni buffer può contenere fino a 4.096 variabili costanti. La sintassi della dichiarazione register-usage è la seguente:

  • b*#*: registro per un buffer costante (cbuffer).
  • t*#*: registrazione per un buffer di trama (tbuffer).
  • s*#*: registrazione per un campionatore. Un campionatore definisce il comportamento di ricerca per i texel nella risorsa trama.

Ad esempio, HLSL per un pixel shader potrebbe accettare una trama e un campionatore come input con una dichiarazione come questa.

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

Spetta all'utente assegnare buffer costanti per i registri: quando si configura la pipeline, si collega un buffer costante allo stesso slot a cui è stato assegnato nel file HLSL. Ad esempio, nell'argomento precedente la chiamata a VSSetConstantBuffers indica '0' per il primo parametro. Ciò indica a Direct3D di allegare la risorsa buffer costante per registrare 0, che corrisponde all'assegnazione del buffer a register(b0) nel file HLSL.

Leggere dai vertex buffer

Il vertex buffer fornisce i dati del triangolo per gli oggetti scena ai vertex shader. Come per il buffer costante, lo struct del buffer dei vertici viene dichiarato nel codice C++, usando regole di compressione simili.

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

Non esiste alcun formato standard per i dati dei vertici in Direct3D 11. Al contrario, definiamo il layout dei dati dei vertici usando un descrittore; I campi dati vengono definiti usando una matrice di strutture D3D11_INPUT_ELEMENT_DESC. Di seguito viene illustrato un layout di input semplice che descrive lo stesso formato dei vertici dello struct precedente:

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

Se si aggiungono dati al formato dei vertici durante la modifica del codice di esempio, assicurarsi di aggiornare anche il layout di input o lo shader non sarà in grado di interpretarlo. È possibile modificare il layout dei vertici in questo modo:

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

In tal caso, è necessario modificare la definizione del layout di input come indicato di seguito.

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

Ognuna delle definizioni degli elementi di layout di input è preceduta da una stringa, ad esempio "POSITION" o "NORMAL", ovvero la semantica descritta in precedenza in questo argomento. È come un handle che consente alla GPU di identificare l'elemento durante l'elaborazione del vertice. Scegliere nomi comuni e significativi per gli elementi dei vertici.

Come per il buffer costante, il vertex shader ha una definizione di buffer corrispondente per gli elementi dei vertici in ingresso. Per questo motivo è stato fornito un riferimento alla risorsa vertex shader durante la creazione del layout di input: Direct3D convalida il layout dei dati per vertice con lo struct di input dello shader. Si noti come la semantica corrisponda tra la definizione del layout di input e questa dichiarazione di buffer HLSL. Tuttavia, COLOR ha un "0" aggiunto ad esso. Non è necessario aggiungere il valore 0 se nel layout è stato dichiarato un COLOR solo elemento, ma è consigliabile aggiungerlo nel caso in cui si scelga di aggiungere altri elementi di colore in futuro.

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

Passare i dati tra shader

Gli shader accettano i tipi di input e restituiscono tipi di output dalle funzioni principali al momento dell'esecuzione. Per il vertex shader definito nella sezione precedente, il tipo di input era la struttura VS_INPUT ed è stato definito un layout di input corrispondente e uno struct C++. Una matrice di questo struct viene usata per creare un buffer dei vertici nel metodo CreateCube .

Il vertex shader restituisce una struttura PS_INPUT, che deve contenere al minimo la posizione finale del vertice a 4 componenti (float4). Questo valore di posizione deve avere la semantica del valore di sistema, SV_POSITION, dichiarata in modo che la GPU disponga dei dati necessari per eseguire il passaggio di disegno successivo. Si noti che non esiste una corrispondenza 1:1 tra l'output del vertex shader e l'input del pixel shader; il vertex shader restituisce una struttura per ogni vertice specificato, ma il pixel shader viene eseguito una volta per ogni pixel. Questo perché i dati per vertice passano prima di tutto attraverso la fase di rasterizzazione. Questa fase decide quali pixel "coprono" la geometria che si sta disegnando, calcola i dati interpolati per ogni vertice per ogni pixel e quindi chiama il pixel shader una volta per ognuno di questi pixel. L'interpolazione è il comportamento predefinito durante la rasterizzazione dei valori di output ed è essenziale in particolare per l'elaborazione corretta dei dati vettoriali di output (vettori leggeri, normali per vertice e tangenti e altri).

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

Esaminare il vertex shader

Il vertex shader di esempio è molto semplice: prendere un vertice (posizione e colore), trasformare la posizione dalle coordinate del modello in coordinate proiettate prospettiche e restituirla (insieme al colore) al rasterizzatore. Si noti che il valore del colore viene interpolato direttamente insieme ai dati di posizione, fornendo un valore diverso per ogni pixel, anche se il vertex shader non ha eseguito alcun calcolo sul valore del colore.

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

Un vertex shader più complesso, ad esempio uno che configura i vertici di un oggetto per l'ombreggiatura Phong, potrebbe essere più simile al seguente. In questo caso, si sfrutta il fatto che i vettori e le normali siano interpolati per approssimare una superficie di aspetto uniforme.

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

Esaminare il pixel shader

Questo pixel shader in questo esempio è probabilmente la quantità minima assoluta di codice che è possibile avere in un pixel shader. Accetta i dati di colore dei pixel interpolati generati durante la rasterizzazione e lo restituisce come output, dove verrà scritto in una destinazione di rendering. Quanto noioso!

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

La parte importante è la semantica del valore di SV_TARGET sistema sul valore restituito. Indica che l'output deve essere scritto nella destinazione di rendering primaria, ovvero il buffer di trama fornito alla catena di scambio per la visualizzazione. Questa operazione è necessaria per i pixel shader: senza i dati dei colori del pixel shader, Direct3D non avrebbe nulla da visualizzare.

Un esempio di pixel shader più complesso per eseguire l'ombreggiatura phong potrebbe essere simile al seguente. Poiché i vettori e le normali sono stati interpolati, non è necessario calcolarli in base al pixel. Tuttavia, dobbiamo ri normalizzarli a causa del funzionamento dell'interpolazione; concettualmente, è necessario "ruotare" gradualmente il vettore dalla direzione in corrispondenza del vertice A alla direzione del vertice B, mantenendone la lunghezza, mentre l'interpolazione wheras taglia invece una linea retta tra i due endpoint vettore.

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 un altro esempio, il pixel shader accetta i propri buffer costanti che contengono informazioni sulla luce e sul materiale. Il layout di input nel vertex shader verrà espanso per includere i dati normali e l'output di tale vertex shader includerà vettori trasformati per il vertice, la luce e la normale vertice nel sistema di coordinate di visualizzazione.

Se sono presenti buffer di trama e campionatori con registri assegnati (rispettivamente t e s), è possibile accedervi anche nel pixel shader.

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

Gli shader sono strumenti molto potenti che possono essere usati per generare risorse procedurali come mappe shadow o trame non significative. Infatti, le tecniche avanzate richiedono che si considerino le trame più astratte, non come elementi visivi, ma come buffer. Contengono dati come le informazioni sull'altezza o altri dati che possono essere campionati nel passaggio finale del pixel shader o in quel particolare frame come parte di un passaggio di effetti a più fasi. Il multi-campionamento è uno strumento potente e la spina dorsale di molti effetti visivi moderni.

Passaggi successivi

Speriamo che tu abbia familiarità con DirectX 11at questo punto e sei pronto per iniziare a lavorare sul tuo progetto. Ecco alcuni collegamenti che consentono di rispondere ad altre domande sullo sviluppo con DirectX e C++:Here are some links to help answer other questions you may have about development with DirectX and C++:

Usare le risorse dei dispositivi DirectX

Informazioni sulla pipeline di rendering di Direct3D 11