Så här skapar du en anpassad 2D-fysikmotor orienterade styva organ

Hittills har vi täckt impulsupplösning, kärnarkitekturen och friktion. I den här sista handledningen i denna serie går vi över ett mycket intressant ämne: orientering.

I denna artikel kommer vi att diskutera följande ämnen:

  • Rotationsmatematik
  • Orienterade former
  • Kollisionsdetektering
  • Kollisionsupplösning

Jag rekommenderar starkt att läsa upp de föregående tre artiklarna i serien innan man försöker ta itu med den här. Mycket av nyckelfakta i tidigare artiklar är förutsättningar för resten av den här artikeln.


Provkod

Jag har skapat en liten provmotor i C ++, och jag rekommenderar att du bläddrar och hänvisar till källkoden under hela läsningen av den här artikeln, eftersom många praktiska implementeringsdetaljer inte kunde passa in i själva artikeln.


Detta GitHub repo innehåller provmotorn själv, tillsammans med ett Visual Studio 2010-projekt. GitHub låter dig se källan utan att behöva ladda ner källan själv, för enkelhets skyld.

relaterade inlägg
  • Philip Diffenderfer har förkastat repo för att skapa en Java-version av motorn också!

Orientering Math

Matematiken som involverar rotationer i 2D är ganska enkel, även om en behärskning av ämnet kommer att krävas för att skapa något av värde i en fysikmotor. Newtons andra lag säger:

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

Det finns en liknande ekvation som relaterar specifikt vinkelkraft och vinkelacceleration. Men innan dessa ekvationer kan visas är en snabb beskrivning av korsprodukten i 2D nödvändig.

Cross Product

Korsprodukten i 3D är en välkänd operation. Korsprodukten i 2D kan dock vara ganska förvirrande, eftersom det inte finns någon solid geometrisk tolkning.

2D-korsprodukten, till skillnad från 3D-versionen, returnerar inte en vektor utan en skalär. Detta skalärvärde representerar faktiskt storleken på den ortogonala vektorn längs z-axeln, om korsprodukten faktiskt skulle utföras i 3D. På ett sätt är 2D-korsprodukten bara en förenklad version av 3D-korsprodukten, eftersom det är en förlängning av 3D-vektor matematik.

Om detta är förvirrande, oroa dig inte: en grundlig förståelse för 2D-korsprodukten är inte allt som behövs. Bara vet exakt hur man utför operationen och vet att orderordningen är viktig: \ (a \ times b \) är inte samma som \ (b \ times a \). Denna artikel kommer att göra stor användning av korsprodukten för att omvandla vinkelhastigheten till linjär hastighet.

Menande på vilket sätt att utföra korsprodukten i 2D är dock väldigt viktigt. Två vektorer kan korsas, en skalär kan korsas med en vektor, och en vektor kan korsas med en skalär. Här är operationerna:

 // Två korsade vektorer returnerar en scalär float CrossProduct (const Vec2 & a, const Vec2 & b) return a.x * b.y - a.y * b.x;  // Mer exotiska (men nödvändiga) former av korsprodukten // med en vektor a och skalar s, båda återvänder en vektor Vec2 CrossProduct (const Vec2 & a, float s) returnera Vec2 (s * ay, -s * ax );  Vec2 CrossProduct (float s, const Vec2 & a) returnera Vec2 (-s * a.y, s * a.x); 

Vridmoment och vinkelhastighet

Som vi alla borde veta från tidigare artiklar, representerar denna ekvation ett förhållande mellan kraften som verkar på en kropp med den kroppens massa och acceleration. Det finns en analog för rotation:

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

\ (T \) står för vridmoment. Vridmomentet är rotationsstyrka.

\ (r \) är en vektor från mitten av massen (COM) till en viss punkt på ett objekt. \ (r \) kan ses som att referera till en "radien" från COM till en punkt. Varje enskild unik punkt på ett objekt kräver att ett annat \ (r \) värde representeras i ekvation 2.

\ (\ omega \) kallas "omega", och refererar till rotationshastighet. Detta förhållande kommer att användas för att integrera vinkelhastigheten hos en styv kropp.

