Slik oppretter du en tilpasset 2D-fysikkmotor Orienterte stive organer

Så langt har vi dekket impulsoppløsning, kjernearkitektur og friksjon. I dette, den endelige opplæringen i denne serien, går vi over et veldig interessant emne: orientering.

I denne artikkelen vil vi diskutere følgende emner:

  • Rotasjonsmatematikk
  • Orienterte former
  • Kollisjonsdeteksjon
  • Kollisjonsoppløsning

Jeg anbefaler på det sterkeste å lese på de tre foregående artiklene i serien før du prøver å takle denne. Mye av nøkkelinformasjonen i de forrige artiklene er forutsetninger for resten av denne artikkelen.


Eksempelkode

Jeg har laget en liten prøvemotor i C ++, og jeg anbefaler at du blar og henviser til kildekoden i hele lesingen av denne artikkelen, så mange praktiske implementeringsdetaljer kunne ikke passe inn i selve artikkelen.


Denne GitHub repo inneholder selve prøvenmoten, sammen med et Visual Studio 2010-prosjekt. GitHub lar deg se kilden uten å måtte laste ned kilden selv, for enkelhets skyld.

Relaterte innlegg
  • Philip Diffenderfer har forked repoen for å lage en Java-versjon av motoren også!

Orientering Math

Matematikken som involverer rotasjoner i 2D er ganske enkel, selv om en mestring av emnet vil være nødvendig for å skape noe av verdi i en fysikkmotor. Newtons andre lov sier:

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

Det er en lignende ligning som relaterer spesielt vinkelkraft og vinkelakselerasjon. Imidlertid, før disse ligningene kan vises, kreves en rask beskrivelse av kryssproduktet i 2D.

Cross produkt

Korsproduktet i 3D er en velkjent operasjon. Kryssproduktet i 2D kan imidlertid være ganske forvirrende, da det ikke er en solid geometrisk tolkning.

2D-kryssproduktet, i motsetning til 3D-versjonen, returnerer ikke en vektor, men en skalar. Denne skalarverdien representerer faktisk størrelsen på den ortogonale vektoren langs z-aksen, hvis kryssproduktet faktisk skulle utføres i 3D. På en måte er 2D-kryssproduktet bare en forenklet versjon av 3D-kryssproduktet, da det er en forlengelse av 3D-vektormatrise.

Hvis dette er forvirrende, vær ikke bekymret: en grundig forståelse av 2D-kryssproduktet er ikke alt som er nødvendig. Bare vet nøyaktig hvordan du skal utføre operasjonen, og vet at rekkefølgen av operasjonene er viktig: \ (a \ ganger b \) er ikke det samme som \ (b \ ganger a \). Denne artikkelen vil gjøre stor bruk av kryssproduktet for å forvandle vinkelhastigheten til lineær hastighet.

Å vite hvordan å utføre kryssproduktet i 2D er imidlertid svært viktig. To vektorer kan krysses, en skalar kan krysses med en vektor, og en vektor kan krysses med en skalar. Her er operasjonene:

 // To kryssede vektorer returnerer en skalarflyt CrossProduct (const Vec2 & a, const Vec2 & b) return a.x * b.y - a.y * b.x;  // Mer eksotiske (men nødvendige) former for kryssproduktet // med en vektor a og skalar s, begge returnerer en vektor Vec2 CrossProduct (const Vec2 og a, float s) return Vec2 (s * ay, -s * øks );  Vec2 CrossProduct (float s, const Vec2 & a) return Vec2 (-s * a.y, s * a.x); 

Moment og vinkelhastighet

Som vi alle burde vite fra de forrige artiklene, representerer denne ligningen et forhold mellom kraften som virker på en kropp med kroppens masse og akselerasjon. Det er en analog for rotasjon:

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

\ (T \) står for moment. Dreiemoment er rotasjonskraft.

\ (r \) er en vektor fra massesenteret (COM) til et bestemt punkt på en gjenstand. \ (r \) kan betraktes som refererer til en "radius" fra COM til et punkt. Hvert enkelt unikt punkt på et objekt vil kreve en annen \ (r \) -verdi som skal representeres i ligning 2.

