OGRE/Wersja do druku

Z Wikibooks, biblioteki wolnych podręczników.
Przejdź do nawigacji Przejdź do wyszukiwania


OGRE




Róg strony.svg
 
 


Spis treści[edytuj]

  1. Wstęp
  2. O Ogre
  3. Instalacja
  4. Pierwszy program
  5. Konfiguracja aplikacji
  6. Jak pracuje Ogre
  7. Kamera, światła i cienie
  8. Teren, niebo i mgła
  9. Frame Listener i niebuforowane wejście
  10. Buforowane wejście
  11. Animacja
  12. Bibliografia
  13. GNU Free Documentation License

Wstęp[edytuj]

Książka ta stawia sobie za cel zapoznanie czytelnika z silnikiem graficznym Ogre. Stara się uporządkować materiały zawarte na stronie OGRE wiki (udostępniane na licencji GNU FDL). W większości są to dokładne lub bardziej luźne tłumaczenia z tej strony. Aby móc zrozumieć treść tej książki powinieneś umieć dobrze programować w C++. Kody źródłowe zawarte w tej książce są udostępniane na licencji public domain.

Kody źródłowe opisane w przykładach zostały przetestowane przez jednego z tłumaczy w środowisku Microsoft Visual Studio 2005 z SDK Ogre 1.2.3 'Dagon' dla tej wersji kompilatora.

Autorzy:[edytuj]


O Ogre[edytuj]

Czym jest Ogre?[edytuj]

OGRE (Object-Oriented Graphics Rendering Engine) jest enginem 3D napisanym w C++ na licencji LGPL. Twórcy biblioteki przyjęli za cel ułatwienie programistom tworzenie aplikacji wykorzystujących sprzętowe wspomaganie 3D przez wprowadzenie prostego interfejsu ukrywającego wywołania bibliotek systemowych typu Direct3D czy OpenGL.

Żebyś mogł napisać program wykorzystujący Ogre, będziesz potrzebował karty graficznej wspomagającej akacelerację 3D. Oczywiście niezbędny będzie także kompilator C++. Jak na razie Ogre dobrze współdziała z takimi kompilatorami jak Visual C++ 6/7/7.1/8 i GCC. Natomiast z Bloodshed i Borland Ogre oficjalnie nie współpracuje. Ponadto na pewno przydadzą się różne narzędzia związane z tworzeniem grafiki trójwymiarowej.

Trzeba zaznaczyć, że Ogre jest jedynie silnikiem graficznym. Nie można go traktować jako pełnego silnika gry. Oznacza to, że nie znajdziemy w nim wyspecjalizowanych możliwości związanych z fizyką czy dostępem do sieci a wbudowane biblioteki obsługi peryferiów są ubogie i niekiedy zawierają błędy. Ogre skupia się głównie na tworzeniu grafiki i do tego celu został stworzony. Na szczęście można go powiązać z innymi bibliotekami, które dadzą nam taką możliwość.

Najnowsza wersja produkcyjna z 25 kwietnia 2010 to Ogre 1.7.1 (Cthugha).

Najważniejsze cechy[edytuj]

Do najważniejszych cech Ogre można zaliczyć:

  • Prosty w użyciu interfejs stworzony z myślą zminimalizowana wywołań bibliotek systemowych np. Direct3D, OpenGL, Glide.
  • Obsługa Direct3D 7, 9 i OpenGL.
  • Działa pod systemami Windows, Linux i Mac OS X.
  • Obsługuje vertex i fragment programy (shadery), napisane zarówno w niskopoziomowym assemblerze, jak i w wysokopoziomowym Cg, DirectX 9 HLSL lub OpenGL GLSL.
  • Obsługuje tekstury w formacie PNG, JPEG, TGA, BMP i DDS, dołączając także tekstury 1D, wolumetryczne i inne.
  • Istnieje wiele eksporterów np. z Milkshape3D, 3D Studio Max, Maya, Blender, Wings3D, Sketchup.
  • Posiada elastyczne menadżery scen.
  • Wiele specjalnych efektów np. cząsteczki, mapowanie sferyczne, bump maping.


Instalacja[edytuj]

Oczywiście najpierw należy pobrać OGRE. Mamy dwie możliwości: albo pobrać prekompilowane SDK, albo pobrać pełne źródła i kompilować je samemu. SDK pozwala zaoszczędzić czas na budowanie bibliotek. Z drugiej strony podczas kompilacji ze żródeł można się zapoznać ze wszystkimi szczegółami pracy kodu. (Dla chętnych i lubiących potyczki z błędami możliwe jest pobranie najnowszych źródeł w wersjach jeszcze niestabilnych.)

Na domowej stronie Ogre znajdują się linki do plików, które trzeba pobrać. Wybierz przycisk SDKs albo Source Relases w zależności czy chcesz instalować z SDK, czy ze źródeł.

Instalacja z SDK[edytuj]

  • Ze strony Ogre 3D/SDK's pobierz SDK odpowiedni dla Twojego kompilatora.
  • Ustal w jakim katalogu ma się znajdować SDK dla Ogre 3D. Polecam stosowanie odrębnego katalogu stworzonego bezpośrednio od korzenia dysku w którym będzie przechowywało się wszystkie materiały dotyczące Ogre, a w nim podkatalog OgreSDK (niektóre instalatory same podpowiadają nazwę).
  • Kliknij podwójnie na instalator. W kolejnych okienkach znajdziesz pytanie o potwierdzenie licencji (potwierdź), pytanie o katalog docelowy (wskaż ten z poprzedniego punktu), prawdopodobnie pytanie o miejsce umieszczenia skrótów w menu. Następnie nastąpi rozpakowanie SDK do wskazanego podkatalogu. Wreszcie okienko końcowe z pytaniem czy nie pokazać readme (znajduje się w podkatalogu docs).
  • Sdk został zainstalowany. Sprawdź w zmiennych środowiskowych: powinna zostać wykreowana zmienna OGRE_HOME wskazująca na katalog instalacyjny SDK.
  • Teraz jeszcze konfiguracja...

Ogre SDK Wizard: W przypadku korzystania z Ms Visual C++ od wersji 7 (standardowej lub expres) można zainstalować wizarda, dzięki któremu będzie można ustawiać interesujące nas środowisko projektowe w momencie tworzenia projektu. Zwłaszcza, jeżeli nie chcesz mieć miesięcy straconych na bezustanne próby samodzielnego konfigurowania projektu i multum różnorakich błędów (najczęśćiej 71 błędów linkera o Naprawdę Długich Nazwach, zaczynających się mniej więcej tak _delspec(dllimport)DalszaCżęśćBłędu- to naprawdę częste i denerwujące błędy)

  • Ściągnij Ogre SDK Wizard opisany na stronach The Complete Blanks Guide To Using The OGRE SDK AppWizard. Wybierz odpowiedni dla Twojego kompilatora.
  • Rozpakuj go do nowego podkatalogu wewnątrz katalogu SDK. Będzie musiał on tam już pozostać.
  • Uruchom skrypt VC*_setup.js (w systemie Windows Vista oraz Windows 7 mogą być problemy instalacją skryptu). Po zakończeniu wykonania wyświetli komunikat o prawidłowej instalacji.
  • Od tego momentu przy tworzeniu nowego projektu będziesz miał możliwość wyboru szablonu "Ogre SDK Application" z kilkoma opcjami. Szablon ten przygotuje automatycznie środowisko dla projektu.
  • Niekiedy trzeba dodawać katalogi z kodami źródłowymi (Project properties/Configuration properties/C\C++/Additional Include Directories) lub katalogów z bibliotekami do linkowania (Project properties/Configuration properties/Linker/Additional Library Directories). Przy pierwszych próbach najczęściej będą to wskazania do odpowiednich podkatalogów, podkatalogu "samples".

Ms Visual C++ 8 Express

Można w pełni korzystać z SDK przy użyciu Microsoft Visual C++ 2005 (v8) Express. Jednakże należy przed zainstalowaniem SDK Ogre wykonać wszystkie czynności opisane na stronie http://msdn.microsoft.com/vstudio/express/visualc/usingpsdk/default.aspx Uwaga- wersja 2008 oraz 2010 mają już Platform SDK w standardzie.

Instalacja z kodów źródłowych[edytuj]

Pobierz kody źródłowe dla twojego systemu operacyjnego. Rozpakuj archiwum ze źródłami gdziekolwiek (np. F:\Ogre). Z archiwum zostanie rozpakowany katalog nazwany ogrenew. Nie zmieniaj struktury, ponieważ może to powodować różne problemy.

Potem, jeśli używasz Windows, z tej samej lokacji pobierz zależności określone dla twojego kompilatora. Dostępne są przekompilowane wersje dla Visual Studio 7.1, 8 i MinGW ([1]). Rozpakuj archiwa do nowo utworzonego katalogu (W przykładzie do F:\Ogre\ogrenew). Te archiwum zawiera gałąź plików, które są standardem w Linuksie, lecz zazwyczaj nie są dołączane do Windows.

Pobieranie najnowszych źródeł[edytuj]

Jak powiedzieliśmy, możliwe jest zastosowanie najnowszych źródeł nad którymi właśnie pracuje grupa tworząca Ogre. Na pewno nie można liczyć, że są one stabilne. Stosować je można jeżeli też się przyłączamy do prac tworzenia Ogre ale na pewno nie należy ich używać jeżeli chcemy tworzyć własne oprogramowanie na bazie Ogre.

Zbiory żródeł Ogre i jego plug-inów przechowywane są w systemie CSV (Concurent Version System). Dostępne są z http://ogre.cvs.sourceforge.net. By ściągnąć je w całości możemy zastosować klienta CSV ściągniętego ze strony CSV. Następnie tworzymy na dysku katalog do którego będziemy ściągać źródła. Następnie przechodzimy do tego podkatalogu (w Windows za pomocą 'okienka DOS') i wydajemy komendę:

c:\> cvs -d:pserver:anonymous@ogre.cvs.sourceforge.net:/cvsroot/ogre login

W momencie zapytania o hasło wciskamy po prostu ENTER. A następnie piszemy:

c:\> cvs -d:pserver:anonymous@ogre.cvs.sourceforge.net:/cvsroot/ogre co -P ogrenew

W tym momencie rozpoczyna się ściąganie źródeł do katalogu. Powtórzenie powyższych czynności w katalogu w którym wcześniej ściągnięto źródła spowoduje tylko uaktualnienie zmienionych plików.

Budowanie Ogre[edytuj]

Ten punkt jest wskazany do budowania silnika OGRE i demand demos.

Budowanie pod Visual C++[edytuj]

Na początku musisz upewnić się czy posiadasz zainstalowany Platform SDK dla swojego systemu operacyjnego. Można je pobrać z http://www.microsoft.com/platformsdk. Trzeba mieć również zainstalowany DirectX SDK, który można pobrać z http://msdn.microsoft.com/directx/sdk/. Po rozpakowaniu źródeł i depediencies jak powiedziano wyżej, przejdź do katalogu 'ogrenew'. Znajdziesz tam pliki definicji solucji Visual Studio (*.sln). Otwórz właściwy dla twojego kompilatora. Dla VC++ 7.1 jest to 'Ogre.sln' a dla VC++ 8.0 jest to 'Ogre_vc8.sln'. Uruchomi się środowisko projektowe i załadują się moduły projektów solucji. Odszukujemy projekty:

  • PlatformManager_Win32
  • RenderSystem_Direct3D9

i w parametrach dodajemy do katalogów źródeł katalog include z DirectX SDK a w katalogach bibliotek (linker) katalog Lib/'typ_systemu' z DirectX SDK. Teraz wybieramy z menu 'Build/Build Solution' i spokojnie czekamy na zakończenie kompilacji. Podczas kompilacji dopuszczalne są wszelkiego rodzaju ostrzeżenia (np. o możliwej utracie danych przy zdefiniowanych w kodzie źródłowych przekształceniach typów wartości oraz o braku vc**.pdb). Nie przejmujmy się tym, gdyż nie wpływają na jakość kompilacji. Dopiero gdy dostaniemy błędy należy im się dokładnie przyjrzeć i spróbować rozwiązać związane z nimi problemy szukając informacji na forach Ogre.

