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 suddivisione a mosaico di primitive di ordine elevato. Le implementazioni che eseguono la tassellatura della superficie dopo l'elaborazione dello shader devono farlo in modo che non sia evidente per l'applicazione e il codice 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 da pixel shader su singoli pixel. I pixel shader funzionano insieme ai vertex shader; l'output di un vertex shader fornisce gli input per un pixel shader. Dopo l'esecuzione dello shader vengono eseguite altre operazioni di pixel (fusione nebbia, stencil e fusione di destinazione di rendering).

Stati della fase trama e campionatore

Un pixel shader sostituisce completamente la funzionalità di fusione dei pixel specificata dal frullatore a più trame, incluse le operazioni definite in precedenza dagli stati della fase della trama. Le operazioni di campionamento e filtro delle trame controllate dagli stati standard della fase della trama per la minificazione, l'ingrandimento, il filtro mip e le modalità di indirizzamento a capo possono essere inizializzate negli shader. L'applicazione può 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.

Input pixel shader

Per le versioni di 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 i valori di colore immessi nel pixel shader siano corretti dal punto di vista, ma questo non è garantito (per tutti gli hardware). I colori campionati dalle coordinate della trama vengono iterati in modo corretto dal punto di vista e vengono bloccati all'intervallo da 0 a 1 durante l'iterazione.

Output 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 frullatore di destinazione del rendering.

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

Input shader e variabili shader

Dichiarazione delle variabili 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 dichiarati e inizializzati 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 uniformi shader

I vertex shader e pixel shader accettano due tipi di dati di input: variabili e uniformi. 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 del mondo 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 restituite all'applicazione tramite la tabella costante. La tabella costante è il nome della tabella dei simboli che definisce il modo in cui le variabili uniform 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 segno di 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 e semantica di shader variabili

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, lo shader non riuscirà a compilare.

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 e i vertex shader hanno set diversi di semantica di input a causa delle diverse parti della pipeline grafica che vengono inserite in ogni unità shader. La semantica di input del vertex shader descrive le informazioni relative al vertice (ad esempio, posizione, normale, coordinate di trama, colore, tangente, binormal e così via) da caricare da un buffer dei vertici in una maschera che può essere utilizzata dal vertex shader. La semantica di input viene mappata direttamente all'utilizzo della dichiarazione dei vertici e all'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 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 sulla coordinata 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 del 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à.

La semantica di output del vertex shader viene usata 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 coordinate di trama e dati di colore forniscono i dati a un pixel shader dopo l'interpolazione.

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

La sintassi per la semantica di output è identica alla sintassi per specificare la semantica di input. La semantica può essere specificata direttamente sui 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 dove provengono i dati. La semantica è identificatori facoltativi che identificano gli input e gli output dello shader. La semantica viene visualizzata in una delle tre posizioni:

  • Dopo un membro della struttura.
  • Dopo un argomento nell'elenco degli argomenti di input di una funzione.
  • Dopo l'elenco di argomenti di input della funzione.

In questo esempio viene usata una struttura per fornire uno o più input dello shader vertex e un'altra struttura per fornire uno o più output dello shader vertex. Ognuno dei membri 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 dal buffer del vertice che fornirà gli input dello shader. Questo shader esegue il mapping dei dati dagli elementi position, normal e blendweight del buffer vertex nei registri del vertex shader. Il tipo di dati di input non deve corrispondere esattamente al tipo di dati della dichiarazione del vertice. Se non corrisponde esattamente, i dati del vertice verranno convertiti automaticamente nel tipo di dati HLSL quando viene scritto nei registri shader. Ad esempio, se i dati normali sono stati definiti per essere di tipo UINT dall'applicazione, verrà convertito in un float3 quando viene letto dallo shader.

