I de to første opplæringene i denne serien dekket jeg emnene for impulsoppløsning og kjernearkitektur. Nå er det på tide å legge til noen av de siste detaljene til vår 2D, impulsbaserte fysikkmotor.
Temaene vi ser på i denne artikkelen er:
Jeg anbefaler på det sterkeste å lese opp de to foregående artiklene i serien før du prøver å takle denne. Noen viktige opplysninger i de forrige artiklene er bygget på i denne artikkelen.
Merk: Selv om denne opplæringen er skrevet med C ++, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.
Her er en rask demonstrasjon av hva vi jobber med i denne delen:
Friksjon er en del av kollisjonsoppløsningen. Friksjon gjelder alltid en kraft på gjenstander i motsatt retning av bevegelsen der de skal reise.
I virkeligheten er friksjon et utrolig komplekst samspill mellom forskjellige stoffer, og for å modellere det blir store antagelser og tilnærminger gjort. Disse antagelsene er underforstått i matematikken, og er vanligvis noe som "friksjonen kan tilnærmet med en enkelt vektor" - på samme måte som stiv kroppsdiamik simulerer virkelige livsinteraksjoner ved å anta organer med jevn tetthet som ikke kan deformere.
Ta en rask titt på videodemonoen fra den første artikkelen i denne serien:
Samspillet mellom kroppene er ganske interessant, og studsingen under kollisjoner føles realistisk. Men når objektene lander på den solide plattformen, sorterer de bare alle trykk unna og skyver av kantene på skjermen. Dette skyldes mangel på friksjonsimulering.
Som du bør huske fra den første artikkelen i denne serien, en bestemt verdi, j
, representerte størrelsen på en impuls som kreves for å separere to gjenstander gjennomtrengning under en kollisjon. Denne størrelsen kan refereres til som jnormal
eller Jn
som det brukes til å endre hastighet langs kollisjonen normal.
Innlemme en friksjonsrespons involverer å beregne en annen størrelse, referert til som jtangent
eller JT
. Friksjon vil bli modellert som en impuls. Denne størrelsen vil endre hastigheten til et objekt langs den negative tangentvektoren til kollisjonen, eller med andre ord langs friksjonsvektoren. I to dimensjoner er løsningen for denne friksjonsvektoren et løselbart problem, men i 3D blir problemet mye mer komplekst.
Friksjon er ganske enkel, og vi kan bruke vår tidligere ligning for j
, bortsett fra at vi vil erstatte alle forekomster av det normale n
med en tangentvektor t
.
\ [Ligning 1: \\
j = \ frac - (1 + e) (V ^ B -V ^ A) \ cdot n)
\ frac 1 masse ^ A + \ frac 1 masse ^ B \]
Erstatte n
med t
:
\ [Ligning 2: \\
j = \ frac - (1 + e) ((V ^ B -V ^ A) \ cdot t)
\ frac 1 masse ^ A + \ frac 1 masse ^ B \]
Selv om bare en enkelt forekomst av n
ble erstattet med t
i denne ligningen, når rotasjoner er introdusert, må noen flere forekomster erstattes utover den eneste i telleren i ligning 2.
Nå er det spørsmålet om hvordan å beregne t
oppstår. Tangentvektoren er en vektor vinkelrett på kollisionsstandarden som vender mot det normale. Dette kan høres forvirrende - ikke bekymre deg, jeg har et diagram!
Nedenfor kan du se tangentvektoren vinkelrett på det normale. Tangentvektoren kan enten peke til venstre eller høyre. Til venstre ville være "mer bort" fra den relative hastigheten. Det er imidlertid definert som den vinkelrett på det normale som peker "mer mot" den relative hastigheten.
Som nevnt kort tidligere vil friksjon være en vektor som vender motsatt til tangentvektoren. Dette betyr at retningen for å påføre friksjon kan beregnes direkte, siden den normale vektoren ble funnet under kollisionsdeteksjonen.
Å vite dette er tangentvektoren (hvor n
er kollisjonen normal):
\ [V ^ R = V ^ B -V ^ A \\
t = V ^ R - (V ^ R \ cdot n) * n \]
Alt som er igjen for å løse for jt
, Friksjonens størrelse er å beregne verdien direkte ved å bruke ligningene over. Det er noen veldig vanskelige brikker etter at denne verdien er beregnet som vil bli dekket kort tid, så dette er ikke den siste tingen som trengs i vår kollisjonsløsning:
// Re-beregne relativ hastighet etter normal impuls // påføres (impuls fra første artikkel, denne koden kommer // direkte deretter i samme løsningsfunksjon) Vec2 rv = VB - VA // Løs for tangentvektoren Vec2 tangent = rv - Dot (rv, normal) * normal tangent.Normalize () // Løs for størrelsen å bruke langs friksjonvektoren float jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB)
Ovennevnte kode følger likning 2 direkte. Igjen er det viktig å innse at friksjonsvektoren peker i motsatt retning av tangentvektoren, og som sådan må vi bruke et negativt tegn når vi doterer den relative hastigheten langs tangenten for å løse for den relative hastigheten langs tangentvektoren. Dette negative tegn vri tangenthastigheten og plutselig peker i retningen der friksjon skal tilnærmet som.
Coulombs lov er den delen av friksjonsimulering som de fleste programmører har problemer med. Jeg måtte selv gjøre litt av å studere for å finne ut den riktige måten å modellere den på. Trikset er at Coulombs lov er en ulikhet.
Coulomb friksjon stater:
\ [Ligning 3: \\
F_f <= \mu F_n \]
Med andre ord, er friksjonskraften alltid mindre enn eller lik den normale kraften multiplisert med noen konstant μ
(hvis verdi avhenger av materialene til objektene).
Den normale kraften er bare vår gamle j
størrelsen multiplisert med kollisjonen normal. Så hvis vi løst jt
(som representerer friksjonskraften) er mindre enn μ
ganger den normale kraften, så kan vi bruke vår jt
størrelse som friksjon. Hvis ikke, må vi bruke våre normale krafttider μ
i stedet. Dette "andre" tilfellet er en form for å klemme friksjonen vår under noen maksimumsverdi, idet maks er den normale krafttiden μ
.
Hele poenget med Coulombs lov er å utføre denne klemprosedyren. Denne klemmen viser seg å være den vanskeligste delen av friksjonsimulering for impulsbasert oppløsning for å finne dokumentasjon overalt - til nå, i det minste! De fleste hvite papirer jeg kunne finne på emnet, enten hoppet over friksjonen helt eller stoppet kort og implementerte feilaktige (eller ikke-eksisterende) klemmeprosedyrer. Forhåpentligvis nå har du en forståelse for å forstå at å få denne delen riktig er viktig.
Kan bare skille ut klemmen på en gang før du forklarer noe. Denne neste kodeblokk er det forrige kodeeksemplet med den ferdige klemprosedyren og friksjonsimpulsapplikasjonen alle sammen:
// Re-beregne relativ hastighet etter normal impuls // påføres (impuls fra første artikkel, denne koden kommer // direkte deretter i samme løsningsfunksjon) Vec2 rv = VB - VA // Løs for tangentvektoren Vec2 tangent = rv - Dot (rv, normal) * normal tangent.Normalize () // Løs for størrelsen å bruke langs friksjonvektoren float jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB) // PythagoreanSolve = A ^ 2 + B ^ 2 = C ^ 2, Løsning for C gitt A og B // Bruk til å approximere mu gitt friksjonskoeffisienter for hver kroppsflate mu = PythagoreanSolve (A-> staticFriction, B-> staticFriction) // Klemstyrke av friksjon og skape impulsvektor Vec2 friksjonImpulse hvis (abs (jt) < j * mu) frictionImpulse = jt * t else dynamicFriction = PythagoreanSolve( A->dynamicFriction, B-> dynamicFriction) friksjonImpulse = -j * t * dynamicFriction // Bruk A-> hastighet - = (1 / A-> masse) * friksjonImpuls B-> hastighet + = (1 / B-> masse) * frictionImpulse
Jeg bestemte meg for å bruke denne formelen for å løse for friksjonskoeffisientene mellom to legemer, gitt en koeffisient for hver kropp:
\ [Likning 4: \\
Friksjon = \ sqrt [] Friksjon ^ 2_A + Friksjon ^ 2_B \]
Jeg så faktisk at noen andre gjorde dette i sin egen fysikkmotor, og jeg likte resultatet. Et gjennomsnitt av de to verdiene ville fungere helt fint for å kvitte seg med bruken av kvadratroten. Egentlig vil enhver form for plukking av friksjonskoeffisienten fungere; dette er akkurat det jeg foretrekker. Et annet alternativ ville være å bruke et oppslagstabell hvor typen av hver kropp brukes som en indeks i en 2D-tabell.
Det er viktig at absolutt verdien av jt
brukes i sammenligningen, siden sammenligningen teoretisk klemmer råstørrelser under noen grense. Siden j
er alltid positiv, det må vendes for å representere en skikkelig friksjonsvektor, i tilfelle dynamisk friksjon brukes.
I den siste kodestykket ble det innført statisk og dynamisk friksjon uten noen forklaring! Jeg vil dedikere hele denne delen for å forklare forskjellen mellom og nødvendigheten av disse to typer verdiene.
Noe interessant skjer med friksjon: det krever en "aktiveringsenergi" for at objekter skal begynne å bevege seg når de er fullstendig hvile. Når to gjenstander hviler på hverandre i det virkelige liv, tar det en del energi til å skyve på en og få den til å bevege seg. Men når du får noe glidende, er det ofte lettere å holde det glidende fra da av.
Dette skyldes hvordan friksjon fungerer på et mikroskopisk nivå. Et annet bilde hjelper her:
Som du kan se, er de små deformitetene mellom overflatene virkelig den store skyldige som skaper friksjon i utgangspunktet. Når en gjenstand hviler på en annen, hviler mikroskopiske deformiteter mellom objektene, sammenkobling. Disse må brytes eller skilles for at objektene skal glide mot hverandre.
Vi trenger en måte å modellere dette i vår motor. En enkel løsning er å gi hver type materiale med to friksjonsverdier: en for statisk og en for dynamisk.
Den statiske friksjonen brukes til å klemme våre jt
omfanget. Hvis det løstes jt
størrelsen er lav nok (under terskelen), da kan vi anta at objektet er i ro, eller nesten som hvile og bruk hele jt
som en impuls.
På flipside, hvis løses jt
er over terskelen, kan det antas at objektet allerede har brutt "aktiveringsenergien", og i en slik situasjon brukes en lavere friksjonsimpuls, som er representert ved en mindre friksjonskoeffisient og en litt annen impulsberegning.
Forutsatt at du ikke hoppet over noen del av friksjonsdelen, godt gjort! Du har fullført den vanskeligste delen av denne hele serien (etter min mening).
De scene
klassen fungerer som en beholder for alt som involverer et fysikk simuleringsscenario. Den ringer og bruker resultatene av en hvilken som helst bred fase, inneholder alle stive legemer, kjører kollisjonskontroller og oppringningsoppløsning. Den integrerer også alle levende objekter. Scenen grensesnittet også med brukeren (som i programmereren ved hjelp av fysikkmotoren).
Her er et eksempel på hvordan en scenestruktur kan se ut:
klasse Scene (offentlig: Scene (Vec2 gravity, real dt); ~ Scene (); void SetGravity (Vec2 gravity) void SetDT (ekte dt) Body * CreateBody (ShapeInterface * form, BodyDef def) // Setter inn en kropp inn i scenen og initialiserer kroppen (beregner masse). // void InsertBody (Body * body) // sletter en kropp fra scenen tomt FjernBody (Body * body) // Oppdaterer scenen med et enkelt tidsrom tomt Steg (tomrom) float GetDT (void) LinkedList * GetBodyList (void) Vec2 GetGravity (void) void QueryAABB (CallBackQuery cb, const AABB & aabb) ugyldig QueryPoint (CallBackQuery cb, const Point2 og punkt) privat: float dt // Timestep i sekunder float inv_dt // Inverse timestep i sekker LinkedList body_list uint32 body_count Vec2 gravity bool debug_draw BroadPhase bredfase;
Det er ikke noe spesielt komplisert om scene
klasse. Tanken er å la brukeren legge til og fjerne stive legemer enkelt. De BodyDef
er en struktur som inneholder all informasjon om en stiv kropp, og kan brukes til å la brukeren sette inn verdier som en slags konfigurasjonsstruktur.
Den andre viktige funksjonen er Skritt()
. Denne funksjonen utfører en enkelt runde med kollisjonskontroll, oppløsning og integrasjon. Dette skal kalles fra tidsslagssløyfen som er skissert i den andre artikkelen i denne serien.
Å spørre et punkt eller AABB innebærer å sjekke for å se hvilke objekter som faktisk kolliderer med enten en peker eller AABB innenfor scenen. Dette gjør det enkelt for gameplay-relatert logikk for å se hvordan ting blir plassert i verden.
Vi trenger en enkel måte å finne ut hvilken kollisjon som skal kalles, basert på typen av to forskjellige objekter.
I C ++ er det to viktige måter som jeg er klar over: dobbelt sending og et 2D hoppebord. I mine egne personlige tester fant jeg 2D hoppebordet til overlegen, så jeg vil gå i detalj om hvordan å implementere det. Hvis du planlegger å bruke et annet språk enn C eller C ++, er det sikkert at en rekke funksjoner eller functorobjekter kan bygges på samme måte som en tabell med funksjonspekere (som en annen grunn jeg valgte å snakke om hoppetabeller i stedet for andre alternativer som er mer spesifikke for C ++).
Et hoppebord i C eller C ++ er et bord med funksjonspekere. Indikatorer som representerer vilkårlig navn eller konstanter brukes til å indeksere i tabellen og ringe til en bestemt funksjon. Bruken kan se slik ut som et 1D hoppebord:
Enum Animal Rabbit Duck Lion; const void (* talk) (void) [] = RabbitTalk, DuckTalk, LionTalk,; // Kall en funksjon fra bordet med 1D virtuell forsendelsessamtale [Kanin] () // kalles RabbitTalk-funksjonen
Ovennevnte kode etterligner faktisk hva C ++-språket selv utfører med virtuelle funksjonssamtaler og arv
. Men C ++ implementerer kun enkeltdimensjonale virtuelle samtaler. Et 2D bord kan bygges for hånd.
Her er noen psuedocode for et 2D hoppebord for å ringe kollisionsrutiner:
collisionCallbackArray = AABBvsAABB AABBvsCircle CirclevsAABB CirclevsCircle // Ring en collsion rutine for kollisjon deteksjon mellom A og B // to colliders uten å vite deres eksakte collider type // type kan være av enten AABB eller Circle collisionCallbackArray [A-> type] [B -> type] (A, B)
Og der har vi det! De faktiske typene av hver collider kan brukes til å indeksere i en 2D-array og velge en funksjon for å løse kollisjon.
Vær imidlertid oppmerksom på det AABBvsCircle
og CirclevsAABB
er nesten duplikater. Dette er nødvendig! Normalen må vendes for en av disse to funksjonene, og det er den eneste forskjellen mellom dem. Dette muliggjør konsekvent kollisjonoppløsning, uansett hvilken kombinasjon av objekter som skal løses.
Nå har vi dekket en stor mengde emner i å sette opp en tilpasset stiv kroppsfysikkmotor helt fra bunnen av! Kollisjonsoppløsning, friksjon og motorarkitektur er alle de temaene som har blitt dekket så langt. En helt vellykket fysikkmotor som passer for mange tosidige spill på to nivåer, kan konstrueres med den kunnskapen som er presentert i denne serien så langt.
Ser fremover inn i fremtiden, jeg planlegger å skrive en ny artikkel viet helt til en svært ønskelig funksjon: rotasjon og orientering. Orienterte gjenstander er overordentlig attraktive for å se interaksjon med hverandre, og er det siste stykket som vår tilpassede fysikkmotor krever.
Oppløsning av rotasjon viser seg å være ganske enkel, selv om kollisjonsdeteksjon tar et treff i kompleksitet. Lykke til til neste gang, og vær så snill å stille spørsmål eller legge inn kommentarer nedenfor!