OGRE/Animacja

Z Wikibooks, biblioteki wolnych podręczników.

Rozpoczęcie[edytuj]

Podobnie jak w poprzednim rozdziale oprzemy się na zalążku programu OGRE, który należy wkleić w pierwszy utworzony plik .cpp. Uzupełnimy go o klasę MoveDemoListener. Odszukaj linię:

 // Dziedziczymy ExampleApplication

i przed nią wstaw poniższy kod:

 #include <deque>
 using namespace std;
 
 class MoveDemoListener : public ExampleFrameListener
 {
 public:
 
     MoveDemoListener(RenderWindow* win, Camera* cam, SceneNode *sn,
         Entity *ent, deque<Vector3> &walk)
         : ExampleFrameListener(win, cam, false, false), mNode(sn), mEntity(ent), mWalkList( walk )
     {
     } // MoveDemoListener
 
     // Funkcja ta jest wywoływana podczas przemieszczania obiektu do następnych pozycji w mWalkList.
     bool nextLocation( )
     {
         return true;
     } // nextLocation( )
 
     bool frameStarted(const FrameEvent &evt)
     {
         return ExampleFrameListener::frameStarted(evt);
     }
 protected:
     Real mDistance;                  // Dystans który pozostał do przesunięcia
     Vector3 mDirection;              // Kierunek ruchu
     Vector3 mDestination;            // Punkt docelowy
 
     AnimationState *mAnimationState; // Bieżący stan animacji jednostki
 
     Entity *mEntity;                 // Jednostka którą animujemy
     SceneNode *mNode;                // Węzeł sceny tejże jednostki
     std::deque<Vector3> mWalkList;   // Lista zawierająca punkty ruchu
 
     Real mWalkSpeed;                 // Szybkość poruszającej się jednostki
 };

Dodatkowo kazaliśmy kompilatorowi włączyć do kompilacji kod źródłowy implementujący kolejki STL (deque).

Zmienne[edytuj]

Do klasy myApp wprowadzamy parę niezbędnych dalej zmiennych. Niniejszy kod wstaw zaraz za dyrektywą protected w klasie myApp.

     Entity *mEntity;                // Jednostka którą będziemy animować
     SceneNode *mNode;               // Węzeł sceny dla poruszającej się jednostki
     std::deque<Vector3> mWalkList;  // Lista zawierająca punkty ruchu

mEntity będzie wskazywać na utworzoną jednostkę, mNode na węzeł, a mWalkList będzie przechowywać wszystkie punktu, po których będzie chodził obiekt.

Rejestracja FrameListenera[edytuj]

Jak pamiętamy z poprzednich przykładów musimy zarejestrować klasę FrameListenera czyli utworzoną przez nas MoveDevoListener. Kod metody createFrameListener wprowadzamy bezpośrednio za lub przed metodą createScene.

     void createFrameListener(void)
     {
         mFrameListener= new MoveDemoListener(mWindow, mCamera, mNode, mEntity, mWalkList);
         mFrameListener->showDebugOverlay(true);
         mRoot->addFrameListener(mFrameListener);
     }

Tworzymy scenę[edytuj]

Przejdźmy do funkcji MyApp::createScene i dodajmy do niej poniższy kod. Najpierw ustawimy światło otoczenia na maksymalne, dzięki czemu będziemy mogli wyraźnie zobaczyć obiekty w scenie.

         // Ustawianie domyślnego światła
         mSceneMgr->setAmbientLight( ColourValue( 1.0f, 1.0f, 1.0f ) );

Następnie dodamy robota. W tym celu stworzymy jednostkę i węzeł sceny, które przeznaczymy dla naszego robota.

         // Tworzenie jednostki
         mEntity = mSceneMgr->createEntity( "Robot", "robot.mesh" );
 
         // Tworzenie węzła sceny
         mNode = mSceneMgr->getRootSceneNode( )->
             createChildSceneNode( "RobotNode", Vector3( 0.0f, 0.0f, 25.0f ) );
         mNode->attachObject( mEntity );