\ (\ omega \) kalles "omega", og refererer til rotasjonshastighet. Dette forholdet vil bli brukt til å integrere vinkelhastigheten til en stiv kropp.

Det er viktig å forstå at lineær hastighet er hastigheten til COM av en stiv kropp. I forrige artikkel hadde alle objekter ingen roterende komponenter, så den lineære hastigheten til COM var den samme hastigheten for alle punkter på en kropp. Når orientering er innført, roterer punkter lenger unna COM'en raskere enn de som er nær COM. Dette betyr at vi trenger en ny ligning for å finne hastigheten til et punkt på en kropp, siden kroppene nå kan snurre og oversette samtidig.

Bruk følgende ligning for å forstå forholdet mellom et punkt på en kropp og hastigheten til det punktet:

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

\ (v \) representerer lineær hastighet. For å omdanne lineær hastighet til vinkelhastighet, krysse \ (r \) radius med \ (v \).

På samme måte kan vi omorganisere ligning 3 for å danne en annen versjon:

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

Ligningene fra den siste delen er ganske kraftige bare hvis stive legemer har jevn tetthet. Ikke-uniform tetthet gjør matematikken involvert i beregning av alt som kreves rotasjon og oppførsel av en stiv kropp altfor komplisert. Videre, hvis poenget som representerer en stiv kropp ikke er på COM, vil beregningene for \ (r \) bli helt wonky.

Inertia

I to dimensjoner roterer en gjenstand rundt den imaginære z-aksen. Denne rotasjonen kan være ganske vanskelig avhengig av hvor mye masse en gjenstand har, og hvor langt unna COM er objektets masse. En sirkel med masse lik en lang tynn stang vil være lettere å rotere enn stangen. Denne "vanskeligheten med å rotere" -faktoren kan betraktes som et moment av treghet av et objekt.

På en måte er inerti rotasjonsmassen til en gjenstand. Jo mer inerti noe har, jo vanskeligere er det å få det til å spinne.

Å vite dette, kan man lagre inertien til et objekt i kroppen som det samme format som masse. Det ville være lurt å også lagre den inverse av denne treghetverdien, vær forsiktig så du ikke utfører en divisjon med null. Vennligst se de forrige artiklene i denne serien for mer informasjon om masse og invers masse.

Integrering

Hver stiv kropp vil kreve noen flere felt for å lagre rotasjonsinformasjon. Her er et raskt eksempel på en struktur for å holde noen ekstra data:

 struct RigidBody Form * form // Linjære komponenter Vec2 posisjon Vec2 hastighet flyt akselerasjon // Vinkelkomponenter flyter orientering // radianer float angularVelocity float moment;

Integrering av vinkelhastigheten og orienteringen av en kropp er svært lik integrasjonen av hastighet og akselerasjon. Her er en rask kodeeksempel som viser hvordan det er gjort (merk: detaljer om integrasjon ble dekket i en tidligere artikkel):

 const Vec2 gravity (0, -10.0f) hastighet + = kraft * (1.0f / masse + tyngdekraften) * dt vinkelhastighet + = dreiemoment * (1.0f / momentOfInertia) * dt posisjon + = hastighet * dt orient + = vinkelhastighet * dt

Med den lille informasjonen frem til nå, bør du kunne begynne å rotere ulike ting på skjermen uten problemer. Med bare noen få linjer med kode, kan noe ganske imponerende bli konstruert, kanskje ved å kaste en form i luften mens den roterer om COM som tyngdekraften trekker den nedover for å danne en buet bane.

Mat22

Orientering bør lagres som en enkelt radian-verdi, som vist ovenfor, men ofte kan bruken av en liten rotasjonsmatrise være et mye bedre valg for visse former.

Et godt eksempel er den Oriented Bounding Box (OBB). OBB består av bredde- og høydestrekning, som begge kan representeres av vektorer. Disse to-gradvektorene kan da roteres med en to-to-to rotasjonsmatrise for å representere aksene til et OBB.

Jeg foreslår at en a Mat22 matriseklasse som skal legges til hva matematikkbibliotek du bruker. Jeg bruker selv et lite tilpasset matematikkbibliotek som er pakket i åpen kildekode-demo. Her er et eksempel på hvordan et slikt objekt kan se ut:

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

