Simuleer Tearable Cloth en Ragdolls met Simple Verlet-integratie

Soft body dynamics gaat over het simuleren van realistische vervormbare objecten. We zullen het hier gebruiken om een ​​uitneembare stoffen gordijn en een set ragdolls te simuleren waarmee je kunt communiceren en over het scherm kunt glijden. Het zal snel, stabiel en eenvoudig genoeg zijn om te doen met wiskunde op het middelbare schoolniveau.

Notitie: Hoewel deze tutorial geschreven is in Processing en gecompileerd met Java, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Eindresultaat voorbeeld

In deze demo kun je een groot gordijn zien (waarop de stoffen simulatie te zien is) en een aantal kleine stickmen (die de ragdoll-simulatie laten zien):

Je kunt de demo ook zelf proberen. Klik en sleep om te communiceren, druk op 'R' om te resetten en druk op 'G' om de zwaartekracht in te stellen.


Stap 1: Een punt en zijn beweging

De bouwstenen van ons spel met zijn het punt. Om dubbelzinnigheid te voorkomen, noemen we het de PointMass. De details staan ​​in de naam: het is een punt in de ruimte en het vertegenwoordigt een hoeveelheid massa.

De meest eenvoudige manier om fysica voor dit punt te implementeren, is om zijn snelheid op de een of andere manier 'vooruit te sturen'.

 x = x + velX y = y + velY

Stap 2: Tijdschema's

We kunnen niet aannemen dat onze game de hele tijd op dezelfde snelheid draait. Het kan voor sommige gebruikers 15 frames per seconde zijn, bij 60 voor anderen. Het is het beste om rekening te houden met framesnelheden van alle bereiken, wat kan worden gedaan met behulp van een tijdspanne.

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

Op deze manier zou het spel nog steeds op dezelfde snelheid draaien als een frame langer zou duren om voor één persoon dan voor een ander te worden verstreken. Voor een physics-engine is dit echter ongelooflijk onstabiel.

Stel je voor dat je spel een seconde of twee vastloopt. De motor zou daarover compenseren en de PointMass voorbij verschillende muren en objecten die anders een botsing zouden hebben gedetecteerd. Dus, niet alleen zou botsing detectie worden beïnvloed, maar ook de methode van constraint oplossen die we zullen gebruiken.

Hoe kunnen we de stabiliteit van de eerste vergelijking hebben, x = x + velX, met de consistentie van de tweede vergelijking, x = x + velX * timeElapsed? Wat als, misschien, we de twee zouden kunnen combineren?

Dat is precies wat we zullen doen. Stel je ons voor verstreken tijd was 30. We zouden precies hetzelfde kunnen doen als de laatste vergelijking, maar met een hogere nauwkeurigheid en resolutie, door te bellen x = x + (velx * 5) zes keer.

 elapsedTime = lastTime - currentTime lastTime = currentTime // reset lastTime // voeg tijd toe die niet gebruikt kon worden laatste frame elapsedTime + = leftOverTime // deel het op in chunks van 16 ms timesteps = floor (elapsedTime / 16) // winkel tijd we konden het niet gebruiken voor het volgende frame. leftOverTime = elapsedTime - timesteps * 16 for (i = 0; i < timesteps; i++)  x = x + velX * 16 y = y + velY * 16 // solve constraints, look for collisions, etc. 

Het algoritme gebruikt hier een vaste tijdzone die groter is dan één. Het vindt de verstreken tijd, verdeelt het in "brokken" van vaste grootte en duwt de resterende hoeveelheid tijd over naar het volgende frame. We voeren de simulatie beetje bij beetje uit voor elk deel waarin onze verstreken tijd wordt opgesplitst.

Ik koos 16 voor de tijdspanne, om de natuurkunde te simuleren alsof het ongeveer 60 frames per seconde was. Conversie van verstreken tijd naar frames per seconde kan worden gedaan met wat wiskunde: 1 seconde / verstreken tijd-seconden.

1s / (16ms / 1000s) = 62.5fps, dus een tijdstempo van 16ms is gelijk aan 62,5 frames per seconde.


Stap 3: beperkingen

