Simula Cloth e Ragdoll indossabili con una semplice integrazione Verlet

La dinamica del corpo morbido consiste nel simulare oggetti deformabili realistici. Lo useremo qui per simulare una tenda di stoffa da indossare e una serie di ragdoll con cui puoi interagire e scappare sullo schermo. Sarà veloce, stabile e abbastanza semplice da fare con la matematica del liceo.

Nota: Sebbene questo tutorial sia scritto in Processing e compilato con Java, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Anteprima del risultato finale

In questa demo, puoi vedere una grande tenda (mostrando la simulazione del tessuto), e un certo numero di piccoli stickmen (mostrando la simulazione ragdoll):

Puoi provare la demo anche tu. Clicca e trascina per interagire, premi 'R' per resettare e premi 'G' per cambiare la gravità.


Passo 1: un punto e il suo movimento

Gli elementi costitutivi del nostro gioco sono il punto. Per evitare l'ambiguità, la chiameremo il PointMass. I dettagli sono nel nome: è un punto nello spazio e rappresenta una quantità di massa.

Il modo più semplice per implementare la fisica per questo punto è "far avanzare" la sua velocità in qualche modo.

 x = x + velX y = y + velY

Passaggio 2: Timestamp

Non possiamo supporre che il nostro gioco funzionerà alla stessa velocità per tutto il tempo. Potrebbe funzionare a 15 frame al secondo per alcuni utenti, ma a 60 per gli altri. È meglio tenere conto dei frame rate di tutti gli intervalli, che possono essere eseguiti utilizzando un timestep.

 x = x + velX * timeElapsed y = y + velY * timeElapsed

In questo modo, se un frame richiedesse più tempo per una persona piuttosto che per un'altra, il gioco funzionerebbe sempre alla stessa velocità. Per un motore fisico, tuttavia, questo è incredibilmente instabile.

Immagina se il tuo gioco si blocca per un secondo o due. Il motore compenserebbe eccessivamente e sposterebbe il PointMass passato diverse pareti e oggetti che altrimenti avrebbe rilevato una collisione con. Pertanto, non solo il rilevamento delle collisioni sarà interessato, ma anche il metodo di risoluzione dei vincoli che useremo.

Come possiamo avere la stabilità della prima equazione, x = x + velX, con la coerenza della seconda equazione, x = x + velX * timeElapsed? Cosa succede se, forse, potremmo combinare i due?

Questo è esattamente ciò che faremo. Immagina il nostro tempo trascorso era 30. Potremmo fare esattamente la stessa cosa dell'ultima equazione, ma con una maggiore accuratezza e risoluzione, chiamando x = x + (velX * 5) sei volte.

 elapsedTime = lastTime - currentTime lastTime = currentTime // reset lastTime // aggiungi tempo che non può essere utilizzato last frame elapsedTime + = leftOverTime // dividerlo in blocchi di 16 ms timesteps = floor (elapsedTime / 16) // store time non potremmo usare per il fotogramma successivo. leftOverTime = elapsedTime - timesteps * 16 for (i = 0; i < timesteps; i++)  x = x + velX * 16 y = y + velY * 16 // solve constraints, look for collisions, etc. 

L'algoritmo qui utilizza un valore temporale fisso maggiore di uno. Trova il tempo trascorso, lo suddivide in "pezzi" di dimensioni fisse e spinge la quantità di tempo rimanente sul fotogramma successivo. Eseguiamo la simulazione a poco a poco per ogni pezzo in cui è suddiviso il nostro tempo trascorso.

Ho scelto 16 per le dimensioni del timestep, per simulare la fisica come se fosse in esecuzione a circa 60 fotogrammi al secondo. Conversione da tempo trascorso a fotogrammi al secondo può essere fatto con qualche matematica: 1 secondo / elapsedTimeInSeconds.

1s / (16ms / 1000s) = 62.5fps, quindi un timestep di 16 ms equivale a 62,5 fotogrammi al secondo.


