Een aangepaste 2D-physics-engine maken wrijving, scène en springtabel

In de eerste twee tutorials in deze serie behandelde ik de onderwerpen Impulse Resolution en Core Architecture. Nu is het tijd om enkele van de laatste hand te leggen aan onze 2D, op impulsen gebaseerde physics engine.

De onderwerpen die we in dit artikel zullen behandelen zijn:

  • Wrijving
  • Tafereel
  • Collision Jump Table

Ik raad ten zeerste aan om de vorige twee artikelen in de serie te lezen voordat ik probeer deze aan te pakken. In dit artikel is een aantal belangrijke informatie in de vorige artikelen behandeld.

Notitie: Hoewel deze tutorial met C ++ is geschreven, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Videodemo

Hier is een korte demo van waar we naartoe werken in dit deel:


Wrijving

Wrijving is een onderdeel van de botsresolutie. Wrijving oefent altijd een kracht uit op objecten in de richting tegengesteld aan de beweging waarin ze moeten reizen.

In het echte leven is wrijving een ongelooflijk complexe interactie tussen verschillende stoffen, en om het te modelleren, worden er enorme aannames en benaderingen gemaakt. Deze veronderstellingen worden geïmpliceerd binnen de wiskunde, en zijn meestal zoiets als "de wrijving kan worden benaderd door een enkele vector" - op dezelfde manier als hoe rigide lichaamsdynamica de interacties van het echte leven simuleert door lichamen aan te nemen met een uniforme dichtheid die niet kunnen vervormen.

Bekijk de videodemo van het eerste artikel in deze serie snel:

De interacties tussen de lichamen zijn behoorlijk interessant en het stuiteren tijdens botsingen voelt realistisch. Zodra de objecten echter op het solide platform landen, tikken ze gewoon een beetje weg en laten ze de randen van het scherm afdrijven. Dit komt door een gebrek aan frictiesimulatie.

Impulsen, nogmaals?

Zoals je uit het eerste artikel in deze serie zou moeten opmaken, een bepaalde waarde, j, vertegenwoordigde de omvang van een impuls die nodig is om de penetratie van twee objecten tijdens een botsing te scheiden. Deze magnitude kan worden aangeduid als jnormal of jN omdat het wordt gebruikt om de snelheid langs de botsingsnorm te wijzigen.

Het opnemen van een frictierespons houdt in het berekenen van een andere magnitude, waarnaar wordt verwezen jtangent of jT. Wrijving zal gemodelleerd worden als een impuls. Deze grootte wijzigt de snelheid van een object langs de negatieve raaklijnvector van de botsing, of met andere woorden langs de wrijvingsvector. In twee dimensies is het oplossen van deze wrijvingsvector een oplosbaar probleem, maar in 3D wordt het probleem veel complexer.

Wrijving is vrij eenvoudig en we kunnen gebruik maken van onze vorige vergelijking voor j, behalve dat we alle instanties van de normale vervangen n met een raakvector t.

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

Vervangen n met t:

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

Hoewel slechts een enkele instantie van n werd vervangen door t in deze vergelijking moeten na het introduceren van rotaties nog enkele exemplaren worden vervangen naast de enige in de teller van vergelijking 2.

Nu de kwestie van hoe te berekenen t ontstaat. De raakvector is een vector loodrecht op de aanvalsnormaal die meer naar de normaal is gekeerd. Dit klinkt misschien verwarrend - maak je geen zorgen, ik heb een diagram!

Hieronder ziet u de raakvector loodrecht op de normaal. De raakvector kan naar links of naar rechts wijzen. Naar links zou "meer weg" zijn van de relatieve snelheid. Het wordt echter gedefinieerd als de loodlijn op de normaal die "meer naar de" relatieve snelheid wijst.


Vectoren van verschillende typen binnen het tijdsbestek van een botsing van onbuigzame lichamen.

Zoals kort gezegd, zal wrijving een vector zijn die tegenover de tangensvector staat. Dit betekent dat de richting waarin wrijving moet worden toegepast direct kan worden berekend, omdat de normale vector werd gevonden tijdens de botsingsdetectie.

Dit wetende, is de raakvector (waar n is de botsing normaal):

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