Det är viktigt att förstå att linjär hastighet är hastigheten hos COM i en stel kropp. I föregående artikel hade alla föremål inga rotationsdelar, så den linjära hastigheten hos COM var samma hastighet för alla punkter på en kropp. När orientering införs, roterar punkter längre bort från COM-enheten snabbare än de som ligger nära COM. Det betyder att vi behöver en ny ekvation för att hitta hastigheten på en punkt på en kropp, eftersom kroppar nu kan snurra och översätta samtidigt.

Använd följande ekvation för att förstå förhållandet mellan en punkt på en kropp och hastigheten för den punkten:

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

\ (v \) representerar linjär hastighet. För att omvandla linjär hastighet till vinkelhastighet, korsa \ (r \) radie med \ (v \).

På samma sätt kan vi omordna ekvation 3 för att bilda en annan version:

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

Ekvationerna från det sista avsnittet är ganska kraftfulla endast om styva kroppar har likformig densitet. Icke-enhetlig densitet gör matematiken involverad i att beräkna allt som krävde att en styv kropps rotation och uppförande är alltför komplicerat. Dessutom, om den punkt som representerar en styv kropp inte är vid COM, kommer beräkningarna avseende \ (r \) att vara helt wonky.

Tröghet

I två dimensioner roterar ett objekt runt den imaginära z-axeln. Denna rotation kan vara ganska svår beroende på hur mycket massa ett objekt har, och hur långt bort från COM-objektets massa. En cirkel med massa som är lika med en lång tunn stav blir lättare att rotera än stången. Denna "svårighetsgrad att rotera" faktor kan betraktas som ett moment av tröghet hos ett föremål.

På ett sätt är tröghet en rotationsmassa av ett föremål. Ju mer tröghet någonting har desto svårare är det att få det att snurra.

Att veta detta kan man lagra trögheten hos ett föremål inom kroppen som samma format som massa. Det vore klokt att också lagra det omvända av detta tröghetsvärde, var försiktig så att du inte utför en uppdelning med noll. Se tidigare artiklar i denna serie för mer information om massa och invers massa.

Integration

Varje styv kropp kommer att kräva några få fält för att lagra rotationsinformation. Här är ett snabbt exempel på en struktur för att hålla några ytterligare data:

 struct RigidBody Form * form // Linjära komponenter Vec2 position Vec2 hastighet float acceleration // Vinkelkomponenter float orientering // radianer float angularVelocity float vridmoment;

Integrering av vinkelhastigheten och orienteringen av en kropp är mycket lik integrationen av hastighet och acceleration. Här är ett snabbkodsexempel för att visa hur det görs (notera: detaljer om integration var täckta i en tidigare artikel):

 const Vec2 gravitation (0, -10.0f) hastighet + = kraft * (1.0f / mass + gravitation) * dt vinkelhastighet + = vridmoment * (1.0f / momentOfInertia) * dt position + = hastighet * dt orient + = vinkelhastighet * dt

Med den lilla mängd information som hittills presenterats bör du kunna börja rotera olika saker på skärmen utan några problem. Med bara några rader av kod kan någonting ganska imponerande konstrueras, kanske genom att kasta en form i luften medan den roterar om COM, eftersom tyngdkraften drar den nedåt för att bilda en spårväg.

Mat22

Orientering bör lagras som ett enda radianvärde, som sedd ovan, men ofta kan användningen av en liten rotationsmatris vara ett mycket bättre val för vissa former.

Ett bra exempel är den orienterade bundet boxen (OBB). OBB består av en bredd och höjdsgrad, som båda kan representeras av vektorer. Dessa två-sträckningsvektorer kan sedan roteras med en två-för-två rotationsmatris för att representera axlarna hos en OBB.

Jag föreslår att en Mat22 matris klass att läggas till vad som helst matte bibliotek du använder. Jag använder mig själv av ett litet anpassat mattebibliotek som är förpackat i open source-demo. Här är ett exempel på hur ett sådant objekt kan se ut:

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

Några användbara operationer inkluderar: konstruktion från vinkel, konstruktion från kolumnvektorer, transponering, multiplicera med Vec2, multiplicera med en annan Mat22, absolutvärde.

