[XNA] Picking, czyli zaznaczanie modeli
Tak, jak wspominałem w ostatnim wpisie, kontynuujemy dzisiaj zmagania z interakcją Gracza z modelami znajdującymi się na scenie. Najpierw zamkniemy każdy z nich w „bańce”, którą następnie będziemy nękać promieniami (ang. rays) i domagać się odpowiedzi na najważniejsze z pytań: „Było przecięcie, czy nie?” (co najlepsze, odpowiedzią może być 42 :)).
Zaznaczanie, czyli kolizja
Zaznaczanie można zrealizować na kilka sposobów. Można np. rysować każdy z obiektów sceny obiekty unikalnym kolorem do bufora. Potem po pobraniu koloru z interesującego nas miejsca możemy stwierdzić który obiekt został „trafiony”. Tu jednak skupimy się na innym podejściu.
Ideą będzie raycasting, czyli rzucanie promienia. Nasza kamera ma dwie interesujące nas właściwości: położenie i kierunek patrzenia. Chcemy stworzyć promień, który wychodzi z punktu położenia kamery, podąża wzdłuż wektora kierunku patrzenia i sprawdza kolizje z kolejnymi obiektami na scenie. W zależności od potrzeb, będzie nas interesował tylko pierwszy obiekt „uderzony” przez promień lub też cała ich kolekcja. Jednak to już bardziej odległa sprawa, na razie zacznijmy od tego, od czego zaczynać się powinno, czyli od początku.
Tworzenie promienia
XNA (jak zwykle) załatwia większą część pracy za nas, dostarczając nieocenionej metody Vector3 Viewport.Unproject(Vector3 source, Matrix projection, Matrix view, Matrix world);
Odwraca ona ciąg przekształceń, który przechodzi każdy punkt sceny; ten ciąg to:
- przemnożenie przez macierz świata (przekształcenie do przestrzeni świata)
- przemnożenie przez macierz widoku (przekształcenie do przestrzeni kamery)
- przemnożenie przez macierz projekcji (rzutowanie na ekran).
W przypadku Unproject
, podając punkt na ekranie (we współrzędnych ekranowych) otrzymujemy wektor w przestrzeni świata, krótko mówiąc jest to zamiana współrzędnych 2D na 3D. Aby wyznaczyć kierunek potrzebujemy dwóch takich punktów (trzeba jednocześnie wiedzieć, który z nich jest „bliżej” a który „dalej” jeszcze przed przekształceniem. Stwórzmy więc takie wektory, wyłuskajmy z nich ich pozycje 3D i stwórzmy wektor kierunku patrzenia kamery.
private Ray CalculateCursorRay(float X, float Y, Matrix world, Matrix view, Matrix projection) { Vector3 nearSource = new Vector3(X, Y, 0f); Vector3 farSource = new Vector3(X, Y, 1f); Vector3 nearPoint = GraphicsDevice.Viewport.Unproject(nearSource, projection, view, world); Vector3 farPoint = GraphicsDevice.Viewport.Unproject(farSource, projection, view, world); Vector3 direction = farPoint - nearPoint; direction.Normalize(); return new Ray(nearPoint, direction); }
Klasa Ray
, obiektem której jest wynik działania powyższej metody, jest również częścią biblioteki XNA.
Model w kuli, czyli BoundingSphere
Po załadowaniu modelu z pliku (przy użyciu Content.Load()
) dla każdej siatki (mesh’a) liczona jest tzw. BoundingSphere
. Jak sama nazwa klasy wskazuje, przechowuje ona sferę otaczającą całą siatkę. Jest to podstawowy etap optymalizacji zagadnienia zaznaczania.
Dla każdej siatki sfera otaczająca przechowywana jest w tzw. Mesh space (pozycja względem odpowiedniej siatki), czyli należy przekształcić jej środek i promień przez odpowiednią kość (and. bone, więcej o tym w notce o modelach). Rozbudujmy klasę MyModel o metodę za to odpowiedzialną:
private static BoundingSphere TransformBoundingSphere(BoundingSphere sphere, Matrix transform) { BoundingSphere transformedSphere; Vector3 scale3 = new Vector3(sphere.Radius, sphere.Radius, sphere.Radius); // transform, do not include translation scale3 = Vector3.TransformNormal(scale3, transform); // choose maximal scale to fit whole mesh inside sphere transformedSphere.Radius = Math.Max(scale3.X, Math.Max(scale3.Y, scale3.Z)); transformedSphere.Center = Vector3.Transform(sphere.Center, transform); return transformedSphere; }
Skoro przekształcenia BoundingSphere
mamy gotowe, możemy zająć się clue całego przedsięwzięcia, czyli faktycznym sprawdzeniem kolizji promienia z kulą otaczającą.
Kolizje, przecięcia..
Podsumujmy fakty:
- mamy dane promienia lecącego przez scenę wzdłuż kierunku patrzenia kamery
- mamy dane kul otaczających każdą siatkę każdego modelu
Jedyne co musimy zrobić to policzyć ich kolizje, policzenie przecięcia promienia (czyli de facto półprostej w przestrzeni 3D) ze sferą nie jest zadaniem specjalnie skomplikowanym, jednak i to XNA rozwiązało za nas, dostarczając narzędzia. Uogólnimy trochę liczenie przecięć tworząc metodę zwracającą informację o kolizji promienia z modelem. Oto ona (umieszczona w klasie MyModel)
public bool Insersects(Ray ray, Matrix world) { // check every mesh of MyModel foreach (ModelMesh mesh in model.Meshes) { // get transformation from Mesh space to world space Matrix w = bones[mesh.ParentBone.Index] * world; // get BoundingSphere in world space coordinates BoundingSphere sphere = TransformBoundingSphere(mesh.BoundingSphere, w); // use Intersects method if (sphere.Intersects(ray) != null) return true; } // no mesh intersects return false; }
Zastosowaliśmy metodę Intersects()
zwracającą nullable float (o typach nullable pisałem tu). Jeżeli wynik metody nie jest null
-em, zawiera odległość od początku promienia do punktu przecięcia, ta informacja jednak nie jest nam potrzebna.
Uwagi
Powyższa metoda jest dobra na początek, jednak przy dalszych zastosowaniach może być niewystarczająca. Dlaczego? Z powodu mniejszej, niż wymagana dokładności. Otóż, jeżeli metoda Intersects()
zwróci false
to wiemy na pewno, że do kolizji nie doszło. Natomiast nie możemy być pewni, czy rzeczywiście trafiliśmy w siatkę, jeżeli Intersects()
zwróci true
. W tym przypadku jest to dobry początek do dalszych obliczeń.
Receptami na ten problem mogą być: złożenie modelu z większej ilości małych siatek lub zastosowanie algorytmów do aproksymacji siatek kulami (postaram się przynajmniej jeden taki opisać).
Jedna odpowiedź Zostaw komentarz
Jest jeszcze jeden powód, dla którego ta metoda jest dobra tylko na początek – wydajność 😉