Mjuk kroppsdynamik handlar om att simulera realistiska deformerbara objekt. Vi använder den här för att simulera en tårbar trasa gardin och en uppsättning ragdolls som du kan interagera med och fluga runt skärmen. Det blir snabbt, stabilt och enkelt att göra med matematik på gymnasiet.
Notera: Även om denna handledning skrivs i Behandling och sammanställd med Java, borde du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst..
I den här demonstrationen kan du se en stor gardin (visar vävnadsimuleringen) och ett antal små stickmen (visar ragdoll-simuleringen):
Du kan också prova demoen själv. Klicka och dra för att interagera, tryck på 'R' för att återställa och slå 'G' för att växla tyngdkraften.
Byggstenarna i vårt spel är punkten. För att undvika tvetydighet, kallar vi det PointMass
. Detaljerna finns i namnet: det är en punkt i rymden, och den representerar en mängd massa.
Det mest grundläggande sättet att genomföra fysik för denna punkt är att "framåt" sin hastighet på något sätt.
x = x + velX y = y + velY
Vi kan inte anta att vårt spel kommer att springa med samma hastighet hela tiden. Det kan springa med 15 bilder per sekund för vissa användare, men vid 60 för andra. Det är bäst att redogöra för ramfrekvenserna för alla intervall, vilket kan göras med hjälp av ett tidsintervall.
x = x + velX * timeElapsed y = y + velY * timeElapsed
På det här sättet, om en ram skulle ta längre tid för att gå för en person än för en annan, skulle spelet fortfarande köras i samma hastighet. För en fysikmotor är det dock otroligt instabil.
Tänk om ditt spel fryser i en sekund eller två. Motorn skulle över kompensera för det och flytta PointMass
Förbi flera väggar och föremål som det annars skulle ha upptäckt kollision med. Således skulle inte bara kollisionsdetektering påverkas, men också den metod för begränsningslösning som vi ska använda.
Hur kan vi ha stabiliteten i den första ekvationen, x = x + velX
, med konsistensen av den andra ekvationen, x = x + velX * timeElapsed
? Vad händer om vi kanske skulle kunna kombinera de två?
Det är precis vad vi ska göra. Föreställ dig vår tiden som gått
var 30
. Vi kunde göra exakt samma sak som den senare ekvationen, men med högre noggrannhet och upplösning, genom att ringa x = x + (velX * 5)
sex gånger.
elapsedTime = lastTime - currentTime lastTime = currentTime // återställt sistTime // lägg till tid som inte kunde användas senaste bildförloppetTid + = leftOverTime // dela upp det i bitar av 16 ms timesteps = golv (förflutitTime / 16) // butikstid vi kunde inte använda för nästa ram. leftOverTime = förflutitTime - timesteps * 16 för (i = 0; i < timesteps; i++) x = x + velX * 16 y = y + velY * 16 // solve constraints, look for collisions, etc.
Algoritmen använder här en fast tidsteg som är större än en. Den finner den förflutna tiden, bryter den upp i fasta "bitar" och trycker in den återstående tiden över till nästa ram. Vi kör simuleringen lite för små för varje bit, vår förflutna tid är uppbruten i.
Jag valde 16 för timestepstorleken, för att simulera fysiken som om den körde på ungefär 60 bilder per sekund. Konvertering från förfluten tid
till bildrutor per sekund kan göras med lite matte: 1 sekund / förflutenTimeInSeconds
.
1s / (16ms / 1000s) = 62,5fps
, så en 16ms timestep motsvarar 62,5 bilder per sekund.
Begränsningar är restriktioner och regler som läggs till i simuleringen, vägledning där PointMasses kan och kan inte gå.
De kan vara enkla som denna begränsningsbegränsning, för att förhindra att PointMasses flyttar sig från skärmens vänstra kant:
om (x < 0) x = 0 if (velX < 0) velX = velX * -1
Lägga till begränsningen för skärmens högra kant görs på samma sätt:
om (x> bredd) x = bredd om (velX> 0) velX = velX * -1
Att göra detta för y-axeln är en fråga om att ändra varje x till en y.
Att ha rätt slags hinder kan resultera i mycket vackra och fängslande interaktioner. Begränsningar kan också bli extremt komplexa. Försök att föreställa sig en vibrerande korg med korn utan att någon av kornen skär, eller en 100-ledig robotarm, eller till och med något enkelt som en stapel lådor. Den typiska processen innebär att hitta punkter av kollisioner, hitta den exakta tiden för kollision och sedan hitta rätt kraft eller impuls att applicera på varje kropp för att förhindra kollisionen.
Att förstå hur mycket komplexitet en uppsättning begränsningar kan ha kan vara svårt och sedan lösa dessa hinder, i realtid är ännu svårare. Vad vi ska göra är att förenkla begränsningslösningen betydligt.
En matematiker och programmerare som heter Thomas Jakobsen utforskade några sätt att simulera karaktären för karaktärer för spel. Han föreslog att noggrannheten inte är lika viktig som trovärdighet och prestanda. Hjärtat i hela hans algoritm var en metod som användes sedan 60-talet för att modellera molekylär dynamik, kallad Verlet Integration. Du kanske är bekant med spelet Hitman: Kodnamn 47. Det var ett av de första spelen att använda ragdollfysik, och använder algoritmerna Jakobsen utvecklade.
Verlet Integration är den metod som vi ska använda för att vidarebefordra positionen för vår PointMass. Vad vi gjorde tidigare, x = x + velX
, är en metod som heter Euler Integration (som jag också använde i kodningsförstörbar Pixel Terrain).
Den stora skillnaden mellan Euler och Verlet Integration är hur snabbt implementeras. Med Euler lagras en hastighet med objektet och läggs till på objektets position varje ram. Användning av Verlet applicerar emellertid tröghet genom att använda föregående och nuvarande position. Ta skillnaden i de två positionerna och lägg till den senaste positionen för att applicera tröghet.
// Tröghet: föremål i rörelse fortsätt. velX = x - lastX velY = y - lastY nextX = x + velX + accX * timestepSq nextY = y + velY + ackY * timestepSq lastX = x lastY = yx = nextX y = nextY
Vi lade till acceleration där för tyngdkraften. Utöver det, accX
och accY
kommer inte vara nödvändigt för att lösa för kollisioner. Med hjälp av Verlet Integration behöver vi inte längre göra någon form av impuls eller kraftlösning för kollisioner. Att ändra positionen ensam kommer att räcka för att ha en stabil, realistisk och snabb simulering. Vad Jakobsen utvecklat är en linjär ersättning för något som annars är olinjärt.
Fördelarna med Verlet Integration kan bäst visas genom exempel. I en tygmotor har vi inte bara PointMasses, men också länkar mellan dem. Våra "länkar" kommer att vara en distansbegränsning mellan två PointMasses. Helst vill vi ha två PointMasses med denna begränsning att alltid vara på ett visst avstånd.
När vi löser denna begränsning bör Verlet Integration hålla dessa punktmassor i rörelse. Till exempel, om den ena änden skulle flyttas snabbt ner, bör den andra änden följa den som en piska genom tröghet.
Vi behöver bara en länk för varje par PointMasses kopplade till varandra. Alla data som du behöver i länken är PointMasses och vilodistanserna. Eventuellt kan du ha styvhet, för mer av en vårbegränsning. I vår demo har vi också en "tårskänslighet", vilket är det avstånd där länken kommer att tas bort.
Jag förklarar bara restingDistance
här men tårans avstånd och styvhet implementeras både i demo och källkod.
Link restingDistance tearDistance stiffness PointMass A PointMass B lösa () math för att lösa avstånd
Du kan använda linjär algebra för att lösa problemet. Hitta avstånden mellan de två, bestämma hur långt längs restingDistance
de är, sedan översätt dem baserat på det och deras skillnader.
// beräkna avståndet diffX = p1.x - p2.x diffY = p1.y - p2.yd = sqrt (diffX * diffX + diffY * diffY) // skillnad skalär skillnad = (vila - d) / d // översättning för varje PointMass. De kommer att tryckas 1/2 avståndet som krävs för att matcha viloplängderna. translateX = diffX * 0.5 * skill translateY = diffY * 0.5 * skillnad p1.x + = translateX p1.y + = translateY p2.x - = translateX p2.y - = translateY
I demo berättar vi också för mass och styvhet. Det finns några problem med att lösa denna begränsning. När det finns mer än två eller tre PointMasses kopplade till varandra kan lösningen av några av dessa hinder strida mot andra hinder som tidigare lösts.
Thomas Jakobsen stötte på detta problem också. I början kan man skapa ett system av ekvationer och lösa alla begränsningar på en gång. Detta skulle dock öka snabbt i komplexiteten, och det skulle vara svårt att lägga till mer än till och med bara några länkar till systemet.
Jakobsen utvecklade en metod som kan tyckas dumt och naivt först. Han skapade en metod som kallas "avkoppling", där istället för att lösa för begränsningen en gång löser vi det flera gånger. Varje gång vi upprepar och löser länkarna blir uppsättningen länkar närmare allt som löses.
För att återskapa, här är hur vår motor fungerar i pseudokod. För ett mer specifikt exempel, kolla in demokällans källkod.
animationLoop numPhysicsUpdates = hur många vi kan passa i den förflutna tiden för (varje numPhysicsUpdates) // (med begränsningSolve att vara något nummer 1 eller högre. Jag brukar använda 3 för (varje begränsningslösning) för (varje länkbegränsning) lösa begränsning // slutlänkbegränsningslösningar // slutbegränsningar uppdatera fysik // (använd verlet!) // end fysikuppdatering rita punkter och länkar
Nu kan vi konstruera tyget själv. Att skapa länkarna ska vara ganska enkelt: länk till vänster när PointMass inte är den första på sin rad och länk upp när den inte är den första i sin kolumn.
Demon använder en endimensionell lista för att lagra PointMasses, och hittar punkter som länkar till användning x + y * bredd
.
// vi vill att y-loppet ska vara på utsidan, så det skannar rad för rad istället för kolumn för kolumn för (varje y från 0 till höjd) för (varje x från 0 till bredd) new PointMass vid x, y // bifoga till vänster om (x! = 0) bifoga PM till sista PM i listan // bifoga till höger om (y! = 0) bifoga PM till PM @ ((y - 1) * ( bredd + 1) + x) i listan om (y == 0) stift PM lägg till PM till listan
Du kanske märker i koden som vi också har "pin PM". Om vi inte vill att vår gardin ska falla, kan vi låsa toppraden PointMasses till sina startpositioner. För att programmera en stiftbegränsning, lägg till några variabler för att hålla reda på stiftplatsen och flytta sedan PointMass till den positionen efter varje begränsning lösa.
Ragdolls var Jakobsens ursprungliga intentioner bakom hans användning av Verlet Integration. Först börjar vi med huvudet. Vi skapar en Circle-begränsning som endast kommer att interagera med gränsen.
Cirkel PointMass-radien löser () om (y < radius) y = 2*(radius) - y; if (y > höjdradie) y = 2 * (höjdradie) - y; om (x> breddradie) x = 2 * (breddradie) - x; om (x < radius) x = 2*radius - x;
Nästa kan vi skapa kroppen. Jag lade till varje kroppsdel för att noggrant matcha mass- och längdproportionerna hos en vanlig mänsklig kropp. Checka ut Body.pde
i källfilerna för fullständiga detaljer. Att göra detta kommer att leda oss till ett annat problem: kroppen kommer lätt att motverka i obekväma former och ser väldigt orealistiskt ut.
Det finns ett antal sätt att fixa detta. I demo använder vi osynliga och väldigt otrevliga länkar från fötterna till axeln och bäckenet mot huvudet för att naturligt trycka kroppen i en mindre obekväm viloposition.
Du kan också skapa fake-vinkelbegränsningar genom att använda länkar. Låt oss säga att vi har tre PointMasses, med två kopplade till en i mitten. Du kan hitta en längd mellan ändarna för att tillfredsställa valfri vinkel. För att hitta den längden kan du använda lagen om kosiner.
A = viloperation från slutet PointMass till centrum PointMass B = vilodstånd från andra PointMass till centrum PointMass längd = sqrt (A * A + B * B - 2 * A * B * cos (vinkel)) skapa länk mellan ändpunktsmassor med längd som viloperation
Ändra länken så att den här begränsningen endast gäller när avståndet är mindre än vilodistans, eller om det är mer än. Detta kommer att hålla vinkeln i mittenpunkten från att vara för nära eller för långt beroende på vad du behöver.
En av de stora sakerna med att ha en helt linjär fysikmotor är att det kan vara vilken dimension du helst vill ha. Allt som gjordes till x gjordes också till ett y-värde, och kan därför utgå från tre eller till och med fyra dimensioner (jag är inte säker på hur du skulle göra det, men!)
Till exempel, här är en länkbegränsning för simuleringen i 3D:
// beräkna avståndet diffX = p1.x - p2.x diffY = p1.y - p2.y diffZ = p1.z - p2.zd = sqrt (diffX * diffX + diffY * diffY + diffZ * diffZ) // skillnad skalär skillnad = (vila - d) / d // översättning för varje PointMass. De kommer att tryckas 1/2 avståndet som krävs för att matcha viloplängderna. translateX = diffX * 0.5 * skill translateY = diffY * 0.5 * skill translateZ = diffZ * 0.5 * skillnad p1.x + = translateX p1.y + = translateY p1.z + = translateZ p2.x - = translateX p2.y - = translateX p2.y - = translateY p2.z - = translateZ
Tack för att du läser! Mycket av simuleringen är starkt baserad på Thomas Jakobsens Advanced Character Physics-artikel från GDC 2001. Jag gjorde mitt bästa för att ta bort de flesta komplicerade grejer och förenkla till den punkt som de flesta programmerare förstår. Om du behöver hjälp, eller har några kommentarer, kan du posta nedan.