Se i dati nel flusso del vertice contengono meno componenti del 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 del triangolo (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 dello shader del vertice suddividendo la coordinata (x, y, z) in base a w. Nello spazio dello schermo, -1 e 1 sono i valori minimi e massimi x e y dei limiti del riquadro di visualizzazione, mentre z viene usato per i test di z-buffer.

La semantica di output è anche simile ai valori in D3DDECLUSAGE. In generale, una struttura di output per un vertex shader può essere usata anche come struttura di input per un pixel shader, purché lo shader pixel non venga letto da alcuna variabile contrassegnata con la posizione, le dimensioni del punto o la semantica della nebbia. Queste semantiche sono associate ai valori scalari per vertice che non vengono usati da un pixel shader. Se questi valori sono necessari per lo shader pixel, possono essere copiati in un'altra variabile di output che usa una semantica pixel shader.

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

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

Si noti che la struttura di output del vertex shader ha usato un input con dati di posizione, che non viene usato dal pixel shader. HLSL consente dati di output validi di un vertex shader non valido per un pixel shader, purché non venga fatto riferimento 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à la semantica incrementata automaticamente dal compilatore:

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

Analogamente alla semantica di input, la semantica di output identifica l'utilizzo dei dati per i dati di output pixel shader. Molti shader pixel scrivono su un solo colore di output. Gli shader pixel possono anche scrivere un valore di profondità in uno o più destinazioni di rendering contemporaneamente (fino a quattro). Come i vertex shader, gli shader pixel usano una struttura per restituire più di un output. Questo shader scrive 0 nei componenti del colore, nonché 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 dello shader pixel 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 COLOR0 non sia già stato scritto. L'output della profondità dello shader pixel deve essere di tipo float1.

Esempi e oggetti trama

Un sampler contiene lo stato dell'esempio. Lo stato di sampler specifica la trama da campionamento e controlla il filtro eseguito durante il campionamento. Sono necessarie tre cose per campionire una trama:

  • Trama
  • Sampler (con stato di sampler)
  • Istruzione di campionamento

Gli esempi possono essere inizializzati con trame e stato del sampler, come illustrato di seguito:

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

Ecco un esempio del codice per esempio di 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 di esempio denominata s_2D. L'sampler contiene lo stato del sampler all'interno di parentesi graffe. Ciò include la trama 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 sampler viene omesso, viene applicato uno stato del sampler predefinito che specifica il filtro lineare e una modalità di wrapping per le coordinate della trama. La funzione sampler accetta una coordinata trama a virgola mobile a due componenti e restituisce un colore a due componenti. Viene rappresentato con il tipo restituito float2 e rappresenta i dati nei componenti rossi e verdi.

Sono definiti quattro tipi di sampler (vedere Parole chiave) e ricerca trama 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 dell'esempio per le impostazioni del filtro e la modalità indirizzo.

Ecco 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 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 verrà generato un avviso.

texture tex0;
sampler s_1D_float;

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

Questo esempio particolare è 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é un float1, il compilatore può usare la coordinata della trama di input perché sa che y viene inizializzato in un elemento.

texture tex0;
sampler s_1D_float2;

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

Tutte le ricerche di trama possono essere aggiunte con "bias" o "proj" (ovvero tex2Dbias (DirectX HLSL), texCUBEproj (DirectX HLSL)). Con il suffisso "proj", la coordinata della trama è divisa dal componente w. Con "bias", il livello mip viene spostato dal componente w. Pertanto, tutte le ricerche di trama con un suffisso accettano sempre un input float4. Rispettivamente tex1D (t) (DirectX HLSL) e tex2D (t) (DirectX HLSL) ignorano rispettivamente i componenti yz e z.

Gli esempi possono essere usati anche nella matrice, anche se attualmente nessun back-end supporta l'accesso di matrice dinamica degli esempi. Di conseguenza, quanto segue è 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 degli esempi è principalmente utile per la scrittura di programmi con cicli letterali. Il codice seguente illustra l'accesso alla matrice 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 rilevare errori di corrispondenza tra il numero di componenti in una trama e un sampler.

 

Scrittura di funzioni

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

Le funzioni HLSL sono simili a funzioni C in diversi modi: contengono una definizione e un corpo di funzioni e dichiarano entrambi i tipi restituiti e gli elenchi di argomenti. Come le funzioni C, la convalida HLSL esegue il controllo dei tipi sugli argomenti, sui tipi di argomento e sul valore restituito durante la compilazione dello shader.

A differenza delle funzioni C, le funzioni del punto di ingresso HLSL usano la semantica per associare gli argomenti delle funzioni agli input e agli output dello shader (funzioni HLSL denominate semanticamente ignorate internamente). In questo modo è più semplice associare i dati del buffer a uno shader e associare gli output shader agli input 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 tutto davanti alle parentesi graffe:

float4 VertexShader_Tutorial_1(float4 inPos : POSITION ) : POSITION

Una dichiarazione di funzione contiene:

  • Tipo restituito
  • Nome della funzione
  • Elenco di argomenti (facoltativo)
  • Semantica di output (facoltativo)
  • Annotazione (facoltativa)

Il tipo restituito può essere uno dei tipi di dati di base HLSL, ad esempio un 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 prima 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. Ecco 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 miscela 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 buffer dei vertici. 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 vengono etichettati con la parola chiave uniforme . Queste sono costanti uniformi che non cambieranno tra le chiamate di disegno. I valori per questi parametri provengono da variabili globali shader.

Gli argomenti possono essere etichettati come input con la parola chiave 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 contrassegnata con la parola chiave inout vengono considerati copie dell'originale fino a quando la funzione non viene restituita e vengono copiate nuovamente. 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 della funzione.

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

Il corpo è costituito da istruzioni che sono circondate da 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 funzione mul (DirectX HLSL), che esegue una moltiplicazione di matrice 4x4. mul (DirectX HLSL) è chiamata funzione intrinseca perché è già incorporata nella libreria HLSL delle funzioni. Le funzioni intrinseche verranno descritte in modo più dettagliato nella sezione successiva.

La matrice moltiplica combina un vettore di input Pos e una matrice composita WorldViewProj. Il risultato è la posizione dei dati trasformati in spazio dello schermo. Questa è l'elaborazione minima del vertex shader che è possibile eseguire. Se si usa la pipeline di funzione fissa anziché un vertex shader, i dati del vertice potrebbero essere disegnati dopo aver eseguito questa trasformazione.

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

I tipi restituiti dalle funzioni possono essere uno 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. Ciò include pixelhader, vertexshader, trama e sampler.

Ecco 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 restituita segnala la fine di una funzione. Questa è l'istruzione restituita più semplice. Restituisce il controllo dalla funzione al programma chiamante. Non restituisce alcun valore.

void main()
{
    return ;
}

Un'istruzione restituita può restituire uno o più valori. In questo esempio viene restituito 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 float4 costruito da una variabile locale e un valore letterale:

return  float4(color.rgb, 1) ;

In questo esempio viene restituito un 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 contenente 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 di flusso

La maggior parte dell'hardware vertex e pixel shader corrente è progettata per eseguire una linea shader per riga, eseguendo ogni istruzione una sola volta. HLSL supporta il controllo del flusso, che include il ramo statico, le istruzioni predicate, il ciclo statico, il ramo dinamico e il ciclo dinamico.

In precedenza, l'uso di un'istruzione if ha generato codice shader del linguaggio assembly che implementa sia il lato if che il lato altro del flusso di codice. Ecco un esempio del codice HLSL compilato per vs_1_1:

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

Ecco 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 l'esecuzione statica o dinamica, ma la maggior parte richiede l'esecuzione lineare. Nei modelli che non supportano il ciclo, tutti i cicli devono essere annullati. Un esempio è l'esempio Di esempio DepthOfField che usa cicli non implementati anche per i ps_1_1 shader.

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

  • branching statico
  • istruzioni predicate
  • ciclo statico
  • branching dinamico
  • ciclo dinamico

Il ramo statico consente ai blocchi di codice shader di essere attivati o disattivati in base a una costante dello shader booleano. Questo è 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 branching più familiare è il ramo dinamico. Con il ramo dinamico, la condizione di confronto si trova in una variabile, 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). Il colpo delle prestazioni è il costo del ramo più il costo delle istruzioni sul lato del ramo preso. Il ramo dinamico viene implementato nel modello shader 3 o superiore. L'ottimizzazione degli shader che funzionano con questi modelli è simile all'ottimizzazione del codice in esecuzione in una CPU.

Guida alla programmazione per HLSL