Beperkingen zijn beperkingen en regels die aan de simulatie zijn toegevoegd, zodat ze kunnen leiden waar PointMasses wel en niet naartoe kan gaan.

Ze kunnen eenvoudig zijn als beperkende beperking, om te voorkomen dat PointMasses van de linkerrand van het scherm verdwijnen:

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

Het toevoegen van de beperking aan de rechterkant van het scherm gebeurt op dezelfde manier:

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

Dit doen voor de y-as is een kwestie van elke x in een y veranderen.

Het hebben van de juiste soort beperkingen kan resulteren in zeer mooie en boeiende interacties. Beperkingen kunnen ook extreem complex worden. Probeer je voor te stellen dat je een trillende mand met korrels simuleert waarbij geen van de korrels elkaar kruisen, of een robotarm met 100 verbindingen, of zelfs iets simpels als een stapel dozen. Het typische proces bestaat erin om botsingspunten te vinden, het exacte tijdstip van de botsing te vinden en vervolgens de juiste kracht of impuls te vinden om op elk lichaam aan te brengen om die botsing te voorkomen.

Het begrijpen van de hoeveelheid complexiteit die een set beperkingen kan hebben, kan moeilijk zijn en vervolgens die beperkingen oplossen, live is nog moeilijker. Wat we zullen doen is het vereenvoudigen van constraint-oplossingen aanzienlijk vereenvoudigen.


Stap 4: Verlet-integratie

Een wiskundige en programmeur genaamd Thomas Jakobsen onderzocht enkele manieren om de fysica van personages voor games te simuleren. Hij stelde dat nauwkeurigheid niet zo belangrijk is als geloofwaardigheid en prestaties. Het hart van zijn hele algoritme was een methode die sinds de jaren 60 werd gebruikt om moleculaire dynamica te modelleren, genaamd Verlet-integratie. Je bent misschien bekend met het spel Hitman: Codename 47. Het was een van de eerste spellen die ragdoll-fysica gebruikte en maakt gebruik van de door Jakobsen ontwikkelde algoritmen.

Verlet-integratie is de methode die we gebruiken om de positie van onze PointMass door te geven. Wat we eerder deden, x = x + velX, is een methode genaamd Euler Integration (die ik ook heb gebruikt in Codering Destructible Pixel Terrain).

Het belangrijkste verschil tussen Euler en Verlet-integratie is hoe snelheid wordt geïmplementeerd. Met behulp van Euler wordt een snelheid opgeslagen met het object en wordt elk frame aan de positie van het object toegevoegd. Het gebruik van Verlet past echter inertie toe door de vorige en huidige positie te gebruiken. Neem het verschil in de twee posities en voeg het toe aan de laatste positie om traagheid toe te passen.

 // Traagheid: objecten in beweging blijven in beweging. velX = x - lastX velY = y - lastY nextX = x + velX + accX * timestepSq nextY = y + velY + accY * timestepSq lastX = x lastY = y x = nextX y = nextY

We hebben daar de versnelling toegevoegd voor de zwaartekracht. Behalve dat, accX en Accy zal niet nodig zijn voor het oplossen van botsingen. Met behulp van Verlet-integratie hoeven we niet langer een soort van impuls of gedwongen oplossing voor botsingen uit te voeren. Alleen de positie wijzigen is voldoende om een ​​stabiele, realistische en snelle simulatie te hebben. Wat Jakobsen heeft ontwikkeld, is een lineaire vervanger voor iets dat anders niet-lineair is.


Stap 5: Verbindingsbeperkingen

De voordelen van Verlet-integratie kunnen het beste worden weergegeven aan de hand van een voorbeeld. In een textielmotor hebben we niet alleen PointMasses, maar ook links ertussen. Onze "links" vormen een afstandsbeperking tussen twee PointMasses. Idealiter willen we dat twee PointMasses met deze beperking altijd op een bepaalde afstand van elkaar staan.

Wanneer we deze beperking oplossen, moet Verlet Integration deze punten in beweging houden. Als het ene uiteinde bijvoorbeeld snel naar beneden zou worden verplaatst, zou het andere uiteinde het als een zweep moeten volgen door traagheid.