Utworzone binaria znajdą się w katalogu ogrenew\Samples w odpowiednich podkatalogach. Na przykład wspólne dla wszystkich programów w \Common\bin (Debug lub Relase).

Kompilacja z wykorzystaniem GCC 3.3/3.4/4.0[edytuj]

Jeżeli używasz dystrybucji Ubuntu skorzystaj z tego tutoriala (angielski). Jeżeli przeszedłeś przez niego bez błędów omiń poniższe rady instalacji OGRE.

Użyj tych instrukcji do budowania Ogre pod Linuksem albo nawet MingW, BSD lub innych, które używają GCC i autotools. Najpierw powinieneś posiadać zainstalowane poniższe programy. Ogre 1.0.3 powinien zostać przekompilowane z tymi lub późniejszymi wersjami.

automake      1.9.5
autoconf      2.59a
make          3.80
libtool       1.5.6
pkg-config    0.17.2
gcc           3.3.5
cpp           3.3.5

Następnie potrzebujesz kilka zależności w tym DevIL, zzlib, freetype i możliwie SDL i CG. Dla Debiana to polecenie chwyta wszystkie potrzebne zależności:

 apt-get install g++ gcc cpp make automake1.9 libtool \
    libsdl1.2-dev libdevil-dev libfreetype6-dev libgtkmm2.0-dev \
    libcegui-mk2-0 libcegui-mk2-dev libcegui-mk2-doc libxerces26-dev \
    libpng3-dev libmng-dev libtiff4-dev liblcms1-dev libpng3 libjpeg62-dev \
    libglademm2.0-dev libzzip-dev libxaw7-dev\
    libopenexr-dev libopenexr2 nvidia-cg-toolkit

Użytkownicy Gentoo potrzebują tych ebuildów:

 sys-devel/automake sys-devel/autoconf sys-devel/libtool dev-util/cppunit
 sys-devel/gcc media-libs/libsdl media-libs/devil media-libs/freetype
 dev-cpp/gtkmm dev-java/xerces media-libs/libpng media-libs/tiff
 media-libs/lcms media-libs/jpeg  dev-cpp/libglademm dev-libs/zziplib
 x11-libs/Xaw3d media-libs/openexr media-gfx/nvidia-cg-toolkit

Opcjonalnie (jest to zamaskowane w gentoo): dev-games/cegui

Po pobraniu źródeł dla Ogre pod Linuksa z Downloads, rozpakuj archiwa i zainstaluj w ten sposób:

 tar xjf ./ogre*
 cd ogrenew
 aclocal
 ./bootstrap
 ./configure           (Dla nvidii dodaj --with-platform=GLX)
 make
 make install          (zazwyczaj musisz to uruchomić jako root)

Przechodź od każdego kroku do następnego, jeśli nie wystąpił żaden błąd. Ostrzeżeniami oczywiście się nie przejmuj, to normalne, że występują. Jeśli wystąpił błąd przeszukaj forum (angielskie). Nie zadawaj pytań, jeśli wpierw go nie przeszukałeś. Proste problemy były na pewno wielokrotnie omawiane.

Być może dobrym wątkiem na temat kompilacji pod Linuksem jest ten (angielski). Jest na temat Debiana, ale powinien się nadawać także do innych dystrybucji. Możesz także przeczytać ten (angielski) plik.

Kiedy Ogre zostanie przekompilowane, przykładowe aplikacje zostaną będą się znajdować w katalogu ./ogrenew/Samples/Common/bin.


Pierwszy program[edytuj]

Po skompilowaniu przykładu zobaczymy taki wynik.

Windows[edytuj]

#include "Ogre.h"
#include "ExampleApplication.h"
#include <windows.h>

// Dziedziczymy ExampleApplication
class FirstApp : public ExampleApplication
{
  public:

     FirstApp() { }

     /** createScene jest funkcją czysto wirtualną w ExampleApplication,
      *  nadpisujemy ją, aby nic nie robiła.
      *  Na początku tworzy ona pustą scenę.
      **/
     void createScene(void) { }
};


INT WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,INT) {
   FirstApp myApp;     // Tworzymy instancję naszej klasy
   try {           // Wychwytuje powstałe wyjątki, które mogą powstać
       myApp.go(); // ExampleApplication dostarcza metodę go, która rozpoczyna rendering
   } catch (Ogre::Exception& e) {
       MessageBox( NULL, e.getFullDescription().c_str(), "Wyjątek!",
           MB_OK | MB_ICONERROR | MB_TASKMODAL);
   }
}

Linux[edytuj]

#include <Ogre.h> /* Wszystkie nagłówki OGRE */
#include <iostream> /* Umożliwia wypisywanie błędów na konsole */
#include "ExampleApplication.h"


// Dziedziczymy ExampleApplication
class FirstApp : public ExampleApplication
{
   public:

   FirstApp() { }
   /** createScene jest funkcją czysto wirtualną w ExampleApplication, nadpisujemy ją, aby nic nie robiła.
     *  Na początku tworzy ona pustą scenę.
     **/
   void createScene(void) { }
};


int main(int argc, char **argv) {
  FirstApp myApp;  // Tworzymy instację naszej klasy
  try {
    myApp.go(); // ExampleApplication dostarcza metodę go, która rozpoczyna rendering

    return 0; // Zwraca 0 w przypadku powodzenia
  } catch (Ogre::Exception& e) {
    std::cerr <<"Wyjątek:\n";
    std::cerr <<e.getFullDescription().c_str() <<"\n";
    return 1; // Zwrócenie liczby różnej od zera oznacza niepowodzenie
  }
}

Dodatkowe informacje związane z Linuksem[edytuj]

Pliki Samples/Common/include/ExampleApplication.h, Samples/Common/include/ExampleFrameListener.h i Samples/Common/include/ExampleLoadingBar.h powinny zostać przekopiowane do katalogu, w którym chcesz zapisać plik z kodem źródłowym.

Zapisz plik jako main.cpp i przekompiluj go poleceniem:

$ gcc `pkg-config --cflags OGRE` `pkg-config --libs OGRE` main.cpp

Następnie przekopiuj plik wykonywalny a.out do Samples/Common/bin, przejdź do tego katalogu i uruchom go poleceniem

$ ./a.out

Jeśli pkg-config nie może odnaleźć OGRE utwórz plik OGRE.pc w odpowiednim katalogu za pomocą polecenia:

$ ln -s /usr/local/lib/pkgconfig/OGRE.pc /usr/lib/pkgconfig/OGRE.pc

Oczywiście zakładamy, że plik OGRE.pl znajduje się w /usr/local/lib/pkgconfig/.

Zamiast kopiować przekompilowany program do Samples/Common/bin możesz przekopiować pliki plugins.cfg i resources.cfg do katalogu ze swoim projektem, a następnie skonfigurować ścieżki w resources.cfg. W przypadku tworzenia większych projektów jest to najlepsze rozwiązanie.

Konfiguracja aplikacji[edytuj]

OGRE/Konfiguracja aplikacji

Jak pracuje Ogre[edytuj]

Menadżer sceny[edytuj]

Wszystko, co jest pokazywane na ekranie jest zarządzane przez menadżera sceny (SceneManager). SceneManager jest klasą, która przechowuje ścieżki obiektów układanych w scenie. Kiedy tworzysz kamerę, z której patrzysz się na scenę, menadżer sceny zachowuje ścieżkę do niej. Menadżer sceny pamięta także ścieżkę do tworzonych kwadratów, billboardów, świateł itp.

Mamy wiele typów menadżerów scen. Wśród nich są między innymi odtwarzające teren, tworzące drzewa BSP i inne.

Jednostka[edytuj]

Jednostka (Entity) jest jednym z rodzajów obiektów, które możesz renderować w scenie. Można ją zdefiniować jako coś, co jest reprezentowane przez siatkę 3D (mesh). Może nią być np. robot, ryba, teren. Z kolei światło, billboard, cząsteczki (particles), lub kamera nimi nie są.

Ogre oddziela obiekty, które są renderowane od ich lokacji i orientacji. Tych parametrów nie możesz ustawić bezpośrednio w jednostce, natomiast musisz powiązać jednostkę (Entity) z węzłem sceny (SceneNode). Węzeł sceny zawiera informacje o położeniu i orientacji.

Węzeł sceny[edytuj]

Jak już było wspomniane węzeł sceny przechowuje ścieżkę do położenia i orientacji wszystkich obiektów, które są z nim powiązane. Jednostka nie jest renderowana, dopóki nie powiążesz ją do węzła sceny. Podobnie sam węzeł sceny nie jest obiektem wyświetlanym na ekranie. Natomiast zostanie coś pokazane wtedy, gdy utworzysz węzeł sceny i powiążesz do niego jakąś jednostkę (lub inny obiekt).

Do węzła sceny może zostać powiązana dowolna liczba obiektów. Załóżmy, że mamy postać i chcemy wygenerować światło wokół niej. Najpierw powinniśmy utworzyć węzeł sceny, potem jednostkę dla postaci i powiązać ją z węzłem sceny. Następnie tworzymy światło i powiązujemy go z węzłem sceny. Węzeł sceny może także być powiązany z innym węzłem sceny.

Należy dodać, że pozycja węzła sceny jest zawsze relatywna od swojego rodzica. Każdy menadżer sceny zawiera korzeń, do którego są powiązane wszystkie inne węzły sceny.

Pierwsza aplikacja[edytuj]

W dalszej części oprzemy się na zalążku programu OGRE, który należy wkleić w pierwszy utworzony plik .cpp. Upewnij się że Twoje środowisko projektowe jest skonfigurowane zgodnie z opisem w rozdziale Środowisko projektowe. Nie zapomnij do konfiguracji kompilatora dodać katalogu $(OGRE_HOME)\samples\include jako jednego z katalogów z plikami źródłowymi. Przekompiluj zalążek programu i uruchom go by sprawdzić czy wszystko jest prawidłowo skonfigurowane.

Podczas pracy już rozbudowanego programu możesz używać przycisków myszy i przesuwać lub ruszać mysz, aby się obrócić. Przycisk Escape kończy pracę programu.

Dodajemy robota[edytuj]

Robot w Ogre.

Poszukajmy w naszym kodzie w definicji klasy myApp funkcję createScene. Skorzystamy z tej funkcji, aby osiągnąć nasz cel, czyli wstawić robota. Ustawimy najpierw kolor światła otoczenia za pomocą funkcji setAmbientLight. Należy nadmienić, że argumentami konstruktora ColorValue są trzy wartości dla koloru czerwonego, zielonego, niebieskiego w przedziale [0..1]. Wstawmy więc do niej linię:

       mSceneMgr->setAmbientLight( ColourValue( 1, 1, 1 ) );

Teraz potrzebujemy utworzyć jednostkę. W tym celu wywołujemy metodę createEntity zawartą w SceneManager:

       Entity *ent1 = mSceneMgr->createEntity( "Robot", "robot.mesh" );

Dwie sprawy wymagają wyjaśnienia -- skąd się wziął mSceneMgr i czym jest, a także jakie argumenty przyjmuje createEntity. mSceneMgr to wskaźnik wskazujący na bieżący menadżer sceny, utworzony przez klasę ExampleApplication. Pierwszym argumentem metody createEntity jest nazwa obiektu. Każda jednostka musi mieć unikalną nazwę. Jeśli spróbujesz utworzyć drugą jednostkę o tej samej nazwie zostanie zwrócony błąd. Drugi argument (w tym przypadku "robot.mesh") określa siatkę z jakiej ma korzystać jednostka. Plik opisujący siatkę 'robot.mesh' dostajemy wraz z Ogre. Znajduje się on w katalogu 'media/models'.

Następnym krokiem jest utworzenie węzła sceny. Ponieważ menadżer sceny przechowuje korzeń sceny, dlatego musimy też utworzyć węzeł sceny, który będzie jego synem. Zrobimy to w ten sposób:

       SceneNode *node1 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode" );

