[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:

  1. przemnożenie przez macierz świata (przekształcenie do przestrzeni świata)
  2. przemnożenie przez macierz widoku (przekształcenie do przestrzeni kamery)
  3. 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ć).

Be Sociable, Share!
czoper opublikowano dnia 2010-10-2 Kategoria: Daj się poznać, Programowanie, Theme Festival | Tagi:, ,

Jedna odpowiedź Zostaw komentarz

  1. #1nilphilus @ 2010-10-4 18:41

    Jest jeszcze jeden powód, dla którego ta metoda jest dobra tylko na początek – wydajność 😉

Zostaw odpowiedź

(Ctrl + Enter)