Przejdź do zawartości

OGRE/Korzystanie z wielu menadżerów sceny

Z Wikibooks, biblioteki wolnych podręczników.
Informacja
Zapraszamy do anglojęzycznego tutoriala na oficjalnym wiki Ogra tutaj
Informacja
Uwaga! ten artykuł jest w trakcie tłumaczenia!

Wstęp

[edytuj]

Spróbujemy pokazać w jaki sposób i w jakim celu można użyć wielu Menadżerów Sceny w jednej aplikacji OGRE. Będziemy używać klasy map STL. Warto zapoznać się z nią przed rozpoczęciem pracy. Zastosujemy też operator warunkowy ?: z C++, warto byś znał jego składnię i możliwości.

Rozpoczęcie

[edytuj]

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

  // Dziedziczymy ExampleApplication

i przed nią wstaw poniższy kod:

#define CAMERA_NAME "SceneCamera"

class SMTutorialListener : public ExampleFrameListener
{
public:
    SMTutorialListener(RenderWindow* win, Camera* cam)
        : ExampleFrameListener(win, cam, false, false)
    {
    }

    bool frameStarted(const FrameEvent &evt)
    {
        // Pozwala wykonać metodę z klasy rodzica
        return ExampleFrameListener::frameStarted(evt);
    }
protected:
    MultiSceneManager *mMSM;
    SceneType mSceneType;    // Bieżący typ menadżera sceny
};

Prócz nowej klasy zdefiniowaliśmy również nazwę kamery, która ułatwi nam później tworzenie kodu. Kod ten nie nadaje się na razie do skompilowania. Musimy z tym poczekać, aż zdefiniujemy klasę MultiSceneManager.

Dodatkowe zmienne i metody w myApp

[edytuj]

Do klasy myApp dodamy jedną zmienną mMSM, która będzie zawierała obiekt klasy MultiSceneManager. Tworzymy też metodę createFrameListener, w której zarejestrujemy nasz FrameListener. Kod wstawiamy zaraz za dyrektywą protected: w klasie myApp.

     MultiSceneManager mMSM;  // Klasa MultiSceneManager
 
     void createFrameListener(void)
     {
         mFrameListener = new SMTutorialListener(mWindow, mCamera);
         mFrameListener->showDebugOverlay(true);
         mRoot->addFrameListener(mFrameListener);
     }

Zanim pójdziemy dalej

[edytuj]

Będziemy używać tu kilku menadżerów sceny w jednej aplikacji. Ogólnie mówiąc wykonuje się to w trzech ruchach. Najpierw zlikwidować trzeba wszystkie Viewport-y i kamery połączone z bieżącym menageram sceny. Po drugie należy wziąć manager sceny który teraz chcesz wykorzystać i zarejestrować go w Root::_setCurrentSceneManager. Wreszcie trzeba odtworzyć kamery i viewport-y. To naprawdę prosty proces. W naszym przykładzie zbudujemy bardzo prostą klasę MultiSceneManager która umożliwi nam przełączanie pomiędzy różnymi menadżerami sceny. W następnym przykładzie rozszerzymy tę prostą klasę tak by mogła zarządzać róznymi kontekstami tego samego menadżera sceny.

Oczywiście zauważysz że najwiecej pracy jak zwykle mają klasy z Example Framework. Example Framework został napisany z założeniem że zmienna mCamera nie będzie zmieniana po utworzeniu, co nie ułatwia sprawy gdy zmieniamy menadżera sceny. Chciałem zachować Example Framework w całum Tutorialu tak, żebyś nie musiał uczyć sie nowej struktury podczas nauki innych rzeczy. Jednakże gdybyś chciał budować co poważniejszego musisz zaprzestać używania Example Framework i napisać swoje własne klasy aplikacji i FrameListener-a dostosowane do twoich celów.