Passaggio 3: vincoli

I vincoli sono restrizioni e regole aggiunte alla simulazione, che guidano dove PointMasses può e non può andare.

Possono essere semplici come limiti di questo limite, per impedire che PointMasses si sposti dal bordo sinistro dello schermo:

 se (x < 0)  x = 0 if (velX < 0)  velX = velX * -1  

L'aggiunta del vincolo per il lato destro dello schermo è analoga:

 if (x> width) x = width if (velX> 0) velX = velX * -1

Fare questo per l'asse y è una questione di cambiare ogni x in y.

Avere il giusto tipo di vincoli può portare a interazioni molto belle e accattivanti. I vincoli possono anche diventare estremamente complessi. Prova a immaginare di simulare un cesto vibrante di grani con nessuno dei grani che si intersecano, o un braccio robotico da 100 articolazioni, o anche qualcosa di semplice come una pila di scatole. Il processo tipico consiste nel trovare punti di collisione, trovare il tempo esatto di collisione e quindi trovare la forza o l'impulso giusto da applicare a ciascun corpo per prevenire tale collisione.

Comprendere la quantità di complessità che può avere una serie di vincoli può essere difficile, e quindi risolvere quei vincoli, in tempo reale è ancora più difficile. Quello che faremo è semplificare significativamente la risoluzione dei vincoli.


Passaggio 4: integrazione Verlet

Un matematico e programmatore di nome Thomas Jakobsen ha esplorato alcuni modi di simulare la fisica dei personaggi per i giochi. Ha proposto che l'accuratezza non è tanto importante quanto la credibilità e le prestazioni. Il cuore del suo intero algoritmo era un metodo usato dagli anni '60 per modellare le dinamiche molecolari, chiamato Integrazione Verlet. Potresti avere familiarità con il gioco Hitman: Codename 47. È stato uno dei primi giochi a usare la fisica ragdoll e utilizza gli algoritmi sviluppati da Jakobsen.

Verlet Integration è il metodo che useremo per inoltrare la posizione del nostro PointMass. Quello che abbiamo fatto prima, x = x + velX, è un metodo chiamato Euler Integration (che ho usato anche in Coding Destructible Pixel Terrain).

La principale differenza tra Eulero e Verlet Integration è come viene implementata la velocità. Usando Eulero, una velocità viene memorizzata con l'oggetto e viene aggiunta alla posizione dell'oggetto ogni fotogramma. L'utilizzo di Verlet, tuttavia, applica l'inerzia utilizzando la posizione precedente e corrente. Prendi la differenza nelle due posizioni e aggiungila all'ultima posizione per applicare l'inerzia.

 // Inerzia: gli oggetti in movimento rimangono in movimento. velX = x - lastX velY = y - lastY nextX = x + velX + accX * timestepSq nextY = y + velY + accY * timestepSq lastX = x lastY = y x = nextX y = nextY

Abbiamo aggiunto accelerazione lì per gravità. Oltre a quello, accX e ACCY non sarà necessario per la risoluzione di collisioni. Usando l'integrazione Verlet, non abbiamo più bisogno di fare alcun tipo di impulso o di soluzione di forza per le collisioni. Cambiare la posizione da sola sarà sufficiente per avere una simulazione stabile, realistica e veloce. Ciò che Jakobsen ha sviluppato è un sostituto lineare di qualcosa che altrimenti non è lineare.


Passaggio 5: collegamento vincoli

I vantaggi di Verlet Integration possono essere illustrati al meglio attraverso l'esempio. In un motore di fabric, non solo PointMasses, ma anche i collegamenti tra di loro. I nostri "link" saranno un vincolo di distanza tra due PointMasses. Idealmente, vogliamo che due PointMass con questo vincolo si trovino sempre a una certa distanza.

Ogni volta che risolviamo questo vincolo, Verlet Integration dovrebbe mantenere in movimento queste entità. Ad esempio, se un'estremità dovesse essere spostata rapidamente verso il basso, l'altra estremità dovrebbe seguirla come una frusta attraverso l'inerzia.

