Så här skapar du en anpassad 2D-fysikmotor friktion, scen och hoppa tabell

I de två första handledningarna i denna serie täckte jag ämnena för impulsupplösning och kärnarkitektur. Nu är det dags att lägga till några av de sista detaljerna till vår 2D-impulsbaserade fysikmotor.

De ämnen vi ska titta på i den här artikeln är:

  • Friktion
  • Scen
  • Kollisionshoppbord

Jag rekommenderar starkt att läsa igenom de föregående två artiklarna i serien innan man försöker ta itu med den här. Några viktiga uppgifter i de tidigare artiklarna är byggda på den här artikeln.

Notera: Även om denna handledning skrivs med C ++, borde du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst.


Video Demo

Här är en snabb demonstration av vad vi arbetar mot i den här delen:


Friktion

Friktion är en del av kollisionsupplösningen. Friktion applicerar alltid en kraft på föremål i motsatt riktning mot rörelsen i vilken de ska resa.

I det verkliga livet är friktion en oerhört komplex samverkan mellan olika ämnen, och för att modellera det görs stora antaganden och approximationer. Dessa antaganden antyds inom matematiken och brukar vara som "friktionen kan approximeras med en enda vektor" - på samma sätt som hur stel kroppsdynamik simulerar verkliga interaktioner genom att man antar kroppar med likformig densitet som inte kan deformeras.

Ta en snabb titt på videodemoen från den första artikeln i serien:

Samspelet mellan kropparna är ganska intressant, och studsningen under kollisioner känns realistisk. Men när objekten landar på den fasta plattformen, så sätter de bara av alla pressar bort och glider av kanterna på skärmen. Detta beror på brist på friktionsimulering.

Impulser, igen?

Som du bör komma ihåg från den första artikeln i denna serie, ett visst värde, j, representerade storleken av en impuls som krävs för att separera två objekt penetration under en kollision. Denna storlek kan hänvisas till som jnormal eller JN som det används för att modifiera hastigheten längs kollisionsnormalen.

Införandet av ett friktionssvar innefattar beräkning av en annan storlek, hänvisad till som jtangent eller jT. Friktion kommer att modelleras som en impuls. Denna storhet kommer att modifiera hastigheten hos ett objekt längs kollisionens negativa tangentvektor, eller med andra ord längs friktionsvektorn. I två dimensioner är lösningen för denna friktionsvektor ett lösbart problem, men i 3D blir problemet mycket mer komplext.

Friktion är ganska enkel, och vi kan använda vår tidigare ekvation för j, förutom att vi kommer att ersätta alla instanser av det normala n med en tangentvektor t.

\ [Ekvation 1: \\
j = \ frac - (1 + e) ​​(V ^ B -V ^ A) \ cdot n)
\ frac 1 mass ^ A + \ frac 1 mass ^ B \]

Byta ut n med t:

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

Även om endast en enda förekomst av n ersattes med t i denna ekvation, när rotationer har införts måste några fler fall ersättas utöver den enda i täljaren av ekvation 2.

Nu är det fråga om hur man beräknar t uppstår. Tangentvektorn är en vektor vinkelrätt mot kollisionsnormalen som vetter mot det normala. Det här låter förvirrande - oroa mig inte, jag har ett diagram!

Nedan kan du se tangentvektorn vinkelrätt mot normal. Tangentvektorn kan antingen peka åt vänster eller höger. Till vänster skulle vara "mer bort" från den relativa hastigheten. Det definieras emellertid som det vinkelräta mot det normala som pekar "mer mot" relativ hastighet.


Vektorer av olika slag inom tidsramen för en kollision av styva kroppar.

Som sagt kort tidigare kommer friktion att vara en vektor som vetter motsatt tangentvektorn. Detta innebär att riktningen för att applicera friktion kan beräknas direkt, eftersom den normala vektorn hittades under kollisionsdetektering.

Genom att veta detta är tangentvektorn (var n är kollisionen normal):

\ [V ^ R = V ^ B -V ^ A \\
t = V ^ R - (V ^ R \ cdot n) * n \]

