Una delle prime grandi caratteristiche introdotte in Direct3D11 è l’introduzione del paradigma della programmazione orientata agli oggetti all’interno degli shader. Ormai i videogiochi moderni possiedono centinaia o addirittura migliaia di shader che vengono gestiti dai motori grafici e la loro gestione sta diventando sempre più complessa e disordinata.
La possibilità di utilizzare oggetti permette di creare shader complessi in modo più semplice ed ordinato. All’interno del linguaggio HLSL è possibile ora definire classi ed interfacce in cui definire metodi e variabili da utilizzare nei nostri Shader ovviamente concedendoci la libertà di utilizzare l’ereditarietà.
Interface Alfa
{
Float GetValue();
};
class Beta:Alfa
{
Float GetValue(){return 1:}
};
class Gamma:Alfa
{
Float GetValue(){return 2:}
};
Alfa myInterface;
Float4 PixelShader():SV_TARGET
{
Return myInterface.GetValue();
}
Come vedete nell’esempio ho definite un’interfaccia Alfa da cui ho ereditato 2 classi, Beta e Gamma.
Nel pixel shader ho quindi utilizzato il metodo dall’interfaccia myInterface di tipo Alfa definita come statica nel codice. Con quale classe sarà instanziata myInterface verrà detto nel codice dove, oltre agli shader, passeremo al device le istanze da utilizzare. In questo modo creandoci un nostro set di oggetti, ognuno riferito ad una determinata istanza, potremo cambiare il comportamento dello shader semplicemente passando una o l’altra a nostro piacimento.
Per prima cosa nel nostro codice C++ creiamo un oggetto ID3D11ClassLinkage
device->CreateClassLinkage(&linkage);
quindi al momento della creazione dello shader lo passiamo al metodo createShader
device->CreatePixelShader(code->GetBufferPointer(), code->GetBufferSize(),linkage,&pixelShader);
ora l’oggetto linkage è in grado di creare istanze delle classi definite nel codice.
ID3D11ClassInstance
linkage->CreateClassInstance(name,0,0,0,0,&inst);
Spiegherò più avanti i dettagli, per ora limitiamoci al nome della classe (ad esempio Gamma). Quindi quando andremo a passare lo shader al nostro Device indicheremo quali istanze utilizzare
context->PSSetShader(pixelShader,instances,count);
dove instances è un array di oggetti ID3D11ClassInstance e count il numero di elementi contenuti li. L’ordine degli elementi sarà quello con cui sono definite nel codice (ricordando sempre che quelle utilizzate solo nel vertex shader non conteggiano nel pixel shader e così via).
Sarà oggetto di futuri tutorial la possibilità di analizzare in dettaglio il contenuto dello shader e ricavare tutte le informazioni.
Il primo modo di utilizzare la programmazione ad oggetti è quindi quella di crearsi delle interfacce comuni per i propri metodi ed un set di classi che ereditano eventualmente l’una dall’altra per definire comportamenti differenti. Ad esempio è possibile creare un’interfaccia con un metodo calcola Luce e da essa ereditare classi diverse per ogni tipo di illuminazione.
Il paradigma object oriented in HLSL è ad ereditarietà singola. Gli scenari sono quindi i seguenti:
- 1 Classe eredita da 1 sola Classe
- 1 Classe eredita da 1 o più interfacce
- 1 Interfaccia eredita da 1 o più interfacce
- 1 Classe eredita da 1 sola Classe e 1 o più interfacce
Non ci sono quindi i problemi dell’ereditarietà multipla esistente in C++ in quanto anche se le interfacce contenessero metodi con firme identiche non avrebbero corpo e quindi non ci sarebbero ambiguità.
All’interno delle variabili statiche dello shader è possibile definire solo interfacce a cui passare la classe in questione. E’ tuttavia possibile utilizzare classi definendole in ConstantBuffer o nel codice HLSL.
Ora passiamo ad una domanda che probabilmente molti si stanno ponendo: e se la classe avesse delle variabili?
Questa possibilità è ovviamente prevista e ci sono più modi per gestirla.
Prendiamo l’esempio
class Beta:Alfa
{
Float4 valore;
Float GetValue(){return valore:}
};
class Gamma:Alfa
{
Float valore;
Float GetValue(){return valore.x + valore.y + valore.z:}
};
Questi semplici esempi riassumono un po’ tutte le possibilità. In Beta e Gamma ci sono delle variabili, nel primo caso un float4, nel secondo un float. Questi valori verranno presi da un constant buffer che sarà abbinato alle classi. Prendiamone uno per entrambe
Cbuffer dati
{
Float4 coloreBeta;
Float4 coloreGamma;
};
Se questo constant buffer si trova ad esempio nello slot 1 (quindi è il secondo constant buffer del pixel shader)
linkage->CreateClassInstance(“Beta”,1,0,0,0,&instanzaBeta);
linkage->CreateClassInstance(“Gamma”,1,1,0,0,&instanzaGamma);
Il primo valore del metodo è lo slot del constant buffer da utilizzare, il second è la posizione nel buffer da cui leggere. Ricordo che un constant buffer per essere corretto deve avere valori che siano multipli di 4 ossia per semplicità tutti vettori float4 (o Float4x4 o comunque float, float2 e float3 che nel totale siano multipli di 4).
Nel caso invece di classi definite in un constant buffer invece basterà conteggiare la posizione delle variabili nelle classi. Ad esempio
Cbuffer buffer Classi
{
Beta b;
Gamma c;
Beta b1;
Gamma c1;
};
Questo buffer si trova ad esempio nello slot 0, allora creeremo un constantBuffer contenente memoria sufficiente per i 4 float4 (ogni classe ne contiene uno) e lo valorizzeremo. In pratica l’associazione è diretta con il constant buffer in cui ci si trova.
Allo stesso modo si ragiona anche con Texture e Sampler senza dimenticare che comunque le classi hanno accesso ai constant buffer e alla altre risorse statiche definite nel codice.
L’innovazione introdotta dagli oggetti inizia a diventare interessante quando si inizia a lavorare con molti shader contemporaneamente. Capita ad esempio di dover avere vari modelli di illuminazione, ognuno con n luci di tipo differente. Senza classi la cosa più naturale è quella di creare cicli for in cui scorrere array di strutture che in base ai loro parametri eseguono il calcolo. Serve ad esempio nella struttura il tipo di luce e già occorre fare il controllo che associ il tipo alla chiamata e tutti dati che magari servono ad un tipo di luce e ad altri no. Se ad esempio un gioco ha 10 tipi di luce sono 10 controlli e 10 funzioni da tenere in memoria. Motori grafici in commercio attualmente hanno funzioni che generano codice a runtime e tengono in memoria centinaia e spesso anche migliaia di shader con conseguente cambio continuo tra uno shader e l’altro. Teoricamente sarebbe possibile arrivare ad uno scenario in cui tutto il gioco si basa su un solo Shader a cui passare le istanze che servono man mano. Ricordo infine che questa caratteristica è propria di Direct3D11 quindi se non supportate lo shader model 5.0 non potrete utilizzare tale feature.
Vi lascio ad un semplice demo che mostra come utilizzare facilmente classi ed interfacce.
Demo CPP
Demo .Net
I commenti sono disabilitati.