We hebben slechts één link nodig voor elk paar PointMasses dat aan elkaar is gekoppeld. Alle gegevens die u in de koppeling nodig hebt, zijn de PointMasses en de rustafstanden. Optioneel kun je stijfheid hebben, voor meer lente-beperkingen. In onze demo hebben we ook een "traangevoeligheid", de afstand waarmee de link wordt verwijderd.

Ik zal het alleen uitleggen restingDistance hier, maar de scheurafstand en stijfheid zijn beide geïmplementeerd in de demo en de broncode.

 Link restingDistance tearDistance-stijfheid PointMass A PointMass B solve () wiskunde voor het oplossen van afstand

U kunt lineaire algebra gebruiken om de beperking op te lossen. Zoek de afstanden tussen de twee, bepaal hoe ver je langs de restingDistance ze zijn, vertaal ze vervolgens op basis van dat en hun verschillen.

 // bereken de afstand diffX = p1.x - p2.x diffY = p1.y - p2.yd = sqrt (diffX * diffX + diffY * diffY) // verschil scalair verschil = (restingDistance - d) / d // vertaling voor elke PointMass. Ze worden 1/2 keer de vereiste afstand geschoven om hun rustafstanden aan te passen. translateX = diffX * 0.5 * difference translateY = diffY * 0.5 * difference p1.x + = translateX p1.y + = translateY p2.x - = translateX p2.y - = translateY

In de demo houden we ook rekening met massa en stijfheid. Er zijn enkele problemen bij het oplossen van deze beperking. Wanneer er meer dan twee of drie PointMasses aan elkaar zijn gekoppeld, kan het oplossen van enkele van deze beperkingen mogelijk andere eerder opgeloste beperkingen schenden.

Thomas Jakobsen kwam ook dit probleem tegen. In eerste instantie zou men een systeem van vergelijkingen kunnen maken en alle beperkingen tegelijkertijd kunnen oplossen. Dit zou echter snel toenemen in complexiteit, en het zou moeilijk zijn om meer dan alleen maar enkele links naar het systeem toe te voegen.

Jakobsen ontwikkelde een methode die in eerste instantie dom en naïef lijkt. Hij creëerde een methode genaamd "ontspanning", waarbij we in plaats van een keer op te lossen voor de beperking, we er verschillende keren voor oplossen. Elke keer dat we de links herhalen en oplossen, wordt de reeks links steeds dichter bij het oplossen.

Stap 6: Breng het samen

Om samen te vatten, hier is hoe onze motor werkt in pseudocode. Voor een meer specifiek voorbeeld, bekijk de broncode van de demo.

 animationLoop numPhysicsUpdates = hoe veel we ook kunnen passen in de verstreken tijd voor (elk numPhysicsUpdates) // (waarbij constraintSolve nummer 1 of hoger is.) Ik gebruik gewoonlijk 3 for (elke constraintSolve) for (elke Link-constraint) solve constraint  // Verbindingskoppelingen voor de eindkoppeling // Einde constraints update fysica // (gebruik verlet!) // end physics update tekenpunten en links

Stap 7: voeg een stof toe

Nu kunnen we de stof zelf bouwen. Het maken van de koppelingen moet vrij eenvoudig zijn: link naar links wanneer PointMass niet de eerste is in de rij en koppelingen maken wanneer het niet de eerste in zijn kolom is.

De demo gebruikt een eendimensionale lijst om PointMasses op te slaan en vindt punten om te linken naar gebruik x + y * breedte.

 // we willen dat de y-lus zich aan de buitenkant bevindt, dus het scant rij per rij in plaats van kolom voor kolom voor (elke y van 0 tot hoogte) voor (elke x van 0 tot breedte) nieuwe PointMass op x, y // links toevoegen als (x! = 0) PM hechten aan laatste PM in lijst // aan rechts toevoegen als (y! = 0) PM aan PM @ ((y - 1) * koppelen ( breedte + 1) + x) in lijst als (y == 0) pin PM PM toevoegen aan lijst

