3rd person camera system tutorial         How to create a basic, flexible camera system


The idea

The idea for this camera system came while I was watching The Making of Silent Hill 2, where some debug screens appeared and I was able to notice some "lines". If you've seen it, you'll understand what I'm talking about.

Well. The main idea behind this camera system is to have only one (usually) camera in the scene at the same moment. As the opposite of other 3rd/1st person camera systems, here the camera is "detached" from the whole scene (except from the root node which is crucial for it to work).

This philosophy allows us to obtain very nice effects and soft movement, giving a more friendly way to see the scene.

In general terms, the camera kernel (here called ExtendedCamera) consists of two scene nodes, which will act as the camera "handler" and the camera target. The handler is supposed to be looking always to the target. Moving the target will result in looking around; Moving the "handler", in pivoting, rolling... around the target; moving both at the same time and amount, panning...

This is a simple version of the system. Advanced enhancements would allow shaking effects, cinematic cameras (crane, rail...) through Virtual cameras.

I'll explain how the three different camera modes included in this demo work

3rd person camera - Chasing


We have a main character, who has a main node (the actor), a sight node (the point the character is supposed to be looking at), and a chase camera node (where we think the best chasing camera should be placed). There are other ways to achieve the same effect, but I will use this for simplicity.

What we will do is to use the sight node as the desired position of the camera target, and the chase camera node as the desired position of the camera itself.

Further sight nodes will mean the character will me more offseted from the center of the screen (for instance, if the character is in "investigation" mode in a 3rd person camera game, in the opposite of a "walk around" mode). This helps to keep a broad view of the scene, while keeping track of the character.

3rd person camera - Fixed

This kind of camera can be seen in many adventure games like Resident Evil, Silent Hill, Alone in the dark... The idea is that the target will follow the character sight, but it will remain fixed in a single position.

The way this camera works is similar to the chasing camera.

Variants of this are crane cameras, rail cameras... used in movie sets.

1st person camera

Instead of using the desired chase camera position, we use the character position as the desired position for the camera. For this mode, cameras with a tightness value of 1 work better (and hiding the character model too).

That's the only difference with the 3rd person cameras described above.

Look here for a how to that uses the ExampleFrameListener in Samples/Common/include - First person camera http://www.ogre3d.org/tikiwiki/tiki-index.php?page=Creating+a+simple+first-person+camera+system

Important things to keep in mind

Coordinate system

As the camera is independent to any other object in the scene, we will work with world coordinates.

Camera movement

The camera (and the target) moves in this way. We calculate the difference between the camera position and the desired position. As a result we get a displacement vector that will be applied to the camera so it moves to the desired position.

Tightness

As the movement described above is too rigid for us to be "user-friendly", we need to find a way to make it smoother. So the concept of movement tightness appear. The thightness factor ranges from 0.0 to 1.0, and determines which proportion of the displacement vector will be actually applied.

  • Tightness factor of 1.0 results in rigid movement: Each unit the object moves, the camera will move too
  • Tightness factor of 0.0 results in no-movement: The displacement vector will have a length of 0, so no movement is done
  • Thightness factors outside of 0.0 - 1.0 results in undesired behaviors.

The source code, explained

You can find this source code ported to OgreDotNet by Alberts here or attached to this page:
 Plugin disabled
Plugin attach cannot be executed.

You can find this tutorial and source code ported to Python-Ogre by Zyzle here


First of all, we include a header to use the example framework of OGRE

/* Camera system tutorial by Kencho */
 
#include "ExampleApplication.h"


Next, we define a generic Character class. Here, the Character class is used to define every object that can be tracked and chased by a camera. Of course, in a game or application, this would have many more members ;)

// Generic Character class
 class Character {
 // Attributes ------------------------------------------------------------------------------
     protected:
         SceneNode *mMainNode; // Main character node
         SceneNode *mSightNode; // "Sight" node - The character is supposed to be looking here
         SceneNode *mCameraNode; // Node for the chase camera
         Entity *mEntity; // Character entity
         SceneManager *mSceneMgr;
     public:
 // Methods ---------------------------------------------------------------------------------
     protected:
     public:
         // Updates the character (movement...)
         virtual void update (Real elapsedTime, OIS::Keyboard *input) = 0;
         // The three methods below returns the two camera-related nodes, 
         // and the current position of the character (for the 1st person camera)
         SceneNode *getSightNode () {
             return mSightNode;
         }
         SceneNode *getCameraNode () {
             return mCameraNode;
         }
         Vector3 getWorldPosition () {
             return mMainNode->_getDerivedPosition ();
         }
 };


