\\ Home : Articoli : Stampa
Integrazione Shaders con Effects
Di RobyDx (del 06/03/2007 @ 11:40:12, in DirectX9, linkato 1689 volte)

L'evoluzione della computer grafica in real time ha un solo nome: Shaders. DirectX9 infatti si è rinnovato maggiormente sulla gestione shaders introdotta in DirectX introducendo nuove versioni del linguaggio ASM (le versioni 2.0 e 3.0) e l' HLSL, il linguaggio simil C che semplifica notevolemente la vita del programmatore eliminando il complesso codice assembler. L'ultima novità fondamentale sono gli Effects. L'effect è una tecnologia che permette di integrare in pochi passaggi complicatissimi effetti shaders all'interno delle applicazioni. All'interno di file testuali, come avviene per gli shaders, è possibile scrivere tutte le impostazioni necessarie per realizzare effetti in vertex e pixel shader. Infiniti codici shaders, impostazioni di luci, texture e settaggi renderstate, tutto questo può essere inserito in un singolo file. Il programmatore non dovrà fare altro che passare le variabili richieste all'effect ed eseguire il rendering dell'oggetto: tutto sarà impostato dall'effect e non dovrete neanche preoccuparvi in quali slot inserire le texture. Questo offre numerosi vantaggi:

  • facilità di integrazione di effetti realizzati anche da altri
  • possibilità di alternare effetti anche molto diversi senza dover complicare il ciclo di rendering
  • massima velocità nell'impostazione dei settaggi

Integrare un effect è abbastanza facile. Le funzionalità sono comunque moltissime e quindi illustrerò solo le principali.

Innanzitutto alcuni accenni sul codice (che verrà spiegato con il maggior dettaglio possibile nel successivo tutorial).

Un file effect è composto da 1 o più effetti chiamati "technique" ed ognuna di queste è composta a sua volta da 1 o più pass. I pass descrivono uno dei passi necessari (alcuni effetti se notate sono infatti composti da più rendering come ad esempio il depth of field che prevede diversi rendering su texture combinati fra loro). All'interno del pass vengono scritti in linguaggio simile al C le impostazioni che il device deve avere come ad esempio se lo ZBuffer è attivato, il tipo di cullMode o le impostazioni di alphablending. Sempre nel pass possono essere passati vertex e pixel shader che vengono inseriti come funzioni all'interno del dello stesso file.

Questo è un esempio di codice effect.

//variabili
float4x4 transform;
float4x4 worldview;//world view trasposta
float3 lightDir;//lightDir
Texture base_T;
Texture banda_T;
sampler banda = sampler_state
{
    Texture =
    MipFilter = POINT;
    MinFilter = POINT;
    MagFilter = POINT;
};
sampler base = sampler_state
{
    Texture =
    MipFilter = LINEAR;
    MinFilter = LINEAR;
    MagFilter = LINEAR;
};
struct VS_OUTPUT{
    float4 position:POSITION;
    float3 texture0:TEXCOORD0;
    float3 texture1:TEXCOORD1;
    float3 texture2:TEXCOORD2;
    float2 texture3:TEXCOORD3;
};//struttura di outPut per il vertex shader ma anche di input per il pixel
struct VS_INPUT{
    float4 position:POSITION;
    float4 normal:NORMAL;
    float2 texture0:TEXCOORD0;
};//struttura di input per il vertex shader
VS_OUTPUT VS(VS_INPUT inP){
    /per brevità non ho inserito qui il codice vertex shader
}
float4 PS(VS_OUTPUT inP):COLOR{
    /per brevità non ho inserito qui il codice pixel shader
}
// technique
technique Cellshading
{
    pass P0
    {
        VertexShader = compile vs_1_1 VS();
        PixelShader = compile ps_2_0 PS();
    }
}

Come vedete in alto ho inserito alcune variabili che saranno usate nel codice shader; notare che non ho usato register perchè sarà automaticamente la classe effect a gestire i registri costanti e il fatto che ho anche dichiarato delle texture e dei sampler state con tanto di filtri. Al centro ho inserito delle strutture che sono a disposizione dei vari codici shader inseriti. In coda c'è la tecnica "Cellshading" con un pass chiamato pass P0. Potete dare nomi che volete alle tecniche, ai pass e a tutte le strutture. Questo è un codice molto semplice ma nulla vieta di inserire un centinaio di codici shader, texture etc. Questo rappresenta anche un risparmio di memoria e prestazioni visto che più codici condividono le stesse risorse.

