Condividi tramite


Scrittura di shader HLSL in Direct3D 9

Nozioni di base su Vertex-Shader

Quando è in funzione, un vertex shader programmabile sostituisce l'elaborazione dei vertici eseguita dalla pipeline grafica Microsoft Direct3D. Quando si usa un vertex shader, le informazioni sullo stato relative alle operazioni di trasformazione e illuminazione vengono ignorate dalla pipeline di funzioni fisse. Quando il vertex shader è disabilitato e viene restituita l'elaborazione di funzioni fisse, vengono applicate tutte le impostazioni di stato correnti.

Prima dell'esecuzione del vertex shader, è necessario eseguire la tassellatura di primitive di ordine elevato. Le implementazioni che eseguono il tassellamento della superficie dopo l'elaborazione dello shader devono farlo in modo che non sia evidente per l'applicazione e il codice dello shader.

Come minimo, un vertex shader deve restituire la posizione dei vertici nello spazio clip omogeneo. Facoltativamente, il vertex shader può restituire coordinate di trama, colore dei vertici, illuminazione dei vertici, fattori di nebbia e così via.

Nozioni di base su Pixel-Shader

L'elaborazione dei pixel viene eseguita dagli shader pixel su singoli pixel. I pixel shader funzionano insieme ai vertex shader; l'output di un vertex shader fornisce gli input per un pixel shader. Altre operazioni sui pixel (fusione della nebbia, operazioni di stencil e fusione del target di rendering) si verificano dopo l'esecuzione dello shader.

Stati delle fasi della trama e stati del campionatore

Un pixel shader sostituisce completamente la funzionalità di fusione dei pixel specificata dal miscelatore multi-texture, incluse le operazioni definite in precedenza dagli stati delle fasi di texture. Le operazioni di campionamento e filtro delle texture controllate dagli stati standard della fase texture per la minificazione, l'ingrandimento, il filtro mip e le modalità di indirizzamento di avvolgimento possono essere inizializzate negli shader. L'applicazione è libera di modificare questi stati senza richiedere la rigenerazione dello shader attualmente associato. L'impostazione dello stato può essere resa ancora più semplice se gli shader sono progettati all'interno di un effetto.

Ingressi per il pixel shader

Per le versioni del pixel shader ps_1_1 - ps_2_0, i colori diffusi e speculari sono saturi (bloccati) nell'intervallo da 0 a 1 prima dell'uso da parte dello shader.

Si presuppone che l'input dei valori di colore per il pixel shader sia corretto dal punto di vista, ma questo non è garantito (per tutti gli hardware). I colori campionati dalle coordinate della texture vengono iterati in modo prospetticamente corretto e vengono limitati all'intervallo da 0 a 1 durante l'iterazione.

Output del Pixel Shader

Per le versioni del pixel shader ps_1_1- ps_1_4, il risultato generato dal pixel shader è il contenuto del registro r0. Qualunque cosa contenga quando lo shader completa l'elaborazione viene inviata alla fase di nebbia e al blender del target di rendering.

Per le versioni del pixel shader ps_2_0 e versioni successive, il colore di output viene emesso da oC0 - oC4.

Input degli shader e variabili dello shader

Dichiarazione delle variabili dello shader

La dichiarazione di variabile più semplice include un tipo e un nome di variabile, ad esempio questa dichiarazione a virgola mobile:

float fVar;

È possibile inizializzare una variabile nella stessa istruzione.

float fVar = 3.1f;

È possibile dichiarare una matrice di variabili,

int iVar[3];

o dichiarato e inizializzato nella stessa istruzione.

int iVar[3] = {1,2,3};

Ecco alcune dichiarazioni che illustrano molte delle caratteristiche delle variabili HLSL (High Level Shader Language):

float4 color;
uniform float4 position : POSITION; 
const float4 lightDirection = {0,0,1};

Le dichiarazioni di dati possono usare qualsiasi tipo valido, tra cui:

Uno shader può avere variabili, argomenti e funzioni di primo livello.

// top-level variable
float globalShaderVariable; 

// top-level function
void function(
in float4 position: POSITION0 // top-level argument
              )
{
  float localShaderVariable; // local variable
  function2(...)
}