Het enige dat nog rest om op te lossen jt, de grootte van de wrijving, is om de waarde direct te berekenen met behulp van de bovenstaande vergelijkingen. Er zijn enkele zeer lastige stukken nadat deze waarde is berekend die binnenkort wordt behandeld, dus dit is niet het laatste dat nodig is in onze botsresolver:

 // Bereken relatieve snelheid opnieuw na normale impuls // wordt toegepast (impuls uit eerste artikel, deze code komt // direct daarna in dezelfde oplosfunctie) Vec2 rv = VB - VA // Oplossen voor de raakvector Vec2 raaklijn = rv - Dot (rv, normaal) * normaal tangent.Normalize () // Los op om magnitude toe te passen langs de wrijvingsvector float jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB)

De bovenstaande code volgt vergelijking 2 rechtstreeks. Nogmaals, het is belangrijk om te beseffen dat de wrijvingsvector wijst in de tegenovergestelde richting van onze raakvector, en als zodanig moeten we een negatief teken toepassen wanneer we de relatieve snelheid langs de tangens stipuleren om de relatieve snelheid langs de raakvector op te lossen. Dit negatieve teken draait de tangensnelheid om en wijst plotseling in de richting waarin de wrijving moet worden benaderd als.

De wet van Coulomb

De wet van Coulomb is het deel van de wrijvingssimulatie waar de meeste programmeurs moeite mee hebben. Ik moest zelf nogal wat studeren doen om erachter te komen hoe het op de juiste manier werd gemodelleerd. De truc is dat de wet van Coulomb een ongelijkheid is.

Coulomb-wrijving stelt:

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

Met andere woorden, de wrijvingskracht is altijd kleiner dan of gelijk aan de normaalkracht vermenigvuldigd met een constante μ (waarvan de waarde afhangt van het materiaal van de objecten).

De normale kracht is gewoon ons oude j magnitude vermenigvuldigd met de botsingsnorm. Dus als onze oplossing is opgelost jt (wat de wrijvingskracht weergeeft) is minder dan μ keer de normale kracht, dan kunnen we onze gebruiken jt magnitude als wrijving. Zo niet, dan moeten we onze normale force-tijden gebruiken μ in plaats daarvan. Dit "anders" geval is een vorm van het vastklemmen van onze wrijving onder een maximale waarde, waarbij de maximum de normale krachttijden is μ.

Het hele punt van de wet van Coulomb is om deze klemprocedure uit te voeren. Deze klemming blijkt het moeilijkste deel van de frictiesimulatie te zijn voor op impulsen gebaseerde resolutie om overal documentatie te vinden - tot nu toe, tenminste! De meeste white papers die ik over het onderwerp kon vinden, sloegen de wrijving helemaal over of stopten kort en implementeerden onjuiste (of niet-bestaande) opspanprocedures. Hopelijk heb je nu een waardering voor het begrip dat het belangrijk is om dit deel goed te krijgen.

Laten we gewoon alles in een keer uitklemmen voordat we iets uitleggen. Dit volgende codeblok is het vorige codevoorbeeld met de voltooide klemprocedure en de toepassing van de wrijvingsimpuls samen:

 // Bereken relatieve snelheid opnieuw na normale impuls // wordt toegepast (impuls uit eerste artikel, deze code komt // direct daarna in dezelfde oplosfunctie) Vec2 rv = VB - VA // Oplossen voor de raakvector Vec2 raaklijn = rv - Dot (rv, normaal) * normaal tangent.Normalize () // Los op om magnitude toe te passen langs de wrijvingsvector float jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB) // PythagoreanSolve = A ^ 2 + B ^ 2 = C ^ 2, oplossen voor C gegeven A en B // Gebruik om mu te berekenen gegeven wrijvingscoëfficiënten van elke lichaamsvliegtuig mu = PythagoreanSolve (A-> staticFriction, B-> staticFriction) // Klem de grootte van de wrijving en maak impulsvector Vec2 frictionImpulse if (abs (jt) < j * mu) frictionImpulse = jt * t else  dynamicFriction = PythagoreanSolve( A->dynamicFriction, B-> dynamicFriction) frictionImpulse = -j * t * dynamicFriction // Apply A-> velocity - = (1 / A-> massa) * frictionImpulse B-> velocity + = (1 / B-> massa) * frictionImpulse

Ik besloot om deze formule te gebruiken om de wrijvingscoëfficiënten tussen twee lichamen op te lossen, gegeven een coëfficiënt voor elk lichaam:

\ [Vergelijking 4: \\
Friction = \ sqrt [] Friction ^ 2_A + Friction ^ 2_B \]

