Come creare un motore di fisica 2D personalizzato corpi rigidi orientati

Finora, abbiamo coperto la risoluzione degli impulsi, l'architettura di base e l'attrito. In questo, il tutorial finale di questa serie, parleremo di un argomento molto interessante: l'orientamento.

In questo articolo discuteremo i seguenti argomenti:

  • Matematica di rotazione
  • Forme orientate
  • Rilevamento delle collisioni
  • Risoluzione delle collisioni

Consiglio vivamente di leggere i precedenti tre articoli della serie prima di tentare di affrontare questo. Gran parte delle informazioni chiave negli articoli precedenti sono i prerequisiti per il resto di questo articolo.


Codice di esempio

Ho creato un piccolo motore di esempio in C ++ e ti consiglio di sfogliare e fare riferimento al codice sorgente durante la lettura di questo articolo, dal momento che molti dettagli di implementazione pratica potrebbero non rientrare nell'articolo stesso.


Questo repository GitHub contiene il motore di esempio stesso, insieme a un progetto di Visual Studio 2010. GitHub ti consente di visualizzare la fonte senza dover scaricare la sorgente stessa, per comodità.

Post correlati
  • Philip Diffenderfer ha biforcato il repo per creare una versione Java del motore!

Matematica di orientamento

La matematica che coinvolge le rotazioni in 2D è abbastanza semplice, anche se sarà necessaria una padronanza del soggetto per creare qualcosa di valore in un motore fisico. La seconda legge di Newton afferma:

\ [Equazione \: 1: \\
F = ma \]

Esiste un'equazione simile che riguarda specificamente la forza angolare e l'accelerazione angolare. Tuttavia, prima che queste equazioni possano essere mostrate, è necessaria una breve descrizione del prodotto incrociato in 2D.

Prodotto incrociato

Il prodotto incrociato in 3D è un'operazione ben nota. Tuttavia, il prodotto incrociato in 2D può essere abbastanza confuso, in quanto non esiste un'interpretazione geometrica solida.

Il prodotto cross 2D, a differenza della versione 3D, non restituisce un vettore ma uno scalare. Questo valore scalare rappresenta effettivamente la grandezza del vettore ortogonale lungo l'asse z, se il prodotto incrociato dovesse effettivamente essere eseguito in 3D. In un certo senso, il prodotto cross 2D è solo una versione semplificata del prodotto 3D cross, in quanto è un'estensione della matematica vettoriale 3D.

Se questo è fonte di confusione, non preoccuparti: una conoscenza approfondita del prodotto cross 2D non è tutto ciò che è necessario. Basta sapere esattamente come eseguire l'operazione e sapere che l'ordine delle operazioni è importante: \ (a \ times b \) non è uguale a \ (b \ times a \). Questo articolo farà un uso pesante del prodotto incrociato al fine di trasformare la velocità angolare in velocità lineare.

sapendo Come eseguire il prodotto incrociato in 2D è comunque molto importante. Due vettori possono essere attraversati, uno scalare può essere attraversato con un vettore e un vettore può essere attraversato con uno scalare. Ecco le operazioni:

 // Due vettori incrociati restituiscono un float scalare CrossProduct (const Vec2 & a, const Vec2 & b) return a.x * b.y - a.y * b.x;  // Forme più esotiche (ma necessarie) del prodotto incrociato // con un vettore a e scalare s, che restituiscono entrambi un vettore Vec2 CrossProduct (const Vec2 & a, float s) return Vec2 (s * ay, -s * ax );  Vec2 CrossProduct (float s, const Vec2 & a) return Vec2 (-s * a.y, s * a.x); 

Coppia e velocità angolare

Come tutti dovremmo sapere dagli articoli precedenti, questa equazione rappresenta una relazione tra la forza che agisce su un corpo con la massa e l'accelerazione di quel corpo. C'è un analogo per la rotazione:

\ [Equazione \: 2: \\
T = r \: \ times \: \ omega \]

\ (T \) sta per momento torcente. La coppia è la forza di rotazione.

\ (r \) è un vettore dal centro di massa (COM) a un particolare punto su un oggetto. \ (r \) può essere pensato come riferito a un "raggio" da COM a un punto. Ogni singolo punto univoco su un oggetto richiede un diverso valore \ (r \) da rappresentare nell'equazione 2.

\ (\ omega \) si chiama "omega" e si riferisce alla velocità di rotazione. Questa relazione verrà utilizzata per integrare la velocità angolare di un corpo rigido.