void function2()
{
  ...
}

Le variabili di primo livello vengono dichiarate all'esterno di tutte le funzioni. Gli argomenti di primo livello sono parametri per una funzione di primo livello. Una funzione di primo livello è qualsiasi funzione chiamata dall'applicazione , anziché una funzione chiamata da un'altra funzione.

Input shader uniformi

I vertex shader e pixel shader accettano due tipi di dati di input: variabile e uniforme. L'input variabile è costituito dai dati univoci per ogni esecuzione dello shader. Per un vertex shader, i dati variabili (ad esempio: posizione, normale e così via) provengono dai flussi dei vertici. I dati uniformi (ad esempio: colore materiale, trasformazione globale e così via) sono costanti per più esecuzioni di uno shader. Per coloro che hanno familiarità con i modelli di assembly shader, i dati uniformi sono specificati da registri costanti e dati variabili dai registri v e t.

I dati uniformi possono essere specificati da due metodi. Il metodo più comune consiste nel dichiarare le variabili globali e usarle all'interno di uno shader. Qualsiasi uso di variabili globali all'interno di uno shader comporterà l'aggiunta di tale variabile all'elenco di variabili uniformi richieste da tale shader. Il secondo metodo consiste nel contrassegnare un parametro di input della funzione shader di primo livello come uniforme. Questo contrassegno specifica che la variabile specificata deve essere aggiunta all'elenco di variabili uniformi.

Le variabili uniformi usate da uno shader vengono comunicate all'applicazione tramite la tabella costante. La tabella costante è il nome della tabella dei simboli che definisce il modo in cui le variabili uniformi usate da uno shader rientrano nei registri costanti. I parametri della funzione uniforme vengono visualizzati nella tabella costante preceduta da un segno di dollaro ($), a differenza delle variabili globali. Il simbolo del dollaro è necessario per evitare conflitti di nomi tra gli input uniformi locali e le variabili globali con lo stesso nome.

La tabella costante contiene le posizioni dei registri costanti di tutte le variabili uniformi usate dallo shader. La tabella include anche le informazioni sul tipo e il valore predefinito, se specificato.

Input variabili e semantica degli shader

I parametri di input variabili (di una funzione shader di primo livello) devono essere contrassegnati con una parola chiave semantica o uniforme che indica che il valore è costante per l'esecuzione dello shader. Se un input dello shader di primo livello non è contrassegnato con una parola chiave semantica o uniforme, non sarà possibile compilare lo shader.

La semantica di input è un nome usato per collegare l'input specificato a un output della parte precedente della pipeline grafica. Ad esempio, la semantica di input POSITION0 viene usata dai vertex shader per specificare dove devono essere collegati i dati di posizione dal buffer dei vertici.

I pixel shader e i vertex shader hanno insiemi differenti di semantiche di input a causa delle diverse parti della pipeline grafica che alimentano ciascuna unità shader. La semantica di input del vertex shader descrive le informazioni per-vertice (ad esempio, posizione, normale, coordinate di trama, colore, tangente, binormale e così via) da caricare da un buffer dei vertici in una forma che può essere consumata dal vertex shader. La semantica dell'input è mappata direttamente sull'uso della dichiarazione dei vertici e sull'indice di utilizzo.

La semantica di input del pixel shader descrive le informazioni fornite per pixel dall'unità di rasterizzazione. I dati vengono generati interpolando tra gli output del vertex shader per ogni vertice della primitiva corrente. La semantica di input del pixel shader di base collega le informazioni sul colore di output e sulle coordinate della trama ai parametri di input.

La semantica di input può essere assegnata all'input dello shader tramite due metodi:

  • Accodamento di due punti e nome semantico alla dichiarazione di parametro.
  • Definizione di una struttura di input con semantica di input assegnata a ogni membro della struttura.