Allt som finns kvar att lösa för jt, Friktionens storlek är att beräkna värdet direkt med hjälp av ekvationerna ovan. Det finns några väldigt knepiga delar efter det att detta värde har beräknats som kommer att täckas inom kort, så det här är inte det sista som behövs i vår kollisionsupplösare:

 // Återberäkna relativ hastighet efter normal impuls // tillämpas (impuls från första artikeln, denna kod kommer // direkt därefter i samma lösenfunktion) Vec2 rv = VB - VA // Lös för tangentvektorn Vec2 tangent = rv - Dot (rv, normal) * normal tangent.Normalize () // Lös för storleksgrad att applicera längs friktionsvektorn float jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB)

Ovanstående kod följer Equation 2 direkt. Återigen är det viktigt att inse att friktionsvektorn pekar i motsatt riktning för tangentvektorn och som sådan måste vi tillämpa ett negativt tecken när vi prickar den relativa hastigheten längs tangenten för att lösa den relativa hastigheten längs tangentvektorn. Detta negativa tecken vrider tangenthastigheten och pekar plötsligt i den riktning i vilken friktionen ska approximeras som.

Coulombs lag

Coulombs lag är den del av friktionsimulering som de flesta programmerare har problem med. Jag själv måste göra en hel del studier för att räkna ut det rätta sättet att modellera det. Tricket är att Coulombs lag är en ojämlikhet.

Coulomb friktionstillstånd:

\ [Ekvation 3: \\
F_f <= \mu F_n \]

Med andra ord är friktionskraften alltid mindre än eller lika med den normala kraften multiplicerad med en viss konstant μ (vars värde beror på föremålets material).

Den normala kraften är bara vår gamla j magnitud multiplicerad med kollisionen normal. Så om vi löste jt (som representerar friktionen) är mindre än μ gånger den normala kraften, då kan vi använda vår jt magnitud som friktion. Om inte, måste vi använda våra normala krafttider μ istället. Detta "annat" fall är en form av klämning av friktionen under ett visst maximivärde, den maximala är den normala krafttiden μ.

Hela punkten i Coulombs lag är att utföra detta klämförfarande. Denna klämning visar sig vara den svåraste delen av friktionssimulering för impulsbaserad upplösning för att hitta dokumentation överallt - tills nu, åtminstone! De flesta vitpapper jag kunde hitta om ämnet misslyckades helt eller helt, eller stannade kort och genomförde felaktiga (eller obefintliga) klämprocedurer. Förhoppningsvis har du nu en uppskattning för att förstå att få den här delen rätt är viktig.

Låt oss bara klä ut klämman helt och hållet innan du förklarar någonting. Detta nästa kodblock är det föregående kodexemplet med det färdiga klämproceduren och friktionsimpulsansökan alla tillsammans:

 // Återberäkna relativ hastighet efter normal impuls // tillämpas (impuls från första artikeln, denna kod kommer // direkt därefter i samma lösenfunktion) Vec2 rv = VB - VA // Lös för tangentvektorn Vec2 tangent = rv - Dot (rv, normal) * normal tangent.Normalize () // Lös för storleksgrad att applicera längs friktionsvektorn float jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB) // PythagoreanSolve = A ^ 2 + B ^ 2 = C ^ 2, lösa för C givet A och B // Använda för att approximera mu-givna friktionskoefficienter för varje kroppsflotta mu = PythagoreanSolve (A-> staticFriction, B-> staticFriction) // Klämstyrka av friktion och skapa impulsvektor Vec2-friktionImpulse om (abs (jt) < j * mu) frictionImpulse = jt * t else  dynamicFriction = PythagoreanSolve( A->dynamicFriction, B-> dynamicFriction) friktionImpulse = -j * t * dynamicFriction // Använd A-> hastighet - = (1 / A-> massa) * friktionImpuls B-> hastighet + = (1 / B-> massa) * frictionImpulse

Jag bestämde mig för att använda denna formel för att lösa friktionskoefficienterna mellan två kroppar, givet en koefficient för varje kropp:

