A differenza delle precedenti versioni, in DirectX10 la base di tutto è lo shader. Uno shader è un codice che descrive le regole di trasformazione che dal vertice generano la scena. I due shader fondamentali sono il vertex ed il pixel shader. Il primo trasforma il vertice dalla sua posizione in object space alla sua posizione in projection space. Il secondo partendo dall'informazioni di output del vertex shader genera tutti i pixel ed esegue per ognuno lo shader passandogli la media pesata in base alla posizione dell'output del vertex shader. Ecco un esempio di codice shader.
struct VS_INPUT
{
float4 Pos : POSITION;
float4 color:COLOR;
};
struct PS_INPUT
{
float4 Pos : SV_POSITION;
float4 color:COLOR;
};
PS_INPUT VS( VS_INPUT input )
{
PS_INPUT output = (PS_INPUT)0;
output.Pos = input.Pos;
output.color=input.color;
return output;
}
float4 PS( PS_INPUT input) : SV_Target
{
return input.color;
}
technique10 Render
{
pass P0
{
SetVertexShader( CompileShader( vs_4_0, VS() ) );
SetGeometryShader(NULL);
SetPixelShader( CompileShader( ps_4_0, PS() ) );
}
}
Questo file si chiama Effect. Un file effect è un file ASCII da caricare durante l'esecuzione; può avere qualsiasi estensione (la tradizione vorrebbe FX). Un effetto può contenere più shader per ogni tipo ma deve avere almeno 1 vertex shader ed 1 pixel shader. Per essere applicati bisogna specificare 1 vertex shader, 1 pixel shader ed, opzionalmente, 1 geometry shader. Questa terna è raggruppata in un Pass.
Un pass è la descrizione di un effetto. I pass sono organizzati in tecniche che possono contenere più pass ed il file Effect può contenere N tecniche.
Applicare uno shader significa specificare una tecnica ed un suo pass in modo da impostare i 3 shader.
technique10 Render
{
pass P0
{
SetVertexShader( CompileShader( vs_4_0, VS() ) );
SetGeometryShader(NULL);
SetPixelShader( CompileShader( ps_4_0, PS() ) );
}
}
In questo esempio abbiamo una tecnica chiamata Render ed un pass chiamato P0. Al suo interno sono impostati i 3 shader di cui il geometry shader è vuoto (NULL), mentre come Vertex e Pixel shader il risultato della compilazione delle funzioni VS e PS definite sopra.
CompileShader( vs_4_0, VS() )
questo comando ordina allo shader di compilare la funzione VS() come un Vertex Shader in versione 4.0 (la versione di DirectX10, conviene utilizzare sempre questa a meno di voler scrivere o utilizzare shader DirectX9). Identicamente si compila il pixel shader (stavolta indicheremo ps_4_0 per dirgli che è un pixel shader) ed identicamente potremo compilare un geometry shader usando gs_4_0 ma questo è qualcosa da vedere in seguito.
struct VS_INPUT
{
float4 Pos : POSITION;
float4 color:COLOR;
};
struct PS_INPUT
{
float4 Pos : SV_POSITION;
float4 color:COLOR;
};
PS_INPUT VS( VS_INPUT input )
{
PS_INPUT output = (PS_INPUT)0;
output.Pos = input.Pos;
output.color=input.color;
return output;
}
la funzione VS è un vertex shader. Questa viene chiamata per ogni vertice della scena. In input riceve una struttura che chiamiamo VS_INPUT che indica il formato del vertice. Questo sarà completamente a nostra discrezione permettendoci di creare dei vertici con qualsiasi tipo di struttura. In output restituiremo un'altra struttura sempre a nostra discrezione. All'interno del codice potremo scrivere ciò che vorremo per trasformare i dati dall'input all'output. Le strutture di scambio sono fondamentali. Gli shader non possono comunicare tra di loro e saranno queste il punto di scambio tra i dati in memoria, gli shaders e l'input finale a video.
struct VS_INPUT
{
float4 Pos : POSITION;
float4 color:COLOR;
};
In questo esempio decidiamo che come input utilizzeremo una variabile float4 (che contiene 4 float chiamati x,y,z e w) che chiameremo Pos ed un'altra float4 chiamata color. Cosa ne faremo sarà decisione nostra, come tutti i nomi utilizzati nello shader che non hanno un significato particolare. La cosa da notare è però ciò che è scritto dopo Pos e color. Il testo che si trova dopo i 2 punti (POSITION e COLOR) si chiamano semantiche. Sono dei nomi che rimangono sempre a nostra discrezione ma saranno proprio questi a permettere a DirectX10 di passare correttamente i dati. Nel nostro caso in DirectX10 creeremo poligoni con vertici formati da questi 8 valori (4 per Pos e 4 per color) e diremo di passare a POSITION e COLOR il contenuto di questi.
float4 PS( PS_INPUT input) : SV_Target
{
return input.color;
}
Il pixel shader è quasi simile al vertex shader con un'unica differenza: restituirà solo un float4, appunto il colore finale. In input prenderà la struttura PS_INPUT che abbiamo generato dal vertex shader.
Questo semplice shader di esempio fa questo: per ogni vertice prende un float4 come posizione, uno come colore e li invia al pixel shader che restituirà il colore mostrandolo sullo schermo. La struttura PS_INPUT che usiamo come scambio non è di per se importante. Potremmo utilizzare anche strutture diverse, l'unica cosa importante è la semantica. Il valore restituito nella semantica COLOR del vertex shader finirà nella semantica COLOR del pixel shader. Alcune semantiche sono però specifiche di DirectX10 e destinate ad usi importanti.
SV_POSITION: indica che quella è la posizione definitiva del vertice. Il vertex shader dovrà restituire per forza un float4 con questa semantica altrimenti DirectX10 non saprà come generare i triangoli.
SV_Target: indica che il valore deve essere mandato sul ViewTarget. Il pixel shader dovrà restituirlo necessariamente.
Ulteriori dettagli saranno dati più avanti.
ID3D10Effect* effect;
ID3D10Blob* error;
HRESULT hr=D3DX10CreateEffectFromFileA(filename,NULL,NULL,"fx_4_0",D3D10_SHADER_ENABLE_STRICTNESS,
0,device,NULL,NULL,&effect,&error,NULL);
L'istruzione D3DX10CreateEffectFromFile carica un file shader, lo compila e lo prepara per la sua esecuzione. Se hr risulterà uguale ad S_OK allora il caricamento sarà andato a buon fine. Altrimenti ci saranno stati degli errori che può essere o un file non trovato o errori interni nel codice shader. La variabile di tipo ID3D10Blog contiene un buffer di memoria che viene riempito con l'errore di compilazione. Questo può essere letto e permettere di correggere lo shader.
LPCSTR errorString=(LPCSTR) error->GetBufferPointer();
se error sarà diverso da null ricordatevi di fare il Release per liberare la memoria.
Per applicare un effetto dovremo specificare la tecnica ed il pass
effect->GetTechniqueByIndex(0)->GetPassByIndex(0)->Apply(0);
GetTechnique restituisce una struttura per memorizzare la tecnica, GetPass restituisce un pass ed apply applica tutto al device (0 è un valore privato di DirectX10 da lasciare come sta). Potremo quindi anche prendere il pass, salvarlo ed usarlo direttamente invece di prenderli ogni volta (cosa per altro consigliata). E' possibile prendere le tecnique anche dal nome passandogli una LPCSTR. Una volta eseguita Apply tutto ciò che verrà utilizzato dopo userà quello shader finchè non specificheremo un altro shader. A differenza di DirectX9 non dovremo dire che non vogliamo più utilizzare quello shader e quindi non esiste una istruzione di End dell'effetto.
Le varie funzioni GetDesc presenti nell'effect, nella tecnica e nel pass ci permetteranno di vedere le caratteristiche dello shader caricato. Nel prossimo tutorial utilizzeremo questo shader per visualizzare un triangolo.
Ulteriori informazioni nell'help di DirectX.