Cum se creează un motor de fizică 2D personalizat Corpuri rigide orientate

Până acum, am acoperit rezoluția impulsului, arhitectura de bază și fricțiunea. În acest tutorial final din această serie, vom trece peste un subiect foarte interesant: orientare.

În acest articol vom discuta următoarele subiecte:

  • Matematică de rotație
  • Forme orientate
  • Detectarea coliziunii
  • Rezolvarea coliziunilor

Am recomandat să citiți ultimele trei articole din serie înainte de a încerca să abordați acest lucru. O mare parte din informațiile cheie din articolele anterioare sunt premise pentru restul acestui articol.


Cod simplu

Am creat un mic motor de eșantion în C ++ și vă recomand să răsfoiți și să vă referiți la codul sursă pe parcursul citirii acestui articol, deoarece multe detalii practice de implementare nu s-au încadrat în articolul în sine.


Acest replică GitHub conține motorul propriu-zis, împreună cu un proiect Visual Studio 2010. GitHub vă permite să vizualizați sursa fără a fi nevoie să descărcați sursa în sine, pentru comoditate.

postări asemănatoare
  • Philip Diffenderfer a facut ca repo sa creeze si o versiune Java a motorului!

Orientare matematică

Matematica care implică rotații în 2D este destul de simplă, deși o stăpânire a subiectului va fi necesară pentru a crea ceva de valoare într-un motor de fizică. Al doilea lege al lui Newton prevede:

\ [Ecuația \: 1: \\
F = ma \]

Există o ecuație similară care se referă în mod special la forța unghiulară și accelerația unghiulară. Cu toate acestea, înainte ca aceste ecuații să poată fi afișate, este necesară o descriere rapidă a produsului încrucișat în 2D.

Produs crucificat

Produsul încrucișat în 3D este o operațiune bine cunoscută. Cu toate acestea, produsul încrucișat în 2D poate fi destul de confuz, deoarece nu există o interpretare geometrică solidă.

Produsul încrucișat 2D, spre deosebire de versiunea 3D, nu returnează un vector, ci un scalar. Această valoare scalară reprezintă de fapt magnitudinea vectorului ortogonal de-a lungul axei z, în cazul în care produsul încrucișat urma să fie efectiv realizat în 3D. Într-un fel, produsul cruce 2D este doar o versiune simplificată a produsului cruce 3D, deoarece este o extensie a matematicii vectoriale 3D.

Dacă acest lucru este confuz, nu vă faceți griji: o înțelegere aprofundată a produsului cruce 2D nu este tot ceea ce este necesar. Doar știm exact cum să efectuăm operația și știm că ordinea operațiilor este importantă: \ (a \ times b \) nu este același cu \ (b \ times a \). Acest articol va folosi în mod greu produsul încrucișat pentru a transforma viteza unghiulară în viteză liniară.

cunoaștere Cum pentru a realiza produsul încrucișat în 2D este foarte important, totuși. Doi vectori pot fi traversați, un scalar poate fi traversat cu un vector și un vector poate fi traversat cu un scalar. Iată operațiile:

 // Două vectori încrucișați returnează un float scalar CrossProduct (const Vec2 & a, const Vec2 & b) returnați a.x * b.y - a.y * b.x;  // Forme mai exotice (dar necesare) ale produsului încrucișat // cu un vector a și scalar s, ambele returnând un vector Vec2 CrossProduct (const Vec2 & a, float s) retur Vec2 (s * ay, -s * ax );  Vec2 CrossProduct (float s, const Vec2 & a) retur Vec2 (-s * a.y, s * a.x); 

Momentul și viteza unghiulară

După cum toți ar trebui să știm din articolele anterioare, această ecuație reprezintă o relație între forța care acționează asupra unui corp cu masa și accelerația acelui corp. Există un analog pentru rotație:

\ [Ecuația \: 2: \\
T = r \: \ ori \: \ omega \]

\ (T \) Cuplu. Cuplul este forța de rotație.

\ (r \) este un vector de la centrul de masă (COM) la un anumit punct de pe un obiect. \ (r \) poate fi considerat ca referindu-se la o "rază" de la COM la un punct. Fiecare punct unic pe un obiect va necesita o valoare diferită \ (r \) pentru a fi reprezentată în Ecuația 2.

\ (\ omega \) se numește "omega" și se referă la viteza de rotație. Această relație va fi utilizată pentru a integra viteza unghiulară a unui corp rigid.

