Abilitatea de a împărți dinamic o formă convexă în două forme separate este o abilitate sau un instrument foarte valoroasă de a avea în arsenalul gamedev. Această divizare permite tipuri de simulare avansate, cum ar fi partiții binare spațiale pentru grafică sau fizică, medii distructive dinamice (fracturarea fragilă!), Simularea oceanelor sau a apei, rezoluția de coliziune pentru motoarele de fizică, partajarea spațială binară și lista continuă. Un exemplu foarte bun este jocul Fruit Ninja pentru Kinect.
Ce inseamna exact sa imparti o forma convexa? În două dimensiuni, ne referim la o formă ca a poligon; în trei dimensiuni, ne referim la o formă ca a poliedru. (poliedre este cuvântul folosit pentru a face referire la mai mult de un poliedru.)
Bacsis: În general, poligoanele convexe și polyhedra simplifică multe aspecte ale manipulării și gestionării volumelor sau rețelelor. Datorită acestei simplificări, întregul articol presupune poligoane convexe și polyhedra convexe. Formele concave nu sunt în afara oricărei discuții aici. În general, formele concave convexe sunt simulate ca o colecție de forme convexe îmbinate.
Pentru a înțelege ideile prezentate în acest articol, veți avea nevoie de cunoștințe de lucru despre un anumit limbaj de programare și de o înțelegere simplă a produsului dot.
O modalitate foarte bună de a împărți formele în ambele două și trei dimensiuni este să folosiți rutina de tăiere Sutherland-Hodgman. Această rutină este destul de simplă și foarte eficientă și poate fi extinsă cu totul ușor pentru a ține cont de robustețea numerică. Dacă nu sunteți familiarizat cu algoritmul, consultați articolul meu anterior despre acest subiect.
O înțelegere a avioanelor în două sau trei dimensiuni este, de asemenea, o necesitate. Trebuie remarcat faptul că un plan bidimensional ar putea fi considerat o proiecție a unui plan tridimensional în două dimensiuni - sau, cu alte cuvinte, o linie.
Vă rugăm să înțelegeți că un avion poate fi considerat, de asemenea, ca o jumătate de spațiu. Calculul distanței sau intersecției punctelor la jumătăți de spații este o cerință preliminară cerută: consultați ultima secțiune a Cum se creează un motor personalizat pentru fizica 2D: motorul central pentru informații despre acesta.
Consultați sursa de demonstrație (de asemenea, pe Bitbucket) pe care am creat-o când citiți acest tutorial. Am folosit această demonstrație pentru crearea tuturor imaginilor GIF din articol. Codul sursă este în C ++ (și ar trebui să fie compatibil cu platformele), dar este scris într-un mod care poate fi ușor ported la orice limbaj de programare.
Înainte de a aborda problema împărțirii unui întreg poligon, să aruncăm o privire la problema împărțirii unui triunghi printr-un plan de tăiere. Aceasta va constitui baza înțelegerii pentru a trece la o soluție robustă și generalizată.
Lucru frumos despre împărțirea formei este că, adesea, algoritmii în 2D pot fi extinși fără prea multe probleme direct în 3D. Aici voi prezenta un algoritm simplu de triunghi de divizare pentru ambele două și trei dimensiuni.
Atunci când un triunghi se intersectează cu un plan de divizare, ar trebui generate trei triunghiuri noi. Iată o imagine care arată un triunghi înainte și după divizarea de-a lungul unui avion:
Având în vedere un plan de despicare, trei triunghiuri noi sunt extrase în timpul operației de tăiere. Să aruncăm un cod în aer liber, presupunând că cele trei noduri ale unui triunghi sunt a, b, c
în ordine în sens invers acelor de ceasornic (CCW), și că știm că marginea ab
(marginea vârfurilor a la b) au intersectat planul de despicare:
// Clip un triunghi impotriva unui avion care stie ca // a la b traverseaza planul de tuns // Referinta: Exact Bouyancy pentru Polyhedra de // Erin Catto in Programarea Jocurilor Gems 6 void SliceTriangle (std :: vector & out, const Vec2 & a, // Primul punct pe triunghi, comanda CCW const Vec2 & b, // Al doilea punct pe triunghi, comanda CCW const Vec2 & c, // Al treilea punct al triunghiului, ordine CCW f32 d1, // Distanta punctului a la planul de despicare f32 d2 , // Distanța de la punctul b la planul de despicare f32 d3 // Distanța dintre punctul c și planul de divizare) // Calculați punctul de intersecție de la a la b Vec2 ab = a + (d1 / (d1 - d2) (b-a); Triunghi tri; în cazul în care (d1 < 0.0f) // b to c crosses the clipping plane if(d3 < 0.0f) // Calculate intersection point from b to c Vec2 bc = b + (d2 / (d2 - d3)) * (c - b); tri.Set( b, bc, ab ); out.push_back( tri ); tri.Set( bc, c, a ); out.push_back( tri ); tri.Set( ab, bc, a ); out.push_back( tri ); // c to a crosses the clipping plane else // Calculate intersection point from a to c Vec2 ac = a + (d1 / (d1 - d3)) * (c - a); tri.Set( a, ab, ac ); out.push_back( tri ); tri.Set( ab, b, c ); out.push_back( tri ); tri.Set( ac, ab, c ); out.push_back( tri ); else // c to a crosses the clipping plane if(d3 < 0.0f) // Calculate intersection point from a to c Vec2 ac = a + (d1 / (d1 - d3)) * (c - a); tri.Set( a, ab, ac ); out.push_back( tri ); tri.Set( ac, ab, b ); out.push_back( tri ); tri.Set( b, c, ac ); out.push_back( tri ); // b to c crosses the clipping plane else // Calculate intersection point from b to c Vec2 bc = b + (d2 / (d2 - d3)) * (c - b); tri.Set( b, bc, ab ); out.push_back( tri ); tri.Set( a, ab, bc ); out.push_back( tri ); tri.Set( c, a, bc ); out.push_back( tri );
Sperăm că codul de mai sus te-a speriat puțin. Dar nu te teme; tot ceea ce facem aici este calculul unor puncte de intersecție pentru a ști cum să generăm trei noi triunghiuri. Dacă cineva ar fi examinat cu atenție imaginea anterioară, locațiile punctelor de intersecție ar putea fi evidente: acestea sunt locul în care linia punctată corespunde planului de despicare și unde muchia ab
intersectează planul de despicare. Iată o diagramă pentru comoditate:
Din această diagramă, este ușor de văzut că triunghiurile de ieșire trebuie să conțină vârfurile a, ac, ab
, ac, c, b
, și ab, ac, b
. (Dar nu neapărat în acest format exact, de exemplu, a, b, c
ar fi același triunghi ca c, b, a
deoarece vârfurile au fost pur și simplu deplasate spre dreapta.)
Pentru a determina care noduri contribuie la care dintre cele trei triunghiuri noi, trebuie să determinăm dacă vertexul A
și vârful c
se află deasupra sau dedesubtul planului. Deoarece presupunem că marginea ab
este cunoscut faptul că se intersectează, putem deduce implicit acest lucru b
este pe partea opusă a planului de tăiere de la A
.
Dacă convenția unei distanțe negative față de planul de despicare înseamnă penetrant în avion, putem formula un predicat pentru a determina dacă un punct intersectează un halfspace: #define HitHalfspace (distanța) (distanța) < 0.0f)
. Acest predicat este folosit în cadrul fiecăruia dacă declarație pentru a verifica și a vedea dacă un punct se află în jumătatea spațiului planului de tăiere.
Există patru cazuri care există de combinații de A
și b
lovind jumătatea spațiului planului de tăiere:
a | T T F F ----------- b | T F T F
Deoarece funcția noastră necesită ambele A
și b
să fie pe laturile opuse ale planului, două dintre aceste cazuri sunt scăpate. Tot ce rămâne este să vezi de ce parte c
stabilește. De aici se cunoaște orientarea celor trei vârfuri; punctele de intersecție și vârfurile de ieșire pot fi calculate direct.
Pentru a utiliza SliceTriangle ()
trebuie să găsim o muchie intersectantă a unui triunghi. Implementarea de mai jos este eficientă și poate fi folosită pe toate triunghiurile din simulare pentru a fi împărțită potențial:
// Se taie toate triunghiurile date unui vector de triunghiuri. // Se generează o nouă listă de triunghiuri de ieșire. Vechea // lista de triunghiuri este aruncată. // n - normalul planului de tăiere // d - distanța planului tăiere de la origine // Referință: Exact Bouyancy pentru Polyhedra de // Erin Catto în programarea jocurilor Gems 6 void SliceAllTriangles (const Vec2 & n, f32 d) std :: vector out; pentru (uint32 i = 0; i < g_tris.size( ); ++i) // Grab a triangle from the global triangle list Triangle tri = g_tris[i]; // Compute distance of each triangle vertex to the clipping plane f32 d1 = Dot( tri.a, n ) - d; f32 d2 = Dot( tri.b, n ) - d; f32 d3 = Dot( tri.c, n ) - d; // a to b crosses the clipping plane if(d1 * d2 < 0.0f) SliceTriangle( out, tri.a, tri.b, tri.c, d1, d2, d3 ); // a to c crosses the clipping plane else if(d1 * d3 < 0.0f) SliceTriangle( out, tri.c, tri.a, tri.b, d3, d1, d2 ); // b to c crosses the clipping plane else if(d2 * d3 < 0.0f) SliceTriangle( out, tri.b, tri.c, tri.a, d2, d3, d1 ); // No clipping plane intersection; keep the whole triangle else out.push_back( tri ); g_tris = out;
După calcularea distanței semnate a fiecărui vârf al triunghiului la planul de divizare, multiplicarea poate fi utilizată pentru a vedea dacă două puncte distincte se află pe laturile opuse ale unui plan. Întrucât distanțele vor avea un flotant pozitiv și negativ într-o pereche, produsul celor două înmulțite trebuie să fie negativ. Aceasta permite folosirea unui predicat simplu pentru a vedea dacă două puncte se află pe ambele părți ale unui avion: #define OnOppositeSides (distanceA, distanceB) ((distanceA) * (distanceB) < 0.0f)
.
O singura data orice se pare că marginea se intersectează cu planul de despicare, vârfurile triunghiului sunt redenumite și deplasate și imediat trecu de-a lungul interiorului SliceTriangle
funcţie. În acest fel, prima margine intersectată găsită este redenumită ab
.
Iata ce poate arata un produs final:
Împărțirea triunghiurilor în acest mod explică în mod independent fiecare triunghi, iar algoritmul prezentat aici se extinde, fără nici o autorizare suplimentară, de la două la trei dimensiuni. Această formă de tăiere triunghiulară este ideală atunci când nu este necesară informația de apropiere a triunghiurilor sau când triunghiurile tăiate nu sunt stocate nicăieri în memorie. Acesta este adesea cazul atunci când se calculează volume de intersecții, ca în simularea flotabilității.
Singura problemă cu separarea triunghiurilor în izolare este aceea că nu există informații despre triunghiurile care sunt adiacent unul celuilalt. Dacă examinați cu atenție GIF de mai sus, puteți observa că multe triunghiuri împărtășesc vârfuri colineare și, ca rezultat, pot fi prăbușite într-un singur triunghi pentru a fi redate eficient. Următoarea secțiune a acestui articol abordează această problemă cu un alt algoritm mai complex (care face uz de toate tacticile prezente în această secțiune).
Acum pentru ultimul subiect. Presupunând o înțelegere de lucru a algoritmului Sutherland-Hodgman, este destul de ușor să extindeți această înțelegere pentru a împărți o formă cu un avion și ieșiri pe ambii laturile planului. Robustețea numerică poate fi (și ar trebui) luată în considerare, de asemenea.
Să examinăm pe scurt vechile cazuri de tăiere Sutherland-Hodgman:
// InFront = plan.Distanță (punct)> 0.0f // Behind = plan.Distance (punct) < 0.0f Vec2 p1, p2; ClipPlane plane; case p1 InFront and p2 InFront push p2 case p1 InFront and p2 Behind push intersection case p1 Behind and p2 InFront push intersection push p2
Aceste trei cazuri funcționează decent, dar nu țin cont de grosimea planului de despicare. Ca rezultat, erorile numerice se pot schimba atunci când obiectele se mișcă, provocând o coerență minimă a cadrului temporal. Acest tip de instabilitate numerică poate avea ca rezultat un colț să fie tăiat într-un cadru și nu într-un alt cadru, iar acest tip de jitter poate fi destul de urât vizibil sau inacceptabil pentru simulare fizică.
Un alt beneficiu al acestui test de plan gros este acela că punctele situate foarte aproape de avion pot fi de fapt considerate ca fiind pe planul, ceea ce face geometria tăiată ușor mai utilă. Este posibil ca un punct de intersecție calculat să se situeze numeric pe o porțiune greșită a unui plan! Avioanele groase evită astfel de probleme ciudate.
Folosind planuri groase pentru teste de intersecție, se poate adăuga un nou tip de carcasă: un punct de așezare direct pe pe un avion.
Sutherland-Hodgman ar trebui să fie modificat ca atare (cu un punct plutitor EPSILON
pentru a ține cont de planurile groase):
// InFront = plan.Distanță (punct)> EPSILON // Behind = plane.Distance (punct) < -EPSILON // OnPlane = !InFront( dist ) && !Behind( dist ) Vec2 p1, p2; ClipPlane plane; case p1 InFront and p2 InFront push p2 case p1 InFront and p2 Behind push intersection case p1 Behind and p2 InFront push intersection push p2 case any p1 and p2 OnPlane push p2
Cu toate acestea, această formă de Sutherland-Hodgman produce doar noduri pe o parte a planului de despicare. Aceste cinci cazuri pot fi ușor extinse la un set de nouă la vârfurile de ieșire de pe ambele părți ale unui plan de despicare:
// InFront = plan.Distanță (punct)> EPSILON // Behind = plane.Distance (punct) < -EPSILON // OnPlane = !InFront( dist ) && !Behind( dist ) Vec2 p1, p2; Poly front, back; ClipPlane plane; case p1 InFront and p2 InFront front.push( p2 ) case p1 OnPlane and p2 InFront front.push( p2 ) case p1 Behind and p2 InFront front.push( intersection ) front.push( p2 ) back.push( intersection ) case p1 InFront and p2 OnPlane front.push( p2 ) case p1 OnPlane and p2 OnPlane front.push( p2 ) case p1 Behind and p2 OnPlane front.push( p2 ) back.push( p2 ) case p1 InFront and p2 Behind front.push( intersection ) back.push( intersection ) back.push( p2 ) case p1 OnPlane and p2 Behind back.push( p1 ) back.push( p2 ) case p1 Behind and p2 Behind back.push( p2 )
O implementare a acestor nouă cazuri ar putea să arate după cum urmează (derivată din detecția coliziunii în timp real a Ericson):
// Divizează un poligon pe jumătate de-a lungul unui plan de despicare folosind un algoritm de tăiere // apelați tăierea lui Sutherland-Hodgman // Resursă: Pagina 367 din Ericson (Detectarea coliziunii în timp real) void SutherlandHodgman (const Vec2 & n, f32 d, const Poly * poly, std :: vector * afară) Poly frontPoly; Poly backPoly; uint32 s = poly-> vertices.size (); Vec2 a = poly-> noduri [s - 1]; f32 da = Dot (n, a) - d; pentru (uint32 i = 0; i < s; ++i) Vec2 b = poly->nodurile [i]; f32 db = Dot (n, b) -d; dacă (InFront (b)) if (În spatele (a)) Vec2 i = Intersect (b, a, db, da); frontPoly.versions.push_back (i); backPoly.versions.push_back (i); frontPoly.versions.push_back (b); altfel dacă (În spatele (b)) if (InFront (a)) Vec2 i = Intersectează (a, b, da, db); frontPoly.versions.push_back (i); backPoly.versions.push_back (i); altceva dacă (On (a)) backPoly.vertions.push_back (a); backPoly.versions.push_back (b); altceva frontPoly.versions.push_back (b); dacă (On (a)) backPoly.versions.push_back (b); a = b; da = db; dacă (frontPoly.versions.size ()) out-> push_back (frontPoly); dacă (backPoly.versions.size ()) out-> push_back (backPoly);
Iată un exemplu de Sutherland-Hodgman în acțiune:
Merită remarcat faptul că poligoanele finale pot fi interpretate ca o listă de vârfuri cu formatul unui ventilator triunghiular.
Există o problemă pe care ar trebui să o cunoașteți: când se calculează un punct de intersecție ab
și un plan de despicare, acest punct suferă cuantificarea numerică. Aceasta înseamnă că orice valoare de intersecție este o aproximare a valorii reale a intersecției. De asemenea, înseamnă că punctul de intersecție ba
nu este numeric același; micul drift numeric va duce de fapt la două valori diferite!
O rutină de tăiere naivă poate face o mare greșeală de a calcula orbește punctele de intersecție orbește, ceea ce poate duce la joncțiuni T sau alte decalaje în geometrie. Pentru a evita o astfel de problemă, trebuie utilizată o orientare consecventă a intersecției. Prin convenție, punctele ar trebui tăiate dintr-o parte a unui avion în altul. Această ordonare strictă a intersecțiilor asigură calcularea aceluiași punct de intersecție și va rezolva lacunele potențiale în geometrie (după cum se arată în imaginea de mai sus, există un rezultat consecvent de tăiere în dreapta).
Pentru a face efectiv texturile peste forme împărțite (poate atunci când se sparte sprite), veți avea nevoie de o înțelegere a coordonatelor UV. O discuție completă a coordonatelor UV și a cartografierii texturilor este mult dincolo de scopul acestui articol, dar dacă aveți deja această înțelegere, ar trebui să fie ușor să transformați punctele de intersecție în spațiul UV.
Vă rugăm să înțelegeți că o transformare din spațiul mondial în spațiul UV necesită o schimbare a transformării de bază. Voi lăsa transformările UV ca un exercițiu pentru cititor!
În acest tutorial, am analizat câteva tehnici simple de algebră liniară pentru a rezolva problema divizării dinamice a formelor generice. Am abordat, de asemenea, unele probleme de robustețe numerică. Acum ar trebui să puteți implementa propriul cod de împărțire a formei sau să utilizați sursa demonstrativă pentru a obține multe efecte sau caracteristici avansate și interesante pentru programarea generală a jocurilor.