Home Page Twitter Facebook Feed RSS
NotJustCode
Apri

Tessellation 2675 Visite) DirectX 11

La tessellation è senza dubbio la caratteristica più interessante introdotta da Direct3D11. Questa caratteristica comprende la possibilità di aumentare il numero di triangoli presenti in un oggetto direttamente all’interno della scheda video definendo attraverso 2 nuovi shader il modo con cui questa operazione deve avvenire. Il funzionamento appare all’inizio un po’ complesso ma una volta capito diventa estremamente semplice fare ciò che si vuole.

La nuova pipeline di Direct3D11 prevede dopo il Vertex Shader la presenza di 3 Step prima del Geometry Shader:

  1. Hull Shader
  2. Tesselation
  3. Domain Shader

http://www.notjustcode.it/public/Tessellation_D052/clip_image002.jpg

Questi 3 lavorano in stretta correlazione. Direct3D a partire dai Vertex Shader genera un elemento chiamato Patch. Questo può essere un triangolo o una griglia di punti (fino a 32). Tali punti corrispondono ai vertici inseriti nel Vertex Buffer che in questo momento non sono più vertici ma punti di controllo (in alcuni corsi universitari vengono chiamati anche manici di controllo).

A questo punto entra in gioco l’hull shader. Questo shader è molto particolare in quanto è uno shader doppio.

L’Hull Comprende due funzioni, una lanciata una volta ogni patch e l’altra per ogni punto della patch stessa. La prima prenderà in input un array di vertici che saranno i punti della patch così come generata dal Vertex Shader e restituirà 2 array di interi che indicheranno in quante parte suddividere la patch (uno per i lati, l’altro per gli interni). La seconda, l’Hull Shader vero e proprio, verrà lanciata una volta per ogni punto di controllo e prenderà in input sempre la patch ma anche un indice che ci dirà quale punto è quello con cui stiamo lavorando. L’uscita sarà a sua volta il manico di controllo trasformato.

A questo punto la palla passa al tesselation stage che è un componente statico che moltiplicherà il numero di triangoli della patch in base ai parametri dati dalle 2 parti dell’Hull Shader.

Ora si passa all’ultimo shader, il Domain Shader che lavora al Tesselator.

L’input di questo shader sarà la patch così come generata dall’Hull e le coordinate lungo di essa in cui ci troviamo ed in base a questi due dati restituiremo il vertice finale indipendentemente dal numero effettivo di parti in cui la patch è stata divisa.

Facciamo un esempio per chiarire il processo. Ipotizziamo che il vertice che esce dal Vertex Shader contenga solo la posizione in world space e che utilizziamo come patch un triangolo.

L’Hull Shader prenderà in input i 3 vertici e l’indice di quello che stiamo utilizzando. Dall’Hull Shader genereremo 3 punti di controllo contenenti la posizione a cui per semplicità non apporteremo alcuna modifica. La funzione associata all’Hull Shader invece prenderà i 3 vertici ed effettuerà un calcolo in base alla distanza dalla telecamera: più vicina è la telecamera e più alta sarà la sua suddivisione. Al termine restituiremo 3 valori di tessellazione per i 3 lati ed uno per l’interno (nel caso di patch quadrate sono 4 valori esterni e 2 interni).

Ora il Domain shader verrà chiamato tante volte quanti vertici sono stati aggiunti. Per ognuno verranno passati al Domain i 3 punti di controllo così come sono usciti dall’Hull Shader e 3 valori di coordinate baricentriche ossia coordinate da 0 ad 1 che indicano la distanza da 1 dei 3 vertici. La loro somma sarà sempre 1. Nel caso di patch quadrate invece saranno solamente 2 valori compresi da 0 ad 1 corrispondenti alle coordinate sulla patch da un angolo a quello opposto.

A seconda della divisione ci saranno un certo di numeri di vertici generati. Se la patch sarà formata da 10 vertici allora il Domain sarà chiamato 10 volte, sempre con gli stessi 3 punti di controllo e varieranno solo le coordinate baricentriche. Ora potremo applicare il nostro algoritmo di modifica della posizione dei vertici prendendoli da una formula (ad esempio le onde per una superficie d’acqua) o da una texture (per simulare ad esempio la rugosità di una superficie). Usando i 3 punti di controlli posizioneremo correttamente il vertice nello spazio (in questo momento se non useremo il Geometry Shader potremo trasformare il vertice da world space a projection space). L’algoritmo essendo parametrico rispetto alle coordinate funzionerà con qualsiasi numero di vertici generati dal tesselator.

Sarà tuttavia difficile ricavare l’intero triangolo e per questo qualora ci servisse di modificare ulteriormente la geometria usando i triangoli come base si dovrà ricorrere al Geometry Shader.

