OGRE/Frame Listener i niebuforowane wejście
Rozpoczęcie
edytujPodobnie 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ę TutorialFrameListener. Odszukaj linię:
// Dziedziczymy ExampleApplication
i przed nią wstaw następujący kod:
class TutorialFrameListener : public ExampleFrameListener
{
public:
TutorialFrameListener( RenderWindow* win, Camera* cam, SceneManager *sceneMgr )
: ExampleFrameListener(win, cam, false, false)
{
}
bool frameStarted(const FrameEvent &evt)
{
return ExampleFrameListener::frameStarted( evt );
}
protected:
bool mMouseDown; // Czy w ostatniej klatce lewy przycisk myszy był wciśnięty
Real mToggle; // Ile czasu ma minąć, do odebrania następnego wydarzenia
Real mRotate; // Stała rotacji
Real mMove; // Stała przesunięcia
SceneManager *mSceneMgr; // Bieżący menadżer sceny
SceneNode *mCamNode; // Węzeł sceny, do którego jest powiązana kamera
};
Dodajmy też dwie metody do naszej klasy myApp. Odszukaj linię:
/** createScene jest funkcją czysto wirtualną w ExampleApplication,
i przed nią wstaw:
void createCamera(void)
{
}
void createFrameListener(void)
{
}
Frame listener
edytujWprowadzenie
edytujW poprzednim rozdziale zajmowaliśmy się tylko tym co możemy wstawić do metody createScene. Tym razem zajmiemy się klasą FrameListener, która definiuje dwie ważne w naszych rozważaniach funkcje:
bool frameStarted(const FrameEvent& evt)
bool frameEnded(const FrameEvent& evt)
Ogrowa pętla główna (Root::startRendering) wygląda mniej więcej tak:
- Korzeń (Root) wywołuje metodę frameStarted na wszystkich zarejestrowanych FrameListenerach.
- Korzeń renderuje jedną klatkę.
- Korzeń wywołuje metodę frameEnded na wszystkich zarejestrowanych FrameListenerach.
Pętla ta wykonuje się, dopóki żaden z FrameListenerów nie zwróci false z funkcji frameStarted lub frameEnded. Wartość, którą zwracają te funkcje określa, czy dalej ma kontynuować rendering. Zatem jeśli zostanie zwrócone false, program zostanie zakończony. Obiekt FrameEvent przechowuje dwie zmienne, ale tylko jedna - timeSinceLastFrame - jest przydatna w FrameListenerze. Zmienna ta przechowuje, ilość czasu jaka upłyneła między ostatnim a obecnym wywołaniem frameStarted lub frameEnded.
Jednym z ważnych spraw w FrameListenerach, jest to, że porządek w jakim są one wywoływane jest zależne całkowicie od Ogre. Nie możesz określić, który FrameListener ma zostać wywołany pierwszy, który drugi, trzeci itd... Jeśli chcesz mieć pewność, że FrameListenery mają zostać wywołane w odpowiednim porządku, powinieneś zarejestrować tylko jednego FrameListenera i z niego wywoływać wszystkie pozostałe w odpowiedniej kolejności.
Rejestrowanie
edytujUtworzony wyżej kod można skompilować by sprawdzić czy wszystko wprowadziliśmy poprawnie. Jednak nie należy uruchamiać skompilowanego programu, gdyż po nadpisaniu metod createFrameListener i createCamera z klasy ExampleApplication zawiśnie nam i trzeba będzie go skilować. Musimy zatem obie metody rozbudować.
Znajdź funkcję myApp::createCamera i dodaj do niej poniższy kod:
// utworzy kamerę, ale zostawi domyślną pozycję
mCamera = mSceneMgr->createCamera("PlayerCam");
mCamera->setNearClipDistance(5);
Utworzyliśmy kamerę o nazwie "PlayerCam" i ustawiliśmy najbliższą możliwą odległość widzenia (to co jest bliżej nie będzie widziane).
Ponieważ klasa Root jest tym, co renderuje klatki, więc musimy jej wskazać ścieżkę do FrameListenera. Najpierw utworzymy nową instancję naszej klasy TutorialFrameListener a następnie zarejestrujemy ją w korzeniu. W tym celu znajdź metodę myApp::createFrameListener i dodaj do niej poniższy kod:
// Tworzy FrameListener
mFrameListener = new TutorialFrameListener(mWindow, mCamera, mSceneMgr);
mRoot->addFrameListener(mFrameListener);
Zmienne mRoot i mFrameListener są już określone w klasie ExampleApplication. Metoda addFrameListener rejestruje FrameListenera, natomiast removeFrameListener wyrejestrowywuje go co oznacza, że tak wyrejestrowany FrameListener nie będzie więcej dostawał żadnych informacji. Argumentem metody addFrameListener i removeFrameListener jest wskaźnik do FrameListenera (nie możesz operować FrameListnerem podając jego nazwę, bo takiej nie posiada). Warto zachowywać wskaźnik do każdego FrameListenera, choćby po to by go, gdy już nie jest potrzebny, usunąć z pamięci.
Gdybyśmy teraz uruchomili nasz program, zobaczylibyśmy czarny ekran, gdyż nic nie umieściliśmy na scenie. Warto byłoby jednak wprowadzić jakiś widoczny element, choćby po to by wiedzieć, że program pracuje poprawnie.
Klasa ExampleFrameListener (którą dziedziczy nasz TutorialFrameListener) dostarcza funkcję showDebugOverlay( bool ), która określa, czy pokazywać w lewym dolnym rogu informacje o szybkości klatek. Możemy go włączyć w ten sposób:
// Mówi, aby pokazywać ramkę ze statystykami
mFrameListener->showDebugOverlay(true);
Od wersji Ogre 1.2 (Dagon) okienko informacji jest domyślnie włączone, więc moglibyśmy pominąc ten kawałek kodu. Wprowadzenie go jednak nie ma także żadnych ujemnych skutków.
Teraz skompilujmy i uruchommy nasz program.
Tworzymy scenę
edytujWprowadzenie
edytujPrzed przejściem do pracy z kodem, omówimy po krótce, co będziemy robić.
Umieścimy jeden obiekt w scenie (ninja) i dodamy jedno światło. Jeśli przyciśniesz lewym przyciskiem myszy, światło będzie włączane lub wyłączane. Trzymając wciśniety prawy przycisk myszy będziesz mógł obracać kamerą. Będziemy także przemieszczać węzły sceny powiązane z kamerą różnych viewportów. Wciskając przycisk 1 lub 2 zmieniamy punkt, z którego patrzy kamera.
Kod
edytujZnajdź metodę myApp::createScene. Pierwszą rzeczą jaką zrobimy będzie ustawienie światła otoczenia, ponieważ chcemy, aby obiekty w scenie były zawsze widoczne, nawet wtedy kiedy światło jest wyłączone.
mSceneMgr->setAmbientLight( ColourValue( 0.25, 0.25, 0.25 ) );
Następnie dodamy jednostkę z ninją:
Entity *ent = mSceneMgr->createEntity( "Ninja", "ninja.mesh" );
SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode( "NinjaNode" );
node->attachObject( ent );
Potem tworzymy punktowe źródło białego światła i umieszczamy je w scenie, w małej odległości od ninji:
Light *light = mSceneMgr->createLight( "Light1" );
light->setType( Light::LT_POINT );
light->setPosition( Vector3(250, 150, 250) );
light->setDiffuseColour( ColourValue::White );
light->setSpecularColour( ColourValue::White );
Musimy teraz utworzyć węzeły sceny, do której przywiążemy kamery. Ważną rzeczą jest to, że kiedy używamy systemu kamer musimy posiadać dodatkowe oddzielne węzły sceny przeznaczone do rotacji każdej kamery. Tak więc utwórzmy węzeł sceny i skierujmy go do ninji:
// Tworzymy węzeł sceny
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode1", Vector3(-400, 200, 400));
//Ustawiamy kamerę na ninje
node->yaw(Degree(-45));
node->attachObject(mCamera);
//Tworzymy drugi węzeł
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode2", Vector3(0, 200, 400));
Kiedy już to zrobimy, możemy przejść do prac w klasie TutorialFrameListener...
TutorialFrameListener
edytujTen artykuł należy dopracować |
Zmienne
edytujZdefiniowaliśmy kilka zmiennych w klasie TutorialFrameListener:
bool mMouseDown; // Czy w ostatniej klatce lewy przycisk myszy był wciśnięty
Real mToggle; // Ile czasu ma minąć do odebrania następnego wydarzenia
Real mRotate; // Stała rotacji
Real mMove; // Stała przesunięcia
SceneManager *mSceneMgr; // Bieżący menadżer sceny
SceneNode *mCamNode; // Węzeł sceny, do którego jest powiązana kamera
mSceneMgr przechowuje wskaźnik do bierzącego menadżera sceny, natomiast mCamNode aktualny węzeł sceny, do którego jest "podczepiona" kamera (chodzi o "CamNode*", nie "PitchNode*"). mRotate i mMove są zmiennymi, przechowywującymi informacje o obracaniu i przesuwaniu. Jeśli chcesz, żeby rotacja lub przesuwanie było szybsze, to zwiększ te wartości, jeśli mają być wolniejsze to je odpowiedni zmniejsz.
Zmienne mToggle i mMouseDown kontrolują nasze wejście. Zastosujemy niebuforowane wejście (buforowane zostanie omówione w kojenym rozdziale). Będziemy je wykorzystywali aby sprawdzić stan klawiatury lub myszy.
Przejdźmy teraz do pewnego interesującego problemu, związanego z użyciem klawiatury w Ogre testowanym co każdą klatkę. Jeśli przyciśniemy klawisz, program może wykonać odpowiednią instrukcję, ale co się stanie przy następnej klatce? Ruch palca jest dłuższy niż czas zmiany klatek, a zatem dostaniemy informację że klawisz znów jest wciśnięty. Czy program ma znów wykonać czynności wykonane przy poprzedniej klatce? W niektórych przypadkach (np. ruch przy pomocy strzałek) tego właśnie byśmy oczekiwali. Problemem stanie się jednak obsługa klawisza przełaczającego np. wyłączającego/włączającego światło. Wtedy, gdy w pierwszej klatce, światło zostaje włączone, w następnej zostaje wyłączone, potem włączone, wyłączone itd., dopóki przycisk nie zostanie opuszczony. Ominąć ten problem możemy zachowując stan klawisza z poprzedniej klatki. Można stosować różne metody przy czym w naszym programie zastosujemy dwie różne.
- Zmienna mMouseDown przechowuje informacje, czy lewy przycisk myszy był wciśniety w ostatniej klatce (czyli jeśli mMouseDown ma wartość true, drugi raz nie wykonujemy tej samej akcji, dopóki klawisz nie zostanie puszczony).
- Zmienna mToggle określa, jaki najkrótszy czas musi upłynąć pomiędzy wciśnięciem klawisza i wykonaniem jakiegoś działania a ponowną możliwością wykonania tej samej czynności. Innymi słowy nie dopuszcza do szybszego przełączania niż po upływie czasu w niej podanego.
Konstruktor
edytujZobaczmy najpierw, jak wykonujemy konstruktor z klasy nadrzędnej ExampleFrameListener:
: ExampleFrameListener(win, cam, false, false)
Zapewne zuważyliśmy, że trzeci i czwarty argument są ustawione na false. Trzeci określa, czy korzystać z buforowanego wejścia, natomiast czwarty, czy buforować wejście myszy (jak na razie nie wykorzystujemy ich).
Wprowadzimy teraz do konstruktora TutorialFrameListener domyślne wartości dla wszystkich zmiennych:
// informacje o stanach klawiatury i myszy
mMouseDown = false;
mToggle = 0.0;
// Wkładanie kamery w odpowiednie miejsce i ustawianie odpowiedniego menadżera sceny
mCamNode = cam->getParentSceneNode( )->getParentSceneNode( );
mSceneMgr = sceneMgr;
// ustawia szybkość przesunięcia i obrotu
mRotate = 0.13;
mMove = 250;
Wykonaliśmy dwa razy getParentSceneNode z obiektu cam, ponieważ pierwszym rodzicem tego obiektu jest PitchNode, a dopiero jego CamNode.
Metoda frameStarted
edytujZajmiemy się teraz elementem stanowiącym jądro naszego wykładu - wykonywaniem pewnych działań co każdą klatkę. Aktualnie metoda frameStarted posiada poniższy kod:
return ExampleFrameListener::frameStarted( evt );
Ten krótki kod umożliwiał nam uruchomienie i sprawdzenie naszego programu bez potrzeby głębszego "dłubania". Funkcja ExampleFrameListener::frameStarted pozwala na obsłużenie wielu zdarzeń (np. związane z przyciskaniem przycisków, przesuwania kamery itp.). Wyczyść zawartość tej metody.
Od wersji 1.4 wprowadzono do Ogre Orientowany Obiektowo System Wejścia z ang. Object Oriented Input System, w skrócie OIS. Zastąpił on zmienną mInputDevice wprowadzając trzy nowe klasy do obsługi myszy, klawiatury oraz joysticka. W związku z powyższym informacje podane poniżej moga być nieaktualne i kompilator zakomunikuje o błędach. Poprawna wersja tego tutorialu znajduje się pod adresem http://www.ogre3d.org/wiki/index.php/Basic_Tutorial_4.
Przełączanie światła klawiszem myszki
edytujPonieważ używamy niebuforowanego wejścia, musimy pobrać bieżący stan klawiatury i myszy. Zrobimy to wywołując metodę OIS::Object::capture. Dodajmy, że klasa ExampleApplication zawiera deklaracje zmiennych mMouse i mKeyboard, na rzecz których wywołujemy funkcję capture():
mMouse->capture();
mKeyboard->capture();
Następnie sprawdzamy, czy został wciśnięty klawisz Escape. Jeśli tak się stało, należy zakończyć program. Sprawdzamy to za pomocą metody OIS::Keyboard::isKeyDown. Jeżeli klawisz Escape został wciśnięty zostanie wywołane polecenie return false i program zostanie zakończony.
if(mKeyboard->isKeyDown(OIS::KC_ESCAPE))
return false;
Na samym końcu funkcji frameStarted umieszczamy polecenie
return true;
Dzięki temu funkcja działać będzie jak pętla.
Cały dalej podany kod powinien zostać wstawiony pomiędzy tymi dwoma pokazanymi wyżej liniami.
Najpierw zrobimy tak, abyśmy po przyciśnięciu lewego przycisku myszy mogli włączyć lub wyłączyć światło. Możemy sprawdzić czy przycisk myszy został wciśnięty wykorzystując funkcję OIS::Mouse:getMouseState.
bool currMouse = mMouse->getMouseState().buttonDown(OIS::MB_Left);
Jako argument funkcji buttonDown możemy użyć jednej z następujących nazw: MB_Left, MB_Right, MB_Middle, MB_Button3, MB_Button4, MB_Button5, MB_Button6, MB_Button7. Pierwsze trzy oznaczają rzecz jasna lewy, prawy i środkowy przycisk myszy (ewentualnie wciśnięcie kółka). Jeśli chodzi o pozostałe to dokumentacja OIS nie mówi nic na ten temat. ( Lecz prawdopodobnie chodzi o dodatkowe przyciski w myszy )
Zmienna currMouse przechowuje wartość true, jeśli lewy przycisk myszy jest przyciśnięty. Teraz będziemy przełączać stan światła, w zależności czy currMouse wynosi true i przycisk ten nie był przyciśniety w poprzedniej klatce (chcemy przecież, aby światło zmieniało się tylko wtedy, gdy przycisk został wciśnięty, a nie co klatkę). Funkcja Light::setVisible określa, czy lampa emituje światło, czy nie.
if (currMouse && ! mMouseDown)
{
Light *light = mSceneMgr->getLight("Light1");
light->setVisible(! light->isVisible());
}
Sprawmy teraz, aby zmienna mMouseDown była równa currMouse. Będzie to przydatne w następnej klatce, abyśmy mogli określić, czy przycisk był także wciśnięty w poprzedniej (czyli teraz).
mMouseDown = currMouse;
Przekompilujmy i uruchommy program. Możemy teraz klikając lewym przyciskiem myszy przełączać stan źródła światła - czyli czy ma świecić, czy nie. Nie możemy jednak teraz przenosić kamery, ponieważ nie wykorzystujemy już metody frameStarted z klasy ExampleFrameListenera.
Przełączanie kamer
edytujDzięki przechowywaniu stanu przycisku myszy w poprzedniej klatce możemy w łatwy sposób określić, co się stało w bieżącej klatce. Wadą jest konieczność używania osobnych zmiennych i testów dla każdego klawisza. Jednym ze sposobów ominięcia tego jest przechowywanie czasu, jaki musi minąć po przyciśnięciu przycisku, a w którym nie wolno wykonywać żadnego kolejnego działania. Wartość tę będziemy przechowywać w zmiennej mToggle. Najpierw zmniejszamy wartość zmiennej o czas, jaki minął od ostatniej klatki.
mToggle -= evt.timeSinceLastFrame;
Kiedy już uaktualnimy mToggle, możemy z niego skorzystać. Przycisk 1 ma odpowiadać za powiązanie kamery z pierwszym węzłem sceny. Jednak przed tym musimy sprawdzić, czy mToggle jest mniejsze od 0:
if ((mToggle < 0.0f ) && mKeyboard->isKeyDown(OIS::KC_1))
{
Ustawmy teraz zmienną, tak by kolejna akcja mogła się rozegrać dopiero po pół sekundy.
mToggle = 0.5f;
Następnie usuńmy kamerę z wezła, z którym jest powiązana. Ustawmy także wskaźnik mCamNode, aby zawierał węzeł CamNode1, a następnie powiążmy do niego kamerę.
mCamera->getParentSceneNode()->detachObject( mCamera );
mCamNode = mSceneMgr->getSceneNode( "CamNode1" );
mCamNode->attachObject(mCamera);
}
Naciśnięcie klawisza 2 oprogramujemy podobnie z tą róznicą, że zastosujemy CamNode2.
else if ((mToggle < 0.0f) && mKeyboard->isKeyDown(OIS::KC_2))
{
mToggle = 0.5f;
mCamera->getParentSceneNode()->detachObject(mCamera);
mCamNode = mSceneMgr->getSceneNode("CamNode2");
mCamNode->attachObject(mCamera);
}
Przekompiluj i uruchom aplikację. Możemy teraz zmieniać widok z kamery przyciskając klawisz 1 lub 2.
Przesuwanie kamery
edytujTeraz spróbujemy oprogramować poruszanie się za pomocą klawiszy ze strzałkami lub klawiszy W,A,S,D. W przeciwieństwie do tego, co omawialiśmy poprzednio, nie musimy blokować akcji na pewien czas, ponieważ chcemy aby ruch był wykonywany za każdym razem, co klatkę, jeśli tylko przycisk jest wciśnięty. Najpierw utworzymy wektor, który będzie pamiętał o ile mamy się przesunąć.
Vector3 transVector = Vector3::ZERO;
Jeśli klawisz W lub strzałka do góry została wciśnięta, posuwamy się do przodu (czyli w kierunku wartości ujemnych na osi z, pamiętając, że ujemny z idzie do wnętrza ekranu komputera).
if (mKeyboard->isKeyDown(OIS::KC_UP) || mKeyboard->isKeyDown(OIS::KC_W))
transVector.z -= mMove;
Robimy podobnie dla przycisku S i strzałki w dół, tylko w kierunku dodatnich wartości na osi z.
if (mKeyboard->isKeyDown(OIS::KC_DOWN) || mKeyboard->isKeyDown(OIS::KC_S))
transVector.z += mMove;
A teraz z kolei będziemy mogli się ruszyć w lewo lub prawo, czyli w kierunku dodatnich lub ujemnych wartości osi x:
if (mKeyboard->isKeyDown(OIS::KC_LEFT) || mKeyboard->isKeyDown(OIS::KC_A))
transVector.x -= mMove;
if (mKeyboard->isKeyDown(OIS::KC_RIGHT) || mKeyboard->isKeyDown(OIS::KC_D))
transVector.x += mMove;
I wreszcie będziemy mogli się poruszać w górę lub dół, względem osi y. Użyjemy przycisku E i PageDown, abyśmy mogli poruszyć się w dół, a także Q i PageUp w ceulu poruszania się do góry:
if (mKeyboard->isKeyDown(OIS::KC_PGUP) || mKeyboard->isKeyDown(OIS::KC_Q))
transVector.y += mMove;
if (mKeyboard->isKeyDown(OIS::KC_PGDOWN) || mKeyboard->isKeyDown(OIS::KC_E))
transVector.y -= mMove;
Jak pamiętamy zmienna transVector przechowuje wektory przesunięcia, który zastosujemy dla węzła sceny naszej kamery. Pierwszą pułapkę możemy napotkać, kiedy się obrócimy. Wówczas nasze współrzędne przemieszenia x, y, z są błędne. Musimy zastosować w tym przypadku wszystkie rotacje w węźle sceny, który chcemy przesuwać. Jest to faktycznie prostrze, niż brzmi.
Ogre nie reprezentuje rotacji jako macierzy transformacji, jak to zaimplementowano w niektórych silnikach graficznych. Zamiast tego używa kwaternionów do wykonywania wszystkich operacji obracania. Matematyka kwanternionów wymaga znajomości czterowymiarowej algebry, która nie jest łatwa. Na szczęscie nie musimy jej znać by używać kwaterionów w programie. U życie ich jest bardzo proste: aby obrócić wektor - wystarczy go przez nie wymnożyć. W naszym przypadku musimy zastosować rotację w weźle sceny do wektora przesunięcia. Możemy pobrać kwanternion reprezentujący rotację poprzez wywołanie SceneNode::getOrientation(). Teraz zastosujemy go do wektora przmieszczenia używając mnożenia.
Musimy też uniknąć innego problemu, związanego z tym, aby szybkość przemieszczania kamery nie zależała od szybkości, z jaką sie zmieniają klatki. Ominiemy go po prostu wymnażając wektor przez czas jaki minął od poprzedniej klatki:
mCamNode->translate(transVector * evt.timeSinceLastFrame, Node::TS_LOCAL);
Teraz spowodujemy jeszcze by można było obracać kamerą za pomocą myszki przy wciśniętym prawym przycisku. Sprawdzmy najpierw czy ów przycisk jest wciśnięty:
if (mMouse->getMouseState().buttonDown(OIS::MB_Right))
{
Będziemy obracać kamerą bazując na przesunięciu myszy pomiędzy tą i poprzednią klatką. Będziemy używać X i Y, określających o ile się mysz przesunęła:
mCamNode->yaw(Degree(-mRotate * mMouse->getMouseState().X.rel), Node::TS_WORLD);
mCamNode->pitch(Degree(-mRotate * mMouse->getMouseState().Y.rel), Node::TS_LOCAL);
}
Przekompiluj i uruchom program. Możemy teraz poruszać się za pomocą klawiatury i obracać kamerą za pomocą myszy. W następnym rozdziale wykorzystamy buforowane wejście, który pomija co klatkową kontrolę, czy dany przycisk został wciśnięty.
Kod źródłowy
edytujDostępny tu jest kompletny kod źródłowy wykorzystany w powyższym artykule.