Este important să înțelegem că viteza liniară este viteza COM a unui corp rigid. În articolul precedent, toate obiectele nu aveau componente rotative, deci viteza liniară a COM a fost aceeași viteză pentru toate punctele de pe un corp. Când se introduce orientarea, punctele mai îndepărtate de COM se rotesc mai repede decât cele din apropierea COM. Aceasta înseamnă că avem nevoie de o nouă ecuație pentru a găsi viteza unui punct pe un corp, deoarece corpurile pot acum să se rotească și să traducă în același timp.

Utilizați următoarea ecuație pentru a înțelege relația dintre un punct pe un corp și viteza acelui punct:

\ [Ecuația \: 3: \\
\ omega = r \: \ timp v \]

\ (v \) reprezintă viteza liniară. Pentru a transforma viteza liniară în viteză unghiulară, traversați raza \ (r \) cu \ (v \).

În mod similar, putem rearanja Ecuația 3 pentru a forma o altă versiune:

\ [Ecuația \: 4: \\
v = \ omega \: \ times r \]

Ecuațiile din ultima secțiune sunt destul de puternice numai dacă corpurile rigide au o densitate uniformă. Densitatea non-uniformă face matematica implicată în calcularea oricărui lucru care necesită rotația și comportamentul unui corp rigid mult prea complicat. Mai mult, dacă punctul reprezentând un corp rigid nu este la COM, atunci calculele referitoare la \ (r \) vor fi în întregime goale.

Inerţie

În două dimensiuni, un obiect se rotește în jurul axei imaginare z. Această rotație poate fi destul de dificilă, în funcție de mărimea unui obiect și de cât de departe de COM este masa obiectului. Un cerc cu o masă egală cu o tijă lungă subțire va fi mai ușor de rotit decât tija. Acest factor "dificil de rotit" poate fi considerat drept momentul inerției unui obiect.

Într-un sens, inerția este masa rotativă a unui obiect. Cu cât mai multă inerție are ceva, cu atât este mai greu să o rotești.

Cunoscând acest lucru, s-ar putea stoca inerția unui obiect din corp în același format ca și masa. Ar fi bine să stocați inversul acestei valori de inerție, fiind atenți să nu efectuați o diviziune la zero. Consultați articolele anterioare din această serie pentru mai multe informații despre masa și masa inversă.

Integrare

Fiecare corp rigid va necesita câțiva câmpuri pentru a stoca informațiile rotative. Iată un exemplu rapid de structură care să dețină câteva date suplimentare:

 struct RigidBody Shape * shape // Componente lineare Vec2 position Vec2 viteza float acceleratie // Componente unghiulare flotare orientare // radiate float angularVelocity float torque;

Integrarea vitezei unghiulare și orientarea unui corp sunt foarte asemănătoare cu integrarea vitezei și accelerației. Iată un exemplu de cod rapid pentru a arăta cum se face (notă: detalii despre integrare au fost acoperite într-un articol anterior):

 const Vec2 gravitate (0, -10.0f) viteza + = forta * (1.0f / masa + gravitate) * dt angularVelocitate + = cuplu * (1.0f / momentOfInertia) * pozitie dt + = viteza * dt orient + = angularVelocity * dt

Cu cantitatea mică de informații prezentate până acum, ar trebui să puteți începe rotirea diferitelor lucruri pe ecran fără probleme. Cu doar câteva linii de cod, poate fi construit ceva destul de impresionant, probabil prin aruncarea unei forme în aer în timp ce acesta se rotește în jurul COM deoarece gravitația îl trage în jos pentru a forma o arcadă de călătorie.

Mat22

Orientarea ar trebui să fie păstrată ca o singură valoare a radianului, așa cum se vede mai sus, deși de multe ori utilizarea unei mici matrici de rotație poate fi o alegere mult mai bună pentru anumite forme.

Un exemplu foarte bun este caseta Oriented Bounding Box (OBB). OBB constă dintr-o întindere de lățime și înălțime, ambele reprezentând vectori. Acești doi vectori de măsură pot fi apoi rotiți de o matrice de rotație de două ori pe două pentru a reprezenta axele unui OBB.

Vă sugerez crearea unui Mat22 clasa de matrice care trebuie adăugată la orice bibliotecă de matematică pe care o utilizați. Eu însumi folosesc o mică bibliotecă de matematică personalizată care este ambalată în demo-ul open source. Iată un exemplu despre cum poate arăta un astfel de obiect:

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