Den sista användbara funktionen är att kunna hämta antingen x eller y kolumn från en vektor. Kolumnfunktionen skulle se ut som:

 Mat22m (PI / 2.0f); Vec2 r = m.ColX (); // hämta kolumnen x-axel

Denna teknik är användbar för att hämta en enhetsvektor längs en rotationsaxel, antingen x eller y axel. Dessutom kan en två-för-två-matris konstrueras från två ortogonala enhetsvektorer, eftersom varje vektor kan införas direkt i raderna. Även om denna byggnadsmetod är lite ovanlig för 2D-fysikmotorer, kan det fortfarande vara mycket användbart att förstå hur rotationer och matriser fungerar i allmänhet.

Denna konstruktör kan se ut som:

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

Eftersom den viktigaste funktionen hos en rotationsmatris är att utföra rotationer som är baserade på en vinkel, är det viktigt att kunna konstruera en matris från en vinkel och multiplicera en vektor genom denna matris (för att vrida vektorn moturs med vinkeln den matrisen konstruerades med):

 Mat2 (reella radianer) reella c = std :: cos (radianer); real s = std :: sin (radianer); m00 = c; m01 = -s; m10 = s; m11 = c;  // Rotera en vektor const Vec2 operatör * (const Vec2 & rhs) const returnera Vec2 (m00 * rhs.x + m01 * rhs.y, m10 * rhs.x + m11 * rhs.y); 

För korthetens skull kommer jag inte att härleda varför den motsatta rotationsmatrisen är av formen:

 a = vinkel cos (a), -in (a) sin (a), cos (a)

Det är emellertid viktigt att i alla fall veta att detta är formen av rotationsmatrisen. Mer information om rotationsmatriser finns på Wikipedia-sidan.

relaterade inlägg
  • Låt oss bygga en 3D-grafikmotor: Linjära transformationer

Förvandlas till en bas

Det är viktigt att förstå skillnaden mellan modell och världsrymd. Modellutrymme är koordinatsystemet lokalt till en fysikform. Ursprunget befinner sig vid COM, och koordinatsystemets orientering är inriktat på själva axelns axlar.

För att förvandla en form till världsrymden måste den roteras och översättas. Rotationen måste ske först, eftersom rotation alltid utförs om ursprunget. Eftersom objektet är i modellutrymme (ursprung hos COM) roterar rotationen om formens COM. Rotationen skulle ske med a Mat22 matris. I provkoden är orienteringsmatriser av namnet u.

Efter att rotation har utförts kan objektet sedan översättas till sin position i världen genom vektortillsats.

När ett objekt är i världsrymden kan det sedan översättas till modellutrymmet för ett helt annat objekt genom att använda inverse transformationer. Omvänd rotation följt av invers translation används för att göra det. Det här är hur mycket matte är förenklat under kollisionsdetektering!

Omvänd transformation (från vänster till höger) från världsrymden till modellrymden i den röda polygonen.

Såsom ses i ovanstående bild, om den inverse transformationen av det röda objektet appliceras på både de röda och blåa polygonerna, kan ett kollisionsdetekteringstest reduceras till formen av ett AABB vs OBB-test istället för att beräkna komplex matte mellan två orienterade former.

I en stor del av provkällkoden transformeras toppunkter ständigt från modell till värld och tillbaka till modell, av olika skäl. Du borde ha en klar förståelse av vad det innebär för att förstå provkollisionsdetekteringskoden.


Kollisionsdetektion och manifoldgenerering

I det här avsnittet presenterar jag snabba konturer av polygon och cirkelkollisioner. Vänligen se provkällkoden för mer detaljerad implementeringsinformation.

Polygon till polygon

Lets börja med den mest komplexa kollisionsdetekteringsrutinen i hela denna serie. Tanken att kontrollera kollision mellan två polygoner görs bäst (enligt min mening) med separationsaxtsatsen (SAT).

I stället för att projicera varje polygons utsträckning på varandra finns det emellertid en något nyare och mer effektiv metod, som skisseras av Dirk Gregorius i sin 2013 GDC-föreläsning (bilder finns här gratis).

Det första som måste läras är begreppet stödpunkter.