Next: Our specialization of the Character class for this demo. I think a nice floating Ogre head is a cute character for a demo, so we define a specialization of the Character class, that will handle, it's nodes, model, and a 3D-type movement (turn left/right, advance...)

// Specialization of the Character class - Our dear Ogre :D
 class OgreCharacter : public Character {
 // Attributes ------------------------------------------------------------------------------
     protected:
         String mName;
     public:
 // Methods ---------------------------------------------------------------------------------
     protected:
     public:
         OgreCharacter (String name, SceneManager *sceneMgr) {
             // Setup basic member references
             mName = name;
             mSceneMgr = sceneMgr;
 
             // Setup basic node structure to handle 3rd person cameras
             mMainNode = mSceneMgr->getRootSceneNode ()->createChildSceneNode (mName);
             mSightNode = mMainNode->createChildSceneNode (mName + "_sight", Vector3 (0, 0, 100));
             mCameraNode = mMainNode->createChildSceneNode (mName + "_camera", Vector3 (0, 50, -100));
 
             // Give this character a shape :)
             mEntity = mSceneMgr->createEntity (mName, "OgreHead.mesh");
             mMainNode->attachObject (mEntity);
         }
         ~OgreCharacter () {
             mMainNode->detachAllObjects ();
             delete mEntity;
             mMainNode->removeAndDestroyAllChildren ();
             mSceneMgr->destroySceneNode (mName);
         }
 
         void update (Real elapsedTime, OIS::Keyboard *input) {
             // Handle movement
             if (input->isKeyDown (OIS::KC_W)) {
                 mMainNode->translate (mMainNode->getOrientation () * Vector3 (0, 0, 100 * elapsedTime));
             }
             if (input->isKeyDown (OIS::KC_S)) {
                 mMainNode->translate (mMainNode->getOrientation () * Vector3 (0, 0, -50 * elapsedTime));
             }
             if (input->isKeyDown (OIS::KC_A)) {
                 mMainNode->yaw (Radian (2 * elapsedTime));
             }
             if (input->isKeyDown (OIS::KC_D)) {
                 mMainNode->yaw (Radian (-2 * elapsedTime));
             }
         }
 
         // Change visibility - Useful for 1st person view ;)
         void setVisible (bool visible) {
             mMainNode->setVisible (visible);
         }
 };

In order to keep simplicity, I've avoided methods to change the sightness of the character, animating the model...

Now, the interesting part: The ExtendedCamera class. It follows the philosophy I described above. The code is self explanatory, so if you have questions about how this works, you can read again the previous sections :-)