Kolejnym krokiem będzie określenie, po których miejscach ma chodzić robot. Dla tych, którzy dotychczas nie mieli do czynienia z STL wyjaśniamy, że obiekt klasy deque implementuje kolejkę dwustronną, czyli taką o dwóch końcach z których można odbierać lub dodawać elementy. Wykorzystamy tylko kilka metod tej klasy. Metody push_front i push_back umożliwiają wstawienie określonych wartości na początek lub na koniec kolejki. Dzięki front i back możemy się dowiedzieć, jakie wartości znajdują się na początku lub na końcu kolejki. Funkcje pop_front i pop_back umożliwiają usunięcie elementu znajdującego się na początku lub na końcu kolejki. Metoda empty zwraca informację, czy kolejka jest pusta. Kod przedstawiony niżej wstawia do kolejki wektory według których będzie się przesuwał nasz robot:

         // Tworzenie listy punktów, po których ma się przesuwać robot
         mWalkList.push_back( Vector3( 550.0f,  0.0f,  50.0f  ) );
         mWalkList.push_back( Vector3(-100.0f,  0.0f, -200.0f ) );

Teraz umieścimy kilka "węzełków" w scenie, abyśmy mogli ocenić, jak robot będzie przesuwany porównując jego pozycję z ich położeniem na ekranie.

         // Tworzymy węzełki, dzięki którym możemy zobaczyć ruch
         Entity *ent;
         SceneNode *node;
 
         ent = mSceneMgr->createEntity( "Knot1", "knot.mesh" );
         node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot1Node",
             Vector3(  0.0f, -10.0f,  25.0f ) );
         node->attachObject( ent );
         node->setScale( 0.1f, 0.1f, 0.1f );
 
         ent = mSceneMgr->createEntity( "Knot2", "knot.mesh" );
         node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot2Node",
             Vector3( 550.0f, -10.0f,  50.0f ) );
         node->attachObject( ent );
         node->setScale( 0.1f, 0.1f, 0.1f );
 
         ent = mSceneMgr->createEntity( "Knot3", "knot.mesh" );
         node = mSceneMgr->getRootSceneNode( )->createChildSceneNode( "Knot3Node",
             Vector3(-100.0f, -10.0f,-200.0f ) );
         node->attachObject( ent );
         node->setScale( 0.1f, 0.1f, 0.1f );

Pewnie zauważycie, że "węzełki" są opuszczone w dół o 10 jednostek w stosunku do pozycji samego robota. Teraz jeszcze ustawimy odpowiednio kamerę. Przeniesiemy ją w pozycje, z której uzyskamy lepszy widok na całość sceny.

         // Ustawiamy kamerę, żeby patrzyła na naszą pracę
         mCamera->setPosition( 90.0f, 280.0f, 535.0f );
         mCamera->pitch( Degree(-30.0f) );
         mCamera->yaw( Degree(-15.0f) );

Przekompiluj i uruchom program. Powinniśmy zobaczyć robota stojącego na jednym z "węzełków" oraz dwa dodatkowe "węzełki" tworzące razem z pierwszym coś w rodzaju trójkąta skierowanego wierzchołkiem w prawo. Na razie nic nam się na ekranie samo nie porusza.

Animacja[edytuj]

Spróbujemy zatem wprowadzić w naszą scenę nieco ruchu. Animacja w Ogre jest bardzo prosta do oprogramowania. W definicji jednostki zawarte już są pewne wprowadzone, przez projektanta obiektu 3D, stany i animacje. Stany to różne pozycje statyczne z róznie ustawionymi elementów, z których się składa jednostka. Animacje to sekwencje takich stanów i przejść pomiędzy nimi. Definicja "robot.mesh" zawiera w sobie animację marszu i musimy ją tylko wywołać. Aby zobaczyć wszystkie animacje dla siatki 3D możesz pobrać OgreMeshViewer i w nim je zobaczyć. Aby to zrobić pobieramy AnimationState z jednostki i ustawiamy jej opcje. Gdy to wszystko mamy przygotowane po prostu włączamy animację. Przejdźmy do konstruktora MoveDemoListener i dodajmy poniższy kod:

         // Konfiguracja animacji
         mAnimationState = ent->getAnimationState( "Idle" );
         mAnimationState->setLoop( true );
         mAnimationState->setEnabled( true );

Druga linia pobiera AnimationState naszej jednostki. W trzeciej linie wywołujemy setLoop( true ), która tworzy pętlę animacji. Dla niektórych animacji (np. w takiej, w której coś ginie), animacja nie będzie powtarzać się w kółko, więc powinniśmy tą opcję ustawić na false. Czwarta linia włącza animację. Ale dlaczego animację pobraliśmy z wartością Idle? Otóż nazwa ta określa to domyślną standardową animację czynności dla danej jednostki w momencie gdy nic ona nie wykonuje. W przypadku jednostki ukazującej człowieka byłoby to na przykład oddychanie.

