Beginner Tutorial 4: Frame Listeners and Unbuffered Input
Original version by Clay Culver
Note: This was written for PyOgre 1.0.5, and is known to work with it. If this is not the current version of PyOgre and something is not working or you do not understand anything that is contained here, please post your questions to the PyOgre Forum.
Table of contents
Prerequisites
This tutorial assumes you have knowledge of Python programming and you have already installed PyOgre. This tutorial builds on the material covered in previous tutorials, and it assumes you have read them.
Introduction
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 download the source here.
Getting Started
As with the previous tutorials, we will be using a pre-constructed code base as our starting point. Create a file named beginner_4.py:
# this code is in the public domain from pyogre import ogre import SampleFramework class TutorialFrameListener(SampleFramework.FrameListener): def __init__(self, renderWindow, camera, sceneManager): SampleFramework.FrameListener.__init__(self, renderWindow, camera) def frameStarted(self, evt): return SampleFramework.FrameListener.frameStarted(self, evt) class TutorialApplication(SampleFramework.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 behaviour for some of the functions above. We will be defining the program controls during this tutorial.
FrameListeners
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 PyOgre 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!
Introduction
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:
- The Root object calls the frameStarted method on all registered FrameListeners.
- The Root object renders one frame.
- 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 ExampleApplication'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 ExampleApplication whether or not to show the framerate box in the bottom left corner. We'll turn that on as well:
self.frameListener.showDebugOverlay(True)
Be sure you can run the application before continuing.
Setting up the Scene
Introduction
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 viewports. Pressing the 1 and 2 buttons chooses which Camera viewpoint to view the scene from.
The Code
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 noticable:
sceneManager = self.sceneManager sceneManager.ambientLight = 0.25, 0.25, 0.25
Now, add a Ninja entity to the scene at the origin:
ent = sceneManager.createEntity("Ninja", "ninja.mesh") node = sceneManager.rootSceneNode.createChildSceneNode("NinjaNode") node.attachObject(ent)
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:
# create the first camera node/pitch node node = sceneManager.rootSceneNode.createChildSceneNode("CamNode1", (-400, 200, 400)) node.yaw(ogre.Degree(-45)) # look at the ninja
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:
# create the second camera node/pitch node node = sceneManager.rootSceneNode.createChildSceneNode("CamNode2", (0, 200, 400)) node.createChildSceneNode("PitchNode2")
Now we are done with the TutorialApplication class. Onto the TutorialFrameListener...
TutorialFrameListener
Variables
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 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. I will present two separate methods for solving this.
The mouseDown 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 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.
Constructor
In the TutorialFrameListener constructor, we will set default values for all variables:
# key and mouse state tracking self.mouseDown = False self.toggle = 0 # 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
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 SampleFramework.FrameListener.frameStarted(self, evt)
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 first thing we 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 InputReader. Note that the ExampleApplication class creates an InputReader for us and stores it in the self.inputDevice variable.
inputDevice = self.inputDevice inputDevice.capture()
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 KeyCode. If the Escape key is pressed, we'll just return False to end the program:
return not inputDevice.isKeyDown(ogre.KC_ESCAPE)
All of the following code that we will be discussing goes in between the return and the inputDevice.capture().
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 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.
currMouse = inputDevice.getMouseButton(0)
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 setVisible method of the Light class determines if the object actually emits light or not:
if currMouse and not self.mouseDown: light = self.sceneManager.getLight("Light1") light.visible = not light.visible
Now we need to set the mMouse down variable to equal whatever the currMouse variable contains. Next frame this will tell us if the mouse button was up or down previously.
self.mouseDown = currMouse
Now 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 (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 self.toggle 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. We only do this if self.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 -= evt.timeSinceLastFrame
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. Before this, we check to make sure the mToggle variable is less than 0:
if self.toggle < 0 and inputDevice.isKeyDown(ogre.KC_1):
Now we need to set the mToggle variable so that it will be 1 second until the next action can be performed:
self.toggle = 1.0
Next, we need to remove the camera from whatever it is currently attached to, set the mCamNode 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 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 inputDevice.isKeyDown(ogre.KC_2): self.toggle = 1.0 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 self.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 inputDevice.isKeyDown(ogre.KC_UP) or inputDevice.isKeyDown(ogre.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 inputDevice.isKeyDown(ogre.KC_DOWN) or inputDevice.isKeyDown(ogre.KC_S): transVector.z += self.move
For left and right movement, we go in the positive or negative x direction:
if inputDevice.isKeyDown(ogre.KC_LEFT) or inputDevice.isKeyDown(ogre.KC_A): transVector.x -= self.move if inputDevice.isKeyDown(ogre.KC_RIGHT) or inputDevice.isKeyDown(ogre.KC_D): transVector.x += self.move
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:
if inputDevice.isKeyDown(ogre.KC_PGUP) or inputDevice.isKeyDown(ogre.KC_Q): transVector.y += self.move if inputDevice.isKeyDown(ogre.KC_PGDOWN) or inputDevice.isKeyDown(ogre.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 * evt.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 inputDevice.getMouseButton(1):
If so, we yaw and pitch the camera based on the amount the mouse has moved since the last frame. The relativeX, relativeY, and relativeZ attributes of InputReader return how much the mouse has moved since the last frame. (You can use the mouseAbsX, mouseAbsY, and mouseAbsZ to get the absolute location of the mouse.) 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 * inputDevice.mouseRelativeX)) self.camNode.getChild(0).pitch(ogre.Degree(-self.rotate * self.inputDevice.mouseRelativeY))
This is where we use the PitchNode instead of the mCamNode. This is pitfall number 3: not understanding how rotations work. If we tried to call this pitch on the mCamNode we would get results like this: http://www.idleengineer.net/images/beginner04_rot.png
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.