OGRE/Kamera, światła i cienie

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.