Gdybyś teraz przekompilował i uruchomił program dalej uzyskałbyś statyczną, nieruchomą scenę. A to dlatego, że pętla animacja nie wie nic o upływie czasu. Znajdźmy metodę MoveDemoListener::frameStarted i dodajmy do niej na początku kod:

         mAnimationState->addTime( evt.timeSinceLastFrame );

Przekazujemy w ten sposób do animacji co każdą klatkę czas jaki upłynął od poprzedniej. System może wtedy tworzyć kolejne klatki z odpowiednio zmienionym wyglądem jednostki. Skompiluj i uruchom aplikację. Robot będzie stał w miejscu ale będzie też lekko poruszał swoim korpusem. Możesz poeksperymentować z innymi nazwami animacji niż "Idle". W przypadku robota będziesz miał jeszcze możliwośc skorzystania z "Walk" i "Die". Za każdym razem zauważ, że robot swą czynność wykonuje w tym samym miejscu. Zapamiętaj, że animacja zmienia wygląd jednostki lecz nie zmienia jej położenia. Gdybyś się pomylił w nazwie i wybrał nieistniejącą animację danej jednostki, ekran po przejciu w tryb graficzny 3D pozostanie ciemny.

Przenoszenie robota[edytuj]

Zajmiemy się teraz ruchem robota z jednego miejsca do drugiego. Zanim zaczniemy wyjaśnimy do czego będą nam potrzebne zmienne utworzone w klasie MoveDemoListener. Użyjemy czterech zmiennych, aby poruszyć naszego robota. Pierwsza zmienna, mDirection, będzie służyła do przechowywania kierunku robota, w którym się porusza. Aktualny cel podróży będziemy przechowywali w zmiennej mDestination. Dystans będziemy przechowywać w mDistance a szybkość przesuwania w mWalkSpeed.

W konstruktorze klasy MoveDemoListener usuńmy wszystkie poprzednie wpisy a ustawmy teraz zmienne na odpowiednie, domyślne wartości. Szybkość chodzenia ustawmy na 35 jednostek na sekundę. Do mDirection przypiszemy wektor zerowy (ZERO) co będzie nam potrzebne do określenia czy robot się porusza czy też nie.

         // Ustawianie domyślnych wartości zmiennych
         mWalkSpeed = 35.0f;
         mDirection = Vector3::ZERO;

Animacja chodu[edytuj]

Sprawmy by robot wyglądał tak jakby chodził. Jak pewnie sobie przypominacie wystarczy w tym celu zmienić obsługiwaną w danej chwili animację zawartą w siatce 3D. Jednak zróbmy to tak by zmiana ta dokonywała się tylko wtedy gdy faktycznie jednostka jest przesuwana na ekranie. Aby sprawdzić czy jest w ruchu będziemy wywoływać funkcję nextLocation. Powinna ona nam zwrócić true jeżeli jeszcze jest jakiś dystans lub kolejny odcinek do przejścia. Jeżeli natomiast dotarliśmy do samego końca powinna zwrócić false. Dodaj ten kod na górze metody MoveDemoListener::frameStarted, przed dwoma istniejącymi tam już liniami.

         if ( mDirection == Vector3::ZERO ) 
         {
             if ( nextLocation() ) 
             {
                 // Set walking animation
                 mAnimationState = mEntity->getAnimationState( "Walk" );
                 mAnimationState->setLoop( true );
                 mAnimationState->setEnabled( true );
             }
         }

Jeśli teraz przekompilujemy i uruchomimy program, zobaczymy robota idącego w miejscu. Jest tak, ponieważ nie oprogramowaliśmy jeszcze przesunięcia jednostki a sam robot jest już ustawiony z kierunkiem ZERO i funkcja MoveDemoListener::nextLocation na razie zawsze zwraca true co powoduje włączenie animacji. W następnym kroku zmienimy nieco funkcję MoveDemoListener::nextLocation co pozwoli nam ominąć ten problem.

Ruch jednostki[edytuj]