Wreszcie, zauważ że kolejność klas w pliku źródłowym musi być właściwa albo program nie da się skompilować. Kolejność powinna być następująca:

  // #defines
  // SceneManagerState class
  // MultiSceneManager class
  // SMTutorialListener class
  // SMTutorialApplication class
  // main method

Klasa SceneManagerState

[edytuj]

Zamierzamy zdefiniować dwie klasy umożliwiające wymienianie menadżerów sceny. Klasa MultiSceneManager będzie publicznym interfejsem do zamiany menadżerów, a SceneManagerState będzie zachowywać ślady zmiennych menadżera który zostaje zlikwidowany kiedy dokonujemy zamiany.

Wprowadź ten kod zaraz za linią <#define CAMERA_NAME "SceneCamera">:

   // SceneManagerState
   class SceneManagerState
   {
   public:
       SceneManagerState( SceneManager *sceneManager, RenderWindow *renderWindow )
           : mSceneManager( sceneManager ), mRenderWindow( renderWindow )
       {
       }
   
       ~SceneManagerState( )
       {
       }
   
       // called when this SceneManager is being displayed
       void showScene( )
       {
       }
   
       // called when this SceneManager is no longer being displayed
       void hideScene( )
       {
       }
   
       // returns the SceneManager
       SceneManager *getSceneManager( )
       {
           return mSceneManager;
       }
   protected:
       Vector3 mCamPosition;        // The camera position
       Quaternion mCamOrientation;  // The camera orientation
       SceneManager *mSceneManager; // The SceneManager this object wraps
       RenderWindow *mRenderWindow; // The RenderWindow the application uses
   };

Klasa ta zawiera podstawowe informacje na temat kamery, ponieważ kiedy niszczymy kamerę tracimy też jej pozycję i orientację. W momencie, gdy klasa ta ma załadować menadżera sceny, wywoływana jest także metoda showScene. Natomiast przy wyładowaniu menadżera wywoływana jest metoda hideScene.

Metoda showScene musi stworzyć kamerę, ustawić proporcje, a także odtworzyć pozycję i orientację kamery. Dodaj poniższy kod do metody showScene:

        // create the camera and viewport
        Camera *cam = mSceneManager->createCamera( CAMERA_NAME );
        Viewport *vp = mRenderWindow->addViewport( cam );
        vp->setBackgroundColour(ColourValue(0,0,0));
  
        // Set the aspect ratio
        cam->setAspectRatio(Real(vp->getActualWidth()) / Real(vp->getActualHeight()));
        cam->setNearClipDistance( 5 );
  
        // Set the camera's position and orientation
        cam->setPosition( mCamPosition );
        cam->setOrientation( mCamOrientation );

Metoda hideScene musi zapisać pozycję i orientację kamery a następnie zlikwidować wszystkie viewport-y i kamery. Dodaj poniższy kod do metody hideScene:

       // save camera position and orientation
       Camera *cam = mSceneManager->getCamera( CAMERA_NAME );
       mCamPosition = cam->getPosition();
       mCamOrientation = cam->getOrientation();
 
       // destroy the camera and viewport
       mRenderWindow->removeAllViewports( );
#if OGRE_VERSION < OGRE_CHANGE1
       mSceneManager->removeAllCameras( );
#else
       mSceneManager->destroyAllCameras( );
#endif

Nasz kod nie może zostać skompilowany, dopóki nie zdefiniujemy klasy MultiSceneManager.

Klasa MultiSceneManager

[edytuj]

Drugą częścią systemu jest klasa MultiSceneManager. By użyć takiego obiektu, trzeba najpierw zarejestrować go w obiekcie Root i w obiekcie RenderWindow. Następnie musisz zarejestrować każdy typ menadżera sceny jaki chcesz użyć, na przykład ST_GENERIC, ST_EXTERIOR_CLOSE, itd. Gdy już ustawisz obiekty Root/RenderWindow i zarejestrujesz typy menadżerów sceny, będziesz mógł użyć tej klasy i ustawiać swoje menadżery sceny.

