Basic Tutorial 4
Basic Tutorial 04: Frame Listeners and Unbuffered Input
Note: This tutorial was written for Python-Ogre version 1.0 and is based on the Beginner Tutorial series found on the OgreWiki.
This tutorial assumes you have knowledge of Python programming and you have already installed Python-Ogre. This tutorial builds on the material covered in previous tutorials, and it assumes you have read them.
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 Ogre's unbuffered input system.
As you go through the tutorial you should be slowly adding code to your own project and watching the results as we build it. If you are having problems you can view the completed source code example posted at the end of this tutorial.
As with the previous tutorials, we will be using a pre-constructed code base as our starting point. Create a file named basic_4.py:
import ogre.renderer.OGRE as ogre import ogre.io.OIS as OIS import SampleFramework as sf class TutorialFrameListener(sf.FrameListener): def __init__(self, renderWindow, camera, sceneManager): sf.FrameListener.__init__(self, renderWindow, camera) def frameStarted(self, frameEvent): return sf.FrameListener.frameStarted(self, frameEvent) class TutorialApplication(sf.Application): def _createScene(self): pass def _createCamera(self): pass def _createFrameListener(self): pass if __name__ == '__main__': try: ta = TutorialApplication() ta.go() except ogre.OgreException, e: print e
Note this program will not work until we define behavior for some of the functions above. We will be defining the program controls during this tutorial.
Also note that if you're using Python-Ogre 1.6, you need to change the frameStarted method to frameRenderingQueued for this tutorial to work.
A Quick Note On Subclassing
In this tutorial we are subclassing an actual C++ class (FrameListener) in Python. It is very important to remember that if you subclass any Python-Ogre class, you must remember to call its constructor, or the program will likely crash. Thus, if you wanted to create a direct subclass of FrameListener, you would need to do this:
class MyFrameListener(ogre.FrameListener): def __init__(self): ogre.FrameListener.__init__(self) # without this, bad things happen!
An introduction to FrameListeners
In the previous tutorials we only looked at what we could do when we add code to the _createScene method. In Ogre, we can register a class to receive notification before and after a frame is rendered to the screen. This FrameListener interface defines two functions:
frameStarted(ogre.FrameEvent) -> bool frameEnded(ogre.FrameEvent) -> bool
Ogre's main loop (Root::startRendering) looks like this:
1. The Root object calls the frameStarted method on all registered FrameListeners. 2. The Root object renders one frame. 3. The Root object calls the frameEnded method on all registered FrameListeners.
This loops until any of the FrameListeners return false from frameStarted or frameEnded. The return values for these functions basically mean "keep rendering". If you return False from either, the program will exit. The parameter to these functions, the ogre.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.
You might also notice that the main loop really only does three things, and since nothing happens in between the frameEnded and frameStarted methods being called, you can use them almost interchangeably. Where you decide to put all of your code is entirely up to you. You can put it all in one big frameStarted or frameEnded method, or you could divide it up between the two.
Registering A FrameListener
Before you can actually run the program, we need to add code to a few methods first. Find the TutorialApplication._createCamera method and add the following code to it:
self.camera = self.sceneManager.createCamera('PlayerCam') self.camera.nearClipDistance = 5
We have not done anything out of the ordinary here. The only reason we need to override the SampleFramework's _createCamera method is because the _createCamera method moves the camera and changes its orientation, which we do not want for this tutorial.
Since the Root class is what renders frames, it also is in charge of keeping track of FrameListeners. The first thing we need to do is create an instance of our TutorialFrameListener and register it with the Root object. Find the TutorialApplication._createFrameListener method, and add this code to it:
self.frameListener = TutorialFrameListener(self.renderWindow, self.camera, self.sceneManager) self.root.addFrameListener(self.frameListener)
Note that we have saved a reference to the FrameListener class in self.frameListener. This is very important. The C++ Ogre library will use this object, but it has no idea that it needs to also hold a reference to this object. If you do not hold a reference to this object in Python land, a crash will result.
The self.root, self.camera, and self.sceneManager variables are defined in the SampleFramework.Application class. The addFrameListener method adds a FrameListener, and the removeFrameListener method removes a FrameListener (that is, the FrameListener will no longer receive updates). Note that the add|removeFrameListener methods only take in a reference to a FrameListener (that is, FrameListeners do not have names you can use to remove them). This means that you will need to hold a reference to each FrameListener that you will later remove.
The SampleFramework.FrameListener (which our TutorialFrameListener is derived from), also provides a showDebugOverlay(bool) function, which tells the SampleFramework whether or not to show the framerate box in the bottom left corner. We'll turn that on as well:
The application will not work at this time due to the empty _createScene() stub overriding the default in the Application class from SampleFramework. If you comment out those two empty methods, the demo (with the overridden _createCamera() and _createFrameListener() methods) will work.
Setting Up The Scene
Before we dive directly into the code, I would like to briefly outline what we will be doing so that you understand where I am 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.
Find the TutorialApplication._createScene 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 noticeable:
sceneManager = self.sceneManager sceneManager.ambientLight = 0.25, 0.25, 0.25
Now, add a Ninja entity to the scene at the origin:
entity = sceneManager.createEntity('Ninja', 'ninja.mesh') node = sceneManager.getRootSceneNode().createChildSceneNode('NinjaNode') node.attachObject(entity)
Now we will create a white point light and place it in the Scene, a small distance (relatively) away from the Ninja:
light = sceneManager.createLight('Light1') light.type = ogre.Light.LT_POINT light.position = 250, 150, 250 light.diffuseColour = 1, 1, 1 light.specularColour = 1, 1, 1
Now we need to create the SceneNodes which the Camera will be attached to. It is important to note that when using this system for Cameras we need to have a separate SceneNode to handle the pitch (up and down rotation) of the Camera. I will go into full detail why we need that when we actually move the Camera later on. For now, lets create the first SceneNode and have it face the ninja:
node = sceneManager.getRootSceneNode().createChildSceneNode('CamNode1', (-400, 200, 400)) node.yaw(ogre.Degree(-45))
Now, we need to create a child SceneNode that will control the pitch of the camera (and it will have the Camera itself attached to it):
node = node.createChildSceneNode('PitchNode1') node.attachObject(self.camera)
Now, we have a hierarchy of nodes. The Camera is attached to "PitchNode1", which is attached to "CamNode1", which is attached to the SceneManager's root SceneNode. When we want to move the camera or yaw/roll it, we do that to "CamNode1". If we want to change the pitch of the Camera, we do that to "PitchNode1".
Now, add a second CamNode and PitchNode. We will use these later as a second viewing position for our Camera:
node = sceneManager.getRootSceneNode().createChildSceneNode('CamNode2', (0, 200, 400)) node.createChildSceneNode('PitchNode2')
Now we are done with the TutorialApplication class. You can test your application once again before we move on to TutorialFrameListener.
We will be using a few variables in the TutorialFrameListener class which I'd like to go over before we get any further:
mouseDown Whether or not the left mouse button was down last frame. toggle The time left until the next toggle. rotate The rotation constant. movement The movement constant. sceneManager The current SceneManager. camNode The SceneNode the camera is currently attached to.
The sceneManager holds a reference to the current SceneManager and the camNode holds the current SceneNode that the Camera is attached to (that would be the "CamNode*", not the "PitchNode*"). The rotate and move variables are our constants of rotation and movement. If you want the movement or rotation to be faster or slower, tweak those variables to be higher or lower.
The other two variables (toggle and mouseDown) 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, lets 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 look at two separate methods for solving this.
The mouseDown keeps track of whether or not the mouse was also down the previous frame (so if mouseDown down is true, we do not perform the same action again until the mouse is released). The toggle button specifies the time until we are allowed to perform an action again. That is, when a button is pressed, toggle is set to some length of time where no other actions can occur.
In the TutorialFrameListener constructor, we will set default values for all variables:
self.toggle = 0 self.mouseDown = False self.camNode = camera.parentSceneNode.parentSceneNode self.sceneManager = sceneManager self.rotate = 0.13 self.move = 250
We use the parentSceneNode attribute twice on the cam object because the first parent will be the PitchNode, and the second will be the CamNode which we are looking for.
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:
return sf.FrameListener.frameStarted(self, frameEvent)
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.
Before we start dealing with user input we should add code that ends the program if Ogre's render window is closed, this is especially useful if you do not run Ogre in fullscreen mode.
if(self.renderWindow.isClosed()): return False
When using unbuffered input the first thing we need to do is capture the current state of the keyboard and mouse.
Note: Ogre3D 1.4.0 uses OIS (Object-oriented Input Library). For more details on Using OIS with Ogre3d, please refer to Using OIS. The input devices are separated into multiple objects. This tutorial refers to both "Mouse" and "Keyboard" for each respective device we are dealing with.
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 and specifying a KeyCode. If the Escape key is pressed, we will return False to end the program:
return not self.Keyboard.isKeyDown(OIS.KC_ESCAPE)
Note: All of the following code that we will be discussing goes in between the lines "self.Mouse.capture()" and "return not self.Keyboard.isKeyDown(OIS.KC_ESCAPE)".
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 using getMouseState().
currMouse = self.Mouse.getMouseState()
The buttonDown(OIS.MB_Left) variable will be true if the mouse button is down. Now we will toggle the light depending on whether or not buttonDown(OIS.MB_Left) 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 visible method of the Light class determines if the object actually emits light or not:
if currMouse.buttonDown(OIS.MB_Left) and not self.mouseDown: light = self.sceneManager.getLight('Light1') light.visible = not light.visible
Now we need to set the mouseDown variable to equal whatever the buttonDown(OIS.MB_Left) variable contains. Next frame this will tell us if the mouse button was up or down previously.
self.mouseDown = currMouse.buttonDown(OIS.MB_Left)
Run the application. Left clicking now toggles the light on and off! Note that since we no longer call the SampleFramework'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 (or have to use a dictionary). 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 self.toggle variable. If toggle is greater than 0, then we do not perform any actions, if toggle 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 toggle variable by the time that has elapsed since the last frame. We only do this if toggle is greater than, or equal to 0, because we do not want the variable to drop too far below 0:
if self.toggle >= 0: self.toggle -= frameEvent.timeSinceLastFrame
Now that we have updated toggle, we can act on it. Our next key binding is making the 1 key attach the Camera to the first SceneNode. Before this, we check to make sure the toggle variable is less than 0:
if self.toggle < 0 and self.Keyboard.isKeyDown(OIS.KC_1):
Now we need to set the toggle variable so that it will be 0.1 seconds until the next action can be performed:
self.toggle = 0.1
Next, we need to remove the camera from whatever it is currently attached to, set the camNode to contain "CamNode1", and attach the camera to "PitchNode1".
self.camera.parentSceneNode.detachObject(self.camera) self.camNode = self.sceneManager.getSceneNode("CamNode1") self.sceneManager.getSceneNode("PitchNode1").attachObject(self.camera)
We will also do this for CamNode2 when the 2 button is pressed. The code is nearly 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):
elif self.toggle < 0 and self.Keyboard.isKeyDown(OIS.KC_2): self.toggle = 0.1 self.camera.parentSceneNode.detachObject(self.camera) self.camNode = self.sceneManager.getSceneNode("CamNode2") self.sceneManager.getSceneNode("PitchNode2").attachObject(self.camera)
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 camNode 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:
transVector = ogre.Vector3(0, 0, 0)
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):
if self.Keyboard.isKeyDown(OIS.KC_UP) or self.Keyboard.isKeyDown(OIS.KC_W): transVector.z -= self.move
We do almost the same thing for the S and Down arrow keys, but we move in the positive z axis instead:
if self.Keyboard.isKeyDown(OIS.KC_DOWN) or self.Keyboard.isKeyDown(OIS.KC_S): transVector.z += self.move
For left and right movement, we go in the positive or negative x direction:
if self.Keyboard.isKeyDown(OIS.KC_LEFT) or self.Keyboard.isKeyDown(OIS.KC_A): transVector.x -= self.move
if self.Keyboard.isKeyDown(OIS.KC_RIGHT) or self.Keyboard.isKeyDown(OIS.KC_D): transVector.x += self.move
Finally, we also want to give a way to move up and down along the y axis. We will use E/PageDown for downwards motion and Q/PageUp for upwards motion:
if self.Keyboard.isKeyDown(OIS.KC_PGUP) or self.Keyboard.isKeyDown(OIS.KC_Q): transVector.y += self.move
if self.Keyboard.isKeyDown(OIS.KC_PGDOWN) or self.Keyboard.isKeyDown(OIS.KC_E): transVector.y -= self.move
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 using the SceneNode.orientation attribute, 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:
self.camNode.translate(self.camNode.orientation * transVector * frameEvent.timeSinceLastFrame)
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. To do this we first check to see if the right mouse button is down:
If so, we yaw and pitch the camera based on the amount the mouse has moved since the last frame. The X.rel (relative X) and Y.rel (relative Y) attributes return how much the mouse has moved since the last frame. We will take the X and Y relative changes and turn these into pitch and yaw function calls:
self.camNode.yaw(ogre.Degree(-self.rotate * currMouse.X.rel).valueRadians()) self.camNode.getChild(0).pitch(ogre.Degree(-self.rotate * currMouse.Y.rel).valueRadians())
This is where we use the PitchNode instead of the camNode. This is pitfall number 3, not understanding how rotations work. 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). For now, just use this technique when you need it and we will come back to Quaternions in a later tutorial.
Run the application. Now we have key movement and mouse movement. In the next tutorial, we will use buffered mouse input instead of checking for keys being down every frame.
Complete Source Code Example
import ogre.renderer.OGRE as ogre import ogre.io.OIS as OIS import SampleFramework as sf class TutorialFrameListener(sf.FrameListener): """A FrameListener class that handles basic user input.""" def __init__(self, renderWindow, camera, sceneManager): # Subclass any Python-Ogre class and you must call its constructor. sf.FrameListener.__init__(self, renderWindow, camera) # Key and mouse state tracking. self.toggle = 0 self.mouseDown = False # Populate the camera and scene manager containers. self.camNode = camera.parentSceneNode.parentSceneNode self.sceneManager = sceneManager # Set the rotation and movement speed. self.rotate = 0.13 self.move = 250 def frameStarted(self, frameEvent): # If the render window has been closed, end the program. if(self.renderWindow.isClosed()): return False # Capture and update each input device. self.Keyboard.capture() self.Mouse.capture() # Get the current mouse state. currMouse = self.Mouse.getMouseState() # Use the Left mouse button to turn Light1 on and off. if currMouse.buttonDown(OIS.MB_Left) and not self.mouseDown: light = self.sceneManager.getLight('Light1') light.visible = not light.visible # Update the mouseDown boolean. self.mouseDown = currMouse.buttonDown(OIS.MB_Left) # Update the toggle timer. if self.toggle >= 0: self.toggle -= frameEvent.timeSinceLastFrame # Swap the camera's viewpoint with the keys 1 or 2. if self.toggle < 0 and self.Keyboard.isKeyDown(OIS.KC_1): # Update the toggle timer. self.toggle = 0.1 # Attach the camera to PitchNode1. self.camera.parentSceneNode.detachObject(self.camera) self.camNode = self.sceneManager.getSceneNode("CamNode1") self.sceneManager.getSceneNode("PitchNode1").attachObject(self.camera) elif self.toggle < 0 and self.Keyboard.isKeyDown(OIS.KC_2): # Update the toggle timer. self.toggle = 0.1 # Attach the camera to PitchNode2. self.camera.parentSceneNode.detachObject(self.camera) self.camNode = self.sceneManager.getSceneNode("CamNode2") self.sceneManager.getSceneNode("PitchNode2").attachObject(self.camera) # Move the camera using keyboard input. transVector = ogre.Vector3(0, 0, 0) # Move Forward. if self.Keyboard.isKeyDown(OIS.KC_UP) or self.Keyboard.isKeyDown(OIS.KC_W): transVector.z -= self.move # Move Backward. if self.Keyboard.isKeyDown(OIS.KC_DOWN) or self.Keyboard.isKeyDown(OIS.KC_S): transVector.z += self.move # Strafe Left. if self.Keyboard.isKeyDown(OIS.KC_LEFT) or self.Keyboard.isKeyDown(OIS.KC_A): transVector.x -= self.move # Strafe Right. if self.Keyboard.isKeyDown(OIS.KC_RIGHT) or self.Keyboard.isKeyDown(OIS.KC_D): transVector.x += self.move # Move Up. if self.Keyboard.isKeyDown(OIS.KC_PGUP) or self.Keyboard.isKeyDown(OIS.KC_Q): transVector.y += self.move # Move Down. if self.Keyboard.isKeyDown(OIS.KC_PGDOWN) or self.Keyboard.isKeyDown(OIS.KC_E): transVector.y -= self.move # Translate the camera based on time. self.camNode.translate(self.camNode.orientation * transVector * frameEvent.timeSinceLastFrame) # Rotate the camera when the Right mouse button is down. if currMouse.buttonDown(OIS.MB_Right): self.camNode.yaw(ogre.Degree(-self.rotate * currMouse.X.rel).valueRadians()) self.camNode.getChild(0).pitch(ogre.Degree(-self.rotate * currMouse.Y.rel).valueRadians()) # If the escape key is pressed end the program. return not self.Keyboard.isKeyDown(OIS.KC_ESCAPE) class TutorialApplication(sf.Application): """The Application class.""" def _createScene(self): # Setup a scene with a low level of ambient light. sceneManager = self.sceneManager sceneManager.ambientLight = 0.25, 0.25, 0.25 # Setup a mesh entity and attach it to a scene node. entity = sceneManager.createEntity('Ninja', 'ninja.mesh') node = sceneManager.getRootSceneNode().createChildSceneNode('NinjaNode') node.attachObject(entity) # Setup a White point light. light = sceneManager.createLight('Light1') light.type = ogre.Light.LT_POINT light.position = 250, 150, 250 light.diffuseColour = 1, 1, 1 light.specularColour = 1, 1, 1 # Setup the first camera node and pitch node and aim it. node = sceneManager.getRootSceneNode().createChildSceneNode('CamNode1', (-400, 200, 400)) node.yaw(ogre.Degree(-45)) node = node.createChildSceneNode('PitchNode1') node.attachObject(self.camera) # Setup the second camera node and pitch node. node = sceneManager.getRootSceneNode().createChildSceneNode('CamNode2', (0, 200, 400)) node.createChildSceneNode('PitchNode2') def _createCamera(self): self.camera = self.sceneManager.createCamera('PlayerCam') self.camera.nearClipDistance = 5 def _createFrameListener(self): self.frameListener = TutorialFrameListener(self.renderWindow, self.camera, self.sceneManager) self.root.addFrameListener(self.frameListener) self.frameListener.showDebugOverlay(True) if __name__ == '__main__': try: ta = TutorialApplication() ta.go() except ogre.OgreException, e: print e
SampleFramework is used and while it isn't as flexible as a full game might require it is a great resource for learning Python-Ogre.
- Proceed to Basic Tutorial 5 Buffered Input