Wywołujemy getRootSceneNode otrzymujemy korzeń sceny, następnie z korzenia wywołujemy createChildSceneNode. W ten sposób utworzyliśmy węzeł sceny, który jest synem korzenia. Na ten węzeł wskazuje teraz node1. Argumentem createChildSceneNode jest nazwa tworzonego węzła. Podobnie, jak podczas tworzenia jednostek, nazwa nie może się powtarzać.

Na koniec powiązujemy jednostkę z węzłem. I w ten sposób będziemy mogli zobaczyć naszego robota.

       node1->attachObject( ent1 );

Przekompiluj teraz program i uruchom go. Powinieneś zobaczyć na ekranie robota.

Uwaga! Jeśli resources.cfg nie jest odpowiednio skonfigurowany, możesz nic nie zobaczyć. Aby nie było problemów powinieneś ustawić odpowiednio ścieżki resources.cfg tak by wskazywały istniejące i właściwe katalogi podkatalogu media. W poniższym przykładzie katalog media znajdziemy wychodząc dwukrotnie w górę z katalogu z binariami. Układ katalogów jest zgodny z podanym w rozdziale Środowisko projektowe. Można także podawać ścieżki w sposób bezwzględny:

FileSystem=../../media/materials/programs
FileSystem=../../media/materials/scripts
FileSystem=../../media/materials/textures
FileSystem=../../media/materials/models
FileSystem=../../media/models

Współrzędne a wektory[edytuj]

Ogre, a także wiele innych silników graficznych używa osi x i z jako poziomą płaszczyznę, a oś y jako pionową. Względem monitora osie idą tak (kierunek od ujemnych do dodatnich współrzędnych): współrzędne osi x idą od strony lewej do prawej, osi y od dołu do góry, a współrzędne z osi z idą od głębi, wychodząc z monitora.

Jak nasz robot jest zwrócony wzdłuż dodatniego kierunku x? To zależy od samej siatki, a także od tego, jak ją zaprojektujesz. Ogre nie zakłada, jak ma mieć orientację model. Każda wczytywana siatka może mieć różny "kierunek początkowy", który określa, jak jest zwrócona siatka.

Ogre używa klasy Vector zarówno do reprezentowania pozycji, jak i kierunku, nie posiada natomiast klasy Point. Istnieją wektory określone dla 2, 3 i 4 wymiarów, nazywają się odpowiednio Vector2, Vector3, Vector4. Najczęściej używany jest Vector3. Jeśli nie jesteś zaznajomiony z wektorami, zobacz Wektory, zanim zaczniesz robić coś poważnego w Ogre. Matematyka oparta na wektorach może się okazać bardzo przydatna, jeśli rozpoczniesz pracę nad skomplikowanymi programami.

Dodawanie innych obiektów[edytuj]

Zrozumiawszy, jak pracuje system współrzędnych, możemy powrócić do naszego kodu. W liniach, które wcześniej zapisaliśmy nie określiliśmy położenia, w którym ma się znajdować robot. Większość funkcji w Ogre mają domyślne argumenty np. SceneNode::createChildSceneNode ma trzy argumenty: nazwa węzła sceny, initializowana pozycja (lokacja) i obrót (orientacja). Współrzędna którą widzieliśmy została ustawiona na (0,0,0). Utwórzmy teraz inny węzeł sceny, lecz tym razem określimy początkową pozycję, inną niż domyślna:

        Entity *ent2 = mSceneMgr->createEntity( "Robot2", "robot.mesh" );
        SceneNode *node2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode2", Vector3( 50, 0, 0 ) );
        node2->attachObject( ent2 );

Kod ten jest podobny do poprzedniego z dwoma wyjątkami. Pierwszy -- zmieniliśmy trochę nazwę jednostki i węzła sceny. Drugi -- zmieniliśmy pozycję początkową na odległą od korzenia sceny o 50 jednostek miary (nie mylić z jednostkami - Entity) współrzędnej x (pamiętamy, że pozycja węzła sceny jest względne do swojego rodzica). Przekompiluj i uruchom demo. Będziemy mieli dwa roboty koło siebie.

Więcej o jednostkach[edytuj]

Klasa Entity jest bardzo rozległa i nie zostaną tutaj opisane wszystkie jej możliwości, tylko tyle, ile potrzebuje początkujący. Opiszemy natomiast tutaj kilka przydatnych metod.

Za pomocą metod Entity::setVisible i Entity::isVisible możemy ustawić, żeby jednostka była widoczna lub nie, albo sprawdzić czy jest widoczna. Jeśli potrzebujesz ukryć jednostkę, lecz później ją chcesz znowu pokazać nie musisz usuwać jej, a potem tworzyć jej od nowa. Wystarczy, jeśli użyjesz tej funkcji.

Funkcja getName zwraca nazwę jednostki, a getParentSceneNode zwraca węzeł sceny, do którego dana jednostka jest powiązana.

Więcej o węzłach sceny[edytuj]

Klasa SceneNode jest bardzo złożona. Za pomocą SceneNode można zrobić wiele rzeczy, zostaną tutaj jednak przedstawione te najbardziej użyteczne.

Możesz pobrać i ustawić pozycję węzła sceny za pomocą metody getPosition i setPosition (zawsze względnie do swojego rodzica). Możesz także przenieść obiekt względnie do bieżącej pozycji używając metody translate.

SceneNode możemy także przeskalować i obrócić. Skalę możemy zmienić za pomocą funkcji scale. Możesz użyć funkcji yaw, roll i pitch, aby obrócić obiekty. Metoda resetOrientation resetuje wszystkie rotacje zrobione na obiekcie. Można także użyć setOrientation, getOrientation i rotate do bardziej zaawansowanych obrotów.

Poprzednio użyliśmy funkcji attachObject. Funkcje podobne do tej są przydatne, jeśli potrzebujesz manipulować obiektami powiązanymi z węzłem sceny: numAttachedObjects, getAttachedObject (jest wiele wersji tej funkcji), detachObject (także wiele wersji), detachAllObjects. Posiada także wiele funkcji wykonująmi pewne zadania na rodzicu i dzieciach węzła.

Ponieważ wszystkie pozycje/translacje robione są względnie do rodzica węzła sceny, możemy przenieść wspólnie dwa węzły sceny w sposób bardzo prosty. Jak na razie mamy taki kod w naszej aplikacji:

        Entity *ent1 = mSceneMgr->createEntity( "Robot", "robot.mesh" );
        SceneNode *node1 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode" );
        node1->attachObject( ent1 );
  
        Entity *ent2 = mSceneMgr->createEntity( "Robot2", "robot.mesh" );
        SceneNode *node2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode2", Vector3( 50, 0, 0 ) );
        node2->attachObject( ent2 );

Zamieńmy 6 linię:

        SceneNode *node2 = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode2", Vector3( 50, 0, 0 ) );

na

        SceneNode *node2 = node1->createChildSceneNode( "RobotNode2", Vector3( 50, 0, 0 ) );

W ten sposób czynimy RobotNode2 dzieckiem RobotNode. Przenosząc node1 będziemy przenosić node2 względem jego, ale przenosząc node2 nie będziemy zmieniać node1. Ten przykład przeniesie tylko RobotNode2:

        node2->translate( Vector3( 10, 0, 10 ) );

Natomiast ten poniższy kod przenosi RobotNode, a ponieważ RobotNode2 jest jego dzieckiem, RobotNode2 zostanie także przeniesiony:

        node1->translate( Vector3( 25, 0, 0 ) );

Kod źródłowy[edytuj]

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


Kamera, światła i cienie[edytuj]

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. Dodamy dodatkowo dwie metody do klasy MyApp - createViewport i createCamera. Odszukaj linie:

     /** createScene jest funkcją czysto wirtualną w ExampleApplication,
      *  nadpisujemy ją, aby nic nie robiła.
      *  Na początku tworzy ona pustą scenę.
      **/
     void createScene(void)
     {
     }

i zamień je na:

     virtual void createCamera(void)
     {
     }
 
     virtual void createViewports(void)
     {
     }
 
     void createScene(void)
     {
         Entity *ent;
         Light *light;
     }

Funkcje te zostały wcześniej zdefiniowane w dziedziczonej klasie ExampleApplication. Teraz dowiemy się, jak tworzyć kamerę i jak używać viewportów.

Kamera[edytuj]

Do czego służy[edytuj]

Kamera umożliwia oglądanie utworzonej sceny. Jest to specjalny obiekt, którym możemy się posługiwać podobnie jak węzłem sceny. Z obiektu klasy Camera możemy wywołać takie metody jak: setPosition, yaw, roll i pitch. Możemy ją także powiązać do dowolnego węzła sceny. Tak jak w węźle sceny, pozycja kamery jest względna do swojego rodzica. Jeśli kamera zostanie powiązana do pewnego węzła sceny, możemy bez problemu obracać i przemieszczać ją używając tylko tego węzła.

Za jednym razem możesz użyć tylko jednej kamery. Nie tworzymy kilku kamer, aby oglądać różne części sceny i w odpowiednim momencie przełączać z której kamery mamy widzieć. Zamiast tego tworzymy węzły sceny, które będą nam służyć jako "rączki". Zostaną one umiejscowione w scenie i będą wskazywać, na co patrzy kamera. Jeśli będziemy chcieli zmienić część sceny, na którą ma patrzeć kamera, po prostu powiążemy kamerę z innym węzłem sceny - inną "rączką".

Tworzenie kamery[edytuj]

Aby ją utworzyć, nadpiszemy domyślną funkcję znajdującą się w ExampleApplication. Ponieważ kamera ma się znajdować wewnątrz menadżera sceny, będziemy jego używać, aby ją stworzyć. Znajdźmy metodę MyApp::createCamera i dodajmy do niej poniższą linię do kodu:

        // tworzenie kamery
        mCamera = mSceneMgr->createCamera("PlayerCam");

W ten sposób mogliśmy utworzyć kamerę o nazwie "PlayerCam". Aby dostać się do kamery o pewnej nazwie, możemy wykorzystać funkcję SceneManager::getCamera. Nie musimy wtedy zapamiętywać wskaźnika do niej.

Teraz ustawmy pozycję kamery, a także kierunek w jakim ma być zwrócona. Ponieważ mamy zamiar umiejscawiać obiekty w okolicach początku układu współrzędnych, ułożymy kamerę w odpowiedniej odległości od niego i zwrócimy kamerę w kierunku początku układu współrzędnych. Dodaj poniższy kod za poprzednio wstawionymi linijkami:

        // ustawianie pozycji i kierunku
        mCamera->setPosition(Vector3(0,10,500));
        mCamera->lookAt(Vector3(0,0,0));

Funkcja lookAt jest bardzo użyteczna i prosta. Dzięki niej możesz ustawić kierunek kamery, bez potrzeby używania wyspecjalizowanych funkcji yaw, rotate, i pitch. Klasa SceneNode także posiada taką funkcję. Okazuje się to w wielu przypadkach bardzo przydatne.

Ostatecznie ustawimy near clipping na 5. Near clipping określa od jakiej odległości od kamery będziemy widzieli obiekty, jeśli coś będzie bliżej to nie będzie widoczne. Okazuje się przydatny w sytuacjach, kiedy obiekty są bardzo ciasno ułożone i jeden obiekt często przysłania inny. Wówczas zwiększając tę wartość możemy zlikwidować ten problem, ponieważ obiekty które są za blisko nie będą widoczne. Ponadto zmniejszając tę wartość przyspieszamy proces renderingu.

        mCamera->setNearClipDistance(5);

Możemy także wykorzystać podobną funkcję setFarClipDistance, która ustawia na jaką maksymalną odległość widzimy obiekty.

Viewport[edytuj]

Viewport w Ogre[edytuj]

