Curve Parametriche 3170 Visite) DirectX 11
Questo articolo verterà sui concetti di curve parametriche a cui appartiene tutta quella famiglie di curve geometriche, come ad esempio le famose Curve di Bezier. La conoscenza di concetti come equazioni o matrici sarà data per scontata e comunque cercherò di dare all’articolo un’impostazione il più possibile pratica per mettere il lettore in condizione di utilizzare immediatamente le curve parametriche lasciando a lui il compito di approfondire eventuali concetti teorici.
Una funzione parametrica è un’equazione le cui variabili dipendono a loro volta da un parametro. Una curva parametrica è quindi una rappresentazione geometrica di tale equazione. Prendiamo ad esempio questa funzione:
X = cos (t) * R
Y = sin (t) * R
Dato un valore fisso ad R e facendo variare t da 0 a 360° otteniamo tutti i punti necessari a disegnare un cerchio. Qualsiasi equazione può quindi essere espressa in forma parametrica.
Esempio: Y = aX + b che esprime una retta diventerà
X = ct + x
Y = dt + y
Dove x ed y sono i valori iniziali di X ed Y e t è il valore da far variare per ottenere la nostra curva (in questo caso la retta).
Il numero di parametri indica anche la dimensione della curva. Un solo parametro creerà linee, 2 parametri delle superfici e 3 dei solidi pieni. Il numero di parametri non influisce sulla dimensione spaziale. Ad esempio una linea che richiede un solo parametro può essere definita sia in un piano che in uno spazio 3D.
A questa categoria appartengono le curve parametriche utilizzate per la grafica 3D, nate in origine nel settore automobilistico e aerospaziale. E’ abbastanza nota infatti la difficoltà di dover riprodurre un oggetto partendo solo da misurazioni numeriche quando questi ha curve non regolari. Gli ingegneri dell’epoca definirono quindi una serie di curve parametriche per rappresentare linee e superfici nel modo migliore possibile. Le curve più utilizzate in computer grafica sono composte da equazioni di terzo grado e a questa categoria appartengono le curve di Hermite e quelle di Bezier.
Curve di Hermite
Le curve di Hermite sono linee curve passanti per un punto iniziale ed uno finale ed il cui andamento è definito da 2 vettori tangenti situati in corrispondenza dei due punti. La soluzione è una equazione formata da 4 funzioni parametriche
h1(u) = (2u^3 – 3u^2 + 1) * P0
h2(u) = (-2u^3 + 3u^2) * P1
h3(u) = (u^3 – 2u^2 + u) * T0
h4(u) = (u^3 - u^2) * T1
Dove P0 e P1 sono i punti iniziale e finale, mentre T0 e T1 sono le tangenti. Variando u da 0 ad 1 e sommando le 4 funzioni si otterranno tutte le coordinate della curva. Per comodità si utilizza la rappresentazione matriciale.
I punti e le tangenti prendono il nome di manici di controllo e definiranno il comportamento della curva. La matrice 4x4 è chiamata matrice di Hermite ed il prodotto del vettore u con la matrice è chiamato base di Hermite.
Curve di Bezier
Le curve di Bezier sono definite per passare per un punto iniziale ed uno finale ed approssimare il loro andamento tramite 2 punti intermedi. Le curve di Bezier sono in realtà una modifica a quelle di Hermite in quanto la direzione tra i primi 2 punti e gli ultimi 2 sono in realtà le tangenti alla curva ma risultano ad ogni modo molto più pratiche. La funzione di Bezier è la seguente
Anche in questo caso u varia da 0 ad 1.
Anche le superfici sono estremamente semplici, basta utilizzare 16 punti anziché 4 ed utilizzare un secondo parametro v insieme ad u. La funzione diventa la seguente
Le curve di Hermite e le curve di Bezier possiedono diverse proprietà fondamentali al rendering. La principale è che si può ruotare, scalare o trasformare l’intera curva applicando la medesima trasformazione ai suoi manici. La curva manterrà la proporzione ed i parallelismi. Le curve di Bezier inoltre hanno una caratteristica migliore delle Hermite in quanto più stabili. Ci sono infatti dei casi in cui la curva si piega su stessa formando un cappio, cosa che in Bezier non accade.
Continuità
Uniamo 2 curve di Bezier, difficilmente queste saranno continue. Se vedete le due curve P1..P4 e P4..P7 non sono continue e nel punto in cui si toccano formano una punta (cuspide).
Questo problema si chiama di discontinuità. Due curve per essere continue devono essere derivabili in quel punto. La derivata è una delle funzioni della matematica che si incontra negli ultimi anni di liceo o nelle facoltà universitarie a carattere scientifico come ingegneria.
Una delle cose calcolabili tramite le derivate sono le tangenti alle curve e due curve per essere continue devono avere la tangente calcolata in quel punto uguale. Matematicamente si può quindi trovare una equazione per cui le rette sono sempre tangenti, tali sono le B-Spline dove la B sta per Bezier.
Nelle curve BSpline i manici di controllo non vengono attraversati ma guidano solamente la curva. Una B-Spline è formata da un numero qualsiasi di sezioni di 2 punti. Alla funzione oltre a questi 2 punti si passano il secondo punto della sezione precedente ed il primo della sezione successiva. In questo modo applicando la funzione a tutte le sezioni si crea una curva continua di lunghezza qualsiasi. La soluzione è questa equazione
I punti P1 e P2 sono quelli della sezione, P0 l’ultimo della curva precedente, P3 della curva dopo. La curva non passerà necessariamente per questi punti ma sarà continua. Esistono molti altri tipi di curve parametriche di grado superiori o non uniformi come le NURBS, destramente potenti ma non adatta ai nostri scopi. Le NURBS infatti oltre ad interpolare i punti come le bezier danno anche un peso differente ai punti.
Utilizzo
Le curve parametriche sono estremamente comode per realizzare superfici ed oggetti curvi partendo da pochi punti. Se ad esempio volessimo realizzare un vaso basterà definire un set di punti di controllo e far variare i parametri u e v da 0 ad 1 per avere tutti i punti in questione. Le curve saranno quanto più morbide quanti più punti prenderemo nei 2 intervalli. Utilizzando DirectX o OpenGL dovremo poi generare da questi punti i triangoli necessari al nostro modello.
DirectX e OpenGL infatti utilizzavano infatti solo triangoli (e rettangoli per OpenGL) e le schede erano in grado di elaborare solo queste. Le curve erano per tanto utilizzabili solo in fase di creazione del modello, cosa comunque molto utile in quanto la definizione del modello dipende solo da quanti punti prenderemo nell’intervallo 0-1.
Il passato è d’obbligo con le Direct3D11. Le ultime versioni Direct3D ora utilizzano oltre ai triangoli anche le patch permettendo di passare gruppi di punti. Ogni gruppo sarà convertito direttamente dalla scheda video nei triangoli effettivamente necessari. Se utilizziamo come patch un rettangolo di 16 punti questi saranno proprio i nostri manici di controllo. Nell’Hull definiremo il numero di intervalli in UV e nel Domain applicheremo una delle formule spiegate nell’articolo. Il risultato sarà appunto la curva di geometrica. Ulteriore miglioramento sarà la possibilità di variare il numero di intervalli UV attraverso una nostra regola (esempio più vicino è l’oggetto e maggiore è la frammentazione). Il tutto calcolato dall’hardware della scheda video a runtime.
I vantaggi sono molti:
1. Il vertex shader è eseguito solo sui manici di controllo e non su tutti i punti generati (usando 16 manici di controllo con il massimo livello di tessellazione che è 64 possiamo ottenere 8192 triangoli per i quali sarebbero serviti nella migliore delle ipotesi 65x65 vertici ossia più di 4000).
2. Possiamo avere un LOD dinamico ossia aumentare il dettaglio all’avvicinarsi di un oggetto o anche di una sola parte. Nel caso di patch quadrate possiamo variare il dettaglio diversamente per ognuno dei 4 lati e ognuna delle 2 diagonali.
3. La memoria utilizzata dalla scheda video è minima e soprattutto veloce in quanto essendo i triangoli generati nella GPU non c’è molto scambio tra GPU e CPU ne tra la GPU e la memoria video
4. Si possono realizzare modelli molto semplici ed usare una texture per impostare il dettaglio
Ultimo dettaglio è il calcolo di normali che verrà fatto nel domain shader. Come detto nell’articolo le tangenti sono le derivate delle funzioni. Moltiplicando la funzione u^3 + u^2 + u + 1 per la matrice e facendo la derivata di detta funzione si otterrà una equazione che moltiplicata per i manici non darà la posizione ma la tangente. Fatto lungo v otterremo il vettore perpendicolare alla tangente ossia la binormale. Facendo il cross product tra tangente e binormale otterremo la normale ed avremo come bonus anche tangente e binormale usata per l’illuminazione in normal map.
Demo
Il semplice demo realizzato per questo esempio visualizza un piccolo paesaggio usando le B-Spline cubiche. Il paesaggio sarà formato da triangoli generati dal Tessellation e tramite un semplice calcolo sulla distanza le zone più vicine alla telecamera avranno un numero maggiore di poligoni di quelli distanti. Il modello userà le normal map per realizzare l’illuminazione tramite l’algoritmo Oren Nayar mentre una texture volumetrica renderà il paesaggio erboso nelle zone piane e terroso in salita.
Per prima cosa si crea la nostra mesh, composta da griglie di 16 punti di controllo che formano in tutto 9 quadrati. Il quadrato centrale sarà quello che verrà renderizzato per ogni patch. I punti di controllo necessitano solo della posizione e delle coordinate texture. Le normali e gli altri parametri saranno calcolati dal Domain Shader.
Il primo step, il Vertex Shader, passerà il punto di controllo così com’è. L’Hull Shader invece avrà il primo compito nella funzione di valutazione. Questa come ricordiamo contiene i punti della patch (nel nostro caso 16). I punti 5,6, 9 e 10 sono il quadrato centrale. Per ognuno dei suoi 4 lati e per il centro calcoliamo la distanza tra questo e la telecamera. In base alla distanza varieremo i valori di tessellazione (per le patch quadrate sono 1 per ognuno dei 4 lati e 2 per le diagonali). L’hull shader vero e proprio invece passerà solamente i punti di controllo allo shader successivo.
Ora veniamo al Domain Shader. Definiamo 2 funzioni per le nostra funzioni base
float4 BSplineBasis (float t)
{
float invT = 1.0f - t;
return float4( invT * invT * invT ,
3.0f * t * t * t - 6.0F * t * t + 4,
3.0f * (-t * t * t + t * t + t ) + 1,
t * t * t ) / 6.0F;
}
Ed una per la derivate di questa funzione
float4 DBSplineBasis(float t)
{
float invT = 1.0f - t;
return float4( -3 * invT * invT,
9 * t * t - 12 * t,
- 9 * t * t + 6 * t + 3,
3 * t * t );
}
Ora salviamo i risultati lungo U e V (vengono passati al domain shader e sono la coordinata che la scheda video sta analizzando in quell’istante)
float4 BasisU = BSplineBasis( UV.x );
float4 BasisV = BSplineBasis( UV.y );
float4 dBasisU = DBSplineBasis( UV.x );
float4 dBasisV = DBSplineBasis( UV.y );
Ora la funzione di valutazione
float3 EvaluateBezier( float3 P[16], float4 BasisU, float4 BasisV )
{
float3 Value = float3(0,0,0);
Value = BasisV.x * ( P[0] * BasisU.x + P[1] * BasisU.y + P[2] * BasisU.z + P[3] * BasisU.w );
Value += BasisV.y * ( P[4] * BasisU.x + P[5] * BasisU.y + P[6] * BasisU.z + P[7] * BasisU.w );
Value += BasisV.z * ( P[8] * BasisU.x + P[9] * BasisU.y + P[10] * BasisU.z + P[11] * BasisU.w );
Value += BasisV.w * ( P[12] * BasisU.x + P[13] * BasisU.y + P[14] * BasisU.z + P[15] * BasisU.w );
return Value;
}
Passando alla funzione i 16 punti di controllo e le 2 basi UV otterremo la posizione finale del vertice. Se invece una delle basi passate sarà la base derivata otterremo la Tangente e la Binormale. Queste 2 sono importanti perché una volta calcolate il cross product sarà la normale vera e propria.
Nella demo i punti passati sono sollevati in altezza tramite una texture che funge da height map ma potete usare altre regole (esempio seno e coseno per simulare l’acqua).
Visto che noi però vogliamo usare le normal map dovremo moltiplicare i vettori “DirezioneLuce” e “DirezioneCamera” per la matrice tangente (l’articolo De Rerum Shader vi può essere d’aiuto).
Calcolati gli ultimi dati passeremo tutto al pixel shader che sarà responsabile al solito di calcolare l’illuminazione.
Vi rimando al tutorial numero 19 della serie (Github o in fondo dal primo tutorial)