History: Basic Tutorial 4
Source of version: 37
- «
- »
Copy to clipboard
{REMARKSBOX(type="warning",title="Has not been ported to the Ogre Wiki Tutorial Framework",close="n")}This tutorial is still pending an update to Ogre 1.7 and the Ogre Wiki Tutorial Framework.
Proceed at your own risk - strange things might happen.{REMARKSBOX}
{TRANSCLUDE(page="tutbox")}In this tutorial we will be introducing one of the most useful Ogre constructs: the FrameListener. By the end of this tutorial you will understand FrameListeners, how to use FrameListeners to do things that require updates every frame, and how to use OIS's unbuffered input system.{TRANSCLUDE}
%tutorialhelp%
{INCLUDE(page="Basic Tutorials Prerequsites")}{INCLUDE}
You can find the code for this tutorial ((BasicTutorial4Source|here)). As you go through the tutorial you should be slowly adding code to your own project and watching the results as we build it.
{maketoc}
!Getting Started
This is the code we're starting off with:
{CODE(caption="BasicTutorial4 header",wrap="1", colors="c++")}class BasicTutorial4 : public BaseApplication
{
public:
BasicTutorial4(void);
virtual ~BasicTutorial4(void);
protected:
virtual void createScene(void);
virtual bool frameRenderingQueued(const Ogre::FrameEvent& evt);
private:
bool processUnbufferedInput(const Ogre::FrameEvent& evt);
};
{CODE}
{CODE(caption="BasicTutorial4 implementation",wrap="1", colors="c++")}void BasicTutorial4::createScene(void)
{
}
//-------------------------------------------------------------------------------------
bool BasicTutorial4::processUnbufferedInput(const Ogre::FrameEvent& evt)
{
return true;
}
//-------------------------------------------------------------------------------------
bool BasicTutorial4::frameRenderingQueued(const Ogre::FrameEvent& evt)
{
bool ret = BaseApplication::frameRenderingQueued(evt);
return ret;
}
//-------------------------------------------------------------------------------------
{CODE}
We are overriding the {MONO()}frameRenderingQueued{MONO} function, and defining a private function called {MONO()}processUnbufferedInput{MONO}.
Let's spend the rest of the tutorial making them do something interesting. (:smile:)
~tc~ bool mMouseDown; // Whether or not the left mouse button was down last frame
Real mToggle; // The time left until next toggle
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
~/tc~
!FrameListeners
!!Introduction
In Ogre, we can register a class to receive notification before and after a frame is rendered to the screen.
That class is known as a FrameListener.
This FrameListener interface declares three functions which can be used to receive frame events:
{CODE(wrap="1", colors="c++")}
virtual bool frameStarted(const FrameEvent& evt);
virtual bool frameRenderingQueued(const FrameEvent& evt);
virtual bool frameEnded(const FrameEvent& evt);
{CODE}
|| {MONO()}frameStarted{MONO} | Called just before a frame is rendered.
{MONO()}frameRenderingQueued{MONO} | Called after all render targets have had their rendering commands issued, but before%%%the render windows have been asked to flip their buffers over
{MONO()}frameEnded{MONO} | Called just after a frame has been rendered. ||
This loops until any of the FrameListeners return false from frameStarted, frameRenderingQueued or frameEnded. The return values for these functions basically mean "keep rendering".
If you return false from either, the program will exit.
The FrameEvent object contains two variables, but only the timeSinceLastFrame is useful in a FrameListener. This variable keeps track of how long it's been since the frameStarted or frameEnded last fired. Note that in the frameStarted method, FrameEvent::timeSinceLastFrame will contain how long it has been since the last __frameStarted__ event was last fired (not the last time a frameEnded method was fired).
One important concept to realize about Ogre's FrameListeners is that the order in which they are called is entirely up to Ogre. You cannot determine which FrameListener is called first, second, third...and so on. If you need to ensure that FrameListeners are called in a certain order, then you should register only one FrameListener and have it call all of the objects in the proper order.
So, which one of the three FrameListener methods should you choose?
That of course depends on what you need to do, but if you only want to update your stuff once per frame, put it in the frameRenderingQueued event, because that one is called just before the GPU is made busy by flipping the render buffer.
So you want to keep your CPU busy while the GPU works.
Or, to quote the API docs:
{QUOTE()} The usefulness of this event comes from the fact that rendering
commands are queued for the GPU to process. These can take a little
while to finish, and so while that is happening the CPU can be doing
useful things. Once the request to 'flip buffers' happens, the thread
requesting it will block until the GPU is ready, which can waste CPU
cycles. Therefore, it is often a good idea to use this callback to
perform per-frame processing. Of course because the frame's rendering
commands have already been issued, any changes you make will only
take effect from the next frame, but in most cases that's not noticeable.{QUOTE}
{TRANSCLUDE(page="dobox")}Use the {MONO()}frameRenderingQueued{MONO} function of the FrameListener to update on a per frame basis if you want performance.{TRANSCLUDE}
!!Registering a FrameListener
Our BasicTutorial4 class already is a FrameListener - surprise. :)
It derives from BaseApplication which inherits a FrameListener:
{CODE(wrap="1", colors="c++")}class BaseApplication : public Ogre::FrameListener{CODE}
{MONO()}BaseApplication{MONO} implements the {MONO()}frameRenderingQueued{MONO} function, and we actually overrode that function in the BasicTutorial3 class in the previous tutorial.
Actually, we also overrode the {MONO()}createFrameListener{MONO} function as well.
But now we'll explain what these functions actually do.
In order to become a fully functional FrameListener, you need to register it with Ogre::Root.
You need to do that because Ogre::Root needs to know what framelisteners to call when a frame event occurs.
To add or remove a FrameListener, we can use two functions: {MONO()}Ogre::Root::addFrameListener{MONO} and {MONO()}Ogre::Root::removeFrameListener{MONO}.
The [http://www.ogre3d.org/docs/api/html/classOgre_1_1Root.html#Ogre_1_1Roota24|addFrameListener] method adds a FrameListener, and the [http://www.ogre3d.org/docs/api/html/classOgre_1_1Root.html#Ogre_1_1Roota25|removeFrameListener] method removes a FrameListener (that is, the FrameListener will no longer receive updates).
Note that the add|removeFrameListener methods only take in a pointer to a FrameListener (that is, FrameListeners do not have names you can use to remove them).
{MONO()}BaseApplication{MONO} uses the following code in {MONO()}createFrameListener{MONO} to register itself with Ogre::Root as a FrameListener:
{CODE(wrap="1", colors="c++")}mRoot->addFrameListener(this);{CODE}
After having done that, it's able to receive frame events from Ogre::Root, by means of the FrameListener functions frameStarted, frameRenderingQueued and frameEnded.
!!So, how does it work?
Let's take a peek at {MONO()}Ogre::Root::renderOneFrame{MONO}:
{CODE(wrap="1", colors="c++")}
bool Root::renderOneFrame(void)
{
if(!_fireFrameStarted())
return false;
if (!_updateAllRenderTargets())
return false;
return _fireFrameEnded();
}
{CODE}
Here you can see that Ogre::Root, when rendering a frame, fires a {MONO()}FrameStarted{MONO} event before updating all render targets.
And then fires the {MONO()}FrameEnded{MONO} event when it's done updating.
To see where Ogre::Root fires the {MONO()}FrameRenderingQueued{MONO} event, we'll take a look at an excerpt from {MONO()}Ogre::Root::_updateAllRenderTargets{MONO}:
{CODE(wrap="1", colors="c++")}
bool Root::_updateAllRenderTargets(void)
{
// update all targets but don't swap buffers
mActiveRenderer->_updateAllRenderTargets(false);
// give client app opportunity to use queued GPU time
bool ret = _fireFrameRenderingQueued();
// block for final swap
mActiveRenderer->_swapAllRenderTargetBuffers(mActiveRenderer->getWaitForVerticalBlank());
// more code follows ...
{CODE}
There you can see that it fires the FrameRenderingQueued event after updating the render targets, but before swapping the render target buffers.
That's all you need to know - for now - about the inner workings of FrameListeners.
Be sure you can compile the application before continuing.
!Setting up the Scene
!!Introduction
Before we dive directly into the code, let's briefly outline what we will be doing so that you understand where we are going when we create and add things to the scene.
We will be placing one object (a ninja) in the scene, and one point light in the scene. If you left click the mouse, the light will toggle on and off. Holding down the right mouse button turns on "mouse look" mode (that is, you look around with the Camera). We will also be placing SceneNodes around the scene which we will be attaching the Camera to for different viewpoints.
Pressing the 1 and 2 buttons chooses which Camera viewpoint to view the scene from.
!!The Code
Find our {MONO()}BasicTutorial4::createScene{MONO} method. The first thing we will be doing is setting the ambient light of the scene very low. We want scene objects to still be visible when the light is off, but we also want the light going on/off to be noticable:
{CODE(wrap="1", colors="c++")}
mSceneMgr->setAmbientLight(Ogre::ColourValue(0.25, 0.25, 0.25));
{CODE}
Now, add a Ninja entity to the scene at the origin:
{CODE(wrap="1", colors="c++")}
Ogre::Entity* ninjaEntity = mSceneMgr->createEntity("Ninja", "ninja.mesh");
Ogre::SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode("NinjaNode");
node->attachObject(ninjaEntity);
{CODE}
Now we will create a white point light and place it in the Scene, a small distance (relatively) away from the Ninja:
{CODE(wrap="1", colors="c++")}
Ogre::Light* pointLight = mSceneMgr->createLight("pointLight");
pointLight->setType(Ogre::Light::LT_POINT);
pointLight->setPosition(Ogre::Vector3(250, 150, 250));
pointLight->setDiffuseColour(Ogre::ColourValue::White);
pointLight->setSpecularColour(Ogre::ColourValue::White);
{CODE}
Now we need to create the SceneNodes which the Camera will be attached to:
{CODE(wrap="1", colors="c++")}
// Create the scene node
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode1", Ogre::Vector3(-400, 200, 400));
node->yaw(Ogre::Degree(-45));
node->attachObject(mCamera);
// create the second camera node
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode2", Ogre::Vector3(0, 200, 400));
{CODE}
That's it for the {MONO()}createScene{MONO} function. On to the {MONO()}frameRenderingQueued{MONO} function...
!TutorialFrameListener
!!Variables
We have defined a few variables in the TutorialFrameListener class.
Let's go over them before we get any further:
{CODE(wrap="1", colors="c++")}bool mMouseDown; // Whether or not the left mouse button was down last frame
Real mToggle; // The time left until next toggle
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
{CODE}
The mSceneMgr holds a pointer to the current SceneManager and the mCamNode holds the current SceneNode that the Camera is attached to. The mRotate and mMove are our constants of rotation and movement.
If you want the movement or rotation to be faster or slower, tweak these variables to be higher or lower.
The other two variables (mToggle and mMouseDown) control our input. We will be using "unbuffered" mouse and key input in this tutorial (buffered input will be the subject of our next tutorial). This means that we will be calling methods during our frame listener to query the state of the keyboard and mouse.
We run into an interesting problem when we try to use the keyboard to change the state of some object on the screen. If we see that a key is down, we can act on this information, but what happens the next frame?
Do we see that the same key is down and do the same thing again?
In some cases (like movement with the arrow keys) this is what we want to do. However, let's say we want the "T" key to toggle between a light being on or off.
The first frame the T key is down, the light gets toggled, the next frame the T key is still down, so it's toggled again... and again and again until the key is released.
We have to keep track of the key's state between frames to avoid this problem.
We will present two separate methods for solving this.
The mMouseDown variable keeps track of whether or not the mouse was also down the previous frame (so if mMouse down is true, we do not perform the same action again until the mouse is released).
The mToggle variable specifies the time until we are allowed to perform an action again. That is, when a button is pressed, mToggle is set to some length of time where no other actions can occur.
!!Constructor
The first thing to notice about the constructor is that we make a call to the ExampleFrameListener's constructor:
{CODE(wrap="1", colors="c++")} : ExampleFrameListener(win, cam, false, false)
{CODE}
The important thing to note is that the third and fourth variables are set to false. The third variable specifies if we want to use buffered key input, the fourth is if we want to use buffered mouse input (which we don't in this tutorial).
In the TutorialFrameListener constructor, we will set default values for all variables:
{CODE(wrap="1", colors="c++")} // key and mouse state tracking
mMouseDown = false;
mToggle = 0.0;
// Populate the camera and scene manager containers
mCamNode = cam->getParentSceneNode();
mSceneMgr = sceneMgr;
// set the rotation and move speed
mRotate = 0.13;
mMove = 250;
{CODE}
That's it. The mCamNode variable is initialized to be whatever the current parent of the camera is.
!!The frameStarted Method
Now we are going to get into the real meat of the tutorial: performing actions every frame.
Currently our frameStarted method has the following code in it:
{CODE(wrap="1", colors="c++")} return ExampleFrameListener::frameStarted(evt);
{CODE}
This chunk of code is what has allowed the tutorial application to run until we could get to this point. The ExampleFrameListener::frameStarted method defines a lot of behavior (such as all of the key bindings, all of the camera movement, etc). __Clear out the contents of the TutorialFrameListener::frameStarted method.__
The Open Input System (OIS) provides three primary classes to retrieve input: Keyboard, Mouse, and Joystick. In these tutorials we will really only be covering how to use the Keyboard and Mouse objects.
If you are interested in using a joystick (or gamepad) with Ogre, you should look into the Joystick class.
The first thing we will need to do when using unbuffered input is to capture the current state of the keyboard and mouse.
We do this by calling the capture method of the Mouse and Keyboard objects.
The example framework already creates these objects for us in the mMouse and mKeyboard variables. Add the following code to the now empty TutorialFrameListener::frameStarted member function:
{CODE(wrap="1", colors="c++")} mMouse->capture();
mKeyboard->capture();
{CODE}
Next, we want to be sure that the program exits if the Escape key is pressed.
We check to see if a button is pressed by calling the isKeyDown method of InputReader and specifying a [http://www.ogre3d.org/docs/api/html/namespaceOgre.html#a662|KeyCode]. If the Escape key is pressed, we'll just return false to end the program:
{CODE(wrap="1", colors="c++")}
if(mKeyboard->isKeyDown(OIS::KC_ESCAPE))
return false;
{CODE}
In order to continue rendering the frameStarted method must return a positive boolean value.
To do this we will add the following line to the end of the method:
{CODE(wrap="1", colors="c++")} return true;
{CODE}
All of the following code that we will be discussing goes __above__ that final "return true" line.
The first thing we are going to do with our FrameListener is make the left mouse button toggle the light on and off.
We can find out if a mouse button is down by calling the [http://www.ogre3d.org/docs/api/html/classOgre_1_1InputReader.html#Ogre_1_1InputReadera17|getMouseButton] method of InputReader with the button we want to query for. Usually 0 is the left mouse button, 1 is the right mouse button, and 2 is the center mouse button. On some systems button 1 is the middle and 2 is the right mouse button. Try this configuration if the mouse buttons don't work as expected.
{CODE(wrap="1", colors="c++")} bool currMouse = mMouse->getMouseState().buttonDown(OIS::MB_Left);
{CODE}
The currMouse variable will be true if the mouse button is down. Now we will toggle the light depending on whether or not currMouse is true, and if the mouse was not held down the previous frame (because we only want to toggle the light once every time the mouse is pressed). Also note that the [http://www.ogre3d.org/docs/api/html/classOgre_1_1Light.html#Ogre_1_1Lighta33|setVisible] method of the [http://www.ogre3d.org/docs/api/html/classOgre_1_1Light.html|Light] class determines if the object actually emits light or not:
{CODE(wrap="1", colors="c++")}if (currMouse && ! mMouseDown)
{
Light *light = mSceneMgr->getLight("pointLight");
light->setVisible(! light->isVisible());
}
{CODE}
Now we need to set the mMouseDown variable to equal whatever the currMouse variable contains. Next frame this will tell us if the mouse button was up or down previously.
{CODE(wrap="1", colors="c++")} mMouseDown = currMouse;
{CODE}
Compile and run the application. Now left clicking toggles the light on and off! Note that since we no longer call the ExampleFrameListener's frameStarted method, we cannot move the camera around (yet).
This method of storing the previous state of the mouse button works well, since we know we already have acted on the mouse state.
The drawback is to use this for every key we bind to an action, we'd need a boolean variable for it.
One way we can get around this is to keep track of the last time any button was pressed, and only allow actions to happen after a certain amount of time has elapsed. We keep track of this state in the mToggle variable.
If mToggle is greater than 0, then we do not perform any actions, if mToggle is less than 0, then we do perform actions.
We'll use this method for the following two key bindings.
The first thing we want to do is decrement the mToggle variable by the time that has elapsed since the last frame:
{CODE(wrap="1", colors="c++")} mToggle -= evt.timeSinceLastFrame;
{CODE}
Now that we have updated mToggle, we can act on it.
Our next key binding is making the 1 key attach the Camera to the first SceneNode. mToggle acts as a 0.5 second delay before any additional changes can take place.
In practice, this delay is longer than necessary, but it illustrates the point.
{CODE(wrap="1", colors="c++")}if ((mToggle < 0.0f ) && mKeyboard->isKeyDown(OIS::KC_1))
{
mToggle = 0.5f;
mCamera->getParentSceneNode()->detachObject(mCamera);
mCamNode = mSceneMgr->getSceneNode("CamNode1");
mCamNode->attachObject(mCamera);
}
{CODE}
The camera is "moved" to CamNode1 by first detaching itself from it's parent SceneNode and then reattaching itself to the CamNode1 SceneNode. We will also do this for CamNode2 when the 2 button is pressed. The code is identical except for changing 1 to 2, and using an else if instead of if (because we wouldn't be doing both at the same time):
{CODE(wrap="1", colors="c++")}
else if ((mToggle < 0.0f) && mKeyboard->isKeyDown(OIS::KC_2))
{
mToggle = 0.5f;
mCamera->getParentSceneNode()->detachObject(mCamera);
mCamNode = mSceneMgr->getSceneNode("CamNode2");
mCamNode->attachObject(mCamera);
}
{CODE}
Compile and run the tutorial. We can now swap the Camera's viewpoint by pressing 1 and 2.
The next thing we need to do is translate mCamNode whenever the user holds down one of the arrow keys or WASD. Unlike the code above, we do not need to keep track of the last time we moved the camera, since for every frame the key is held down we want to translate it again. This makes our code relatively simple. First we will create a Vector3 to hold where we want to translate to:
{CODE(wrap="1", colors="c++")} Vector3 transVector = Vector3::ZERO;
{CODE}
Now, when the W key or the up arrow is pressed, we want to move straight forward (which is the negative z axis, remember negative z is straight into the computer screen):
{CODE(wrap="1", colors="c++")}if (mKeyboard->isKeyDown(OIS::KC_UP) || mKeyboard->isKeyDown(OIS::KC_W))
transVector.z -= mMove;
{CODE}
We do almost the same thing for the S and Down arrow keys, but we move in the positive z axis instead:
{CODE(wrap="1", colors="c++")}
if (mKeyboard->isKeyDown(OIS::KC_DOWN) || mKeyboard->isKeyDown(OIS::KC_S))
transVector.z += mMove;
{CODE}
For left and right movement, we go in the positive or negative x direction:
{CODE(wrap="1", colors="c++")}
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;
{CODE}
Finally, we also want to give a way to move up and down along the y axis. I personally use E/PageDown for downwards motion and Q/PageUp for upwards motion:
{CODE(wrap="1", colors="c++")}
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;
{CODE}
Now, our transVector variable has the translation we wish to apply to the camera's SceneNode. The first pitfall we can encounter when doing this is that if you rotate the SceneNode, then our x, y, and z coordinates will be wrong when translating. To fix this, we need to apply all of the rotations we have done to the SceneNode to our translation node. This is actually simpler than it sounds.
To represent rotations, Ogre does not use transformation matrices like some graphics engines. Instead it uses Quaternions for all rotation operations. The math behind Quaternions involves four dimensional linear algebra, which is very difficult to understand. Thankfully, you do not have to understand the math behind them to understand how to use them. Quite simply, to use a Quaternion to rotate a vector, all you have to do is multiply the two together. In this case, we want to apply all of the rotations done to the SceneNode to the translation vector. We can get a Quaternion representing these rotations by calling SceneNode::getOrientation(), then we can apply them to the translation node using multiplication.
The second pitfall we have to watch out for is we have to scale the amount we translate by the amount of time since the last frame. Otherwise, how fast you move would be dependent on the framerate of the application. Definitely not what we want. This is the function call we need to make to translate our camera node without encountering these problems:
{CODE(wrap="1", colors="c++")} mCamNode->translate(transVector * evt.timeSinceLastFrame, Node::TS_LOCAL);
{CODE}
Now we have introduced something new. Whenever you translate a node, or rotate it about any axis, you can specify which Transformation Space you want to use to move the object. Normally when you translate an object, you do not have to set this parameter. It defaults to TS_PARENT, meaning that the object is moved in whatever transformation space the parent node is in. In this case, the parent node is the root scene node. When we press the W button (to move forward), we subtracted from the Z direction, meaning we move towards the negative Z axis. If we did not specify TS_LOCAL in this previous line of code, we would move the camera along the global -Z axis. However, since we are trying to make a camera which goes ''forward'' when we press W, we need it to go in the direction that the node is actually facing. Hence, we use the "local" transformation space.
There is another way we can do this (though it is less direct). We could have gotten the orientation of the node, a quaternion, and multiplied this by the direction vector to get the same result. This would be perfectly valid:
{CODE(wrap="1", colors="c++")}
// Do not add this to the program
mCamNode->translate(mCamNode->getOrientation() * transVector * evt.timeSinceLastFrame, Node::TS_WORLD);
{CODE}
This ''also'' translates the camera node in the local space. In this case, there is no real reason to do this. Ogre defines three transform spaces: TS_LOCAL, TS_PARENT, and TS_WORLD. There may be a case where you need to make a translation or a rotation in ''another'' vector space than these three. If that is the case, you would do it similar to the previous line of code. Take a quaternion representing the vector space (or the orientation of whatever object you are trying to match), multiply it by the translation vector to get the corrected translation vector, and then move it in the TS_WORLD space. This will probably not come up for quite a while though, and we will not refer to it in any of the future tutorials.
Now that we have key movement down, we want to have the mouse affect which direction we are looking in, but only if the user is holding down the right mouse button:
{CODE(wrap="1", colors="c++")}
if (mMouse->getMouseState().buttonDown(OIS::MB_Right))
{
mCamNode->yaw(Degree(-mRotate * mMouse->getMouseState().X.rel), Node::TS_WORLD);
mCamNode->pitch(Degree(-mRotate * mMouse->getMouseState().Y.rel), Node::TS_LOCAL);
}
{CODE}
We yaw and pitch the camera based on the amount the mouse has moved since the last frame. To do this, we will take the X and Y relative changes and turn these into pitch and yaw function calls.
Note that we have used the TS_WORLD vector space for the yaw (rotation functions always use TS_LOCAL as a default, if not specified). We are trying to ensure that the pitch of the object does not affect the yaw in any way. We always want the yaw to rotate us around the same axis. This is the third pitfall: forgetting the interactions between rotations. If we set the yaw to take place in TS_LOCAL, we would get something like this happening:
{img fileId="1790" thumb="y" alt="" rel="box[g]" width="400"}
Compile the program and try it out.
This tutorial is not meant to be a full walkthrough on rotations and Quaternions (that is enough material to fill an entire tutorial by itself). In the next tutorial, we will use buffered mouse input instead of checking for keys being down every frame.
Proceed to ((Basic Tutorial 5)) ''Buffered Input''
---
Alias: (alias(Basic_Tutorial_4))