Używania vieportów jest bardzo pomocne, kiedy zaczynamy używać wielu kamer. Dzięki zapoznaniu się z tym tematem będziemy mogli zrozumieć, jak nasz silnik graficzny określa, z której kamery renderowana jest scena. Ogre może obsługiwać wiele menadżerów sceny na raz. Daje także możliwość podzielenia ekranu na wiele części, a na każdym z tych obszarów pokazany jest widok z innej kamery np. jest tak w grach na 2 osoby, ekran jest wtedy podzielony na dwie części. Tym problemem zajmiemy się w innym rozdziale.

Aby zrozumieć, w jaki sposób Ogre renderuje scenę, rozważymy trzy konstrukcje z Ogre: kamera, menadżer sceny i okno renderingu (klasa RenderWindow). Okno renderingu to normalne okno, w którym wszystko jest pokazywane. Poprzez menadżera sceny tworzymy kamerę, którą oglądamy scenę. Ponadto potem musimy określić, z której kamery ma być wyświetlany dany viewport. Z reguły będziemy tworzyli tylko jedną kamerę przeznaczoną dla całego okna.

Tworzenie viewportu[edytuj]

Nadpiszmy metodę w ExampleApplication tworzącą viewport. W tym celu znajdźmy funkcję MyApp::createViewports i użyjmy metody addViewport z klasy RenderWindow. Skorzystamy ze wskaźnika mWindow, który wskazuje na obiekt typu RenderWindow. Został on wcześniej utworzony w dziedziczonej klasie ExampleApplication. Wstawmy do createViewports takie oto linie:

        // Tworzenie jednego viewport, na całe okno
        Viewport* vp = mWindow->addViewport(mCamera);

Właśnie utworzyliśmy nasz viewport. Co możemy z nim zrobić? Nie za wiele. Najbardziej przydatnymi czynnościami jest ustawianie koloru tła, za pomocą funkcji setBackgroundColour. Ustawmy więc kolor tła na czarny:

        vp->setBackgroundColour(ColourValue(0,0,0));

Argumentami ColorValue są kolejno współczynnik koloru czerwonego, zielonego i niebieskiego. Kolory te są podawane w przedziale od 0 do 1. Przydatne jest jeszcze ustawienie proporcji widoku z naszej kamery. Jeśli używamy innego niż standardowy pełnoekranowy viewport, wtedy wygląd sceny może wyglądać nienaturalnie i być bardzo rozciągnięty. Wyrównajmy nasz widok używając domyślnych proporcji:

        // Zmieniamy proporcje widoku kamery na zgodny z naszym viewportem
        mCamera->setAspectRatio(Real(vp->getActualWidth()) / Real(vp->getActualHeight()));

I to wszystko czego potrzebowaliśmy, aby stworzyć własny viewport.

Światło i cień[edytuj]

Rodzaje cieni[edytuj]

Aktualnie Ogre obsługuje trzy rodzaje cieni:

  • Modulative Texture Shadows (SHADOWTYPE_TEXTURE_MODULATIVE) - daje najgorszy efekt. Tworzy cień jako teksturę czarno-białą (1 bitowa).
  • Modulative Stencil Shadows (SHADOWTYPE_STENCIL_MODULATIVE) - uzyskujemy lepszy efekt. Polega na renderowaniu wszystkich wartości cienia przez modulację po wszystkich nieprzezroczystych obiektach w scenie. Nie jest tym samym, co Additive Stencil Shadows.
  • Additive Stencil Shadows (SHADOWTYPE_STENCIL_ADDITIVE) - technika ta polega na tym, że cień jest renderowany kolejno dla każdej lampy. Proces ten jest bardzo wymagający dla karty graficznej, ale daje najlepszy efekt.

Silnik Ogre nie wspomaga miękkich cieni (soft shadows). Jeśli potrzebujesz ich użyć musisz samemu napisać vertex lub fragment programu.

Używanie cienia w Ogre[edytuj]

Nasz nindża oświetlony przez trzy różne lampy, o innych kolorach

Korzystanie z cienia w Ogre jest dosyć proste. Klasa SceneManager posiada metodę setShadowTechnique, której możemy użyć, aby ustawić rodzaj cienia. Ponadto jeśli wykorzystamy tę funkcję, będziemy musieli określić dla każdej tworzonej jednostki, czy ma rzucać cień, czy nie. Służy do tego funkcja setCastShadows.

Najpierw ustawimy światło otoczenia, a potem typ cienia. W tym celu odnajdźmy funkcję MyApp::createScene i wstawmy do niej poniższy kod:

        mSceneMgr->setAmbientLight( ColourValue(0, 0, 0) );
        mSceneMgr->setShadowTechnique( SHADOWTYPE_STENCIL_ADDITIVE );

Dzięki temu możemy użyć cienia o nazwie additive stencil shadows.

UWAGA! Wstawiając te linie może wystąpić błąd kompilacji wyglądający mniej więcej tak:

"variable 'vtable for Ogre::MeshPtr' can't be auto-imported. Please read the documentation for ld's  --enable-auto-import for details."

Aby go pominąć będziemy musieli wstawić -Wl,--enable-runtime-pseudo-reloc do opcji linkera.

Możemy teraz wstawić jednostkę, która będzie rzucała cień.

        ent = mSceneMgr->createEntity( "Ninja", "ninja.mesh" );
        ent->setCastShadows( true );
        mSceneMgr->getRootSceneNode()->createChildSceneNode( )->attachObject( ent );

Dzięki tym liniom mogliśmy wczytać siatkę 3D ninja.mesh i wstawić ją do sceny.

Następnym krokiem będzie wstawienie podłoża, na którym będzie stał nindża i będzie widoczny cień. Zrobimy to tworząc prosty kwadrat. W tym celu użyjemy menadżera siatki (MeshManager).

       Plane plane( Vector3::UNIT_Y, 0 );

Utworzyliśmy definicję płaszczyzny, którą możemy wykorzystać. Klasa MeshManager przechowuje ścieżkę wszystkich siatek wczytanych do programu (np. robot.mesh, ninja.mesh). Wykorzystajmy teraz funkcję createPlane operującą na definicji płaszczyzny w celu utworzenia odpowiedniej siatki (a ściślej kwadratu):

       MeshManager::getSingleton().createPlane("ground",
           ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane,
           1500,1500,20,20,true,1,5,5,Vector3::UNIT_Z);

Wielkość naszej płaszczyzny ustawiliśmy na 1500 i nazwaliśmy ją "ground". Powinniśmy jeszcze utworzyć jednostkę, która da nam możliwość umiejscowienia w scenie naszej siatki:

       ent = mSceneMgr->createEntity( "GroundEntity", "ground" );
       mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(ent);

I na koniec musimy ustawić jeszcze dwie rzeczy: pierwsza - musimy poinformować menadżera sceny, że nasza płaszczyzna nie rzuca cienia (ale na niej będzie się pokazywał cień) i druga - chcemy nadać temu obiektowi jakąś teksturę. Siatki robot i nindża mają już ustawione materiały, jednak jak ręcznie tworzymy siatkę, nie mamy określonej domyślnej tekstury. Użyjmy tekstury Examples/Rockwall, która jest skryptem materiału dostępnym w przykładach Ogre:

       ent->setMaterialName( "Examples/Rockwall" );
       ent->setCastShadows( false );

I w końcu możemy przekompilować i uruchomić nasz program. Niestety na ekranie widać ciemność... tylko ciemność... Trzeba by ją rozjaśnić i zapalić jakieś światło.

Rodzaje świateł[edytuj]

Ogre posiada trzy typy świateł:

  • Punktowe (LT_POINT) - światło idzie z pewnego miejsca we wszystkich kierunkach
  • Stożkowe (LT_SPOTLIGHT) - światło oświetlające w podobny sposób jak latarka. Możesz ustawić, gdzie światło się zaczyna, a także jego kierunek. Ponadto możesz także określić, jak duży ma być kąt pomiędzy jednym okręgiem światła, a drugim.
  • Kierunkowe (LT_DIRECTIONAL) - naśladuje odległe światło, idące w stronę sceny w pewnym kierunku. Przydatne jest, kiedy na przykład chcemy stworzyć noc i dodać do niej światło księżyca. Wstawiamy wtedy ten rodzaj światła i wskazujemy pod jakim kątem odbija się światło idące z księżyca. Inną alternatywą jest odpowiednie skonfigurowanie światła otoczenia, jednak nie osiągnie się zbytniej realistyki, ponieważ scena będzie oświetlona wszędzie tak samo.

Klasa Lights posiada wiele parametrów określających wygląd światła. Najbardziej popularnymi są kolor dyfuzji i odbicia. Każdy skrypt materiału określa stopień dyfuzję i odbicia światła. Zajmiemy się tym później.

Tworzenie światła[edytuj]

Aby utworzyć światło użyjemy metody z klasy SceneManager o nazwie createLight, której argumentem jest nazwa światła. Po utworzeniu światła, możemy zarówno ustawić manualnie pozycję, jak i powiązać z węzłem sceny dzięki któremu będziemy mogli przesuwać lampę. W odróżnieniu od kamery, światło ma tylko metody setPosition i setDirection (a nie pełny zestaw funkcji funkcji związanych z ruchem jak translate, pitch, yaw, roll itp.). Jeśli potrzebujesz utworzyć stacjonarne światło, powinieneś użyć funkcji setPosition, natomiast jeśli światło ma się przemieszczać (np. gdy światło ma się znajdować zawsze koło bohatera), wtedy powinieneś powiązać światło z węzłem sceny.

Utworzymy proste, punktowe światło. W tym celu tworzymy światło, ustawiamy jego rodzaj i pozycję:

       light = mSceneMgr->createLight( "Light1" );
       light->setType( Light::LT_POINT );
       light->setPosition( Vector3(0, 150, 250) );

Teraz ustawimy jego kolor dyfuzji i odbicia.

       light->setDiffuseColour( 1.0, 0.0, 0.0 );
       light->setSpecularColour( 1.0, 0.0, 0.0 );

Możemy teraz sprawdzić jak działa nasz program. Zobaczymy nindże, który rzuca cień. Oczywiście możemy spojrzeć na niego z przodu, przyjrzeć mu się dokładniej. Jedyną rzeczą jaką nie możemy zobaczyć to źródło światła. Widzimy tylko wygenerowane światło, ale nie rzeczywisty świecący obiekt. Czasami w miejsce, skąd idzie światło wstawiany jest dodatkowy obiekt przypominający świecące światło.

Dodajmy teraz światło koloru żółtego, który będzie oświetlał z przodu nindżę. Zrobimy to w podobny sposób, jak przed chwilą:

       light = mSceneMgr->createLight( "Light2" );
       light->setType( Light::LT_DIRECTIONAL );
       light->setDiffuseColour( ColourValue( .25, .25, 0 ) );
       light->setSpecularColour( ColourValue( .25, .25, 0 ) );

Ponieważ światło kierunkowe zachowuje się tak, jakby świeciło z dalekiego dystansu, nie możemy ustawić pozycji światła, tylko jego kierunek. Zatem ustawimy kierunek światła w kierunku dodatniego z i ujemnego y (idzie ono pod kątem 45 stopni od góry z przodu na naszą postać postaci):

       light->setDirection( Vector3( 0, -1, 1 ) );

Przekompiluj i uruchom program. Zobaczysz dwa cienie na ekranie. Ponieważ kierunkowe światło jest płaskie, cień jest także płaski.

Wykorzystamy teraz ostatni rodzaj światła - światło stożkowe. Będzie koloru niebieskiego:

       light = mSceneMgr->createLight( "Light3" );
       light->setType( Light::LT_SPOTLIGHT );
       light->setDiffuseColour( 0, 0, 1.0 );
       light->setSpecularColour( 0, 0, 1.0 );

Potrzebujemy także ustawić pozycję, jak i kierunek świecenia. Umieścimy je tak, aby świeciło w stronę bark nindży:

       light->setDirection( -1, -1, 0 );
       light->setPosition( Vector3( 300, 300, 0 ) );

Światło stożkowe umożliwia także określenie, jak szeroka jest wiązka światła. Ustawimy zakres światła za pomocą funkcji setSpotlightRange:

       light->setSpotlightRange( Degree(35), Degree(50) );

Możesz teraz ponownie przekompilować i uruchomić program.