Un effetto è gestito interamente da una classe effect.

Dim f As Effect
Dim effectPool As new EffectPool
Dim errori as String
f = Effect.FromFile("filesrc", Nothing, ShaderFlags.None, effectPool, errori)

In questo modo si carica un effetto da file, passandogli il path del file, una classe effectPool e, opzionalmente, una stringa che  serve, in caso ci sia un errore, a contenere gli errori presenti nel codice compreso il numero di riga. Questo è molto utile per il debug del programma. La classe EffectPool serve per mettere in condivisione variabili tra effect diversi. Se il pool viene distrutto gli effetti continuano a funzionare ma non condivideranno le variabili.

Ogni cosa negli effect è utilizzata tramite un sistema di riferimento tramite classi EffectHandle

Dim EffHandle As EffectHandle

La prima azione da fare è caricare le tecniche. Se conoscete il file potete caricarle tramite string

ad esempio

Dim tec As EffectHandle
tec1 = f.GetTechnique("Cellshading")

Con questo codice carico la tecnica chiamata CellShading

In alternativa invece del nome potete inserire un numero che rappresenta l'ordine in cui sono scritti nel codice. Se inserirete un numero inesistente verrà restituito nothing. Semplicemente potete enumerare le tecniche inserite finchè non viene restituito nothing.

Per avere informazioni sulla tecnica

Dim descrizione As TechniqueDescription = f.GetTechniqueDescription(tec1)

La classe descrizione conterrà informazioni sulla tecnica inserita nell'handle. Più semplicemente

f.Description.

Enumera le caratteristiche dell'effetto. Per passare una tecnica all'effect

f.Technique = tec1

Caricando tutte le tecniche potrete alternarle con una semplice assegnazione

Ora dovrete usare i parametri (ad esempio le matrici e le texture in cima al codice). Creare un handle ad un parametro è piuttosto semplice.

Dim h_Tranform As EffectHandle = f.GetParameter(Nothing, "transform"")

Passate il nome della variabile ed avete fatto. Create tutti gli handle presenti nel codice prima di iniziare ad usarli in modo continuo. Ne guadagnerete in velocità. Per assegnare un parametro dovete solo usare l'istruzione SetValue passandogli l'handle della variabile da modificare ed il dato. Potete passare all'istruzione matrici, array di single ed addirittura texture e shader (nulla vieta infatti che gli shader siano delle variabili).

f.SetValue(h_Tranform, matrice)

Potete assegnare variabili in ogni momento. Per attivare un effect è sufficiente scrivere un blocco di codice come questo

f.Begin(FX.DoNotSaveShaderState)
f.Pass(0)
    oggetto.drawSubset(0)
f.EndPass()
f.End()

Con begin inizio l'effetto. Il flag serve solo ad indicare se le impostazioni inserite rimarranno nel device al termine (ad esempio le texture negli slot o le impostazioni del renderState). L'istruzione restituisce un intero che indica il numero dei pass. Subito dopo si attiva il pass scelto tramite un intero. Ora si può renderizzare la scena. Non dovete passare nulla al device, tutto va passato alla classe effect e non necessariamente all'interno del pass (potete ad esempio impostare le cose che non cambiano all'inizio). Con EndPass chiuderete il blocco del pass. Se l'effetto presenta più pass potrete alternarli qui come volete, basta chiamare l'istruzione BeginPass e dargli il numero ricordandosi di chiudere con endPass. Infine con l'istruzione End chiudete l'effetto. Ora potete caricare un altra tecnica o un'altra classe effect.