I vertex shader e pixel shader forniscono i dati di output alla fase successiva della pipeline grafica. La semantica di output viene usata per specificare il modo in cui i dati generati dallo shader devono essere collegati agli input della fase successiva. Ad esempio, la semantica di output per un vertex shader viene usata per collegare gli output degli interpolatori nel rasterizzatore per generare i dati di input per il pixel shader. Gli output del pixel shader sono i valori forniti all'unità di fusione alfa per ognuna delle destinazioni di rendering o il valore di profondità scritto nel buffer di profondità.

Le semantiche di output del vertex shader vengono utilizzate per collegare lo shader sia al pixel shader che alla fase di rasterizzazione. Un vertex shader utilizzato dal rasterizzatore e non esposto al pixel shader deve generare i dati di posizione come minimo. I vertex shader che generano dati di coordinate e colori della trama forniscono i dati a un pixel shader dopo l'interpolazione.

La semantica di output del pixel shader associa i colori di output di un pixel shader alla destinazione di rendering corretta. Il colore di output dello shader pixel è collegato alla fase di fusione alfa, che determina la modalità in cui vengono modificate le destinazioni di rendering. L'output di profondità del pixel shader può essere utilizzato per modificare i valori di profondità di destinazione nella posizione corrente del raster. L'output di profondità e più destinazioni di rendering sono supportate solo con alcuni modelli di shader.

La sintassi per la semantica di output è identica alla sintassi per specificare la semantica di input. La semantica può essere specificata direttamente nei parametri dichiarati come parametri "out" o assegnati durante la definizione di una struttura restituita come parametro "out" o come valore restituito da una funzione.

La semantica identifica la provenienza dei dati. Le semantiche sono identificatori facoltativi che identificano gli input e gli output dello shader. La semantica viene visualizzata in una delle tre posizioni seguenti:

  • Dopo un elemento della struttura.
  • Dopo un argomento nella lista degli argomenti di input di una funzione.
  • Dopo l'elenco di argomenti di input della funzione.

In questo esempio viene utilizzata una struttura per fornire uno o più input del vertex shader e un'altra struttura per fornire uno o più output del vertex shader. Ogni membro della struttura usa una semantica.

vector vClr;

struct VS_INPUT
{
    float4 vPosition : POSITION;
    float3 vNormal : NORMAL;
    float4 vBlendWeights : BLENDWEIGHT;
};

struct VS_OUTPUT
{
    float4  vPosition : POSITION;
    float4  vDiffuse : COLOR;

};

float4x4 mWld1;
float4x4 mWld2;
float4x4 mWld3;
float4x4 mWld4;

float Len;
float4 vLight;

float4x4 mTot;

VS_OUTPUT VS_Skinning_Example(const VS_INPUT v, uniform float len=100)
{
    VS_OUTPUT out;

    // Skin position (to world space)
    float3 vPosition = 
        mul(v.vPosition, (float4x3) mWld1) * v.vBlendWeights.x +
        mul(v.vPosition, (float4x3) mWld2) * v.vBlendWeights.y +
        mul(v.vPosition, (float4x3) mWld3) * v.vBlendWeights.z +
        mul(v.vPosition, (float4x3) mWld4) * v.vBlendWeights.w;
    // Skin normal (to world space)
    float3 vNormal =
        mul(v.vNormal, (float3x3) mWld1) * v.vBlendWeights.x + 
        mul(v.vNormal, (float3x3) mWld2) * v.vBlendWeights.y + 
        mul(v.vNormal, (float3x3) mWld3) * v.vBlendWeights.z + 
        mul(v.vNormal, (float3x3) mWld4) * v.vBlendWeights.w;
    
    // Output stuff
    out.vPosition    = mul(float4(vPosition + vNormal * Len, 1), mTot);
    out.vDiffuse  = dot(vLight,vNormal);

    return out;
}

La struttura di input identifica i dati del vertex buffer che fornirà gli input dello shader. Questo shader esegue il mapping dei dati degli elementi position, normal e blendweight del vertex buffer in registri vertex shader. Il tipo di dati di input non deve corrispondere esattamente al tipo di dati di dichiarazione del vertice. Se non corrisponde esattamente, i dati dei vertici verranno convertiti automaticamente nel tipo di dati HLSL quando vengono scritti nei registri dello shader. Ad esempio, se i dati normali sono stati definiti come di tipo UINT dall'applicazione, verranno convertiti in float3 quando letti dallo shader.