Supportpunkter

Stödpunkten för en polygon är det vertex som är längst längs en given riktning. Om två hörn har lika avstånd längs den givna riktningen är antingen en acceptabel.

För att beräkna en stödpunkt måste punktprodukten användas för att hitta ett signerat avstånd längs en given riktning. Eftersom det här är väldigt enkelt visar jag ett snabbt exempel i den här artikeln:

 // Extrempunktet längs en riktning inom en polygon Vec2 GetSupport (const Vec2 & dir) real bestProjection = -FLT_MAX; Vec2 bestVertex; för (uint32 i = 0; i < m_vertexCount; ++i)  Vec2 v = m_vertices[i]; real projection = Dot( v, dir ); if(projection > bestProjection) bestVertex = v; bestProjection = projektion;  returnera bestVertex; 

Dotprodukten används på varje vertex. Punktprodukten representerar ett signerat avstånd i en given riktning, så att vertexet med det största projicerade avståndet skulle vara toppunktet att återvända. Denna operation utförs i modellutrymme av den givna polygonen i provmotorn.

Hitta axel av separation

Genom att använda begreppet stödpunkter kan en sökning efter separationsaxeln utföras mellan två polygoner (polygon A och polygon B). Tanken med denna sökning är att slinga längs alla ansikten av polygon A och hitta stödpunkten i det negativa som är normalt för det ansiktet.

I ovanstående bild visas två stödpunkter: en på varje objekt. Den blåa normalen skulle motsvara stödpunkten på den andra polygonen som vertex längst längs i motsatt riktning av den blåa normalen. På samma sätt skulle den röda normalen användas för att hitta stödpunkten som ligger i slutet av den röda pilen.

Avståndet från varje stödpunkt till det aktuella ansiktet skulle vara den signerade penetrationen. Genom att lagra det största avståndet kan en eventuell penetrationshastighet registreras.

Här är en exempelfunktion från provkällkoden som finner den möjliga axeln för minimalt penetration med hjälp av Få stöd fungera:

 verklig FindAxisLeastPenetration (uint32 * faceIndex, PolygonShape * A, PolygonShape * B) real bestDistance = -FLT_MAX; uint32 bestIndex; för (uint32 i = 0; i < A->m_vertexCount; ++ i) // Hämta ett ansikte normalt från A Vec2 n = A-> m_normals [i]; // Hämta supportpunkt från B längs -n Vec2 s = B-> GetSupport (-n); // Hämta vertex på ansikte från A, omvandla till // B: s modellutrymme Vec2 v = A-> m_vertices [i]; // Beräkna penetreringsavståndet (i B: s modellutrymme) reellt d ​​= Dot (n, s - v); // Spara största avstånd om (d> bestDistance) bestDistance = d; bestIndex = i;  * faceIndex = bestIndex; returnera bestDistance; 

Eftersom denna funktion ger den största penetrationen, om denna penetration är positiv betyder det att de två formerna inte överlappar varandra (negativ penetration skulle inte innebära någon separeringsaxel).

Den här funktionen måste kallas två gånger, och A och B vänder mot varje samtal.

Clipping Incident och Reference Face

Härifrån måste incident- och referensyta identifieras, och händelsens ansikte måste klippas mot referensytans sidoväggar. Detta är en ganska icke-trivial operation, även om Erin Catto (skapare av Box2D, och all fysik som för närvarande används av Blizzard) har skapat några bra bilder som täcker detta ämne i detalj.

Denna klippning kommer att generera två potentiella kontaktpunkter. Alla kontaktpunkter bakom referensytan kan betraktas som kontaktpunkter.

Utöver Erin Cattos bilder har provmotorn också kliprutinerna som ett exempel.

Cirkel till polygon

Cirkeln vs polygonkollision rutin är ganska lite enklare än polygon vs polygon kollisionsdetektering. För det första beräknas närmaste ansikte på polygonen till mitten av cirkeln på samma sätt som att använda stödpunkter från föregående avsnitt: genom att lösa över varje ansikte som är normalt i polygonen och hitta avståndet från cirkelns centrum till ansiktet.