Skupmy się teraz na ruszeniu robota z miejsca. Będziemy przesuwać jednostkę o pewien mały dystans co każdą klatkę. Dystans ten będzie zależny od czasu jaki upłynął pomiędzy klatkami. Przejdźmy do metody MoveDemoListener::frameStarted. Wstawimy kod który będzie odpowiadał za sytuację w której robot jest już w trakcie przemieszczania (mDirection nie równa się Vector3::ZERO). Poniższy kod dodamy zaraz przed linią zawierającą instrukcję return. Będzie on tworzyć dalszą część instrukcji if.

         else
         {
             Real move = mWalkSpeed * evt.timeSinceLastFrame;
             mDistance -= move;

Musimy skontrolować czy nasz robot nie poszedł za daleko. Nie możemy sprawdzać czy już znajduje się w pozycji celu gdyż w czasie trwania jednej ramki może się zdarzyć, że przejdzie trochę dalej, a w przestrzeni trójwymiarowej nie stwierdzimy czy faktycznie jest za czy przed celem. Zastosujemy zmienną mDistance która stanie się ujemna po przejściu celu. Gdy stanie się ujemna musimy cofnąć się "skokowo" do punktu w kierunku którego podążaliśmy. Ustawimy wówczas także mDirection na wektor zerowy. Jeśli wywoływana w następnej części metoda nextLocation nie zmieni mDirection, wtedy nie musimy już dalej się poruszać, ponieważ dotarliśmy do celu.

             if (mDistance <= 0.0f)
             {
                 mNode->setPosition( mDestination );
                 mDirection = Vector3::ZERO;

Kiedy już dotarliśmy do punktu określającego koniec prostej musimy sprawdzić czy to już koniec czy też mamy do prześcia kolejny odcinek. Po sprawdzeniu ustawiamy odpowiednią animację.

                // Ta animacja ustawiana jest jeśli już dalej nie mamy gdzie iść. 
                if (! nextLocation( ) )
                {
                    // Set Idle animation                     
                    mAnimationState = mEntity->getAnimationState( "Idle" );
                    mAnimationState->setLoop( true );
                    mAnimationState->setEnabled( true );
                } 
                else
                {
                    // Tu później wstawimy kod obracjący robota
                }
             }

Animacja zmieniana jest tylko jeżeli osiągneliśmy punkt docelowy, w przeciwnym razie nie musimy zmieniać ustawionej wcześniej animacji "Walk". Trzeba będzie jednak obrócić naszego robota przodem w kierunku, w którym ma, gdyż inaczej będzie chodził tyłem lub bokiem. Na razie pominiemy ten fragment ale oczywiście później do niego wrócimy.

Wróćmy teraz do normalnej sytuacji gdy robot idzie ale jeszcze nie dotarł do celu. W takim przypadku po prostu przesuniemy naszą jednostkę w kierunku w którym podążamy uwzględniając wartość zmiennej move.

             else
             {
                 mNode->translate( mDirection * move );
             } // else
         } // if

Oprogramowaliśmy ogólne zasady ruchu jednostki ale jeszcze nie wiemy którędy ma ona się poruszać. Odnajdźmy funkcję MoveDemoListener::nextLocation. Powinna zwrócić ona false, kiedy osiągniemy lub miniemy punkt docelowy czyli kiedy nie będzie w kolejce żadnych dalszych odcinków do przejścia. To będzie pierwsza linia naszej funkcji. (Powinniśmy pozostawić instrukcję return true na końcu funkcji.)

         if ( mWalkList.empty() )
             return false;

Jeżeli jednak mamy jeszcze jakiś odcinek do przejścia w kolejce to zdejmiemy z kolejki wektor wskazujący na cel naszej dalszej podróży. Wektor kierunku ustawiamy odejmując od wcześniej pobranego wektora celu aktualną pozycję węzła sceny. mDirection musi być wektorem o długości jednej jednostki a wskazującym tylko kierunek ruchu, gdyż stosujemy go jako jeden z czynników w mnożeniu przez wartość move przy obliczaniu przesunięcia jednostki. W tej chwili jego wartość podaje całą odległość do celu. Wykonujemy zatem normalizację wartości wektora za pomocą funkcji normalise dodatkowo otrzymując poprzednią wartość (czyli odległość do celu) którą wprowadzamy do mDistance. Dzięki tej konstrukcji zaoszczędzimy jedną linię kodu, która normalnie by przepisywała wartość wcześniejszego mDirection do mDeistance.

         mDestination = mWalkList.front( );  // this gets the front of the deque
         mWalkList.pop_front( );             // this removes the front of the deque
 
         mDirection = mDestination - mNode->getPosition( );
         mDistance = mDirection.normalise( );