// Our extended camera class
 class ExtendedCamera {
 // Attributes ------------------------------------------------------------------------------
     protected:
         SceneNode *mTargetNode; // The camera target
         SceneNode *mCameraNode; // The camera itself
         Camera *mCamera; // Ogre camera
 
         SceneManager *mSceneMgr;
         String mName;
 
         bool mOwnCamera; // To know if the ogre camera binded has been created outside or inside of this class
 
         Real mTightness; // Determines the movement of the camera - 1 means tight movement, while 0 means no movement
     public:
 // Methods ---------------------------------------------------------------------------------
     protected:
     public:
         ExtendedCamera (String name, SceneManager *sceneMgr, Camera *camera = 0) {
             // Basic member references setup
             mName = name;
             mSceneMgr = sceneMgr;
 
             // Create the camera's node structure
             mCameraNode = mSceneMgr->getRootSceneNode ()->createChildSceneNode (mName);
             mTargetNode = mSceneMgr->getRootSceneNode ()->createChildSceneNode (mName + "_target");
             mCameraNode->setAutoTracking (true, mTargetNode); // The camera will always look at the camera target
             mCameraNode->setFixedYawAxis (true); // Needed because of auto tracking
 
             // Create our camera if it wasn't passed as a parameter
             if (camera == 0) {
                 mCamera = mSceneMgr->createCamera (mName);
                 mOwnCamera = true;
             }
             else {
                 mCamera = camera;
                                 // just to make sure that mCamera is set to 'origin' (same position as the mCameraNode)
                                 mCamera->setPosition(0.0,0.0,0.0);
                 mOwnCamera = false;
             }
             // ... and attach the Ogre camera to the camera node
             mCameraNode->attachObject (mCamera);
 
             // Default tightness
             mTightness = 0.01f;
         }
         ~ExtendedCamera () {
             mCameraNode->detachAllObjects ();
             if (mOwnCamera)
                 delete mCamera;
             mSceneMgr->destroySceneNode (mName);
             mSceneMgr->destroySceneNode (mName + "_target");
         }
 
         void setTightness (Real tightness) {
             mTightness = tightness;
         }
 
         Real getTightness () {
             return mTightness;
         }
 
         Vector3 getCameraPosition () {
             return mCameraNode->getPosition ();
         }
 
         void instantUpdate (Vector3 cameraPosition, Vector3 targetPosition) {
             mCameraNode->setPosition (cameraPosition);
             mTargetNode->setPosition (targetPosition);
         }
 
         void update (Real elapsedTime, Vector3 cameraPosition, Vector3 targetPosition) {
             // Handle movement
             Vector3 displacement;
 
             displacement = (cameraPosition - mCameraNode->getPosition ()) * mTightness;
             mCameraNode->translate (displacement);
 
             displacement = (targetPosition - mTargetNode->getPosition ()) * mTightness;
             mTargetNode->translate (displacement);
         }
 };

A sample frame listener that will handle the update of the character and the camera, and camera mode changes. Again, self explanatory code and philosophy explained above.

class SampleListener : public ExampleFrameListener
 {
 protected:
     // References to the main character and the camera
     Character *mChar;
     ExtendedCamera *mExCamera;
 
     // Camera mode - Now supports 1st person, 3rd person (chasing) and 3rd person (fixed)
     unsigned int mMode;
 
 public:
     SampleListener(RenderWindow* win, Camera* cam)
     : ExampleFrameListener(win, cam)
     {
         mChar = 0;
         mExCamera = 0;
         mMode = 0;
     }
 
     void setCharacter (Character *character) {
         mChar = character;
     }
 
     void setExtendedCamera (ExtendedCamera *cam) {
         mExCamera = cam;
     }
 
     bool frameStarted(const FrameEvent& evt)
     {
         mKeyboard->capture();
 
         if (mChar) {
             mChar->update (evt.timeSinceLastFrame, mKeyboard);
 
             if (mExCamera) {
                 switch (mMode) {
                     case 0: // 3rd person chase
                         mExCamera->update (evt.timeSinceLastFrame, 
                                             mChar->getCameraNode ()->getWorldPosition (), 
                                             mChar->getSightNode ()->getWorldPosition ());
                         break;
                     case 1: // 3rd person fixed
                         mExCamera->update (evt.timeSinceLastFrame, 
                                             Vector3 (0, 200, 0), 
                                             mChar->getSightNode ()->getWorldPosition ());
                         break;
                     case 2: // 1st person
                         mExCamera->update (evt.timeSinceLastFrame, 
                                             mChar->getWorldPosition (), 
                                             mChar->getSightNode ()->getWorldPosition ());
                         break;
                 }
             }
         }
 
         // 3rd Person - Chase Camera
         if (mKeyboard->isKeyDown (OIS::KC_F1)) {
             mMode = 0;
             if (mChar)
                 static_cast<OgreCharacter *>(mChar)->setVisible (true);
             if (mExCamera) {
                 if (mChar)
                     mExCamera->instantUpdate (mChar->getCameraNode ()->getWorldPosition (), mChar->getSightNode ()->getWorldPosition ());
                 mExCamera->setTightness (0.01f);
             }
          }
         // 3rd Person - Fixed Camera
         if (mKeyboard->isKeyDown (OIS::KC_F2)) {
             mMode = 1;
             if (mChar)
                 static_cast<OgreCharacter *>(mChar)->setVisible (true);
             if (mExCamera) {
                 if (mChar)
                     mExCamera->instantUpdate (Vector3 (0, 200, 0), mChar->getSightNode ()->getWorldPosition ());
                 mExCamera->setTightness (0.01f);
             }
         }
         // 1st Person
         if (mKeyboard->isKeyDown (OIS::KC_F3))  {
             mMode = 2;
             if (mChar)
                 static_cast<OgreCharacter *>(mChar)->setVisible (false);
             if (mExCamera) {
                 if (mChar)
                     mExCamera->instantUpdate (mChar->getWorldPosition (), mChar->getSightNode ()->getWorldPosition ());
                 mExCamera->setTightness (1.0f);
             }
         }
 
         // Exit if we press Esc
         if(mKeyboard->isKeyDown (OIS::KC_ESCAPE))
             return false;
 
         return true;
     }
 };