Se i dati nel flusso dei vertici contengono meno componenti rispetto al tipo di dati shader corrispondente, i componenti mancanti verranno inizializzati su 0 (ad eccezione di w, inizializzato su 1).

La semantica di input è simile ai valori nella D3DDECLUSAGE.

La struttura di output identifica i parametri di output del vertex shader di posizione e colore. Questi output verranno usati dalla pipeline per la rasterizzazione dei triangoli (nell'elaborazione primitiva). L'output contrassegnato come dati di posizione indica la posizione di un vertice nello spazio omogeneo. Come minimo, un vertex shader deve generare dati di posizione. La posizione dello spazio dello schermo viene calcolata dopo il completamento del vertex shader dividendo la coordinata (x, y, z) per w. Nello spazio dello schermo, -1 e 1 sono i valori minimo e massimo x e y dei limiti del riquadro di visualizzazione, mentre z viene usato per i test del buffer z.

Anche la semantica di output è simile ai valori in D3DDECLUSAGE. In generale, una struttura di output per un vertex shader può essere utilizzata come struttura di input per un pixel shader, a condizione che il pixel shader non legga da variabili contrassegnate con le semantiche di posizione, dimensione del punto o nebbia. Queste semantiche sono associate a valori scalari per vertice che non vengono usati da un pixel shader. Se questi valori sono necessari per il pixel shader, possono essere copiati in un'altra variabile di output che usa una semantica pixel shader.

Le variabili globali vengono assegnate automaticamente ai registri dal compilatore. Le variabili globali sono dette anche parametri uniformi perché il contenuto della variabile è lo stesso per tutti i pixel elaborati ogni volta che viene chiamato lo shader. I registri sono contenuti nella tabella costante, che può essere letta usando l'interfacciaID3DXConstantTable.

La semantica di input per i pixel shader esegue il mapping dei valori in registri hardware specifici per il trasporto tra vertex shader e pixel shader. Ogni tipo di registro ha proprietà specifiche. Poiché attualmente esistono solo due semantiche per le coordinate di colore e di trama, è comune che la maggior parte dei dati venga contrassegnata come coordinata di trama anche quando non dovrebbe esserlo.

Si noti che la struttura di output del vertex shader ha usato un input con i dati di posizione, che non viene usato dal pixel shader. HLSL consente dati di output validi del vertex shader che non sono dati di input validi per un pixel shader, a condizione che non sia referenziato nel pixel shader.

Gli argomenti di input possono anche essere matrici. La semantica viene incrementata automaticamente dal compilatore per ogni elemento della matrice. Si consideri, ad esempio, la dichiarazione esplicita seguente:

struct VS_OUTPUT
{
    float4 Position   : POSITION;
    float3 Diffuse    : COLOR0;
    float3 Specular   : COLOR1;               
    float3 HalfVector : TEXCOORD3;
    float3 Fresnel    : TEXCOORD2;               
    float3 Reflection : TEXCOORD0;               
    float3 NoiseCoord : TEXCOORD1;               
};

float4 Sparkle(VS_OUTPUT In) : COLOR

La dichiarazione esplicita specificata in precedenza equivale alla dichiarazione seguente che avrà una semantica incrementata automaticamente dal compilatore:

float4 Sparkle(float4 Position : POSITION,
                 float3 Col[2] : COLOR0,
                 float3 Tex[4] : TEXCOORD0) : COLOR0
{
   // shader statements
   ...

Proprio come la semantica di input, la semantica di output identifica l'utilizzo dei dati per i dati di output del pixel shader. Molti pixel shader scrivono in un solo colore di output. I pixel shader possono anche scrivere un valore di profondità in una o più destinazioni di rendering contemporaneamente (fino a quattro). Come i vertex shader, i pixel shader usano una struttura per restituire più output. Questo shader scrive 0 nei componenti di colore e nel componente di profondità.

struct PS_OUTPUT
{
    float4 Color[4] : COLOR0;
    float  Depth  : DEPTH;
};

PS_OUTPUT main(void)
{
    PS_OUTPUT out;

   // Shader statements
   ...

  // Write up to four pixel shader output colors
  out.Color[0] =  ...
  out.Color[1] =  ...
  out.Color[2] =  ...
  out.Color[3] =  ...

  // Write pixel depth 
  out.Depth =  ...

    return out;
}

I colori di output del pixel shader devono essere di tipo float4. Quando si scrivono più colori, tutti i colori di output devono essere usati in modo contiguo. In altre parole, COLOR1 non può essere un output a meno che non sia già stato scritto COLOR0. L'output di profondità del pixel shader deve essere di tipo float1.

Campionatori e oggetti texture

Un campionatore contiene lo stato del campionatore. Lo stato sampler specifica la trama da campionare e controlla il filtro eseguito durante il campionamento. Per campionare una trama sono necessari tre elementi:

  • Una consistenza
  • Un sampler (con stato del sampler)
  • Un'istruzione di campionamento

I campionatori possono essere inizializzati con trame e stato campionatore, come illustrato di seguito:

sampler s = sampler_state 
{ 
  texture = NULL; 
  mipfilter = LINEAR; 
};

Ecco un esempio di codice per campionare una trama 2D:

texture tex0;
sampler2D s_2D;

float2 sample_2D(float2 tex : TEXCOORD0) : COLOR
{
  return tex2D(s_2D, tex);
}

La trama viene dichiarata con una variabile di trama tex0.

In questo esempio viene dichiarata una variabile sampler denominata s_2D. Il campionatore contiene lo stato del campionatore all'interno di parentesi graffe. Ciò include la texture che verrà campionata e, facoltativamente, lo stato del filtro, ovvero le modalità di wrapping, le modalità di filtro e così via. Se lo stato del campionatore viene omesso, viene applicato uno stato di campionatore predefinito che specifica il filtro lineare e una modalità di ripetizione per le coordinate della texture. La funzione sampler accetta una coordinata di trama a virgola mobile a due componenti e restituisce un colore a due componenti. Viene rappresentato con il tipo di ritorno float2 e rappresenta i dati nei componenti rosso e verde.

Sono definiti quattro tipi di campionatori (vedere Parole chiave) e le ricerche di texture vengono eseguite dalle funzioni intrinseche: tex1D(s, t) (DirectX HLSL), tex2D(s, t) (DirectX HLSL), tex3D(s, t) (DirectX HLSL), texCUBE(s, t) (DirectX HLSL). Ecco un esempio di campionamento 3D:

texture tex0;
sampler3D s_3D;

float3 sample_3D(float3 tex : TEXCOORD0) : COLOR
{
  return tex3D(s_3D, tex);
}

Questa dichiarazione di sampler usa lo stato predefinito del sampler per le impostazioni del filtro e la modalità di indirizzamento.

Di seguito è riportato l'esempio di campionamento del cubo corrispondente:

texture tex0;
samplerCUBE s_CUBE;

float3 sample_CUBE(float3 tex : TEXCOORD0) : COLOR
{
  return texCUBE(s_CUBE, tex);
}

Infine, ecco l'esempio di campionamento 1D:

texture tex0;
sampler1D s_1D;

float sample_1D(float tex : TEXCOORD0) : COLOR
{
  return tex1D(s_1D, tex);
}

Poiché il runtime non supporta trame 1D, il compilatore userà una trama 2D con la conoscenza che la coordinata y non è importante. Poiché tex1D(s, t) (DirectX HLSL) viene implementato come ricerca di trama 2D, il compilatore è libero di scegliere il componente y in modo efficiente. In alcuni scenari rari, il compilatore non può scegliere un componente y efficiente, nel qual caso genererà un avviso.

texture tex0;
sampler s_1D_float;

float4 main(float texCoords : TEXCOORD) : COLOR
{
    return tex1D(s_1D_float, texCoords);
}

Questo particolare esempio è inefficiente perché il compilatore deve spostare la coordinata di input in un altro registro (perché una ricerca 1D viene implementata come ricerca 2D e la coordinata della trama viene dichiarata come float1). Se il codice viene riscritto usando un input float2 anziché float1, il compilatore può usare la coordinata di texture di input perché sa che y è stato inizializzato a un certo valore.

texture tex0;
sampler s_1D_float2;

float4 main(float2 texCoords : TEXCOORD) : COLOR
{
    return tex1D(s_1D_float2, texCoords);
}

Tutti i lookups di texture possono avere come suffisso "bias" o "proj", ovvero tex2Dbias (DirectX HLSL), texCUBEproj (DirectX HLSL)). Con il suffisso "proj", la coordinata della texture è divisa per il componente w. Con "bias", il livello mip viene spostato attraverso il componente w. Pertanto, tutti gli accessi a trame con un suffisso richiedono sempre un input di tipo float4. tex1D(s, t) (DirectX HLSL) e tex2D(s, t) (DirectX HLSL) ignora i componenti yz e z rispettivamente.

I campionatori possono essere usati anche in array, anche se attualmente nessun sistema di supporto posteriore consente l'accesso dinamico agli array dei campionatori. Di conseguenza, il codice seguente è valido perché può essere risolto in fase di compilazione:

tex2D(s[0],tex)

Tuttavia, questo esempio non è valido.

tex2D(s[a],tex)

L'accesso dinamico dei campionatori è utile principalmente per la scrittura di programmi con cicli letterali. Il codice seguente illustra l'accesso a un array di sampler:

sampler sm[4];

float4 main(float4 tex[4] : TEXCOORD) : COLOR
{
    float4 retColor = 1;

    for(int i = 0; i < 4;i++)
    {
        retColor *= tex2D(sm[i],tex[i]);
    }

    return retColor;
}

Nota

L'uso del runtime di debug di Microsoft Direct3D consente di intercettare le mancate corrispondenze tra il numero di componenti in una trama e un campionatore.

 

Scrittura di funzioni

Le funzioni suddivideno le attività di grandi dimensioni in quelle più piccole. Le piccole attività sono più facili da eseguire per il debug e possono essere riutilizzate, una volta comprovate. Le funzioni possono essere usate per nascondere i dettagli di altre funzioni, che semplificano il completamento di un programma costituito da funzioni.

Le funzioni HLSL sono simili alle funzioni C in diversi modi: entrambe contengono una definizione e un corpo di funzione e dichiarano entrambi tipi restituiti ed elenchi di argomenti. Analogamente alle funzioni C, la convalida HLSL esegue il controllo dei tipi degli argomenti e del valore restituito nella fase di compilazione dello shader.

A differenza delle funzioni C, le funzioni di punto di ingresso HLSL usano la semantica per associare gli argomenti della funzione agli input e agli output dello shader (le funzioni HLSL chiamate internamente ignorano la semantica). In questo modo è più semplice associare i dati del buffer a uno shader e associare gli output dello shader agli input dello shader.

Una funzione contiene una dichiarazione e un corpo e la dichiarazione deve precedere il corpo.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
    return mul(inPos, WorldViewProj );
};