\ [Ekvation 4: \\
Friktion = \ sqrt [] friktion ^ 2_A + friktion ^ 2_B \]

Jag såg faktiskt någon annan göra det i sin egen fysikmotor, och jag gillade resultatet. Ett medelvärde av de två värdena skulle fungera utmärkt bra för att bli av med kvadratroten. Verkligen fungerar någon form av plockning av friktionskoefficienten; det här är bara vad jag föredrar. Ett annat alternativ skulle vara att använda ett uppslagstabell där typen av varje kropp används som ett index i en 2D-tabell.

Det är viktigt att det absoluta värdet av jt används i jämförelsen, eftersom jämförelsen teoretiskt klämmer fast grovhöjder under ett visst tröskelvärde. Eftersom j är alltid positiv måste den vridas för att representera en korrekt friktionsvektor, i det fall dynamisk friktion används.

Statisk och dynamisk friktion

I det sista kodstycket introducerades statisk och dynamisk friktion utan någon förklaring! Jag kommer att ägna hela detta avsnitt för att förklara skillnaden mellan och nödvändigheten av dessa två typer av värden.

Något intressant händer med friktion: det kräver en "aktiveringsenergi" för att objekt ska börja röra sig vid fullstöd. När två objekt vilar på varandra i verkligheten, tar det en hel del energi att driva på en och få den att röra sig. Men när du får något glidande är det ofta lättare att hålla det glidande från och med dess.

Detta beror på hur friktionen fungerar på en mikroskopisk nivå. En annan bild hjälper här:


Mikroskopisk syn på vad som orsakar energi av aktivering på grund av friktion.

Som du kan se är de små missbildningarna mellan ytorna verkligen den stora skyldige som skapar friktion i första hand. När ett objekt ligger vilat på en annan vilar mikroskopiska deformiteter mellan föremålen, sammankoppling. Dessa måste brytas eller separeras för att objekten ska glida mot varandra.

Vi behöver ett sätt att modellera detta inom vår motor. En enkel lösning är att tillhandahålla varje typ av material med två friktionsvärden: en för statisk och en för dynamisk.

Den statiska friktionen används för att klämma fast jt magnitud. Om det löste jt storleksordningen är tillräckligt låg (under tröskeln), då kan vi anta att objektet är vilat eller nästan som vila och använda hela jt som en impuls.

På flipsidan, om vi löste jt är över tröskeln, kan det antas att objektet redan har brutit "aktiveringsenergin" och i en sådan situation används en lägre friktionsimpuls, vilken representeras av en mindre friktionskoefficient och en något annorlunda impulsberäkning.


Scen

Förutsatt att du inte hoppa över någon del av friktionsdelen, bra gjort! Du har slutfört den hårdaste delen av hela serien (enligt min mening).

De Scen klassen fungerar som en behållare för allt som involverar ett fysik simuleringsscenario. Den ringer och använder resultaten från en bred fas, innehåller alla styva kroppar, kör kollisionskontroller och samtalsupplösning. Det integrerar också alla levande objekt. Scenen gränsar också med användaren (som i programmeraren med hjälp av fysikmotorn).

Här är ett exempel på hur en scenstruktur kan se ut:

 klass Scen public: Scene (Vec2 gravitation, real dt); ~ Scen (); void SetGravity (Vec2 gravity) void SetDT (real dt) Body * CreateBody (ShapeInterface * form, BodyDef def) // Sätter in en kropp i scenen och initierar kroppen (beräknar massa). / / void InsertBody (Body * body) // Tar bort en kropp från scenens tomrum RemoveBody (Body * body) // Uppdaterar scenen med ett enda tidssteg tomt Steg (tomrum) float GetDT (void) LinkedList * GetBodyList (void) Vec2 GetGravity (void) void QueryAABB (CallBackQuery cb, const AABB & aabb) void QueryPoint (CallBackQuery cb, const Punkt2 och punkt) privat: float dt // Tidsgräns i sekunder float inv_dt // Inverse timestep i sceounds LinkedList body_list uint32 body_count Vec2 gravitation bool debug_draw BroadPhase broadphase;