Riassumendo:

  • caricate un effect da file (o da una string, nessuno lo vieta)
  • create gli effectHandle delle tecniche
  • create gli effectHandle delle variabili
  • assegnate l'handle con la tecnica desiderata alla classe effect
  • renderizzate la scena attivando l'effect con begin e con l'istruzione beginPass
  • chiudete con EndPass
  • Ripetete queste ultime 2 per tutti i pass
  • chiudete con l'istruzione End
  • Come vedete la semplicità è massima. Per utilizzare correttamente un effetto contenuto in un file dovete semplicemente conoscere i parametri da passargli ed eventualmente dove renderizzare la scena (ad esempio un parametro potrebbe  essere una texture che in uno dei pass viene usata come render target). Tutte le altre istruzioni che vedete servono per recuperare parametri e descrizioni.

    Potete infine disassemblare l'effect con l'istruzione disassemble. Il risultato sarà il vostro effect ma scritto in assembler. Utile per il debug.

    Scrivere un effect

    Il codice degli effect è in pratica una gestione di tutti i settaggi del device.

    Parametri

    Nel codice occorre dichiarare i parametri da utilizzare nelle varie parti, soprattutto nei codici shader.  Un parametro ha questa forma

    usage type id [: semantic] [< annotation(s) >] [= expression];

    I parametri possono essere dichiarati di 3 tipi (usage)

  • const :costanti
  • shared :condivise tra tutti gli effetti che utilizzano lo stesso effectPool
  • static : non possono essere viste  fuori dal codice effect tramite handle
  • Questo a scopo di ottimizzare il processo. I tipi (type) possono essere matrici (float4x4 ad esempio), vettori, texture, interi o sampler state. L' ID è il nome del parametro. Annotation è una stringa che può essere letta tramite il descrittore mentre expression altro non è che un valore iniziale. Alcuni tools li usano ad esempio per integrare alcune funzionalità proprie del tools. DirectX li legge come commenti.

    Potete scrivere anche funzioni. La sintassi è quella C quindi dovete usare le parentesi graffe (ALT+123 o 125). Idem per le strutture che vi possono servire.

    Tecniche e Pass

    Questo è un esempio

    technique Cellshading
    {
        pass P0
        {
            VertexShader = compile vs_1_1 CodiceVS();
            PixelShader = compile ps_2_0 CodicePS();
        }
        pass P1
        {
            VertexShader = compile vs_1_1 SecondoVS();
            PixelShader = compile ps_2_0 SecondoPS();
            ZEnable=false;
        }
    }

    Poteve vedere che è come passare direttamente i valori al device. Infatti si assegna ad esempio al VertexShader la compilazione in versione vs_1_1 del codice nella funzione CodiceVS. Nel secondo pass ho anche disabilitato lo ZEnable. Potete modificare qualsiasi cosa che si trovino direttamente nel device o nel renderstate, textureState ecc. Non ve le dirò tutte  perchè i nomi sono uguali a quelli del normale codice (tranne per i suffissi ad esempio non dovete scrivere RenderState.ZEnable ma solo ZEnable). In pratica tutti i settaggi disponibili. Potete consultare l'help di directX (al momento solo quello per C++ nella sezione effect).

    Shaders

    Per utilizzare un codice shader in assembler dovete creare una funzione di questo tipo

    VertexShader funzione = asm{
        vs.1.1
        m4x4 oPos,v0,c0

        ...
    };

    Nel pass ponete VertexShader=funzione;

    I registri costanti vanno invece dichiarati in questo modo

    #define Matrice 0

    In questo modo potete caricare nel registro c0 il valore tramite l'handle "Matrice.

    Per il codice HLSL usate invece un normale codice HLSL ed assegnate allo shadere tramite l'istruzione compile seguita dalla versione e dal nome della funzione. 

    PixelShader = compile ps_2_0 SecondoPS();

    Le variabili invece sono quelle normali dichiarate.float4x4 transform;

    Texture

    Importante caratteristica è la possibilità di inserire direttamente la texture nell'FX. Oltre alla texture dovrete creare dei samplerState.

    Texture Mappa;

    sampler mappaSampler = sampler_state
    {
        Texture =
        MipFilter = LINEAR;
        MinFilter = LINEAR;
        MagFilter = LINEAR;
    };

    Nel codice HLSL dovete passare il sampler, non la texture. Potete specificare il tipo (sampler2D, samplerCUBE o samplerVOLUME). Nella inizializzazione potete specificare le caratteristiche del sampler come ad esempio i filtri.

    Ricordatevi le <> perchè significano "riferimento", ossia non crea una copia della texture ma la referenzia.

    Come vedete non c'è altro. Ovviamente dovete conoscere bene gli shader, meglio ancora se in HLSL.

    Consiglio di usare gli effect sia per la semplicità di implementazione che soprattutto per il fatto che usare gli effect migliora sensibilmente le prestazioni.

    Vi lascio ad un esempio di implementazione di un codice per il rendering del legno.

    Esempio VB.Net

    Esempio C#