Ik zag eigenlijk iemand anders dit doen in hun eigen physics-engine, en ik vond het resultaat goed. Een gemiddelde van de twee waarden zou perfect werken om van het gebruik van vierkantswortel af te komen. Echt, elke vorm van het kiezen van de wrijvingscoëfficiënt zal werken; dit is precies waar ik de voorkeur aan geef. Een andere optie is om een ​​opzoektabel te gebruiken waarbij het type van elke instantie wordt gebruikt als een index in een 2D-tabel.

Het is belangrijk dat de absolute waarde van jt wordt gebruikt in de vergelijking, omdat de vergelijking in theorie ruwe grootheden onder een bepaalde drempelwaarde klemt. Sinds j is altijd positief, het moet worden omgedraaid om een ​​juiste wrijvingsvector weer te geven, in het geval dat dynamische wrijving wordt gebruikt.

Statische en dynamische wrijving

In het laatste codefragment werden statische en dynamische wrijvingen geïntroduceerd zonder enige verklaring! Ik zal deze hele sectie wijden aan het verklaren van het verschil tussen en de noodzaak van deze twee soorten waarden.

Er gebeurt iets interessants met wrijving: het vereist een "activeringsenergie" om voorwerpen te laten bewegen wanneer ze volledig rusten. Wanneer twee objecten in het echte leven op elkaar rusten, kost het een behoorlijke hoeveelheid energie om erop te drukken en het in beweging te krijgen. Zodra je echter iets hebt glijden, is het vaak makkelijker om het te laten glijden.

Dit komt door de manier waarop wrijving werkt op microscopisch niveau. Een andere foto helpt hier:


Microscopisch beeld van wat de energie van activering veroorzaakt door wrijving.

Zoals je kunt zien, zijn de kleine misvormingen tussen de oppervlakken echt de grootste boosdoener die in de eerste plaats wrijving veroorzaakt. Wanneer het ene object op een ander rust, vallen microscopische misvormingen tussen de objecten, in elkaar grijpend. Deze moeten worden verbroken of gescheiden om de objecten tegen elkaar te laten glijden.

We hebben een manier nodig om dit in onze engine te modelleren. Een eenvoudige oplossing is om elk type materiaal te voorzien van twee wrijvingswaarden: één voor statisch en één voor dynamisch.

De statische wrijving wordt gebruikt om onze vast te klemmen jt omvang. Als het is opgelost jt magnitude is laag genoeg (onder onze drempelwaarde), dan kunnen we aannemen dat het object in rust is, of bijna als rust en gebruik het geheel jt als een impuls.

Aan de keerzijde, als onze oplossing is opgelost jt boven de drempelwaarde ligt, kan worden aangenomen dat het object de "activeringsenergie" reeds heeft verbroken, en in een dergelijke situatie wordt een lagere wrijvingsimpuls gebruikt, die wordt weergegeven door een kleinere wrijvingscoëfficiënt en een iets andere impulsberekening.


Tafereel

Ervan uitgaande dat je geen gedeelte van de wrijvingssectie hebt overgeslagen, goed gedaan! Je hebt het moeilijkste deel van deze hele reeks voltooid (naar mijn mening).

De Tafereel class fungeert als een container voor alles met een natuurkundig simulatiescenario. Het roept en gebruikt de resultaten van elke brede fase, bevat alle onbuigzame instanties, voert botsingscontroles uit en roept de resolutie op. Het integreert ook alle levende objecten. De scène werkt ook samen met de gebruiker (zoals in de programmeur die de physics-engine gebruikt).

Hier is een voorbeeld van hoe een scènestructuur eruit kan zien:

 class Scene public: Scene (Vec2 gravity, real dt); ~ Scene (); void SetGravity (Vec2 gravity) void SetDT (real dt) Body * CreateBody (ShapeInterface * shape, BodyDef def) // Voegt een lichaam in de scène en initialiseert het lichaam (berekent de massa). // void InsertBody (Body * body) // Wist een body uit de scene void RemoveBody (Body * body) // Updates van de scène met een enkele timeestep ongeldig Step (void) float GetDT (void) LinkedList * GetBodyList (leeg) Vec2 GetGravity (void) void QueryAABB (CallBackQuery cb, const AABB en aabb) void QueryPoint (CallBackQuery cb, const Point2 & point) private: float dt // Timestep in seconden float inv_dt // Inverse timestep in sceBonden LinkedList body_list uint32 body_count Vec2 zwaartekracht bool debug_draw BroadPhase broadphase;