Avremo solo bisogno di un link per ogni coppia di PointMasses collegati tra loro. Tutti i dati necessari nel collegamento sono PointMasses e le distanze di riposo. Opzionalmente puoi avere rigidità, per più di un vincolo di primavera. Nella nostra demo abbiamo anche una "sensibilità alla lacerazione", che è la distanza alla quale il collegamento verrà rimosso.

Ti spiegherò solo restingDistance qui, ma la distanza di rottura e la rigidità sono entrambe implementate nella demo e nel codice sorgente.

 Link restingDistance tearDistance rigidità PointMass A PointMass B solve () matematica per risolvere la distanza

È possibile utilizzare l'algebra lineare per risolvere il vincolo. Trova le distanze tra i due, determina quanto lontano lungo il restingDistance lo sono, quindi li traducono in base a questo e alle loro differenze.

 // calcola la distanza diffX = p1.x - p2.x diffY = p1.y - p2.yd = sqrt (diffX * diffX + diffY * diffY) // differenza scalare differenza = (restingDistance - d) / d // translation per ogni PointMass. Saranno spinti 1/2 della distanza richiesta per abbinare le loro distanze di riposo. translateX = diffX * 0.5 * differenza translateY = diffY * 0.5 * differenza p1.x + = translateX p1.y + = translateY p2.x - = translateX p2.y - = translateY

Nella demo, consideriamo anche la massa e la rigidità. Ci sono alcuni problemi nel risolvere questo vincolo. Quando ci sono più di due o tre PointMasses collegati tra loro, la risoluzione di alcuni di questi vincoli può violare altri vincoli precedentemente risolti.

Anche Thomas Jakobsen ha riscontrato questo problema. All'inizio, si potrebbe creare un sistema di equazioni e risolvere tutti i vincoli contemporaneamente. Ciò tuttavia aumenterebbe rapidamente la complessità e sarebbe difficile aggiungere più di pochi collegamenti al sistema.

Jakobsen ha sviluppato un metodo che potrebbe sembrare sciocco e ingenuo in un primo momento. Ha creato un metodo chiamato "rilassamento", dove invece di risolvere il vincolo una volta, lo risolviamo diverse volte. Ogni volta che ripetiamo e risolviamo i collegamenti, l'insieme dei collegamenti diventa sempre più vicino a tutti quelli che vengono risolti.

Step 6: Portalo insieme

Per ricapitolare, ecco come funziona il nostro motore in pseudocodice. Per un esempio più specifico, controlla il codice sorgente della demo.

 animationLoop numPhysicsUpdates = comunque molti di quelli che possiamo inserire nel tempo trascorso (ogni numPhysicsUpdates) // (con constraintSolve che è qualsiasi numero 1 o superiore.In genere uso 3 per (ogni constraintSolve) per (ogni vincolo di collegamento) risolvere il vincolo  // risolve il vincolo del link finale // i vincoli finali aggiornano la fisica // (usa verlet!) // fine fisica aggiorna punti e collegamenti

Passaggio 7: aggiungere un tessuto

Ora possiamo costruire il tessuto stesso. La creazione dei collegamenti dovrebbe essere abbastanza semplice: link a sinistra quando PointMass non è il primo sulla sua riga e si collega quando non è il primo nella sua colonna.

La demo utilizza un elenco unidimensionale per memorizzare PointMasses e trova i punti da collegare all'uso x + y * larghezza.

 // vogliamo che il ciclo y sia all'esterno, quindi esegue una scansione riga per riga invece di colonna per colonna (ogni y da 0 ad altezza) per (ogni x da 0 a larghezza) nuovo PointMass a x, y // collega a sinistra se (x! = 0) collega PM all'ultimo PM di lista // collega a destra se (y! = 0) collega PM a PM @ ((y - 1) * ( width + 1) + x) nella lista if (y == 0) pin PM aggiungi PM alla lista