Noen nyttige operasjoner inkluderer: konstruksjon fra vinkel, konstruksjon fra kolonnevektorer, transponere, multiplisere med Vec2, multiplisere med en annen Mat22, absolutt verdi.

Den siste nyttige funksjonen er å kunne hente enten x eller y kolonne fra en vektor. Kolonnefunksjonen ser ut som:

 Mat22 m (PI / 2.0f); Vec2 r = m.ColX (); // hente x-aksen kolonnen

Denne teknikken er nyttig for å hente en enhedsvektor langs en rotasjonsakse, enten x eller y akser. I tillegg kan en to-to-to matrise konstrueres fra to ortogonale enhetvektorer, da hver vektor kan settes direkte inn i radene. Selv om denne konstruksjonsmetoden er litt uvanlig for 2D fysikkmotorer, kan det fortsatt være veldig nyttig å forstå hvordan rotasjoner og matriser fungerer generelt.

Denne konstruktøren ser kanskje 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; 

Siden den viktigste driften av en rotasjonsmatrise er å utføre rotasjoner basert på en vinkel, er det viktig å kunne konstruere en matrise fra en vinkel og multiplisere en vektor av denne matrisen (for å rotere vektoren mot klokka med vinkelen den matrise ble konstruert med):

 Mat2 (ekte radianer) ekte c = std :: cos (radianer); ekte s = std :: sin (radianer); m00 = c; m01 = -s; m10 = s; m11 = c;  // Roter en vektor const Vec2 operatør * (const Vec2 & rhs) const return Vec2 (m00 * rhs.x + m01 * rhs.y, m10 * rhs.x + m11 * rhs.y); 

For korthetens skyld vil jeg ikke avlede hvorfor rotasjonsmatrisen mot urviseren er av formen:

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

Det er imidlertid viktig å i det minste vite at dette er formen på rotasjonsmatrisen. For mer informasjon om rotasjonsmatriser, se Wikipedia-siden.

Relaterte innlegg
  • La oss bygge en 3D-grafikkmotor: Linjære transformasjoner

Transformere til et grunnlag

Det er viktig å forstå forskjellen mellom modell og verdensrom. Modellrom er koordinatsystemet lokalt til en fysikkform. Opprinnelsen er ved COM, og koordinatsystemets orientering er justert med aksene i selve formen.

For å forvandle en form til verdensrom må den roteres og oversettes. Rotasjon må skje først, da rotasjon alltid utføres om opprinnelsen. Siden objektet er i modell mellomrom (opprinnelse hos COM), roterer rotasjonen om COM i formen. Rotasjon ville oppstå med a Mat22 matrise. I prøvekoden er orienteringsmatriser av navnet u.

Etter at rotasjonen er utført, kan objektet så oversettes til sin posisjon i verden ved vektortilsetning.

Når et objekt er i verdensrommet, kan det da oversettes til modellrommet til et helt annet objekt ved å bruke inverse transformasjoner. Omvendt rotasjon etterfulgt av invers oversettelse brukes til å gjøre det. Dette er hvor mye matte er forenklet under kollisjonsdeteksjon!

Omvendt transformasjon (venstre til høyre) fra verdensrommet til modellrommet på den røde polygonen.

Som vist i bildet ovenfor, hvis den omvendte transformasjonen av den røde gjenstanden blir brukt på både de røde og blå polygonene, kan en kollisjonsdeteksjonstest reduseres til formen av en AABB vs OBB-test, i stedet for å beregne komplisert matte mellom to orienterte former.

I en stor del av prøvekilden blir forandringer konstant omformet fra modell til verden og tilbake til modell, for alle slags grunner. Du bør ha en klar forståelse av hva dette betyr for å forstå prøven for kollisjonsdeteksjonskoden.


Kollisjonsdeteksjon og manifoldgenerering

I denne delen presenterer jeg raske konturer av polygon og sirkelkollisjoner. Vennligst se sample kildekoden for mer detaljerte implementeringsdetaljer.

Polygon til polygon

La oss starte med den mest komplekse kollisionsdetekteringsrutinen i hele denne artikkelserien. Tanken om å sjekke for kollisjon mellom to polygoner er best gjort (etter min mening) med Separating Axis Theorem (SAT).