Kod źródłowy[edytuj]

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


Teren, niebo i mgła[edytuj]

Główny obiekt i tworzenie menadżera sceny[edytuj]

Podobnie jak w poprzednim rozdziale oprzemy się na zalążku programu OGRE, który należy wkleić w pierwszy utworzony plik .cpp.

Korzeń[edytuj]

W tym przykładzie zajmiemy się renderowaniem terenu w Ogre. Aby to zrobić, potrzebujemy ustawić menadżera sceny na TerrainSceneManager. Odszukaj linie:

     /** createScene jest funkcją czysto wirtualną w ExampleApplication,

i wstaw przed nią następujący kod:

     // Tworzenie Menadżera Sceny
     void chooseSceneManager(void)
     {
 #if OGRE_VERSION < OGRE_CHANGE1
        mSceneMgr = mRoot->getSceneManager(ST_EXTERIOR_CLOSE);
 #else
        mSceneMgr = mRoot->createSceneManager(ST_EXTERIOR_CLOSE, "TerrainSceneManager");
 #endif
     }

Uwaga! Tworzenie Menadżera Sceny zostało zmienione w wersji 1.2 (Dagon) w stosunku do poprzednich. Stąd wykorzystujemy dyrektywy prekompilacji umożliwiające wybór odpowiedniej wersji kodu do wersji systemu.(z forum)

Obiekt Root (mRoot jest instancją klasy Root) jest rdzeniem ("core") Ogrowych obiektów. Tutaj możesz zobaczyć diagram UML zależności miedzy obiektami. W tym kawałku kodu mówimy korzeniowi, że potrzebujemy menadżera sceny typu ST_EXTERIOR_CLOSE. Korzeń wykorzystuje SceneManagerEnumerator, aby odnaleźć odpowiedni menadżer sceny i potem go zwraca.

Po tym, jak aplikcja zostanie skonfigurowana nie będziesz już musiał używać obiektu Root.

Tworzenie menadżera sceny[edytuj]

Można utworzyć menadżera sceny bezpośrednio wywołując new SceneManager, bynajmniej nie trzeba używać metody getSceneManager ani createSceneManager z obiektu Root. Jednoczesnie można mieć wiele menadżerów scen o różnych kształtach i innych jednostkach. Można też zmieniać menadżera podczas odnawiania viewportu lub wyświetlać wiele menadżerów używając wielu vieportów.

Dlaczego zatem używamy getSceneManager/createSceneManager zamiast tworzyć go normalnie? System wtyczek w Ogre daje nam duży poziom elastyczności podczas pracy z menadżerami scen. Zaś w "normalnym" przypadku mamy dostępnych tylko kilka rodzajów menadżerów scen. ExampleApplication ma domyślnego menadżera sceny, który jest ST_GENERIC. Może się wydawać, że jest to klasa bazowa, dopóki nie zacznie się głębiej sięgać w konfigurację pluginów. Gdy używamy pluginu OctreeSceneManager, ten rejestruje siebie, i nadpisuje klasę bazową SceneManager. OctreeSceneManager używa systemu zrywania elementów, które nie są aktualnie na scenie. W rezultacie działa szybciej niż wbudowany, regularny menadżer sceny. Jeśli zaś usunąłeś OctreeSceneManager z pliku plugins.cfg i żądasz ST_GENERIC w getSceneManager, prawdopodobnie używasz wtedy prostego menadżera sceny.

Żądanie ST_GENERIC to prosty i łatwy sposób tworzenia menadżera, ale może powodować parę problemów. Bez względu na okoliczności nie gwarantuje, że dostaniesz takiego menadżera sceny, którego chciałeś, ponieważ niektóre pluginy próbują nadpisać ten sam typ ST_*. Raczej jednak nie występują tego typu problemy. Niemniej, jeśli potrzebujesz zaawansowanego, konkretnego menadżera sceny, powinieneś utworzyć go samemu. A jeśli potrzebujesz standardowych rzeczy możesz po prostu używać dostępnych menadżerów. Przy okazji pamiętaj że SceneManagerEnumerator nie jest czymś w rodzaju fabryki, dlatego nigdy nie powinieneś bezpośrednio usuwać zwróconego menadżera sceny.

Teren[edytuj]

Dodawanie terenu do sceny[edytuj]

Teren jaki uda nam się stworzyć przy pomocy Ogre

Zajmiemy się teraz tworzeniem terenu. Bazowa klasa SceneMenager określa metodę setWorld, której używają klasy pochodne w celu utworzenie sceny. W klasie TerrainSceneManager funkcja ta wymaga nazwy pliku, z którego ma wczytać konfigurację terenu. Znajdź funkcję createScene i dodaj poniższy kod:

         mSceneMgr->setWorldGeometry( "terrain.cfg" );

Przekompiluj teraz i uruchom program. W niektórych przypadkach zauważysz, że teren znajduje się nie tak jak na załaczonym rysunku lecz u góry po prawej strony. Wówczas dodaj dodatkowy poniższy kod poprawiający ustawienie kamery:

         // Poprawienie ustawienia kamery
         mCamera->setPosition( 500.0f, 90.0f, 500.0f );

Można również osiągnąć to przesuwając kamerę za pomocą myszki.

Plik terrain.cfg[edytuj]

Mamy wiele opcji w tym pliku. Zajmiemy się tylko niektórymi. TerrainSceneManager został zaprojektowany w celu takim, aby miał możliwość stronicowania, niestety jednak wciąż jest jeszcze to implementowane. System stronnicowania terenu polega na tym, że teren jest dzielony na kawałki i kawałek jest wyświetlany wtedy, kiedy użytkownik go widzi. Umożliwia to stworzenie dużego świata, bez obniżenia wydajności w szybkości wyświetlania klatek. Na szczęście istnieje wtyczka, która daje taką możliwość: Paging Scene Manager.

TerrainSceneManager używa mapy wysokości, aby utworzyć teren. Możesz określić ją, jeśli zmienisz właściwość Heightmap.image. Możesz wstawić teksturę terenu za pomocą proporcji WorldTexture. Właściwość DetailTexture umożliwia określić poziom interpolacji tekstury, tworząc w ten sposób teren bardziej realistyczny.

Niebo[edytuj]

Ogre dostarcza trzy różne rodzaje nieba: SkyBox, SkyDome i SkyPlane. Omówimy każdy po kolei.

SkyBox[edytuj]

Teren wraz z niebem

SkyBox jest prostym, wielkim sześcianem, który otacza wszystkie obiekty w scenie. Najszybszym sposobem, jak to działa jest zobaczenie przykładu. Tak więc wstawmy poniższy kod do funkcji createScene:

         mSceneMgr->setSkyBox( true, "Examples/SpaceSkyBox" );

Przekompiluj i uruchom program. (SkyBox jest pikselowate, ponieważ użyta tekstura jest niskiej jakości. Wstawiając teksturę o większych wymiarach, otrzymamy lepszy wygląd.) Mamy kilka przydatnych argumentów dla SkyBoxów, które możemy ustawić za pomocą funkcji setSkyBox. Pierwszy mówi, czy włączyć lub wyłączyć SkyBox. Jeśli potrzebujesz wyłączyć SkyBox po prostu użyj mSceneMgr->setSkyBox( false, "" ). Drugim parametrem jest skrypt materiału użytego dla nieba. Trzeci argument określa dystans, o jaki SkyBox ma być oddalony od kamery. Czwarty argument określa, czy SkyBox ma być renderowany przed, czy po narysowaniu sceny. Zobaczmy, co się stanie, kiedy zmienimy domyślny dystans SkyBox (5000j) na bardzo mały:

         mSceneMgr->setSkyBox( true, "Examples/SpaceSkyBox", 10 );

Nic się nie zmieniło. Jest tak, ponieważ czwarty argument określa, czy niebo ma być najpierw rysowane. Domyślnie jest włączony (true). Jeśli SkyBox jest najpierw rysowany, wtedy reszta jest renderowana po nim, nakładając się w ten sposób na niego. Powoduje to, że SkyBox jest zawsze w tle. Należy zaznaczyć, że odległość nieba od kamery powinna być tak ustawiona, aby mieściło się ono między dystansami obcinania kamery. W przeciwnym wypadku nie zobaczymy nieba. Nie jest zalecane rysowanie całego nieba na początku, ponieważ obniża to szybkość działania klatek. Kiedy niebo jest rysowane pod koniec, wtedy tylko widoczne części są renderowane. Umożliwia to przyszybszenie czasu działania. Ma to także swoje ciemne strony. Uczyńmy, aby nasz SkyBox był rysowany na końcu:

         mSceneMgr->setSkyBox( true, "Examples/SpaceSkyBox", 5000, false );

Wygląda to identycznie, jak poprzednio, jednak nie widoczne części nie były renderowane. Należy dodać, że jeśli utworzony SkyBox jest za ciasny, możesz zobaczyć wycięte przez niebo fragmenty świata. Nie wygląda to za ciekawie. Zobaczmy:

         mSceneMgr->setSkyBox( true, "Examples/SpaceSkyBox", 100, false );

Możesz teraz zobaczyć teren, który jest ograniczony i zapakowany przez niebo. Szybka metoda renderowania nieba jest bardzo ograniczona i nie obchodzi się zbyt ostrożnie z geometrią sceny. Innymi słowy pominięcie dwóch ostatnich argumentów jest bardziej bezpieczne, nie musisz wtedy obawiać się o nieporządane efekty.

SkyDome[edytuj]

Niebo SkyDome z poziomem krzywizny ustawionym na 2

SkyDome bardzo przypomina SkyBox. Możesz go użyć wywołując funkcję setSkyDome. Duży sześcian jest tworzony naokoło kamery a następnie renderowany (sześcian jest mniejszy od terenu i przesuwa się wraz z kamerą). Jednak istnieją duże różnice między nimi, otóż tekstura w SkyDome jest pokazywana w sferyczny sposób. Spoglądając w sześcian masz wrażenie, że wygląda on tak, jakby tekstura była nałożona na sferę. Inną widoczną rzeczą jest to, że sześcian ten nie ma podstawy.

Zobaczmy teraz przykład. Usuńmy wstawioną wcześniej funkcję setSkyBox i wstawmy zamiast tego poniższy kod:

         mSceneMgr->setSkyDome( true, "Examples/CloudySky", 5, 8 );
Niebo SkyDome z poziomem krzywizny ustawionym na 64

Przekompiluj to teraz i uruchom. Przesuń kamerę w centrum terenu, tak aby z każdej strony było widać nasze góry. Następnie spójrz na niebo, jak pięknie wygląda.Patrząc na to przyciśnij przycisk R, aby przełączyć widok na widok siatki. Zapewne zauważysz, że patrzysz na sześcian, ale tekstura chmur jest tak nałożona, że wygląda jak sfera.

Pierwsze dwa argumenty oznaczają to samo, co w funkcji setSkyBox. Możesz wyłączyć SkyDome poprzez wywołanie mSceneMgr->setSkyDome( false, "" ). Trzeci argument ustawia poziom krzywizny. Najlepiej, aby argument ten był między 2 a 65. Mniejsze wartości sprawiają wrażenie, że niebo jest dalej, natomiast większe, że jest bliżej i jest bardziej wygładzone. Czwarty argument jest liczbą rzeczywistą, która określa na ile kawałków ma zostać podzielona tekstura. Należy dodać, że argument ten może być ułamkiem np. 1.234. Piąty i szósty argument opisuje odległość od kamery, a także czy niebo ma być rysowane na samym początku. Te dwa argumenty zostały one opisane szerzej w sekcji SkyBox.

SkyPlane[edytuj]

Nasz SkyPlane

SkyPlane wielce się różni od SkyBoxów i SkyDome. Zamiast renderować niebo na sześcianie, wykorzystuje on pojedyńczą płaszczyznę. Wyczyśćmy nasz kod SkyDome z funkcji createScene. Pierwszą rzeczą jaką zrobimy to utworzymy płaszczyznę i skierujemy ją ku dołowi. Wywołując metodę setSkyPlane nie podajemy argumentu określającego odległość jak było w SkyBox i SkyPlane. Zamiast tego ustawiamy parametr d w naszej płaszczyźnie:

         Plane plane;
         plane.d = 1000;
         plane.normal = Vector3::NEGATIVE_UNIT_Y;

Kiedy już mamy naszą płaszczyznę, możemy utworzyć SkyPlane. Dodajmy, że czwarty parametr jest wielkością SkyPlane (w tym przypadku 1500x1500), a piąty określa jak wiele razy ma podzielić teksturę:

         mSceneMgr->setSkyPlane( true, plane, "Examples/SpaceSkyPlane", 1500, 75 );

Przekompiluj i uruchom program. Mamy dwa problemy związane z utworzonym tutaj SkyPlanem. Pierwszy polega na tym, że użyta tekstura jest za mała, dlatego też nie pokrywa ona nieba za dobrze. Możemy tego problemu się pozbyć tworząc dobrą i większą teksturę. Jakkolwiek głównym problemem z tą techniką jest to, że gdy spoglądniesz na horyzont, możesz zobaczyć koniec SkyPlane. Nawet jeśli masz dobrą teksturę, może nie wyglądać najlepiej, ponieważ zobaczysz koniec horyzontu. Te podstawowe użycie SkyPlanu jest przydatne tylko wtedy, kiedy masz wysokie ściany naokoło viewportu. W takiej sytuacji używanie SkyPlanu może być korzystniejsze niż tworzenie pełnych SkyBoxów/SkyDome.

Na szczęście, nie jest to wszystko, co możemy zrobić ze SkyPlane. Szósty argument jest znanym już "renderFirst", opisanym w sekcji SkyBox i SkyDome. Siódmy argument umożliwia określanie poziomu krzywizny SkyPlanu, tak więc nie musimy dłużej używać płaszczyny, zamiast tego może to być krzywa powierzchnia. Możemy także ustawić liczbę segmentów x i y, które mają zostać użyte do tworzenia SkyPlane (domyślnie SkyPlane jest wielkim kwadratem, ale jeśli chcemy go skrzywić potrzebujemy naszą płaszczyznę podzielić na mniejsze kwadraty). Ósmy i dziewiąty argument są liczbą segmentów na x i y:

         mSceneMgr->setSkyPlane( true, plane, "Examples/SpaceSkyPlane", 1500, 50, true, 1.5f, 150, 150 );

Przekompiluj i ruchom program. Teraz nasz SkyPlane wygląda o wiele lepiej, chociaż używa trochę pracy. Możesz także użyć inny materiał chmur:

         mSceneMgr->setSkyPlane( true, plane, "Examples/CloudySky", 1500, 40, true, 1.5f, 150, 150  );

Przekompiluj i uruchom program.

Możesz wyczyścić SkyPlane wywołując 'mSceneMgr->setSkyPlane( false, Plane(), "" );'

Którego nieba użyć[edytuj]

Które niebo zastosować, zależy wyłącznie od twojej aplikacji. Jeśli musisz widzieć wszystko dookoła, nawet ujemny kierunek osi y, wtedy jedyną możliwością którą musisz zastosować jest SkyBox. Jeśli masz teren lub jakieś podłogi z blokami, które zakrywają ujemny kierunek osi y, wtedy używając SkyDome uzyskamy bardziej realistyczny efekt. W obszarach, gdzie nie możesz zobaczyć horyzontu (np. dolina ze wszystkich stron otoczona górami), SkyPlane będzie trafnym wyborem, który najmniej obciąża GPU. Główną przyczyną, aby używać SkyPlane, co zobaczymy w następnej sekcji, jest to, że dobrze współgra z mgłą.

Mgła[edytuj]

Wprowadzenie[edytuj]

Mgła

Używanie mgły jest bardzo proste. Jest jedno ostrzeżenie, zanim zrozumiesz jak wykorzystać mgłę w swoim programie. Kiedy używasz TerrainSceneManager, musisz wywołać setFog przed funkcją setWorldGeometry. (W innych menadżerach nie ma większej różnicy). W zależności, co wywołamy najpierw, różna vertex programy będą wybrane, aby tworzyć mgłę na teranie.

Zanim rozpoczniesz, wyczyść całą zawartość funkcji createScene, z wyjątkiem setWorldGeometry.

Najbardziej ważną rzeczą, jaką trzeba zrozumieć o mgle, jest to, że nie jest ona w rzeczywistości jednostką tworzoną w pustej przestrzeni, jak mogłeś sobie wyobrażać. Zamiast tego jest po prostu filtrem używanym dla obiektów, na które aktualnie spoglądasz. Jeśli nie patrzysz na żaden obiekt, nie zobaczysz mgły - zobaczysz tylko kolor tła. Tak więc kolor tła powinien być taki sam, jak kolor mgły.

Mamy dwa typy mgieł: liniową i wykładnicza. Najłatwiej zobaczyć różnicę między nimi spoglądając na przykłady.

Rodzaje mgieł[edytuj]

Pierwszą mgłą, najłatwiejszą do zrozumienia, jest mgła liniowa. Najpierw co musimy zrobić przed wywołaniem funkcji setWorldGeometry jest ustawienie koloru tła viewportu. Moglibyśmy tak jak w poprzednim rozdziale użyć funkcję createViewport, ale spróbujemy to zrobić bez ponownego tworzenia viewportu. W tym celu na początku metody createScene dodajemy:

         ColourValue fadeColour( 0.9, 0.9, 0.9 );
         mWindow->getViewport(0)->setBackgroundColour( fadeColour );

Jeżeli posiadamy więcej niż jeden viewPort należałoby użyć getNumViewports, aby pobrać ich liczbę, a następnie iterować każdy po kolei. Jednak w naszym przypadku mamy tylko jeden viewport, więc używamy go bezpośrednio. Kiedy już ustawiliśmy kolor tła, możemy utworzyć mgłę. Wstawiamy kolejną linię po wyżej podanych:

         mSceneMgr->setFog( FOG_LINEAR, fadeColour, 0.0, 50, 500 );

Pierwszym argumentem jest metody setFog jest typ mgły (w tym przypadku liniowy). Drugi argument określa kolor mgły (w tym przypadku prawie biały). Trzeci argument jest używany do nie liniowej mgły. Czwarty i piąty określa od kąd się zaczyna mgła, a gdzie kończy. W tym zaczyna się w odległości 50 od kamery, a kończy się na 500. Zależność między gęstością mgły jest liniowa. Wszystko, co jest za 500 od kamery nie jest widoczne. Przekompiluj i uruchom program.

Innym typem mgły jest mgła wykładnicza. Zamiast ustawiania początku i końca mgły, okreśłamy gęstość mgły (czwarty i piąty argument jest nieokreślony). Zamieńmy poprzedni kod na ten:

         mSceneMgr->setFog( FOG_EXP, fadeColour, 0.005 );

Przekompiluj i uruchom program. Mgła ta wygląda inaczej, niż poprzednia. Mamy do dyspozycji także inny typ tej mgły, która jest jeszcze bardziej sroga od pierwszej. Zamień poprzednie wywołanie setFog na takie:

         mSceneMgr->setFog( FOG_EXP2, fadeColour, 0.003 );

Przekompiluj i uruchom ten program. Mgła ta jest najbardziej zmienna, pomiędzy wszystkimi innymi. Spróbuj poeksperymentować z różnymi funkcjami mgły.

Mgła a niebo[edytuj]

Może powstać kilka interesujących problemów, kiedy próbujesz używać mgły razem z SkyBox i SkyDome. Ponieważ SkyDome i SkyBox są sześcianami, kiedy używamy mgły powstają problemy, ze względu na to, że mgła działa w sposób sferyczny. Wyczyśćmy zawartość metody createScene. Jeśli sprytnie dobierzemy argumenty dla SkyDome i mgły, możemy zobaczyć bezpośrednio problem:

         ColourValue fadeColour( 0.9, 0.9, 0.9 );
         mSceneMgr->setFog( FOG_LINEAR, fadeColour, 0.0, 50, 515 );
         mWindow->getViewport(0)->setBackgroundColour( fadeColour );
         
         mSceneMgr->setSkyDome( true, "Examples/CloudySky", 5, 8, 500 );

Przkompiluj i uruchom program. Jeśli będziesz ruszał kamerę naokoło, zobaczysz różną gęstość mgły na SkyDome, która zależy od tego, na którą część patrzymy.

Na pewno takiego czegoś nie chcemy. Inną możłiwością jest użycie SkyPlanu. Zamieńmy kod w createScene na ten:

         ColourValue fadeColour( 0.9, 0.9, 0.9 );
         mSceneMgr->setFog( FOG_LINEAR, fadeColour, 0.0, 0, 130 );
         mWindow->getViewport(0)->setBackgroundColour( fadeColour );
 
         Plane plane;
         plane.d = 100;
         plane.normal = Vector3::NEGATIVE_UNIT_Y;
 
         mSceneMgr->setWorldGeometry( "terrain.cfg" );
 
         mSceneMgr->setSkyPlane( true, plane, "Examples/CloudySky", 500, 20, true, 0.5, 150, 150 );

To wygląda poprawnie. Jeśli patrzymy do góry możemy zobaczyć niebo. Jednak jeśli używa się krzywizny lub nie, może powstać problem związany z tym, że zobaczymy horyzont, który nie wygląda poprawnie.

Mgła jako ciemność[edytuj]

Nieraz jest tak, że nie chcemy, aby mgła była widoczna na niebie np. chcemy stworzyć ciemność. W tym celu możemy wykorzystać pewien użyteczny trik. Ustawmy kolor mgły zamiast na jasny, na ciemny kolor i zobaczmy co będzie (zauważmy, że SkyPlane jest oddalone tylko o 10 jednostek miary od kamery):

        ColourValue fadeColour( 0.1, 0.1, 0.1 );
        mWindow->getViewport(0)->setBackgroundColour( fadeColour );
        mSceneMgr->setFog( FOG_LINEAR, fadeColour, 0.0, 10, 150 );

        mSceneMgr->setWorldGeometry( "terrain.cfg" );

        Plane plane;
        plane.d = 10;
        plane.normal = Vector3::NEGATIVE_UNIT_Y;

        mSceneMgr->setSkyPlane( true, plane, "Examples/SpaceSkyPlane", 100, 45, true, 0.5, 150, 150 );

Przekompiluj i uruchom program.

Oczywiście, zamiast tego chwytu możemy wykorzystać odpowiednio dobraną lampę, ale ten przykład pokazuje, w czym jeszcze może zostać wykorzystana mgła. Używając ciemnej mgły można uzyskać interesujący efekt mroku lub zaślepienia np. w grach, w których poruszamy się postacią pierwszoplanową.

Kod źródłowy[edytuj]

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


Frame Listener i niebuforowane wejście[edytuj]

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ę 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[edytuj]

Wprowadzenie[edytuj]

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

  1. Korzeń (Root) wywołuje metodę frameStarted na wszystkich zarejestrowanych FrameListenerach.
  2. Korzeń renderuje jedną klatkę.
  3. 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[edytuj]

Utworzony 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ę[edytuj]

Wprowadzenie[edytuj]

Przed 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[edytuj]

Znajdź 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[edytuj]

Zmienne[edytuj]

Zdefiniowaliś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.

I znowu ninja, tym razem w innej odsłonie

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[edytuj]

Zobaczmy 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[edytuj]

Zajmiemy 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[edytuj]

Ponieważ 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[edytuj]

Dzię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[edytuj]

Teraz 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[edytuj]

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


Buforowane wejście[edytuj]

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 w trakcie pracy o klasę TutorialFrameListener. Na razie nadpiszmy metody dziedziczone z klasy ExampleApplication. Odszukaj linię:

    /** createScene jest funkcją czysto wirtualną w ExampleApplication,

i przed nią wstaw poniższy kod:

     void createCamera(void)
     {
         // create camera, but leave at default position
         mCamera = mSceneMgr->createCamera("PlayerCam"); 
         mCamera->setNearClipDistance(5);
     }
 
     void createFrameListener(void)
     {
         // Create the FrameListener
         mFrameListener = new TutorialFrameListener(mWindow, mCamera, mSceneMgr);
         mRoot->addFrameListener(mFrameListener);
 
         // Show the frame stats overlay
         mFrameListener->showDebugOverlay(true);
     }

Oraz wewnątrz metody createScene wstaw:

         mSceneMgr->setAmbientLight( ColourValue( 0.25, 0.25, 0.25 ) );
 
         // add the ninja
         Entity *ent = mSceneMgr->createEntity( "Ninja", "ninja.mesh" );
         SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode( "NinjaNode" );
         node->attachObject( ent );
 
         // create the light
         Light *light = mSceneMgr->createLight( "Light1" );
         light->setType( Light::LT_POINT );
         light->setPosition( Vector3(250, 150, 250) );
         light->setDiffuseColour( ColourValue::White );
         light->setSpecularColour( ColourValue::White );
 
         // Create the scene node
         node = mSceneMgr->getRootSceneNode()->createChildSceneNode( "CamNode1", Vector3( -400, 200, 400 ) );
 
         // Make it look towards the ninja
         node->yaw( Degree(-45) );
 
         // Create the pitch node
         node = node->createChildSceneNode( "PitchNode1" );
         node->attachObject( mCamera );
 
         // create the second camera node/pitch node
         node = mSceneMgr->getRootSceneNode()->createChildSceneNode( "CamNode2", Vector3( 0, 200, 400 ) );
         node = node->createChildSceneNode( "PitchNode2" );

Buforowane wejście[edytuj]

Wprowadzenie[edytuj]

W poprzednim rozdziale używaliśmy niebuforowanego wejścia, dlatego też co każdą klatkę sprawdzaliśmy stan obiektu InputReader, który przechowuje informację jakie przyciski klawiatury i myszy są wciśnięte. Buforowane wejście używa interfejsu mouse i key listenera, aby auaktualniać dane np. kiedy przycisk został wciśnięty zdarzenie KeyListener::keyPressed jest wywoływane, natomiast kiedy zostanie on opuszczony (przecisk nie jest już przyciskany) KeyListener::keyReleased zostaje wywołany. Dzięki temu nie musimy przechowywać dodatkowych zmiennych, aby określać, czy dany przycisk został wciśniety, a także blokować akcji na pewien czas.

Wady[edytuj]

Ogrowy system buforowanego wejścia nie jest całkowicie spójny, o czym będziemy mówić później. Istnieją takie części systemu, które są całkowicie błędne i nie mogą być używane. Ogre nie obsługuje też joysticków i gamepadów, co może stanowić duży problem w pewnych typach gier.

Dlaczego Ogrowy system wejścia jest częściowo wadliwy i nie zgodny wewnętrznie? Odpowiedź jest prosta - Ogre jest projektowany jako silnik graficzny. Prawdopodobnie system wejścia w Ogre nie istniałby, gdyby nie potrzeba tworzenia dem dla różnych platformy.

W skrócie system wejścia w wielu przypadkach jest dobry i wiele problemów może zostać pokonanych. Może zaistnieć systuacja, że będziemy potrzebować innych systemów wejść. Zostanie to omówione na końcu rozdziału.

Interfejs buforowanego wejścia[edytuj]

Zarejestrujemy jeden FrameListener, aby odbierał zdarzenia zarówno od myszy jak i klawiatury. Nie zawsze jest to najlepszym wyjściem. W większych projektach często może być wygodniej i praktyczniej podzielić zdarzenia myszy i klawiatury na dwa odrębne FrameListener-y.

Użyjemy trzech interfejsów. Interfejs KeyListener będzie używany do odbierania wszystkich zdarzeń związanych z klawiaturą, MouseListener używac będziemy do zdarzeń związanych z przyciskaniem przycisków myszy, a interfejs MouseMotionListener będzie związany z ruchem myszy.

KeyListener[edytuj]

Interfejst KeyListener określa kilka funkcji służących do pobierania wejścia klawiatury. Metoda keyPressed jest wywoływana, kiedy przycisk zostanie wciśnięty. Metoda keyReleased wywoływana jest, kiedy przycisk zostanie opuszczony. Funkcja keyClicked jest wywoływana, kiedy przycisk został wciśnięty i zostaje opuszczony (prawie zawsze jest odpowiednikiem keyReleased). Kiedy przycisk zostanie wciśnięty będą wywołane trzy zdarzenia: pierwsze - kiedy przycisk został wciśnięty (keyPressed), drugi - kiedy przycisk zostanie zwolniony (keyReleased), a ostatnie po tych obu (keyClicked).

MouseListener[edytuj]

Interfejs MouseListener określa funkcje odpowiedzialne za zdarzenia związane z przyciskaniem przycisku myszy. Funkcja mousePressed zostanie wywołana, kiedy użytkownik wciśnie przycisk myszy. Funkcja mouseReleased zostanie uruchomiona, kiedy przycisk myszy zostanie opuszczony. Funkcja mouseClicked nie działa, dlatego nie próbuj jej używać (ale ponieważ jest to funkcja wirtualna, możemy ją utworzyć). mouseEntered i mouseExited są funkcjami wirtualnymi, których nie będziemy używać, więc nadpiszemy je, aby były puste.

MouseMotionListener[edytuj]

Interfejs MouseMotionListener definiuje funkcje służące odpowiedzialne za zdarzenia związane z ruchem myszy. Funkcja mouseMoved jest wywoływana, kiedy mysz jest przesuwana, mouseDragged jest wywoływana, kiedy mysz jest przesuwana z przyciśniętym przyciskiem. Istnieje jeszcze trzecia funcja mouseDragMoved, która wywoływana jest gdy puszcza się coś co się przeciągało (w założeniu miała obsługiwać przeniesienie przesuwanego obiektu do celu) ale jak się wydaje jest ona napisana z błędem.

TutorialFrameListener[edytuj]

Zdefiniujmy naszą nową klasę TutorialFrameListener pamiętając o zmianach jakie chcemy wprowadzić w stosunku do poprzedniego artykułu. Po pierwsze dodajemy więcej interfejsów. Poniższy kod zacznij wprowadzać przed linią:

 // Dziedziczymy ExampleApplication

 class TutorialFrameListener : public ExampleFrameListener, public MouseMotionListener, public MouseListener
 {
 public:

Dzięki takiej definicji odziedziczyliśmy klasy MouseMotionListener i MouseListener, które umożliwiają nam odbieranie odpowiednich zdarzeń. Normalnie powinieneś dodać też KeyListener, ale ponieważ ExampleFrameListener dziedziczy ten interfejs, my nie musimy tego robić.

Zmieniamy także konstruktor ExampleFrameListener:

    TutorialFrameListener( RenderWindow* win, Camera* cam, SceneManager *sceneMgr )
        : ExampleFrameListener(win, cam, true, true)
    {
    }

Dwa argumenty true mówią, że będziemy używali buforowanego wejścia dla klawiatury i myszy.

Zmienne[edytuj]

W stosunku do poprzedniego rozdziału wprowadziliśmy kilka zmian w zmiennych klasy TutorailFrameListener. Usuneliśmy mToggle i mMouseDown, ponieważ przy buforowanym wejściu ich nie potrzebujemy. Dodaliśmy za to inne.

 protected:
     Real mRotate;          // The rotate constant
     Real mMove;            // The movement constant
 
     SceneManager *mSceneMgr;   // The current SceneManager
     SceneNode *mCamNode;   // The SceneNode the camera is currently attached to
 
     bool mContinue;        // Whether to continue rendering or not
     Vector3 mDirection;     // Value to move in the correct direction
 
     enum DirectionCodes
     {
         X = 0,
         Y = 1,
         Z = 2
     };
 };

Zmienne mRotate, mMove, mSceneMgr i mCamNode' służą do tego samego, co w poprzednim tutorialu (chociaż mMove nie będzie tu stała - wykorzystamy ją inaczej). Zmienna mContinue jest zwracana przez metodę frameStarted. Jeśli ustawimy mContinue na false, to zakończymy program. Zmienna mDirection będzie przechowywać informację, jak mamy przesunąć węzeł kamery co każdą klatkę.

Dodatkowo zdefiniowaliśmy potrzebną później stałą wyliczeniową DirectionCodes.

Konstruktor[edytuj]

Wewnątrz konstruktora zainicjujmy zmienne.

         // Wstawienie kamery i ustawienie managera sceny
         mCamNode = cam->getParentSceneNode( )->getParentSceneNode( );
         mSceneMgr = sceneMgr;
 
         // ustawienie szybkości obrotu i przesunięcia
         mRotate = 72;
         mMove = 250;
 
         // kontunujemy rendering
         mContinue = true;

Powinniśmy teraz zarejestrować nasz TutorialFrameListener jako Key, Mouse i MouseMotion listener. Klasa ExampleFrameListener zarejestrowała już samego siebie jako KeyListener, więc tę część zostawimy zakomentowaną. W normalnym przypadku jednak trzeba by było rejestrację wykonać samodzielnie.

         //mEventProcessor->addKeyListener( this );
         mEventProcessor->addMouseListener( this );
         mEventProcessor->addMouseMotionListener( this );

Na koniec musimy zainitializować mDirection jako wektor zerowy (ponieważ na początku nie chcemy się poruszać):

         mDirection = Vector3::ZERO;

Reagowanie na klawiaturę KeyListener[edytuj]

keyPressed[edytuj]

Zdefiniujmy teraz metodę TutorialFrameListener::keyPressed. Metoda ta będzie wykonywana, gdy jakiś klawisz zostanie wciśnięty. Jako argument wprowadzimy wskaźnik do obiektu KeyEvent. Możemy sprawdzić, jaki klawisz został wciśnięty (KC_*) wywołując z tego obiektu funkcję getKey. Użyjemy instrukcji switch, aby do określonego klawisza przypisać pewną akcję. Wszystkie opisane niżej metody wpisujemy w dowolnym miejscu klasy TutorialFrameListener przed dyrektywą protected.

   // KeyListener
   virtual void keyPressed(KeyEvent* e) 
   {
      switch ( e->getKey( ) )
      {

Sprawdzimy najpierw, czy nie został przyciśnięty klawisz Escape. Jeśli tak się stanie, to kończymy program. Zrobimy to ustawiając zmienną mContinue na false.

        case KC_ESCAPE:
            mContinue = false;
            break;

Wstawmy teraz obsługę pozostałych klawiszy. Umożliwimy użytkownikowi, aby mógł przestawiać widok z kamery używając klawiszy 1 i 2. Kod jest taki sam jak w poprzednim rozdziale, z wykątkiem tego, że znajduje się w instrukcji switch i nie używamy już zmiennej mToggle:

        case KC_1:
             mCamera->getParentSceneNode()->detachObject( mCamera );
             mCamNode = mSceneMgr->getSceneNode( "CamNode1" );
             mSceneMgr->getSceneNode( "PitchNode1" )->attachObject( mCamera );
             break;
 
        case KC_2:
             mCamera->getParentSceneNode()->detachObject( mCamera );
             mCamNode = mSceneMgr->getSceneNode( "CamNode2" );
             mSceneMgr->getSceneNode( "PitchNode2" )->attachObject( mCamera );
             break;

Jak zapewne zauważyłeś, kod ten wygląda czyściej i ładniej, niż ten z dodatkową zmienną wstrzymującą odbieranie zdarzeń przez pewien czas.

Dodajmy teraz możliwość przesuwania się za pomocą klawiatury. Za każdym razem, kiedy użytkownik wciśnie klawisz odpowiedzialny za ruch, będziemy dodawać lub odejmować mMove (w zależności od kierunku) odpowiednią wspołrzędną wektora przesunięcia:

        case KC_UP:
        case KC_W:
            mDirection.z -= mMove;
            break;
 
        case KC_DOWN:
        case KC_S:
            mDirection.z += mMove;
            break;
 
        case KC_LEFT:
        case KC_A:
            mDirection.x -= mMove;
            break;
 
        case KC_RIGHT:
        case KC_D:
            mDirection.x += mMove;
            break;
 
        case KC_PGDOWN:
        case KC_E:
            mDirection.y -= mMove;
            break;
 
        case KC_PGUP:
        case KC_Q:
            mDirection.y += mMove;
            break;
      }
   }

keyRelased[edytuj]

Kiedy klawisz zostanie zwolniony musimy musimy wycofać zmiany poczynione przy jego przyciśnięciu. W tym celu zdefiniujemy podobną do wyżej opisanej metodę TutorialFrameListener::keyRelased.

   virtual void keyReleased(KeyEvent* e)
   {
      switch ( e->getKey( ) )
      {
        case KC_UP:
        case KC_W:
            mDirection.z += mMove;
            break;
 
        case KC_DOWN:
        case KC_S:
            mDirection.z -= mMove;
            break;
 
        case KC_LEFT:
        case KC_A:
            mDirection.x += mMove;
            break;
 
        case KC_RIGHT:
        case KC_D:
            mDirection.x -= mMove;
            break;
 
        case KC_PGDOWN:
        case KC_E:
            mDirection.y += mMove;
            break;
 
        case KC_PGUP:
        case KC_Q:
            mDirection.y -= mMove;
            break;
        } // switch
      }

keyClicked[edytuj]

Dodatkowo zdefiniujemy pustą metodę TutorialFrameListener::keyClicked bo nie chcemy by cokolwiek było wykonywane w takim przypadku.

   virtual void keyClicked(KeyEvent* e) { }

powiązanie z ruchem kamery we frameStarted[edytuj]

I wreszcie powiążemy nasz uaktualniony za pomocą klawiatury wektor przesunięcia z ruchem kamery wewnątrz metody frameStarted. Należy dodać, że gdybyśmy wstawili ten kod do funkcji keyPressed, ruch został by wykonany tylko w chwili przyciśnięcia przycisku. My jednak chcemy, aby poruszał się zawsze, co klatkę, kiedy odpowiedni klawisz jest wciśnięty. Zdefiniujmy zatem metodę TutorialFrameListener::frameStarted:

    bool frameStarted(const FrameEvent &evt)
    {
        mCamNode->translate( mCamNode->getOrientation() * mDirection * evt.timeSinceLastFrame );
        return mContinue;
    }

Jeżeli chcesz możesz teraz sprawdzić swój program. Jednak by klasa TutorialFrameListener mogła być zainicjowana musimy tymczasowo wprowadzić definicje pozostałych wymaganych metod. Wprowadzić je możemy za lub przed metodą frameStarted.

   // MouseDragged
   void mouseMoved(MouseEvent* e) { }
   void mouseDragged(MouseEvent* e) { }
   // MouseListener
   void mouseClicked(MouseEvent* e) { }
   void mouseEntered(MouseEvent* e) { }
   void mouseExited(MouseEvent* e) { }
   void mousePressed(MouseEvent* e) { }
   void mouseReleased(MouseEvent* e) { }

Przekompiluj teraz i uruchom program. Można poruszać kamerą za pomocą klawiatury.

Reagowanie na mysz[edytuj]

mousePressed[edytuj]

Teraz oprogramujemy obsługę myszy. Zaczniemy od włączania lub wyłączania swiatła za pomocą lewego przycisku myszy. Znajdź metodę mousePressed. Jest ona wywoływana z obiektem MouseEvent. Wcześniej używaliśmy wartości 0, 1, 2 itd., aby określić który przycisk został wciśnięty. Tym razem do tego celu użyjemy maski. Poniższy kod wprowadź wewnątrz metody.

        if ( e->getButtonID() & MouseEvent::BUTTON0_MASK )
        {

Jest to bardziej estetyczne, ale zawsze musisz pamiętać, że używając wywołań MouseListenera stosujesz maskę przycisków, natomiast używając wszystkich innych funkcji związanych myszką musisz używać numeru przycisku (0, 1, 2...), aby skontrolować, czy jest wciśnięty. Ostatecznie dodajemy jeszcze w mousePressed instrukcje identyczne, jak w poprzednim rozdziale:

             Light *light = mSceneMgr->getLight( "Light1" );
             light->setVisible( ! light->isVisible() );
        }

Możesz teraz przekompilować i uruchomić aplikację. Jak widać nasze założenia powoli się spełniają.

mouseMoved[edytuj]

Musimy jeszcze tylko wstawić możliwość obracania kamery za pomocą poruszania myszą z wciśniętym prawym przyciskiem myszy. W tym celu wewnątrz metody mouseMoved wstawiamy kod który po rozpoznaniu za pomocą maski czy wciśnięty jest prawy przycisk zmienia ustawienia kamery zgodnie z ruchem myszki:

        if ( e->getButtonID() & MouseEvent::BUTTON1_MASK )
        {
             mCamNode->yaw( Degree(-e->getRelX( ) * mRotate) );
             mCamNode->getChild( 0 )->pitch( Degree(-e->getRelY() * mRotate) );
        }

Przekompiluj i uruchom program.

Ups! Nie działa! Otóż znów ocieramy się o błędy w funkcjach wejścia systemu Ogre. Jak dotychczas (do Ogre 1.2 włącznie) w wyłowaniach MouseMotion otrzymujemy obiekt MouseEvent, w którym metoda getButtonID zawsze zwraca 0. Pomińmy więc ten problem i zróbmy tak, aby nasza metoda działała dla ruchu myszką bez wciśniętego żadnego przycisku myszy. Usuwamy funkcję if pozostawiając jej wnętrze:

        //if ( e->getButtonID() & MouseEvent::BUTTON1_MASK )
        //{
             mCamNode->yaw( Degree(-e->getRelX( ) * mRotate) );
             mCamNode->getChild( 0 )->pitch( Degree(-e->getRelY() * mRotate) );
        //}

Przekompiluj i uruchom program. Teraz przesuwając mysz bez wciśniętego żadnego klawisza poruszymy kamerę. Można usunąć ten problem poprzez umieszczaniu w mousePressed i mouseReleased wartość logiczną, która będzie wynosiła true lub false, w zależności czy przycisk jest wciśnięty czy nie.

Można też powyższy kod przenieść do metody mouseDragged co spowoduje, że ruchy kamerą będą odbywać się przy wciśniętym dowolnym przycisku myszki.

Inne systemy wejścia[edytuj]

Jak wspomnieliśmy wcześniej, możesz użyć w Ogre także innych systemów wejścia. Można nawet skorzystać z innych systemów zarządzania oknami, takich jak wxWidgets, który został dobrze zintegorwany z Ogre. Warte uwagi jest także SDL, który dostarcza wieloplatformowy system wejścia i tworzenia okien. Jedną z jego cech jest to, że obsługuje wejście joystick/gamepad, które nie istnieje w Ogre. Aby uruchomić SDLowy system joysticków, wstaw do swojej aplikacji wywołania SDL_Init i SDL_Quit (może to być w funkcji main albo w twoim obiekcie aplikacji):

        SDL_Init( SDL_INIT_JOYSTICK | SDL_INIT_NOPARACHUTE );
        SDL_JoystickEventState( SDL_ENABLE );
 
        app.go();
  
        SDL_Quit( );

Aby skonfigurować Joystick wywołaj SDL_JoystickOpen z numerem Joysticka (możesz używać wiele joysticków używając 0, 1, 2...):

    SDL_Joystick* mJoystick;
    mJoystick = SDL_JoystickOpen( 0 );
 
    if ( mJoystick == NULL )
        ; // wychwytywanie błędów

Jeśłi SDL_JoystickOpen zwróci NULL, wtedy napotkaliśmy problem z otworzeniem joysticka. Prawie zawsze to oznacza, że joystick o który prosiliśmy nie istnieje. Możesz użyć SDL_NumJoysticks, aby się dowiedzieć ile joysticków jest w systemie. Potrzebujesz także zamknąć the joystick przed zakończeniem programu:

    SDL_JoystickClose( mJoystick );

Aby skorzystać z joysticka wywołujemy funkcje SDL_JoystickGetButton i SDL_JoystickGetAxis. Tak więc możemy napisać kod okreśłający wektor przemieszczenia np. w ten sposób:

        SDL_JoystickUpdate();
 
        mTrans.z += evt.timeSinceLastFrame * mMoveAmount * SDL_JoystickGetAxis( mJoystick, 1 ) / 32767;
        mTrans.x += evt.timeSinceLastFrame * mMoveAmount * SDL_JoystickGetAxis( mJoystick, 0 ) / 32767;
 
        xRot -= evt.timeSinceLastFrame * mRotAmount * SDL_JoystickGetAxis( mJoystick, 3 ) / 32767;
        yRot -= evt.timeSinceLastFrame * mRotAmount * SDL_JoystickGetAxis( mJoystick, 2 ) / 32767;

mTrans będzie później wstawiane do kamery metody SceneNode::translate, xRot będzie wykorzystywane w SceneNode::yaw, a yRot zostanie użyte do SceneNode::pitch. Należy dodać, że SDL_JoystickGetAxis zwraca wartość pomiędzy -32767 i 32767. Tak więc w kodzie wartość ta zostanie przeskalowana na mieszczącą się między -1 i 1. Więcej informacji o joystickach w SDL możemy się znaleźć nagłówku.

Jeśli zamierzasz na poważnie wykorzystać SDL, powinieneś się także zapoznać z dokumentacją.

Kod źródłowy[edytuj]

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


Animacja[edytuj]

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.


Bibliografia[edytuj]

  1. Wprowadzenie
    Na podstawie
    OGRE Wiki:Get Started (O Ogre)
    autor wersji angielskiej - Antiarc
    The absolute newbies guide to OGRE (Instalacja, Pierwszy program)
    autor wersji angielskiej Antiarc
  2. Sposób działania Ogre
    Na podstawie
    Basic tutorial 1 (Jak pracuje Ogre)
    autor wersji angielskiej - Clay Culver
    Setting up an Application (Konfiguracja aplikacji)
    autor wersji angielskiej (?)
    Basic Tutorial 2 (Kamera, światła i cienie)
    autor wersji angielskiej Clay Culver
    Basic Tutorial 3 (Teren, niebo i mgła)
    autor wersji angielskiej Clay Culver

GNU Free Documentation License[edytuj]

Version 1.2, November 2002

Copyright (C) 2000,2001,2002  Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.

0. PREAMBLE[edytuj]

The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.

This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.

We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

1. APPLICABILITY AND DEFINITIONS[edytuj]

This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.

A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.

A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.

The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.

The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.

A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque".

Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.

The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.

A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition.

The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.

2. VERBATIM COPYING[edytuj]

You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.

You may also lend copies, under the same conditions stated above, and you may publicly display copies.

3. COPYING IN QUANTITY[edytuj]

If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.

If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.

If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.

It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

4. MODIFICATIONS[edytuj]

You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:

  • A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission.
  • B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement.
  • C. State on the Title page the name of the publisher of the Modified Version, as the publisher.
  • D. Preserve all the copyright notices of the Document.
  • E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices.
  • F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below.
  • G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice.
  • H. Include an unaltered copy of this License.
  • I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence.
  • J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission.
  • K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein.
  • L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles.
  • M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version.
  • N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section.
  • O. Preserve any Warranty Disclaimers.

If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.

You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.

You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.

The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.

5. COMBINING DOCUMENTS[edytuj]

You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.

The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.

In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements."

6. COLLECTIONS OF DOCUMENTS[edytuj]

You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.

You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

7. AGGREGATION WITH INDEPENDENT WORKS[edytuj]

A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.

If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.

8. TRANSLATION[edytuj]

Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.

If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.

9. TERMINATION[edytuj]

You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

10. FUTURE REVISIONS OF THIS LICENSE[edytuj]

The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.

Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.

How to use this License for your documents[edytuj]

To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page:

Copyright (c)  YEAR  YOUR NAME.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.2
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".

If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the "with...Texts." line with this:

with the Invariant Sections being LIST THEIR TITLES, with the
Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.

If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation.

If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.