Si potrebbe notare nel codice che abbiamo anche "pin PM". Se non vogliamo che la nostra tenda cada, possiamo bloccare la prima fila di PointMass alle loro posizioni iniziali. Per programmare un vincolo di pin, aggiungi alcune variabili per tenere traccia della posizione del pin, quindi sposta PointMass in quella posizione dopo che ogni vincolo è stato risolto.


Passaggio 8: aggiungere alcuni Ragdoll

I Ragdoll erano le intenzioni originali di Jakobsen dietro al suo uso di Verlet Integration. Innanzitutto inizieremo con le teste. Creeremo un vincolo Circle che interagirà solo con il limite.

 Cerchio PointMass radius solve () if (y < radius) y = 2*(radius) - y; if (y > altezza-raggio) y = 2 * (altezza - raggio) - y; se (x> larghezza-raggio) x = 2 * (larghezza - raggio) - x; se (x < radius) x = 2*radius - x;  

Quindi possiamo creare il corpo. Ho aggiunto ciascuna parte del corpo in modo abbastanza accurato con le proporzioni di massa e lunghezza di un normale corpo umano. Check-out Body.pde nei file di origine per tutti i dettagli. Questo ci porterà ad un altro problema: il corpo si contorcerà facilmente in forme scomode e apparirà molto irrealistico.

Ci sono diversi modi per risolvere questo problema. Nella demo, utilizziamo collegamenti invisibili e molto aperti dal piede alla spalla e al bacino alla testa per spingere naturalmente il corpo in una posizione di riposo meno scomoda.

Puoi anche creare vincoli di falsi angoli usando i collegamenti. Diciamo che abbiamo tre PointMass, con due collegati a uno nel mezzo. È possibile trovare una lunghezza tra le estremità al fine di soddisfare qualsiasi angolo scelto. Per trovare quella lunghezza, puoi usare la Legge dei Coseni.

 A = distanza di riposo dall'estremità PuntoMassa al centro PuntoMassa B = distanza di riposo da un altro PuntoMassa al centro PuntoMassa lunghezza = sqrt (A * A + B * B - 2 * A * B * cos (angolo)) crea un collegamento tra PointMasses finali usando la lunghezza come distanza di riposo

Modificare il collegamento in modo che questo vincolo si applichi solo quando la distanza è inferiore alla distanza di riposo o, se è maggiore di. Ciò manterrà l'angolo al punto centrale da essere sempre troppo vicino o troppo lontano, a seconda di ciò che ti serve.


Passaggio 9: più dimensioni!

Una delle cose migliori di avere un motore fisico completamente lineare è il fatto che può essere qualsiasi dimensione che desideri. Tutto ciò che è stato fatto a x è stato fatto anche per un valore y, e la forza può essere estesa a tre o anche a quattro dimensioni (non sono sicuro di come lo renderesti, però!)

Ad esempio, ecco un vincolo di collegamento per la simulazione in 3D:

 // calcola la distanza diffX = p1.x - p2.x diffY = p1.y - p2.y diffZ = p1.z - p2.zd = sqrt (diffX * diffX + diffY * diffY + diffZ * diffZ) // difference differenza scalare = (restingDistance - d) / d // traduzione per ogni PointMass. Saranno spinti 1/2 della distanza richiesta per abbinare le loro distanze di riposo. translateX = diffX * 0.5 * differenza translateY = diffY * 0.5 * differenza translateZ = diffZ * 0.5 * differenza p1.x + = translateX p1.y + = translateY p1.z + = translateZ p2.x - = translateX p2.y - = translateY p2.z - = translateZ

Conclusione

Grazie per aver letto! Gran parte della simulazione è fortemente basata sull'articolo di Advanced Character Physics di Thomas Jakobsen di GDC 2001. Ho fatto del mio meglio per rimuovere la maggior parte delle cose complicate e semplificare al punto che la maggior parte dei programmatori capirà. Se hai bisogno di aiuto o commenti, sentiti libero di postare qui sotto.