È importante capire che la velocità lineare è la velocità della COM di un corpo rigido. Nel precedente articolo, tutti gli oggetti non avevano componenti rotazionali, quindi la velocità lineare della COM era la stessa velocità per tutti i punti di un corpo. Quando viene introdotto l'orientamento, i punti più lontani dalla COM ruotano più velocemente di quelli vicino alla COM. Ciò significa che abbiamo bisogno di una nuova equazione per trovare la velocità di un punto su un corpo, dal momento che i corpi ora possono girare e tradurre allo stesso tempo.

Usa la seguente equazione per capire la relazione tra un punto su un corpo e la velocità di quel punto:

\ [Equazione \: 3: \\
\ omega = r \: \ times v \]

\ (v \) rappresenta la velocità lineare. Per trasformare la velocità lineare in velocità angolare, attraversa il raggio \ (r \) con \ (v \).

Allo stesso modo, possiamo riorganizzare l'equazione 3 per formare un'altra versione:

\ [Equazione \: 4: \\
v = \ omega \: \ times r \]

Le equazioni dell'ultima sezione sono abbastanza potenti solo se i corpi rigidi hanno densità uniforme. La densità non uniforme rende la matematica coinvolta nel calcolo di qualsiasi cosa che richieda la rotazione e il comportamento di un corpo rigido troppo complicato. Inoltre, se il punto che rappresenta un corpo rigido non è al COM, allora i calcoli relativi a \ (r \) saranno del tutto vistosi.

Inerzia

In due dimensioni un oggetto ruota attorno all'asse z immaginario. Questa rotazione può essere abbastanza difficile a seconda di quanto massa ha un oggetto e quanto lontano dalla COM è la massa dell'oggetto. Un cerchio con una massa pari a una lunga asta sottile sarà più facile da ruotare rispetto alla canna. Questo fattore di "difficoltà a ruotare" può essere pensato come il momento di inerzia di un oggetto.

In un certo senso, l'inerzia è la massa rotazionale di un oggetto. Più l'inerzia ha qualcosa, più difficile è farlo girare.

Sapendo questo, si potrebbe immagazzinare l'inerzia di un oggetto all'interno del corpo come lo stesso formato della massa. Sarebbe saggio anche memorizzare l'inverso di questo valore di inerzia, facendo attenzione a non eseguire una divisione per zero. Si prega di consultare gli articoli precedenti di questa serie per ulteriori informazioni sulla massa e sulla massa inversa.

Integrazione

Ogni corpo rigido richiederà altri campi per memorizzare le informazioni di rotazione. Ecco un rapido esempio di una struttura per contenere alcuni dati aggiuntivi:

 struct RigidBody Shape * shape // Componenti lineari Posizione Vec2 Vec2 accelerazione di velocità del galleggiamento // Componenti angolari orientamento flottante // radianti flottanti angolariVelocità coppia flottante;

L'integrazione della velocità angolare e l'orientamento di un corpo sono molto simili all'integrazione di velocità e accelerazione. Ecco un breve esempio di codice per mostrare come è fatto (nota: i dettagli sull'integrazione sono stati trattati in un precedente articolo):

 velocità Vec2 const (0, -10.0f) velocità + = forza * (1.0f / massa + gravità) * dt angularVelocity + = torque * (1.0f / momentOfInertia) * dt position + = velocity * dt orient + = angularVelocity * dt

Con la piccola quantità di informazioni presentate finora, dovresti essere in grado di iniziare a ruotare varie cose sullo schermo senza problemi. Con poche righe di codice, si può costruire qualcosa di piuttosto impressionante, magari lanciando una forma nell'aria mentre ruota attorno al COM quando la gravità lo tira verso il basso per formare un percorso di viaggio.

Mat22

L'orientamento deve essere memorizzato come un singolo valore radiante, come visto sopra, anche se spesso l'uso di una piccola matrice di rotazione può essere una scelta molto migliore per certe forme.

Un ottimo esempio è l'Oriented Bounding Box (OBB). L'OBB è costituito da un'ampiezza di larghezza e altezza, entrambe le quali possono essere rappresentate da vettori. Questi due vettori di estensione possono quindi essere ruotati da una matrice di rotazione due per due per rappresentare gli assi di un OBB.

Suggerisco la creazione di a Mat22 classe matrix da aggiungere a qualsiasi libreria di matematica che si sta utilizzando. Io stesso uso una piccola libreria matematica personalizzata che è inclusa nella demo open source. Ecco un esempio di come può essere un simile oggetto:

 struct Mat22 union struct float m00, m01 float m10, m11; ; struct Vec2 xCol; Vec2 yCol; ; ; ;