Unele operații utile includ: construcție din unghi, construcție din vectori de coloane, transpunere, înmulțire cu Vec2, înmulțiți cu alta Mat22, valoare absolută.

Ultima funcție utilă este de a putea recupera fie X sau y coloană de la un vector. Funcția de coloană ar arăta cam ca:

 Mat22m (PI / 2,0f); Vec2 r = m.ColX (); // extrageți coloana axei x

Această tehnică este utilă pentru extragerea unui vector unic de-a lungul unei axe de rotație, fie X sau y axă. În plus, poate fi construită o matrice de două ori pe doi din doi vectori de unitate ortogonali, deoarece fiecare vector poate fi inserat direct în rânduri. Deși această metodă de construcție este un pic neobișnuită pentru motoarele fizice 2D, poate fi totuși foarte util să se înțeleagă modul în care rotațiile și matricele funcționează în general.

Acest constructor ar putea arata cam ca:

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

Deoarece cea mai importantă operație a matricei de rotație este de a efectua rotații bazate pe un unghi, este important să puteți construi o matrice dintr-un unghi și să multiplicați un vector prin această matrice (pentru a roti vectorul în sens contrar acelor de ceasornic cu unghiul matricea a fost construită cu):

 Mat2 (radiani reale) real c = std :: cos (radiani); adevărat s = std :: sin (radiani); m00 = c; m01 = -s; m10 = s; m11 = c;  // Rotiți un vector const const Vec2 operator * (const Vec2 & rhs) const întoarcere Vec2 (m00 * rhs.x + m01 * rhs.y, m10 * rhs.x + m11 * rhs.y); 

Din motive de scurtă durată, nu voi determina de ce matricea de rotație în sens invers acelor de ceasornic are forma:

 a = unghiul cos (a), -sin (a) sin (a), cos (a)

Cu toate acestea, este important să știți cel puțin că aceasta este forma matricei de rotație. Pentru mai multe informații despre matricele de rotație, consultați pagina Wikipedia.

postări asemănatoare
  • Să construim un motor grafic 3D: transformări liniare

Transformarea la o bază

Este important să înțelegem diferența dintre model și spațiul mondial. Spațiul model este sistemul de coordonate local pentru o formă de fizică. Originea se află la COM, iar orientarea sistemului de coordonate este aliniată cu axele formei însăși.

Pentru a transforma o formă în spațiul mondial, trebuie să fie rotită și tradusă. Rotația trebuie să aibă loc mai întâi, deoarece rotația se efectuează întotdeauna cu privire la origine. Deoarece obiectul se află în spațiul modelului (originea la COM), rotația se va roti în jurul formei COM a formei. Rotația ar avea loc cu un a Mat22 matrice. În codul de probă, matricele de orientare sunt de nume u.

După efectuarea rotației, obiectul poate fi apoi tradus în poziția sa în lume prin adăugarea de vectori.

Odată ce un obiect este în spațiul mondial, acesta poate fi apoi tradus în spațiul model al unui obiect complet diferit, folosind transformări inverse. Rotația inversă urmată de traducerea inversă este folosită pentru a face acest lucru. Acesta este cât de mult matematica este simplificată în timpul detectării coliziunilor!

Transformarea inversă (de la stânga la dreapta) din spațiul mondial pentru modelul spațiului poligonului roșu.

După cum se vede în imaginea de mai sus, dacă transformarea inversă a obiectului roșu este aplicată poligonului roșu și cel albastru, atunci un test de detecție a coliziunii poate fi redus la forma unui test AABB vs OBB, în loc de a calcula matematica complexă între două orientate.

În mare parte din codul sursă eșantion, vârfurile sunt transformate în mod constant de la model la lume și înapoi la model, pentru tot felul de motive. Ar trebui să aveți o înțelegere clară a ceea ce înseamnă acest lucru pentru a înțelege codul de detectare a coliziunii.


Detectarea coliziunii și generarea galeriilor

În această secțiune, voi prezenta schițe rapide ale coliziunilor poligonale și ale cercurilor. Consultați codul sursă de probă pentru detalii detaliate privind implementarea.

Poligonul către poligon

Să începem cu cea mai complexă rutină de detectare a coliziunilor din întreaga serie de articole. Ideea de a verifica coliziunea dintre două poligoane este cel mai bine realizată (după părerea mea) cu Teorema Axei de Separare (SAT).

Cu toate acestea, în loc de a proiecta extensiile fiecărui poligon unul pe celălalt, există o metodă ușor mai nouă și mai eficientă, după cum a subliniat Dirk Gregorius în cadrul conferinței sale din 2013 GDC (diapozitive disponibile aici gratuit).