Om mitten av cirkeln ligger bakom det närmaste ansiktet, kan specifik kontaktinformation genereras och rutinen kan omedelbart sluta.

När det närmaste ansiktet har identifierats, avviker testet i ett linjesegment vs. cirkeltest. Ett linjesegment har tre intressanta regioner som heter Voronoi regioner. Undersök följande diagram:

Voronoi regioner av ett linjesegment.

Intuitivt, beroende på var centrum av cirkeln befinner sig, kan olika kontaktuppgifter härledas. Föreställ dig att cirkelns mitt ligger på båda vertexområdena. Det betyder att den närmaste punkten till cirkelns centrum kommer att vara ett kanten vertex, och den korrekta kollisionsnormalen kommer att vara en vektor från denna toppunkt till cirkelcentret.

Om cirkeln befinner sig inom ansiktsområdet kommer närmaste punkt i segmentet till cirkelns centrum att vara cirkelens mittprojekt på segmentet. Kollisionsnormalen kommer bara att vara ansiktet normalt.

För att beräkna vilken Voronoi-region cirkeln ligger inom använder vi prickprodukten mellan ett par punkter. Tanken är att skapa en imaginär triangel och testa för att se om vinkeln på hörnet som är konstruerat med segmentets toppunkt är över eller under 90 grader. En triangel skapas för varje toppunkt i linjesegmentet.

Projicerar vektorn från kanten vertex till cirkel centrum på kanten.

Ett värde på över 90 grader betyder att en kantregion har identifierats. Om ingen av trekantens korsformade vinklar är över 90 grader, måste cirkelns centrum projiceras på segmentet för att generera mångfaldig information. Såsom ses i bilden ovan, om vektorn från kanten vertex till cirkelcentralen prickad med kanten vektorn själv är negativ, då Voronoi regionen cirkeln ligger inom är känd.

Lyckligtvis kan punktprodukten användas för att beräkna ett signerat projektion, och detta tecken kommer att vara negativt om det är över 90 grader och positivt om det är under.


Kollisionsupplösning

Det är den tiden igen: vi kommer tillbaka till vår impulsupplösningskod för en tredje och sista gång. Vid det här laget bör du vara helt bekväm att skriva sin egen upplösningskod som beräknar upplösningsimpulser, tillsammans med friktionsimpulser, och kan också utföra linjär projektion för att lösa återstående penetration.

Rotationskomponenter måste läggas till både friktion och penetrationsupplösning. En del energi kommer att placeras i vinkelhastighet.

Här är vår impulsupplösning som vi lämnade den från föregående artikel om friktion:

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

Om vi ​​slänger in rotationsdelar ser den sista ekvationen ut så här:

\ [Ekvation 6: \\
j = \ frac - (1 + e) ​​(V ^ A - V ^ B) * t) \ frac 1 mass ^ A + \ frac 1 massa ^ B + \ frac (r ^ A \ tider ^) ^ 2 I ^ A + \ frac (r ^ B \ gånger t) ^ 2 I ^ B
\]

I ovanstående ekvation är \ (r \) åter en "radie", som i en vektor från COM av ett objekt till kontaktpunkten. En mer djupgående avledning av denna ekvation finns på Chris Hecks webbplats.

Det är viktigt att inse att hastigheten för en given punkt på ett objekt är:

\ [Ekvation 7: \\
V '= V + \ omega \ gånger r
\]

Tillämpningen av impulser ändras något för att redogöra för rotationsvillkoren:

 tomrummet: ApplyImpulse (const Vec2 & impuls, const Vec2 & contactVector) hastighet + = 1,0f / mass * impuls; vinkelVelocity + = 1.0f / tröghet * Kors (kontaktVector, impuls); 

Slutsats

Detta avslutar den slutliga artikeln i den här serien. Hittills har en hel del ämnen blivit täckta, inklusive impulsbaserad upplösning, mångfaldigande generation, friktion och orientering, allt i två dimensioner.

Om du har gjort det här långt, måste jag gratulera dig! Fysikmotorprogrammering för spel är ett extremt svårt studieområde. Jag önskar alla läsare lycka till, och du är välkommen att kommentera eller ställa frågor nedan.