Er is niets bijzonders ingewikkeld aan de Tafereel klasse. Het idee is om de gebruiker toe te staan ​​starre lichamen eenvoudig toe te voegen en te verwijderen. De BodyDef is een structuur die alle informatie bevat over een star lichaam, en kan worden gebruikt om de gebruiker toe te staan ​​waarden in te voegen als een soort configuratiestructuur.

De andere belangrijke functie is Stap(). Deze functie voert een enkele ronde van collisionchecks, resolutie en integratie uit. Dit moet worden aangeroepen vanuit de tijdspoorlus die wordt beschreven in het tweede artikel van deze serie.

Als u een punt of AABB zoekt, moet u controleren om te zien welke objecten daadwerkelijk in botsing komen met een aanwijzer of AABB in de scène. Dit maakt het gemakkelijk voor gameplay-gerelateerde logica om te zien hoe dingen in de wereld worden geplaatst.


Jump-tafel

We hebben een eenvoudige manier nodig om uit te zoeken welke botsfunctie moet worden aangeroepen, op basis van het type van twee verschillende objecten.

In C ++ zijn er twee belangrijke manieren die ik ken: dubbele verzending en een 2D-springtabel. In mijn eigen persoonlijke tests vond ik de 2D-springtafel superieur, dus ik zal in detail gaan over hoe je dat kunt implementeren. Als u van plan bent om een ​​andere taal dan C of C ++ te gebruiken, weet ik zeker dat een reeks functies of functorobjecten op dezelfde manier kunnen worden geconstrueerd als een tabel met functie-aanwijzers (wat ook een reden is om te praten over springtabellen in plaats van andere opties) die meer specifiek zijn voor C ++).

Een springtafel in C of C ++ is een tabel met functie-aanwijzers. Indices die willekeurige namen of constanten vertegenwoordigen, worden gebruikt om in de tabel te indexeren en een specifieke functie aan te roepen. Het gebruik kan er ongeveer zo uitzien voor een 1D springtafel:

 enum Animal Rabbit Duck Lion; const ongeldig (* talk) (void) [] = RabbitTalk, DuckTalk, LionTalk,; // Roep een functie uit de tabel met 1D virtuele berichtoproep [Rabbit] () // roept de RabbitTalk-functie op

De bovenstaande code bootst eigenlijk na waar de C ++ -taal zelf mee werkt virtuele functieaanroepen en overerving. C ++ implementeert echter alleen eendimensionale virtuele oproepen. Een 2D-tafel kan met de hand worden gemaakt.

Hier is een psuedocode voor een 2D-springtabel om botsroutines aan te roepen:

 collisionCallbackArray = AABBvsAABB AABBvsCircle CirclevsAABB CirclevsCircle // Roep een collsion-routine op voor botsingsdetectie tussen A en B // twee colliders zonder het exacte collider-type te kennen // type kan van AABB of Circle collisionCallbackArray zijn [A-> type] [B -> type] (A, B)

En daar hebben we het! De werkelijke typen van elke collider kunnen worden gebruikt om te indexeren in een 2D-array en een functie kiezen om een ​​botsing op te lossen.

Merk echter op dat AABBvsCircle en CirclevsAABB zijn bijna duplicaten. Dit is noodzakelijk! De normale moet worden omgedraaid voor een van deze twee functies, en dat is het enige verschil tussen beide. Dit maakt een consistente collision resolution mogelijk, ongeacht de combinatie van op te lossen objecten.


Conclusie

Inmiddels hebben we een groot aantal onderwerpen behandeld bij het helemaal opnieuw creëren van een aangepaste rigide body physics-engine! Botsingsresolutie, wrijving en motorarchitectuur zijn allemaal onderwerpen die tot nu toe zijn behandeld. Een volledig succesvolle physics-engine die geschikt is voor vele tweedimensionale games op productieniveau, kan worden opgebouwd met de kennis die tot nu toe in deze serie is gepresenteerd.

Vooruitblikkend naar de toekomst, ben ik van plan nog een artikel te schrijven dat volledig gewijd is aan een zeer gewenste eigenschap: rotatie en oriëntatie. Georiënteerde objecten zijn buitengewoon aantrekkelijk om met elkaar te kijken en zijn het laatste stuk dat onze engine voor aangepaste fysica vereist.

Rotatieresolutie blijkt vrij eenvoudig te zijn, hoewel botsingsdetectie een hit in complexiteit heeft. Veel succes tot de volgende keer, en stel alsjeblieft vragen of opmerkingen hieronder!