La dichiarazione di funzione include tutti gli elementi davanti alle parentesi graffe:

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION

Una dichiarazione di funzione contiene:

  • Tipo restituito
  • Nome di una funzione
  • Elenco di argomenti (facoltativo)
  • Una semantica di output (facoltativa)
  • Annotazione (facoltativa)

Il tipo restituito può essere uno qualsiasi dei tipi di dati di base HLSL, ad esempio float4:

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
   ...
}

Il tipo restituito può essere una struttura già definita:

struct VS_OUTPUT
{
    float4  vPosition        : POSITION;
    float4  vDiffuse         : COLOR;
}; 

VS_OUTPUT VertexShader_Tutorial_1(float4 inPos : POSITION )
{
   ...
}

Se la funzione non restituisce un valore, void può essere usato come tipo restituito.

void VertexShader_Tutorial_1(float4 inPos : POSITION )
{
   ...
}

Il tipo restituito viene sempre visualizzato per primo in una dichiarazione di funzione.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION

Un elenco di argomenti dichiara gli argomenti di input in una funzione. Può anche dichiarare valori che verranno restituiti. Alcuni argomenti sono sia argomenti di input che di output. Di seguito è riportato un esempio di shader che accetta quattro argomenti di input.

float4 Light(float3 LightDir : TEXCOORD1, 
             uniform float4 LightColor,  
             float2 texcrd : TEXCOORD0, 
             uniform sampler samp) : COLOR 
{
    float3 Normal = tex2D(samp,texcrd);

    return dot((Normal*2 - 1), LightDir)*LightColor;
}