I stedet for å projisere hver polygons utvides på hverandre, er det en litt nyere og mer effektiv metode, som skissert av Dirk Gregorius i sin 2013 GDC-forelesning (lysbilder tilgjengelig her gratis).

Det første som må læres er begrepet støttepunkter.

Støttepunkter

Støttepunktet til et polygon er toppunktet som er lengst langs en gitt retning. Hvis to hjørner har like avstander langs den angitte retningen, er det en som er akseptabelt.

For å beregne et støttepunkt, må punktproduktet brukes til å finne en signert avstand langs en bestemt retning. Siden dette er veldig enkelt, viser jeg et raskt eksempel i denne artikkelen:

 // Ekstremt punktet langs en retning i en polygon Vec2 GetSupport (const Vec2 & dir) real bestProjection = -FLT_MAX; Vec2 bestVertex; for (uint32 i = 0; i < m_vertexCount; ++i)  Vec2 v = m_vertices[i]; real projection = Dot( v, dir ); if(projection > bestProjection) bestVertex = v; bestProjection = projeksjon;  returner bestVertex; 

Dotproduktet brukes på hvert vertex. Dotproduktet representerer en signert avstand i en gitt retning, så toppunktet med den største projiserte avstanden vil være toppunktet for å returnere. Denne operasjonen utføres i modellrommet av det gitte polygonet i prøvemotoren.

Finne akse av separasjon

Ved å bruke begrepet støttepunkter, kan et søk etter separasjonsaksen utføres mellom to polygoner (polygon A og polygon B). Tanken med dette søket er å sløyfe langs alle flater av polygon A og finne støttepunktet i det negative som er normalt for det ansiktet.

I bildet ovenfor vises to støttesteder: en på hver gjenstand. Den blå normal ville tilsvare støttepunktet på den andre polygonen som vertexet lengst langs i motsatt retning av den blå normal. På samme måte vil den røde normal brukes til å finne støttepunktet som ligger på slutten av den røde pilen.

Avstanden fra hvert støttepunkt til det nåværende ansiktet ville være den signerte penetrasjonen. Ved å lagre den største avstanden kan en mulig minimal aksen for penetrasjon registreres.

Her er et eksempelfunksjon fra prøvekilden som finner den mulige aksen for minimumspenetrasjon ved hjelp av Få støtte funksjon:

 ekte FindAxisLeastPenetration (uint32 * faceIndex, PolygonShape * A, PolygonShape * B) real bestDistance = -FLT_MAX; uint32 bestIndex; for (uint32 i = 0; i < A->m_vertexCount; ++ i) // Hent et ansikt som er normalt fra A Vec2 n = A-> m_normals [i]; // Hent støttepunkt fra B langs -n Vec2 s = B-> GetSupport (-n); // Hent vertex på ansikt fra A, forvandle til // Bs modellrom Vec2 v = A-> m_vertices [i]; // Beregn penetreringsavstand (i B's modellrom) ekte d = Dot (n, s - v); // Lagre største avstand hvis (d> bestDistance) bestDistance = d; bestIndex = i;  * faceIndex = bestIndex; returner bestdistanse; 

Siden denne funksjonen gir den største penetrasjonen, dersom denne penetrasjonen er positiv betyr det at de to figurene ikke overlapper (negativ inntrengning betyr ingen separasjonsakse).

Denne funksjonen må kalles to ganger, og A og B gjenstander for hvert anrop.

Clipping Incident og Reference Face

Herfra må hendelses- og referansefronten identifiseres, og hendelsesoverflaten må klippes mot sideflatene til referanseflaten. Dette er en ganske ikke-trivial operasjon, selv om Erin Catto (skaperen av Box2D, og ​​all fysikk som nå brukes av Blizzard) har skapt noen flotte lysbilder som dekker dette emnet i detalj.

Denne klippingen vil generere to potensielle kontaktpunkter. Alle kontaktpunkter bak referansesiden kan betraktes som kontaktpunkter.

Utover Erin Cattos lysbilder har prøvemotoren også klippeprosedyrene implementert som et eksempel.

Sirkel til polygon