Primul lucru care trebuie învățat este conceptul de puncte de sprijin.

Puncte de sprijin

Punctul de susținere al unui poligon este vârful care este cel mai îndepărtat de-a lungul unei direcții date. Dacă două vârfuri au distanțe egale de-a lungul direcției date, fie una este acceptabilă.

Pentru a calcula un punct de sprijin, produsul dot trebuie folosit pentru a găsi o distanță semnalizată de-a lungul unei direcții date. Din moment ce acest lucru este foarte simplu, voi arăta un exemplu rapid în acest articol:

 // Punctul extrem de-a lungul unei direcții în interiorul unui poligon Vec2 GetSupport (const Vec2 & dir) real bestProjection = -FLT_MAX; Vec2 bestVertex; pentru (uint32 i = 0; i < m_vertexCount; ++i)  Vec2 v = m_vertices[i]; real projection = Dot( v, dir ); if(projection > bestProjection) bestVertex = v; bestProjection = proiecție;  return bestVertex; 

Produsul dot este utilizat pe fiecare vârf. Produsul punct reprezintă o distanță semnalizată într-o anumită direcție, astfel încât vârful cu cea mai mare distanță proiectată ar fi vârful care va reveni. Această operație este efectuată în spațiul model al poligonului dat în cadrul motorului de eșantionare.

Găsirea axei de separare

Prin utilizarea conceptului de puncte de sprijin, o căutare a axei de separare poate fi efectuată între două poligoane (poligonul A și poligonul B). Ideea acestei căutări este de a buclă de-a lungul tuturor fețelor poligonului A și de a găsi punctul de susținere în negativul normal față de acea față.

În imaginea de mai sus sunt afișate două puncte de sprijin: câte unul pe fiecare obiect. Normala albastră ar corespunde punctului de sprijin pe celălalt poligon ca vârful cel mai îndepărtat de-a lungul direcției opuse celui normal albastru. În mod similar, normalul roșu va fi folosit pentru a găsi punctul de sprijin situat la capătul săgeții roșii.

Distanța de la fiecare punct de susținere la fața curentă ar fi semnalul de penetrare. Prin stocarea celei mai mari distanțe se poate înregistra o posibilă axă minimă de penetrare.

Iată o funcție de exemplu din codul sursă eșantion care găsește axa posibilă de penetrare minimă folosind GetSupport funcţie:

 reală FindAxisLeastPenetration (uint32 * faceIndex, PolygonShape * A, PolygonShape * B) real bestDistance = -FLT_MAX; uint32 bestIndex; pentru (uint32 i = 0; i < A->m_vertexCount; ++ i) // Returna unei fețe normală de la A Vec2 n = A-> m_normals [i]; // Returnați punctul de sprijin de la B de-n Vec2 s = B-> GetSupport (-n); // Returnați vârful pe fața de la A, transformați-vă în spațiul modelului // B Vec2 v = A-> m_vertices [i]; // Calculați distanța de penetrare (în spațiul modelului B) real d = Dot (n, s - v); // Stocați distanța cea mai mare dacă (d> bestDistance) bestDistance = d; bestIndex = i;  * faceIndex = bestIndex; retur de bestDistance; 

Deoarece această funcție returnează cea mai mare penetrare, dacă această pătrundere este pozitivă, înseamnă că cele două forme nu se suprapun (penetrarea negativă nu ar însemna o axă separatoare).

Această funcție va trebui apelată de două ori, aruncând obiectele A și B la fiecare apel.

Clipping Incident și Face de referință

De aici trebuie identificată fața incidentului și de referință, iar fața incidentului trebuie tăiată în planurile laterale ale feței de referință. Aceasta este o operație destul de non-trivială, deși Erin Catto (creatorul Box2D și toată fizica utilizată în prezent de Blizzard) a creat câteva diapozitive grozave care acoperă acest subiect în detaliu.

Această tăiere va genera două puncte potențiale de contact. Toate punctele de contact din spatele feței de referință pot fi considerate puncte de contact.

Dincolo de diapozitivele lui Erin Catto, motorul eșantion are și rutinele de tăiere implementate ca exemplu.

Cerc pentru Poligon