Questa funzione restituisce un colore finale, ovvero una fusione di un campione di trama e il colore chiaro. La funzione accetta quattro input. Due input hanno semantica: LightDir ha la semantica TEXCOORD1 e texcrd ha la semantica TEXCOORD0. La semantica indica che i dati per queste variabili provengono dal vertex buffer. Anche se la variabile LightDir ha una semantica TEXCOORD1, il parametro probabilmente non è una coordinata della trama. Il tipo semantico TEXCOORDn viene spesso usato per fornire una semantica per un tipo non predefinito (non esiste una semantica di input del vertex shader per una direzione di luce).

Gli altri due input LightColor e samp sono etichettati con la parola chiave uniform. Si tratta di costanti uniformi che non cambieranno tra le chiamate di disegno. I valori per questi parametri provengono dalle variabili globali dello shader.

Gli argomenti possono essere etichettati come input con la parola chiave in e gli argomenti di output con la parola chiave out. Gli argomenti non possono essere passati per riferimento; Tuttavia, un argomento può essere sia un input che un output se viene dichiarato con la parola chiave inout. Gli argomenti passati a una funzione contrassegnati con la parola chiave inout vengono considerati copie dell'originale fino al termine dell'esecuzione della funzione, e vengono riportati alle loro posizioni originali. Ecco un esempio che usa inout:

void Increment_ByVal(inout float A, inout float B) 
{ 
    A++; B++;
}

Questa funzione incrementa i valori in A e B e li restituisce.

Il corpo della funzione è tutto il codice dopo la dichiarazione di funzione.

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION
{
    return mul(inPos, WorldViewProj );
};

Il corpo è costituito da istruzioni racchiuse tra parentesi graffe. Il corpo della funzione implementa tutte le funzionalità usando variabili, valori letterali, espressioni e istruzioni.

Il corpo dello shader esegue due operazioni: esegue una moltiplicazione della matrice e restituisce un risultato float4. La moltiplicazione della matrice viene eseguita con la funzionemul (DirectX HLSL), che esegue una moltiplicazione di matrice 4x4. mul (DirectX HLSL) viene chiamata funzione intrinseca perché è già incorporata nella libreria HLSL di funzioni. Le funzioni intrinseche verranno illustrate in modo più dettagliato nella sezione successiva.

Il prodotto di matrici combina il vettore di input Pos e la matrice composita WorldViewProj. Il risultato è che i dati di posizione vengono trasformati in spazio dello schermo. Si tratta dell'elaborazione minima del vertex shader che è possibile eseguire. Se utilizzassimo la pipeline a funzione fissa anziché un vertex shader, i dati dei vertici potrebbero essere disegnati dopo aver effettuato questa trasformazione.

L'ultima istruzione nel corpo di una funzione è un'istruzione di return. Analogamente a C, questa istruzione restituisce il controllo dalla funzione all'istruzione che ha chiamato la funzione .

I tipi restituiti dalla funzione possono essere uno qualsiasi dei tipi di dati semplici definiti in HLSL, tra cui bool, int half, float e double. I tipi restituiti possono essere uno dei tipi di dati complessi, ad esempio vettori e matrici. I tipi HLSL che fanno riferimento agli oggetti non possono essere usati come tipi restituiti. Sono inclusi pixelhader, vertexshader, texture e sampler.

Di seguito è riportato un esempio di funzione che usa una struttura per un tipo restituito.

float4x4 WorldViewProj : WORLDVIEWPROJ;

struct VS_OUTPUT
{
    float4 Pos  : POSITION;
};

VS_OUTPUT VS_HLL_Example(float4 inPos : POSITION )
{
    VS_OUTPUT Out;

    Out.Pos = mul(inPos,  WorldViewProj );

    return Out;
};