By skonfigurować menadżer sceny (na przykład ustawić wstępny widok), wywołaj metodę getSceneManager z typem menadżera sceny który chcesz użyć. Metoda zwróci obiekt menadżera sceny. Przy ustawianiu, czy przełączaniu po prostu wywołuje się metodę setSceneManager z typem menadżera.

Jądro klasy MultiSceneManager dodajemy na początku definicji klas.

   // MultiSceneManager
   class MultiSceneManager
   {
   public:
       MultiSceneManager( )
           : mCurrentState( 0 )
       {
       }
   
       ~MultiSceneManager( )
       {
       }
   
       // sets the Root object
       void setRoot( Root *root )
       {
           mRoot = root;
       }
   
       // sets the RenderWindow
       void setRenderWindow( RenderWindow *renderWindow )
       {
           mRenderWindow = renderWindow;
       }
   
       // registers a scene manager class for usage, you cannot call
       // setSceneManager on any type without first calling registerSceneManager
       void registerSceneManager( SceneType st )
       {
       }
   
       // makes st the current SceneManager
       void setSceneManager( SceneType st )
       {
       }
   
       // returns the SceneManager that's currently rendering
       SceneManager *getCurrentSceneManager( )
       {
           return mCurrentState ? mCurrentState->getSceneManager() : 0;
       }
   
       // returns the SceneManager associated with type st (must already be registered)
       SceneManager *getSceneManager( SceneType st )
       {
       }
   protected:
       // finds and returns the SceneManagerState associated with st
       SceneManagerState *findState( SceneType st )
       {
       }
   
       std::map< SceneType, SceneManagerState * > mStateMap;  // The map of registered states
       SceneManagerState *mCurrentState;  // the current state
       Root *mRoot;                       // the Root object
       RenderWindow *mRenderWindow;       // the RenderWindow
   };

Zaczynamy z metodą registerSceneManager. Metoda dodaje nowy element mStateMap dla wyspecyfikowanego SceneType. Dokonujemy tego tworząc objekt SceneManagerState i kojarząc go z kluczem st:

        mStateMap[ st ] = new SceneManagerState( SceneManagerEnumerator::getSingleton().getSceneManager( st ), mRenderWindow );

W Ogre 1.2, zamień getSceneManager na createSceneManager.

Użyliśmy operatora new, musimy więc później pamiętać o usunięciu obiektu. W destruktorze usuniemy elementy mapy. Najpierw utworzymy iterator, następnie iterujemy poprzez elementy mapy, usuwając obiekty SceneManagerState (dodaj ten kod do ~MultiSceneManager):

        std::map< SceneType, SceneManagerState * >::iterator itr;
   
        for (itr = mStateMap.begin( ); itr != mStateMap.end( ); ++itr)
            delete itr->second;

The itr->second points to the SceneManagerState, whereas itr->first would point to the SceneType that we use as a key for the map.