Rutina de coliziune a cercului vs. poligon este destul de simplă decât detecția coliziunii poligonale vs. poligon. Mai întâi, cea mai apropiată față a poligonului până la centrul cercului se calculează în mod similar cu utilizarea punctelor de susținere din secțiunea anterioară: prin looparea peste fiecare față normală a poligonului și găsirea distanței de la centrul cercului până la fața.

Dacă centrul cercului se află în spatele acestei cele mai apropiate fețe, se pot genera informații de contact specifice, iar rutina se poate termina imediat.

După ce se identifică fața cea mai apropiată, testul devine într-un segment de linie / test de cerc. Un segment de linie are trei regiuni interesante numite Voronoi regiuni. Examinați următoarea diagramă:

Voronoi regiuni ale unui segment de linie.

Intuitiv, în funcție de locul unde se află centrul cercului, pot fi obținute informații de contact diferite. Imaginați-vă că centrul cercului este situat pe o regiune de vârf. Aceasta înseamnă că punctul cel mai apropiat de centrul cercului va fi un vârf de margine, iar coliziunea corectă normală va fi un vector de la acest vârf la centrul cercului.

Dacă cercul se află în regiunea feței, atunci cel mai apropiat punct al segmentului la centrul cercului va fi proiectul centrului cercului pe segment. Coliziunea normală va fi doar fața normală.

Pentru a calcula regiunea Voronoi în care se află cercul, vom folosi produsul punct între câteva vertebre. Ideea este de a crea un triunghi imaginar și de a verifica dacă unghiul colțului construit cu vârful segmentului este peste sau sub 90 de grade. Un triunghi este creat pentru fiecare vârf al segmentului de linie.

Proiectarea vectorului de la vârful muchiei până la centrul cercului pe margine.

O valoare de peste 90 de grade va însemna că o regiune de margine a fost identificată. Dacă unghiurile vârfurilor triunghiului nu sunt mai mari de 90 de grade, atunci centrul cercului trebuie să fie proiectat pe segmentul însuși pentru a genera informații multiple. Așa cum se vede în imaginea de mai sus, dacă vectorul de la vârful muchiei până la centrul cercului punctat cu vectorul de margine însăși este negativ, atunci este cunoscută regiunea Voronoi.

Din fericire, produsul dot poate fi folosit pentru a calcula o proiecție semnată, iar acest semn va fi negativ dacă este peste 90 de grade și pozitiv dacă este mai jos.


Rezoluția de coliziune

Este din nou timpul: vom reveni la codul de rezoluție a impulsului pentru a treia și ultima oară. Până acum, ar trebui să fii complet confortabil scriind propriul cod de rezoluție care calculează impulsurile de rezoluție împreună cu impulsurile de frecare și poate, de asemenea, realiza proiecție liniară pentru a rezolva pătrunderea rămasă.

Componentele rotative trebuie să fie adăugate la rezoluția de frecare și de penetrare. Unele energii vor fi plasate în viteză unghiulară.

Iată rezoluția impulsului pe care l-am lăsat din articolul precedent despre frecare:

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

Dacă aruncăm componente rotative, ecuația finală arată astfel:

\ [Ecuația 6: \\
j = \ frac - (1 + e) ​​(V ^ A - V ^ B) * t) \ frac 1 mass ^ A ^ F r ^ A \ ori t) ^ 2 I ^ A + \ frac I ^ B
\]

În ecuația de mai sus, \ (r \) este din nou o "rază", ca într-un vector din COM a unui obiect până la punctul de contact. O derivare mai aprofundată a acestei ecuații poate fi găsită pe site-ul lui Chris Hecker.

Este important să ne dăm seama că viteza unui punct dat pe un obiect este:

\ [Ecuația 7: \\
V '= V + \ omega \ ori r
\]

Aplicarea impulsurilor se modifică ușor pentru a se lua în considerare termenii de rotație:

 void Body :: ApplyImpulse (const Vec2 și impuls, const Vec2 & contactVector) velocity + = 1.0f / mass * impuls; angularVelocitate + = 1.0f / inerție * Cruce (contactVector, impuls); 

Concluzie

Acest lucru încheie articolul final al acestei serii. Până acum au fost acoperite câteva subiecte, inclusiv rezoluția bazată pe impulsuri, generarea generațiilor, frecarea și orientarea, toate în două dimensiuni.

Dacă ați reușit acest lucru, trebuie să vă felicit! Programarea motorului fizic pentru jocuri este un domeniu extrem de dificil de studiu. Doresc tuturor cititorilor noroc și, din nou, vă rugăm să vă simțiți liberi să comentați sau să adresați întrebări de mai jos.