Alcune operazioni utili includono: costruzione da angolo, costruzione da vettori colonna, trasposizione, moltiplicazione con vec2, moltiplicare con un altro Mat22, valore assoluto.

L'ultima funzione utile è quella di essere in grado di recuperare il file X o y colonna da un vettore. La funzione della colonna sarebbe simile a qualcosa:

 Mat22 m (PI / 2.0f); Vec2 r = m.ColX (); // recupera la colonna dell'asse x

Questa tecnica è utile per recuperare un vettore unitario lungo l'asse di rotazione, o X o y asse. Inoltre, una matrice due per due può essere costruita da due vettori di unità ortogonali, poiché ciascun vettore può essere direttamente inserito nelle righe. Sebbene questo metodo di costruzione sia un po 'raro per i motori di fisica 2D, può comunque essere molto utile capire come le rotazioni e le matrici funzionano in generale.

Questo costruttore potrebbe sembrare qualcosa di simile:

 Mat22 :: Mat22 (const Vec2 & x, const Vec2 & y) m00 = x.x; m01 = x.y; m01 = y.x; m11 = y.y;  // o Mat22 :: Mat22 (const Vec2 & x, const Vec2 & y) xCol = x; yCol = y; 

Poiché l'operazione più importante di una matrice di rotazione consiste nell'eseguire rotazioni basate su un angolo, è importante essere in grado di costruire una matrice da un angolo e moltiplicare un vettore con questa matrice (per ruotare il vettore in senso antiorario per l'angolo il la matrice è stata costruita con):

 Mat2 (radianti reali) real c = std :: cos (radianti); real s = std :: sin (radianti); m00 = c; m01 = -s; m10 = s; m11 = c;  // Ruota un vettore const Operatore Vec2 * (const Vec2 & rhs) const return Vec2 (m00 * rhs.x + m01 * rhs.y, m10 * rhs.x + m11 * rhs.y); 

Per brevità, non deriverò il motivo per cui la matrice di rotazione antioraria abbia la forma:

 a = angolo cos (a), -sin (a) sin (a), cos (a)

Tuttavia è importante per lo meno sapere che questa è la forma della matrice di rotazione. Per ulteriori informazioni sulle matrici di rotazione, consultare la pagina di Wikipedia.

Post correlati
  • Costruiamo un motore grafico 3D: Trasformazioni lineari

Trasformarsi in una base

È importante capire la differenza tra il modello e lo spazio mondiale. Lo spazio modello è il sistema di coordinate locale a una forma fisica. L'origine è al COM e l'orientamento del sistema di coordinate è allineato con gli assi della forma stessa.

Per trasformare una forma in uno spazio mondiale deve essere ruotato e tradotto. La rotazione deve avvenire prima, poiché la rotazione viene sempre eseguita sull'origine. Poiché l'oggetto si trova nello spazio modello (origine in COM), la rotazione ruoterà attorno alla COM della forma. La rotazione avverrebbe con a Mat22 matrice. Nel codice di esempio, le matrici di orientamento sono del nome u.

Dopo aver eseguito la rotazione, l'oggetto può quindi essere tradotto nella sua posizione nel mondo mediante aggiunta vettoriale.

Una volta che un oggetto si trova nello spazio del mondo, può essere tradotto nello spazio modello di un oggetto completamente diverso utilizzando trasformazioni inverse. Rotazione inversa seguita da traslazione inversa per farlo. Questo è quanto la matematica è semplificata durante il rilevamento delle collisioni!

Trasformazione inversa (da sinistra a destra) dallo spazio del mondo allo spazio modello del poligono rosso.

Come visto nell'immagine sopra, se la trasformazione inversa dell'oggetto rosso viene applicata a entrambi i poligoni rosso e blu, allora un test di rilevamento collisione può essere ridotto alla forma di un test AABB vs OBB, invece di calcolare una matematica complessa tra due forme orientate.

In gran parte del codice sorgente di esempio, i vertici vengono costantemente trasformati da modello in mondo e di nuovo in modello, per tutti i tipi di motivi. Dovresti avere una chiara comprensione di ciò che significa per comprendere il codice di rilevamento della collisione del campione.


Rilevamento collisioni e generazione di manifold

In questa sezione, presenterò i contorni rapidi delle collisioni di poligoni e cerchi. Si prega di consultare il codice sorgente di esempio per ulteriori dettagli di implementazione approfonditi.

Poligono a poligono

Iniziamo con la routine di rilevamento collisioni più complessa in questa intera serie di articoli. L'idea di controllare la collisione tra due poligoni è meglio (secondo me) con il Teorema dell'asse di separazione (SAT).

Tuttavia, invece di proiettare le estensioni di ciascun poligono l'una sull'altra, c'è un metodo leggermente più nuovo e più efficiente, come delineato da Dirk Gregorius nella sua GDC Lecture 2013 (le diapositive sono disponibili qui gratuitamente).

La prima cosa da imparare è il concetto di punti di supporto.

Punti di supporto

Il punto di supporto di un poligono è il vertice che è il più lontano lungo una determinata direzione. Se due vertici hanno le stesse distanze lungo la direzione data, uno è accettabile.

Per calcolare un punto di supporto, è necessario utilizzare il prodotto punto per trovare una distanza segnata lungo una determinata direzione. Poiché questo è molto semplice, mostrerò un rapido esempio in questo articolo:

 // Il punto estremo lungo una direzione all'interno di un poligono Vec2 GetSupport (const Vec2 & dir) real bestProjection = -FLT_MAX; Vec2 bestVertex; per (uint32 i = 0; i < m_vertexCount; ++i)  Vec2 v = m_vertices[i]; real projection = Dot( v, dir ); if(projection > bestProjection) bestVertex = v; bestProjection = proiezione;  return bestVertex; 

Il prodotto punto viene utilizzato su ciascun vertice. Il prodotto punto rappresenta una distanza segnata in una data direzione, quindi il vertice con la distanza proiettata più grande sarebbe il vertice da restituire. Questa operazione viene eseguita nello spazio modello del poligono specificato all'interno del motore di esempio.

Trovare l'asse della separazione

Utilizzando il concetto di punti di supporto, è possibile eseguire una ricerca per l'asse di separazione tra due poligoni (poligono A e poligono B). L'idea di questa ricerca è quella di scorrere lungo tutte le facce del poligono A e trovare il punto di supporto nella normale negativa a quella faccia.

Nell'immagine sopra sono mostrati due punti di supporto: uno su ciascun oggetto. Il blu normale corrisponderebbe al punto di supporto dell'altro poligono come il vertice più lontano lungo la direzione opposta del blu normale. Allo stesso modo, la normale rossa verrebbe utilizzata per trovare il punto di supporto situato alla fine della freccia rossa.

La distanza da ciascun punto di supporto alla faccia corrente sarebbe la penetrazione firmata. Memorizzando la massima distanza possibile registrare un possibile asse minimo di penetrazione.

Ecco una funzione di esempio dal codice sorgente di esempio che trova il possibile asse di penetrazione minima usando il Ottenere supporto funzione:

 real FindAxisLeastPenetration (uint32 * faceIndex, PolygonShape * A, PolygonShape * B) real bestDistance = -FLT_MAX; uint32 bestIndex; per (uint32 i = 0; i < A->m_vertexCount; ++ i) // Recupera una faccia normale da A Vec2 n = A-> m_normals [i]; // Recupera il punto di supporto da B lungo -n Vec2 s = B-> GetSupport (-n); // Recupera vertice su faccia da A, trasforma in // spazio modello B Vec2 v = A-> m_vertices [i]; // Calcola la distanza di penetrazione (nello spazio del modello di B) reale d = Dot (n, s-v); // Memorizza la distanza massima se (d> bestDistance) bestDistance = d; bestIndex = i;  * faceIndex = bestIndex; return bestDistance; 

Poiché questa funzione restituisce la massima penetrazione, se questa penetrazione è positiva ciò significa che le due forme non si sovrappongono (la penetrazione negativa non significa nessun asse di separazione).

Questa funzione dovrà essere chiamata due volte, invertendo gli oggetti A e B ogni chiamata.

Fronte di incidente e riferimento di ritaglio

Da qui, è necessario identificare l'incidente e la faccia di riferimento e la faccia dell'incidente deve essere agganciata ai piani laterali della faccia di riferimento. Questa è un'operazione piuttosto banale, anche se Erin Catto (creatore di Box2D e tutta la fisica attualmente utilizzata da Blizzard) ha creato alcune grandi diapositive che trattano questo argomento in dettaglio.

Questo ritaglio genererà due potenziali punti di contatto. Tutti i punti di contatto dietro la faccia di riferimento possono essere considerati punti di contatto.

Oltre alle diapositive di Erin Catto, il motore di esempio ha anche le routine di ritaglio implementate come esempio.

Cerchia al poligono

La routine di collisione tra cerchio e poligono è un po 'più semplice del rilevamento di collisione poligono e poligono. Innanzitutto, la faccia più vicina sul poligono al centro del cerchio viene calcolata in modo simile all'utilizzo dei punti di supporto della sezione precedente: facendo scorrere su ciascuna faccia normale del poligono e trovando la distanza dal centro del cerchio alla faccia.

Se il centro del cerchio si trova dietro questa faccia più vicina, è possibile generare informazioni di contatto specifiche e la routine può immediatamente terminare.

Dopo che il volto più vicino è stato identificato, il test si trasforma in un segmento di linea rispetto al test del cerchio. Un segmento di linea ha tre regioni interessanti chiamate Regioni di Voronoi. Esaminare il seguente diagramma:

Regioni di Voronoi di un segmento di linea.

Intuitivamente, a seconda di dove si trova il centro del cerchio, è possibile ricavare informazioni di contatto diverse. Immagina che il centro del cerchio si trovi su entrambe le regioni dei vertici. Ciò significa che il punto più vicino al centro del cerchio sarà un vertice del bordo e la normale collisione corretta sarà un vettore da questo vertice al centro del cerchio.

Se il cerchio si trova all'interno della regione della faccia, il punto più vicino del segmento al centro del cerchio sarà il progetto centrale del cerchio sul segmento. La collisione normale sarà solo la faccia normale.

Per calcolare quale regione di Voronoi si trova all'interno del cerchio, utilizziamo il prodotto di punti tra un paio di vertici. L'idea è di creare un triangolo immaginario e verificare se l'angolo dell'angolo costruito con il vertice del segmento è superiore o inferiore a 90 gradi. Un triangolo viene creato per ogni vertice del segmento di linea.

Proiezione del vettore dal vertice del bordo al centro del cerchio sul bordo.

Un valore superiore a 90 gradi significherà che una regione di bordo è stata identificata. Se gli angoli di vertice di entrambi i triangoli non superano i 90 gradi, il centro del cerchio deve essere proiettato sul segmento stesso per generare informazioni multiple. Come visto nell'immagine sopra, se il vettore dal vertice del bordo al centro del cerchio punteggiato dal vettore del bordo stesso è negativo, allora la regione di Voronoi che il cerchio si trova all'interno è conosciuta.

Fortunatamente, il prodotto punto può essere utilizzato per calcolare una proiezione firmata, e questo segno sarà negativo se superiore a 90 gradi e positivo se inferiore.


Risoluzione collisione

È ancora una volta: torneremo al nostro codice di risoluzione dell'impulso per la terza e ultima volta. A questo punto, dovresti essere completamente a tuo agio nel scrivere il proprio codice di risoluzione che calcola gli impulsi di risoluzione, insieme agli impulsi di attrito, e può anche eseguire la proiezione lineare per risolvere la penetrazione avanzata.

I componenti rotazionali devono essere aggiunti sia alla frizione che alla risoluzione di penetrazione. Una certa energia sarà posta in velocità angolare.

Ecco la nostra risoluzione d'impulso come l'abbiamo lasciata dal precedente articolo sull'attrito:

\ [Equazione 5: \\
j = \ frac - (1 + e) ​​((V ^ A - V ^ B) * t) \ frac 1 massa ^ A + \ frac 1 massa ^ B
\]

Se inseriamo componenti rotazionali, l'equazione finale assomiglia a questa:

\ [Equazione 6: \\
j = \ frac - (1 + e) ​​((V ^ A - V ^ B) * t) \ frac 1 massa ^ A + \ frac 1 massa ^ B + \ frac (r ^ A \ volte t) ^ 2 I ^ A + \ frac (r ^ B \ volte t) ^ 2 I ^ B
\]

Nell'equazione precedente, \ (r \) è di nuovo un "raggio", come in un vettore dalla COM di un oggetto al punto di contatto. Una derivazione più approfondita di questa equazione può essere trovata sul sito di Chris Hecker.

È importante rendersi conto che la velocità di un dato punto su un oggetto è:

\ [Equazione 7: \\
V '= V + \ omega \ volte r
\]

L'applicazione degli impulsi cambia leggermente per tenere conto dei termini di rotazione:

 void Body :: ApplyImpulse (const Vec2 & impulse, const Vec2 & contactVector) velocity + = 1.0f / mass * impulso; angularVelocity + = 1.0f / inertia * Cross (contactVector, impulso); 

Conclusione

Questo conclude l'articolo finale di questa serie. Ormai, sono stati trattati alcuni argomenti, tra cui la risoluzione basata sugli impulsi, la generazione multipla, l'attrito e l'orientamento, il tutto in due dimensioni.

Se sei arrivato così lontano, devo congratularmi con te! La programmazione dei motori fisici per i giochi è un'area di studio estremamente difficile. Auguro buona fortuna a tutti i lettori, e ancora una volta non esitate a commentare o porre domande qui sotto.