A sample application. If you have doubts about this, try reading other sections of the wiki :-)

class SampleApplication : public ExampleApplication
 {
 protected:
 public:
     SampleApplication()
     {
     }
     ~SampleApplication()
     {
     }
 
 protected:
     // Just override the mandatory create scene method
     void createScene(void)
     {
         // Set ambient light
         mSceneMgr->setAmbientLight(ColourValue(0.2, 0.2, 0.2));
 
         // LIGHTS!!
         // Create a point light
         Light* l = mSceneMgr->createLight("MainLight");
         // Accept default settings: point light, white diffuse, just set position
         // NB I could attach the light to a SceneNode if I wanted it to move automatically with
         //  other objects, but I don't
         l->setType(Light::LT_DIRECTIONAL);
         l->setDirection(-0.5, -0.5, 0);
 
         // CAMERA!!
         mCamera->setPosition (0, 0, 0);    // Required or else the camera will have an offset
 
         // ACTION!!!
         // Fill the scene with some razors
         SceneNode *razorNode;
         Entity *razorEntity;
         for (unsigned int i = 0; i < 30; ++i) {
             razorNode = mSceneMgr->getRootSceneNode ()->createChildSceneNode (StringConverter::toString (i), Vector3 (Math::RangeRandom (-1000, 1000), 0, Math::RangeRandom (-1000, 1000)));
             razorEntity = mSceneMgr->createEntity (StringConverter::toString (i), "razor.mesh");
             razorNode->attachObject (razorEntity);
         }
 
         // Main character
         OgreCharacter *ogre = new OgreCharacter ("Ogre 1", mSceneMgr);
         ExtendedCamera *exCamera = new ExtendedCamera ("Extended Camera", mSceneMgr, mCamera);
 
         // Frame listener to manage both character and camera updating and different camera modes
         // Need to create it here as we want to change some parameters here, thus avoiding defining 
         // ogre and exCamera as member variables
         mFrameListener = new SampleListener (mWindow, mCamera);
         static_cast<SampleListener *>(mFrameListener)->setCharacter (ogre);
         static_cast<SampleListener *>(mFrameListener)->setExtendedCamera (exCamera);
     }
     void destroyScene(void)
     {
     }
     void createFrameListener(void)
     {
         // This is where we instantiate our own frame listener
 //        mFrameListener= new SampleListener(mWindow, mCamera);
         mRoot->addFrameListener(mFrameListener);
     }
 };

Note that in the method createFrameListener(), the frame listener won't be constructed as we did in the createScene() method, so there won't be two frame listeners. This is important as the second (and used) one, if we would create it in createFrameListener(), wouldn't be configured, and thus would crash the application.

To make it able to run

#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
 #define WIN32_LEAN_AND_MEAN
 #include "windows.h"
 
 INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
 #else
 int main(int argc, char **argv)
 #endif
 {
    // Create application object
    SampleApplication app;
 
    try {
        app.go();
    } catch( Exception& e ) {
 #if OGRE_PLATFORM == OGRE_PLATFORM_WIN32 
        MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK |      MB_ICONERROR | MB_TASKMODAL);
 #else
        fprintf(stderr, "An exception has occured: %s\n",
                e.getFullDescription().c_str());
 #endif
    }
 
    return 0;
 }

Closing words

Hope you find this useful. I'll be updating this later, fixing some explanations and methods. Thanks for reading.
Kencho


Alias: 3rd_person_camera_system_tutorial