U zou in de code kunnen opmerken dat wij ook "pin PM" hebben. Als we niet willen dat ons gordijn valt, kunnen we de bovenste rij PointMasses vergrendelen naar hun startpositie. Om een ​​pinconstraint te programmeren, voegt u enkele variabelen toe om de pinlocatie bij te houden en vervolgens de PointMass naar die positie te verplaatsen nadat elke constraint is opgelost.


Stap 8: voeg een aantal Ragdolls toe

Ragdolls waren de oorspronkelijke bedoelingen van Jakobsen achter zijn gebruik van Verlet Integratie. Eerst beginnen we met de hoofden. We zullen een Cirkelbeperking creëren die alleen met de grens interageert.

 Cirkel PointMass radius solve () if (y < radius) y = 2*(radius) - y; if (y > hoogte-straal) y = 2 * (hoogte - radius) - y; if (x> width-radius) x = 2 * (width - radius) - x; als (x < radius) x = 2*radius - x;  

Vervolgens kunnen we het lichaam creëren. Ik voegde elk lichaamsdeel toe om de massa en lengteverhoudingen van een gewoon menselijk lichaam enigszins te evenaren. Uitchecken Body.pde in de bronbestanden voor volledige details. Dit doen zal ons naar een ander probleem leiden: het lichaam zal gemakkelijk in ongemakkelijke vormen draaien en ziet er erg onrealistisch uit.

Er zijn een aantal manieren om dit op te lossen. In de demo gebruiken we onzichtbare en zeer onstabiele koppelingen van de voeten naar de schouder en het bekken naar het hoofd om het lichaam op natuurlijke wijze in een minder onhandige rustpositie te duwen.

U kunt ook nep-hoekbeperkingen maken met behulp van koppelingen. Laten we zeggen dat we drie PointMasses hebben, waarvan er twee zijn gekoppeld aan een in het midden. U kunt een lengte tussen de uiteinden vinden om aan elke gekozen hoek te voldoen. Om die lengte te vinden, kun je de Wet van Cosinus gebruiken.

 A = rustafstand van uiteinde PointMass tot middelpunt PointMass B = rustafstand van andere PointMass tot middelpunt PointMass-lengte = sqrt (A * A + B * B - 2 * A * B * cos (hoek)) maak een koppeling tussen eindpunten PointMasses met lengte als rustafstand

Pas de link aan zodat deze beperking alleen van toepassing is wanneer de afstand kleiner is dan de rustafstand of, als het meer is dan. Hierdoor blijft de hoek op het middelpunt altijd te dichtbij of te ver, afhankelijk van wat je nodig hebt.


Stap 9: Meer dimensies!

Een van de geweldige dingen met het hebben van een volledig lineaire fysica-engine is het feit dat het elke gewenste dimensie kan zijn. Alles dat aan x werd gedaan, werd ook gedaan met een y-waarde, en kan daarom worden overgebracht naar drie of zelfs vier dimensies (ik weet echter niet zeker hoe je dat rendert!)

Hier volgt een linkbeperking voor de simulatie in 3D:

 // bereken de afstand diffX = p1.x - p2.x diffY = p1.y - p2.y diffZ = p1.z - p2.zd = sqrt (diffX * diffX + diffY * diffY + diffZ * diffZ) // difference scalair verschil = (restingDistance - d) / d // vertaling voor elke PointMass. Ze worden 1/2 keer de vereiste afstand geschoven om hun rustafstanden aan te passen. translateX = diffX * 0.5 * difference translateY = diffY * 0.5 * difference translateZ = diffZ * 0.5 * difference p1.x + = translateX p1.y + = translateY p1.z + = translateZ p2.x - = translateX p2.y - = translateY p2.z - = translateZ

Conclusie

Bedankt voor het lezen! Veel van de simulatie is sterk gebaseerd op het Advanced Character Physics-artikel van Thomas Jakobsen uit GDC 2001. Ik heb mijn best gedaan om de meeste gecompliceerde dingen te strippen en te vereenvoudigen tot het punt dat de meeste programmeurs zullen begrijpen. Als je hulp nodig hebt of opmerkingen hebt, kun je hieronder berichten plaatsen.