Przekompilujmy i uruchommy ten program. Robot przechodzi po wszystkich punktach, ale zawsze jest zwrócony w kierunku Vector3::UNIT_X (domyślny). Nie oprogramowaliśmy jeszcze jego obracania, co wykonamy w następnej części.

Obrót zgodny z kierunkiem ruchu[edytuj]

Obrócimy teraz naszego robota tak by zawsze jego przód był zwrócony w kierunku ruchu. W tym celu użyjemy funkcji rotate. Wstawmy poniższy kod, w miejscu w którym zostawiliśmy wcześniej tylko komentarz. W pierwszej linii pobieramy kierunek, gdzie jest skierowany robot. W drugiej linii tworzymy kwaternion, który reprezentuje kąt pod jakim robot musi iść do celu. Trzecia linia obraca robota.

         Vector3 src = mNode->getOrientation( ) * Vector3::UNIT_X;
         Ogre::Quaternion quat = src.getRotationTo( mDirection );
         mNode->rotate( quat );

Kwaterniony reprezentują rotację w trójwymiarze. Używamy ich, aby przekształcić informacje o kierunku obiektów w przestrzeni. W pierwszej linie wywołaliśmy metodę getOrientation, która zwraca kwaternion reprezentujący dokąd robot jest skierowany. Mnożąc to przez wektor UNIT_X, otrzymujemy kierunek robota w postaci wektora, który przechowamy w src. W drugiej linii getRotationTo zwraca nam kwaternion, który reprezentuje rotacje o jaką trzeba jeszcze obrócić robota. Dzięki trzeciej linii mogliśmy obrócić węzeł do odpowiedniej orientacji.

Pozostała jeszcze tylko jeden problem z utworzonym kodem. Jest jeden specjalny przypadek, w który SceneNode::rotate działa błędnie. Jeśli spróbujemy obrócić robota o 180, kod rotate zatrzyma się na błędzie związanym z dzieleniem przez zero. Aby go zlikwidować, będziemy sprawdzać, czy kąt wynosi 180 stopni. Jeśli tak będzie, użyjemy po prostu funkcję yaw z argumentem wynoszącym 180 stopni, zamiast używać funkcji rotate. W tym celu, usuńmy poprzednio trzy wstawione linie i zamiast tego wstawmy:

         Vector3 src = mNode->getOrientation( ) * Vector3::UNIT_X;
         if ( (1.0f + src.dotProduct( mDirection )) < 0.0001f ) 
         {
             mNode->yaw( Degree(180) );
         }
         else
         {
             Ogre::Quaternion quat = src.getRotationTo( mDirection );
             mNode->rotate( quat );
         } // else

Powinno być już wszystko znajome, z wyjątkiem wyrażenia w if. Jeśli dwa jednostkowe wektory będą przeciwne (czyli kąt między nimi wynosi 180 stopni), to ich iloczyn skalarny wynosić będzie -1. Czyli jeśli iloczyn skalarny wynosi -1.0f, wtedy musimy wykonać yaw o 180 stopni, w przeciwnym wypaku skorzystamy z rotate. Dlaczego dodajemy 1.0f i sprawdzamy, czy wynik jest mniejszy niż 0.0001f? Nie możemy zapomnieć o błędach zaokrągleń występujących w liczbach zmiennoprzecinkowych. Nigdy nie powinno się bezpośrednio porównywać dwóch liczb zmiennoprzecinkowych. W tym przypadku iloczyn skalarny dwóch wektorów będzie się zawierał w przedziale [-1, 1]. Jak widzimy w programowaniu graficznym niezbędna jest chociaż minimalna znajomość algebry liniowej. Warto przypomnieć sobie (lub postudiować) podstawowe wiadomości o operacjach na wektorach i macierzach.

Nasz kod jest już kompletny! Możemy teraz przekompilować i uruchomić. Nasz robot będzie się poruszał od punktu początkowego do końcowego obracając się w punkcie środkowym.

Kod źródłowy[edytuj]

Dostępny tu jest kompletny kod źródłowy wykorzystany w powyższym artykule.