Det finns inte något särskilt komplicerat om Scen klass. Tanken är att tillåta användaren att enkelt lägga till och ta bort styva kroppar. De BodyDef är en struktur som innehåller all information om en styv kropp och kan användas för att tillåta användaren att infoga värden som en slags konfigurationsstruktur.

Den andra viktiga funktionen är Steg(). Denna funktion utför en enda runda kollisionskontroller, upplösning och integration. Detta borde hämtas från den tidsslutande slingan som skisseras i den andra artikeln i den här serien.

Att fråga en punkt eller AABB innebär att man kontrollerar för att se vilka objekt som faktiskt kolliderar med antingen en pekare eller AABB inom scenen. Detta gör det enkelt för gameplay-relaterad logik att se hur saker placeras i världen.


Hoppa tabell

Vi behöver ett enkelt sätt att välja vilken kollisionsfunktion som ska kallas, baserat på typen av två olika objekt.

I C ++ finns två viktiga sätt som jag är medveten om: dubbeldistribution och ett 2D-hoppbord. I mina personliga tester hittade jag 2D hoppbordet till överlägsen, så jag kommer att gå in i detalj om hur man implementerar det. Om du planerar att använda ett annat språk än C eller C ++ är det säkert att en rad funktioner eller funktorobjekt kan konstrueras på samma sätt som en tabell med funktionspekare (vilket är en annan anledning jag valde att prata om hoppbord i stället för andra alternativ som är mer specifika för C ++).

Ett hoppbord i C eller C ++ är en tabell med funktionspekare. Index som representerar godtyckliga namn eller konstanter används för att indexera i tabellen och ringa en specifik funktion. Användningen kan se ut så här för ett 1D hoppbord:

 Enum Animal Rabbit Duck Lion; const void (* talk) (void) [] = RabbitTalk, DuckTalk, LionTalk,; // Ring en funktion från bordet med 1D virtuellt sändningstal [Kanin] () // RabbitTalk-funktionen

Ovanstående kod efterliknar faktiskt vad C ++-språket själv implementerar med virtuella funktionssamtal och arv. C ++ implementerar emellertid bara enkla virtuella samtal. Ett 2D bord kan byggas för hand.

Här är några psuedokoder för ett 2D-hoppbord för att ringa kollisionsrutiner:

 collisionCallbackArray = AABBvsAABB AABBvsCirkelkretsarAABB CirclevsCircle // Ring en collsionrutin för kollisionsdetektering mellan A och B // två colliders utan att veta att deras exakta collider typ // typ kan vara av antingen AABB eller Circle collisionCallbackArray [A-> typ] [B -> typ] (A, B)

Och där har vi det! De faktiska typerna av varje collider kan användas för att indexera till en 2D-array och välja en funktion för att lösa kollision.

Observera dock att AABBvsCircle och CirclevsAABB är nästan dubbletter. Detta är nödvändigt! Normalen behöver vändas för en av dessa två funktioner, och det är den enda skillnaden mellan dem. Detta möjliggör en konsekvent kollisionsupplösning, oavsett vilken kombination av objekt som ska lösas.


Slutsats

Nu har vi täckt en hel del ämnen när du installerar en anpassad styv kroppsfysikmotor helt från början! Kollisionsupplösning, friktion och motorarkitektur är alla ämnen som hittills har täckts. En helt framgångsrik fysikmotor som är lämplig för många produktionsdimensionella spel kan byggas med den kunskap som presenteras i denna serie hittills.

Ser fram emot framtiden planerar jag att skriva en ny artikel som helt och hållet ägnas åt en mycket önskvärd funktion: rotation och orientering. Orienterade objekt är överlägset attraktiva för att se interagera med varandra, och är det sista stycket som vår anpassade fysikmotor kräver.

Upplösning av rotation visar sig vara ganska enkel, men kollisionsdetektering tar en träff i komplexitet. Lycka till nästa gång, och snälla fråga frågor eller skriv kommentarer nedan!