Il tipo restituito float4 è stato sostituito con la struttura VS_OUTPUT, che ora contiene un singolo membro float4.

Un'istruzione return segnala la fine di una funzione. Questa è l'istruzione return più semplice. Restituisce il controllo dalla funzione al programma chiamante. Non restituisce alcun valore.

void main()
{
    return ;
}

Un'istruzione return può restituire uno o più valori. Questo esempio restituisce un valore letterale:

float main( float input : COLOR0) : COLOR0
{
    return 0;
}

In questo esempio viene restituito il risultato scalare di un'espressione:

return  light.enabled;

In questo esempio viene restituito un valore float4 costruito da una variabile locale e da un valore letterale:

return  float4(color.rgb, 1) ;

Questo esempio restituisce un valore float4 costruito dal risultato restituito da una funzione intrinseca e alcuni valori letterali:

float4 func(float2 a: POSITION): COLOR
{
    return float4(sin(length(a) * 100.0) * 0.5 + 0.5, sin(a.y * 50.0), 0, 1);
}

In questo esempio viene restituita una struttura che contiene uno o più membri:

float4x4 WorldViewProj;

struct VS_OUTPUT
{
    float4 Pos  : POSITION;
};

VS_OUTPUT VertexShader_Tutorial_1(float4 inPos : POSITION )
{
    VS_OUTPUT out;
    out.Pos = mul(inPos, WorldViewProj );
    return out;
};

Controllo del flusso

La maggior parte degli hardware per vertex e pixel shader attuali è progettata per eseguire gli shader riga per riga, eseguendo ogni istruzione una sola volta. HLSL supporta il controllo del flusso, che include la diramazione statica, le istruzioni predicate, il ciclo statico, il diramazione dinamica e il ciclo dinamico.

In precedenza, l'uso di un'istruzione if generava il codice shader del linguaggio assembly che implementava sia il ramo if che il ramo else del flusso di codice. Di seguito è riportato un esempio del codice HLSL compilato per vs_1_1:

if (Value > 0)
    oPos = Value1; 
else
    oPos = Value2; 

Di seguito è riportato il codice assembly risultante:

// Calculate linear interpolation value in r0.w
mov r1.w, c2.x               
slt r0.w, c3.x, r1.w         
// Linear interpolation between value1 and value2
mov r7, -c1                      
add r2, r7, c0                   
mad oPos, r0.w, r2, c1  

Alcuni hardware consentono il ciclo statico o dinamico, ma la maggior parte richiede l'esecuzione lineare. Nei modelli che non supportano l'esecuzione di loop, tutti i loop devono essere srotolati. Un esempio è l'esempio di esempio DepthOfField che usa cicli senza rollback anche per gli shader ps_1_1.

HLSL include ora il supporto per ognuno di questi tipi di controllo del flusso:

  • diramazione statica
  • istruzioni predicate
  • ciclo statico
  • diramazione dinamica
  • ciclo dinamico

La diramazione statica consente l'accensione o la disattivazione di blocchi di codice shader in base a una costante dello shader booleano. Si tratta di un metodo pratico per abilitare o disabilitare i percorsi di codice in base al tipo di oggetto attualmente sottoposto a rendering. Tra le chiamate di disegno, è possibile decidere quali funzionalità si desidera supportare con lo shader corrente e quindi impostare i flag booleani necessari per ottenere tale comportamento. Tutte le istruzioni disabilitate da una costante booleana vengono ignorate durante l'esecuzione dello shader.

Il supporto di diramazione più familiare è la diramazione dinamica. Con la diramazione dinamica, la condizione di confronto risiede in una variabile, il che significa che il confronto viene eseguito per ogni vertice o ogni pixel in fase di esecuzione (anziché il confronto in fase di compilazione o tra due chiamate di disegno). La perdita di prestazioni è il costo del branch più il costo delle istruzioni relative al branch preso. La diramazione dinamica viene implementata nel modello di shader 3 o versione successiva. L'ottimizzazione degli shader che funzionano con questi modelli è simile all'ottimizzazione del codice eseguito in una CPU.

Guida alla programmazione di per HLSL