Il processo si conclude qui e lo step successivo sarà il Geometry Shader o direttamente il Pixel Shader. Generalmente l’Hull Shader restituisce i punti di controllo così come gli arrivano dal Vertex Shader ma nulla vieta di posizionarli come si preferisce scaricando magari il lavoro dal Domain che viene eseguito molte più volte rispetto all’Hull. La funzione associata all’Hull invece aumenta il fattore di tesselazione in base ad informazioni che possono essere la telecamera o una texture che può dirci quali punti del modello sono più complessi e quindi necessitano di una maggiore suddivisione.

Il Domain si occupa dell’applicazione del nostro algoritmo di modifica e presenta la maggiore complessità. Sarà lui a doversi occupare di posizionare i vertici e calcolare tutte le informazioni necessarie quali normali, texture e via dicendo.

Passiamo ora al codice.

struct HS_INPUT

{

float4 Pos : POSITION;

};

struct DS_INPUT

{

float4 Pos : POSITION;

};

struct HS_CONSTANT_DATA

{

float Edges[3] : SV_TessFactor;

float Inside[1] : SV_InsideTessFactor;

};

HS_CONSTANT_DATA SampleHSFunction( InputPatch<HS_INPUT, 3> ip,uint PatchID : SV_PrimitiveID )

{

HS_CONSTANT_DATA Output;

Output.Edges[0] = Output.Edges[1] = Output.Edges[2] = tessFactor;

Output.Inside[0] = TessAmount;

return Output;

}

[domain("tri")]

[partitioning("pow2")]

[outputtopology("triangle_cw")]

[outputcontrolpoints(3)]

[patchconstantfunc("SampleHSFunction")]

DS_INPUT HSMain( InputPatchh<HS_INPUT, 3> p, uint i : SV_OutputControlPointID,uint PatchID : SV_PrimitiveID )

{

DS_INPUT Output;

Output.Pos = p[i].Pos;

return Output;

}

La funzione HSMain, il nostro Hull Shader presenta diversi tag che definiscono come il tesse latore dovrà lavorare.

Il valore Domain indica il tipo di patch che ci aspettiamo in input (tri per triangolo, quad per il rettangolo e isoline per linee qualora lavorassimo per vertici che magari sarà il Geometry Shader a trasformare in triangoli).

Il Tag partitioning descrive l’algoritmo utilizzato per la suddivisione, c’è ne sono 4: integer, fractional_odd, fractional_even e pow2. E’ più semplice vederli che spiegarne il funzionamento.

Output topology indica l’output del nostro processo e può essere line, triangle_cw e triangle_ccw (triangoli in senso orario o antiorario).

OutputControlPoints indica il numero di punti di uscita della patch. Solitamente corrisponde alla dimensione dell’array InputPatch che arriva alla funzione ma è possibile modificare il numero di tali punti per cambiare ad esempio la tipologia delle patch.

PatchConstantFunc infine è il nome della funzione che verrà eseguita una volta per patch e che restituirà la suddivisione di quest’ultima. Nel nostro esempio la funzione valorizzerà in modo uguale i 2 array SV_TessFactor e SV_InsideTessFactor ma questa valorizzazione potrà essere gestita a piacimento. Oltre alla struttura potrete generare altri dati che potranno essere utilizzati successivamente.

Ora il Domain

[domain("tri")]

GS_INPUT DSMain( HS_CONSTANT_DATA input,

float3 UV : SV_DomainLocation,

const OutputPatch<DS_INPUT,3> TrianglePatch )

{

GS_INPUT outP;

outP.Pos=UV.x * TrianglePatch[0].Pos + UV.y * TrianglePatch[1].Pos + UV.z * TrianglePatch[2].Pos;

outP.Pos/=outP.Pos.w;

outP.Pos.y= 2.0F * sin(outP.Pos.x/4.0F) * cos(outP.Pos.z/4.0F);

outP.Pos=mul(viewProj,outP.Pos);

return outP;

}

Come vedete ci sono come input la constant data generata dalla funzione associata all’Hull ed l’array con i 3 punti di controllo generati dall’Hull. Notare anche il vettore SV_DomainLocation che indica la posizione del punto nella patch.

Nell’esempio utilizzo il metodo per alzare ed abbassare la Y del vertice in modo da generare un’onda. Infine porto il vertice.

La compilazione degli shader è identica a quanto visto per gli altri tipi di shader. Molto importante è invece utilizzare appropriatamente i vertex ed index buffer. Se come patch usate un triangolo non dovrete apportarvi modifiche ma nel caso utilizziate patch rettangolari dovrete creare una spirare. Idem se decidete di utilizzare patch triangolari con più vertici.

Anche la topologia sarà differente, non avrete più infatti triangle_list ma una delle

PatchListWithNControlPoints

Con N che va da 1 a 32 che è la dimensione massima di una patch ossia 32 punti di controllo. Queste andranno passate al DeviceContext tramite la funzione PrimitiveTopology.

 

Vi rimando al tutorial numero 11 della serie (Github o in fondo dal primo tutorial)