We will now implement the helper function findState. The findState function searches the map for the specified state, and returns it (or 0 if it's not found).

        // Search map
        std::map< SceneType, SceneManagerState * >::iterator itr = mStateMap.find( st );
        
        // return the result or 0
        return (itr != mStateMap.end( )) ? itr->second : 0;

Now that findState is implemented, we can implement the getSceneManager function easily. The getSceneManager function returns the SceneManager assocated with the SceneType specified, but does NOT set it as the current SceneManager. We will first search for the requested SceneType, then return it (or 0 if the SceneType was not registered):

        SceneManagerState *sms = findState(st);
        return sms ? sms->getSceneManager( ) : 0;

The last function we have to implement is the setSceneManager method. The basic flow of the method is to first hide the current SceneManager, find the requested SceneManager and start it rendering:

        // hide the current SceneManager
        if ( mCurrentState )
            mCurrentState->hideScene( );
   
        // find the next SceneManager
        mCurrentState = findState( st );
  
        // set the current SceneManager in the Root class after initializing
        // the state
        if ( mCurrentState )
        {
            mCurrentState->showScene( );
            mRoot->_setCurrentSceneManager( mCurrentState->getSceneManager() );
        }

That wraps up the MultiSceneManager class. Be sure your code compiles before continuing.

Klasa SMTutorialApplication

[edytuj]

The first thing we have to do in the SMTutorialApplication class is stop the ExampleApplication class from trying to create cameras and viewports on its own. To do this, override the createCamera and createViewports with empty functions in the protected portion of the class:

    // We overide this method with an empty class and hide the base method
    // since all of this functionality is contained in the chooseSceneManager
    // method now.
    void createCamera()
    {
    }
    
    // We overide this method with an empty class and hide the base method
    // since all of this functionality is contained in the chooseSceneManager
    // method now.
    void createViewports()
    {
    }

Now we need to set up the MultiSceneManager class. We do this by adding code to the chooseSceneManager method. First we register the information that the MultiSceneManager class needs to operate. We cannot pass these as constructor values, since at the time the mMSM object is constructed the Root and RenderWindow variables are not yet set.

        // Set the required information
        mMSM.setRoot( mRoot );
        mMSM.setRenderWindow( mWindow );

Now we need to register the SceneTypes that we will be using in the application. You may register these as you use them instead of all at the beginning if you like. The only restriction is that a SceneType must be registered before you call setSceneManager or getSceneManager with that type.

        // Register SceneTypes we will be using: 
        mMSM.registerSceneManager( ST_GENERIC );
        mMSM.registerSceneManager( ST_EXTERIOR_CLOSE );

Now we need to set the startup SceneManager:

        // Set the startup SceneType:
        mMSM.setSceneManager( ST_GENERIC );

Normally this would be all that you need to do during your SceneManager creation method. However, ExampleApplication uses two internal variables mSceneMgr and mCamera, which we should populate if we are using the ExampleFramework. Note however that as soon as we swap states both variables are invalid. If you rewrite the Example framework you should make the classes aware of this and query for the SceneManager/Camera when they are needed. This is the last chunk of code that is needed in the chooseSceneManager method:

        // Add variables that ExampleApplication expects
        mSceneMgr = mMSM.getSceneManager( ST_GENERIC );
        mCamera = mSceneMgr->getCamera( CAMERA_NAME );

Now we are going to setup the scene for each of our SceneManagers. Find the createScene method. We will set the Terrain and sky box for the TerrainSceneManager, and add a Robot to the Generic SceneManager:

        // Setup the TerrainSceneManager
        SceneManager *tsm = mMSM.getSceneManager( ST_EXTERIOR_CLOSE );
        tsm->setOption("PrimaryCamera", mCamera);
        tsm->setWorldGeometry( "Terrain.cfg" );
        tsm->setSkyBox(true, "Examples/SpaceSkyBox", 50 );
       
        // Setup the Generic SceneManager
        Entity *ent;
        SceneNode *sn;
        SceneManager *gsm = mMSM.getSceneManager( ST_GENERIC );
  
        mCamera->lookAt( 0, 0, -250 );
        ent = gsm->createEntity( "Robot", "robot.mesh" );
        ent->setMaterialName( "Examples/Robot" );
        sn = gsm->getRootSceneNode()->createChildSceneNode( "RobotNode" );
        sn->attachObject( ent );
        sn->setPosition( 0, 0, -250 );

Note that we have used mCamera here. We can do this for the current SceneManager, but we cannot do this for the TerrainSceneManager due to the limitations of this design. Compile and run the application. You should now be staring at a Robot, but since we have not added any key bindings for swapping SceneManagers we currently don't have any way to swap SceneManagers.

Klasa SMTutorialFrameListener

[edytuj]

The last thing we need to do is add a way to swap SceneManagers. First, find the following line in SMTutorialApplication::createFrameListener:

        mFrameListener = new SMTutorialListener(mWindow, mCamera);

Since we will need the MultiSceneManager class in the FrameListener, change it to this:

        mFrameListener = new SMTutorialListener(mWindow, mCamera, &mMSM);

Also change the SMTutorialListener class from this:

    SMTutorialListener(RenderWindow* win, Camera* cam)
        : ExampleFrameListener(win, cam, false, false)
    {
    }

To this:

    SMTutorialListener(RenderWindow* win, Camera* cam, MultiSceneManager *msm)
        : ExampleFrameListener(win, cam, false, false)
    {
        mMSM = msm;
        mSceneType = ST_GENERIC;
    }

Finally, we will add some code to the frameStarted method to swap between SceneManagers every time the C key is pressed:

        if ( mInputDevice->isKeyDown( OIS::KC_C ) && (mTimeUntilNextToggle < 0.0f))
        {
            mSceneType = ( mSceneType == ST_GENERIC ) ? ST_EXTERIOR_CLOSE : ST_GENERIC;
  
            mMSM->setSceneManager( mSceneType );
            mCamera = mMSM->getSceneManager( mSceneType )->getCamera( CAMERA_NAME );
            mTimeUntilNextToggle = 1.0f;
        } // if

Note that we have to update mCamera every time we swap the SceneManager due to limitations of the Example framework. Compile and run the application. We are now done! C swaps SceneManagers, though note that you will have to look to your right to see the Terrain when swapping to that manager. Also note that the Camera does indeed save your position when you swap SceneManagers.

Ćwiczenia

[edytuj]

Łatwe ćwiczenia

[edytuj]

A memory leak will occur if you try to register the same SceneType multiple times. Instead of allowing the user to register the same SceneType multiple times, throw an Ogre::Exception class when the user tries to do this. Currently if a SceneType is not registered, the MultiSceneManager will just ignore it and return 0. Instead, throw an Ogre::Exception every time the user tries to use a SceneType that's not registered.

Średniotrudne ćwiczenia

[edytuj]
  1. Zmodyfikuj klasę MultiSceneManager tak aby zamiast rzucać wyjątek z powodu niezarejestrowanych SceneTypes, tworzyła nowy gdy napotka taki niezarejestrowany SceneType. Jak uważasz, które z tych rozwiązań jest lepsze?
  2. W aplikacji może tak naprawdę istnieć tylko jeden obiekt MultiSceneManager. Przerób tą klasę tak aby dziedziczyła po Ogre::Singleton.

Zaawansowane ćwiczenia

[edytuj]
  1. Modify the code so that you can set a default Camera position and Orientation.

The way cameras are currently handled there is only one camera ever, and it is retrieved based on a #defined Camera name. Make this sytem more robust by adding a method to add multiple Cameras to each SceneManager and to be able to query for them. Don't forget that every Camera that is created will need to be stored when they are destroyed during the hideScene method.

  1. When a SceneManager is swapped for another SceneManager, some classes will need to be made aware of this. For example, it would be inefficient to query for the current SceneManager and Camera EVERY frame if the SceneManager is changed very rarely. Implement a SceneChangeEvent which is fired every time the SceneManager is changed. Create a SceneChangeListener with an abstract interface for receiving these events. Note that the SceneChangeEvent should probably include the SceneManager which is being hidden and the SceneManager which is being shown. When should this event occur? Before the current SceneManager is hidden? After the new SceneManager is shown? In between the two events?

Ćwiczenia na później

[edytuj]

Design and implement a similar set of classes which manipulate "SceneManager contexts" instead of SceneManagers. You may need to have a single SceneManager do multiple things. For example, you may need have the TerrainSceneManager display two seperate sets of Geometry and Entities which you swap back and forth between. This is currently not possible with the methods implemented here.