Table of contents
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
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