Sirkelen vs. polygon kollisjon rutine er ganske enklere enn polygon vs polygon kollisjon gjenkjenning. For det første beregnes nærmeste ansikt på polygonen til sirkelens senter på samme måte som bruken av støttepunkter fra forrige seksjon: ved å løfte over hvert ansikt som er vanlig for polygonen og finne avstanden fra sirkelens senter til ansiktet.

Hvis senterets senter ligger bak dette nærmeste ansikt, kan spesiell kontaktinformasjon genereres, og rutinen kan umiddelbart avsluttes.

Etter at nærmeste ansikt er identifisert, går testen inn i et linjesegment vs. sirkeltest. Et linjesegment har tre interessante regioner kalt Voronoi regioner. Undersøk følgende diagram:

Voronoi-regioner i et linjesegment.

Intuitivt, avhengig av hvor senterets sirkel er plassert, kan annen kontaktinformasjon utledes. Tenk deg at senterets senter ligger i begge vertex-områder. Dette betyr at det nærmeste punktet til sirkelsenteret vil være et kanten vertex, og riktig kollisjon normalt vil være en vektor fra dette toppunktet til sirkelsenteret.

Hvis sirkelen er innenfor ansiktsområdet, vil nærmeste punkt i segmentet til sirkelens senter være sirkelens senterprosjekt på segmentet. Kollisjonen normal vil bare være ansiktet normalt.

For å beregne hvilken Voronoi-region sirkelen ligger innenfor, bruker vi prikkproduktet mellom et par hjørner. Tanken er å lage en imaginær trekant og teste for å se om hjørnet på hjørnet konstruert med segmentets toppunkt er over eller under 90 grader. En trekant er opprettet for hvert toppunkt av linjesegmentet.

Projiserer vektor fra kanten vertex til sirkel senter på kanten.

En verdi på over 90 grader vil bety at et kantområde er identifisert. Hvis ingen av trekantens kantlinjevinkler er over 90 grader, må sirkelsenteret projiseres på selve segmentet for å generere manifoldinformasjon. Som vist i bildet over, hvis vektoren fra kanten vertex til sirkel senter som er prikket med kanten vektoren selv er negativ, så er Voronoi regionen sirkelen ligger innenfor er kjent.

Heldigvis kan prikkproduktet brukes til å beregne en signert projeksjon, og dette tegnet vil være negativt hvis det er over 90 grader og positivt hvis det er under.


Kollisjonsoppløsning

Det er den tiden igjen: Vi kommer tilbake til vår impulsoppløsningskode for en tredje og endelige tid. Ved nå bør du være helt komfortabel å skrive sin egen oppløsningskode som beregner oppløsningsimpulser, sammen med friksjonsimpulser, og kan også utføre lineær projeksjon for å løse gjenværende penetrasjon.

Rotasjonskomponenter må legges til både friksjon og penetreringsoppløsning. En viss energi vil bli plassert i vinkelhastighet.

Her er vår impulsoppløsning som vi forlot den fra forrige artikkel om friksjon:

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

Hvis vi kaster inn roterende komponenter, ser den endelige ligningen slik ut:

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

I ovennevnte ligning er \ (r \) igjen en "radius", som i en vektor fra COM av en gjenstand til kontaktpunktet. En mer grundig avledning av denne ligningen finnes på Chris Hecks nettsted.

Det er viktig å innse at hastigheten til et gitt punkt på en gjenstand er:

\ [Ligning 7: \\
V '= V + \ omega \ ganger r
\]

Påføringen av impulser endres noe for å redegjøre for rotasjonsbetingelsene:

 void Body: ApplyImpulse (const Vec2 og impuls, const Vec2 & contactVector) hastighet + = 1.0f / masse * impuls; angularVelocity + = 1.0f / inertia * Kors (kontaktVector, impuls); 

Konklusjon

Dette avsluttes den endelige artikkelen i denne serien. I dag er det tatt ganske mange emner, inkludert impulsbasert oppløsning, manifoldgenerering, friksjon og orientering, alt i to dimensjoner.

Hvis du har gjort det så langt, må jeg gratulere deg! Fysikkmotor programmering for spill er et ekstremt vanskelig studieområde. Jeg ønsker alle leserne lykke, og igjen, vær så snill å kommentere eller stille spørsmål nedenfor.