Intermediate Tutorial 2
From PyWiki
Intermediate Tutorial 2: RaySceneQueries and Basic Mouse Usage
Introduction
In this tutorial we will create the beginnings of a basic Scene Editor. During this process, we will cover:
- How to use RaySceneQueries to keep the camera from falling through the terrain
- How to use the MouseListener and MouseMotionListener interfaces
- Using the mouse to select x and y coordinates on the terrain
You can find the code for this tutorial 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.
Prerequisites
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.
Getting Started
In this tutorial, we will be using a predefined code base as a starting point. As long as you have worked through the previous tutorials, this should all be familiar to you. Create a project in the compiler of your choice for this project, and add a source file which contains this code:
#!/usr/bin/env python # This code is Public Domain. """Python-Ogre Intermediate Tutorial 02: Initial code """ import ogre.renderer.OGRE as ogre import ogre.gui.CEGUI as CEGUI import ogre.io.OIS as OIS import SampleFramework as sf class MouseQueryListener(sf.FrameListener, OIS.MouseListener): """A FrameListener class that handles basic user input.""" def __init__(self, win, cam, sc, renderer): # Subclass any Python-Ogre class and you must call its constructor. sf.FrameListener.__init__(self, win, cam, True, True) OIS.MouseListener.__init__(self) self.sceneManager = sc self.renderer = renderer self.camera = cam self.Mouse.setEventCallback(self) def frameStarted(self, evt): return sf.FrameListener.frameStarted(self, evt) def mouseMoved(self, evt): return True def mousePressed(self, evt, id): return True def mouseReleased(self, evt, id): return True def _processUnbufferedMouseInput(self,frameEvent): pass class TutorialApplication(sf.Application): """Application class.""" def _chooseSceneManager(self): self.sceneManager = self.root.createSceneManager(ogre.ST_EXTERIOR_CLOSE, 'TerrainSM') def _createScene(self): pass def _createFrameListener(self): self.frameListener = MouseQueryListener(self.renderWindow, self.camera, self.sceneManager, self.ceguiRenderer ) self.root.addFrameListener(self.frameListener) self.frameListener.showDebugOverlay(True) if __name__ == '__main__': ta = TutorialApplication() ta.go()
Be sure you can run this code before continuing.
Setting up the Scene
Go to the _createScene method. The following code should all be familiar. If you do not know what something does, please consult the previous tutorials continuing. Add this to _createScene:
# Set up a simple scene with terrain self.sceneManager.setAmbientLight((0.5, 0.5, 0.5)) self.sceneManager.setSkyDome(True, "Examples/CloudySky", 5, 8) self.sceneManager.setWorldGeometry("terrain.cfg") self.camera.setPosition(40, 100, 580) self.camera.pitch(ogre.Degree(-30)) self.camera.yaw(ogre.Degree(-45))
Now that we have the basic world geometry set up, we need to turn on the cursor. We do this using some CEGUI function calls. Before we can do that, however, we need to start up CEGUI. We first create an OgreCEGUIRenderer, then we create the System object and give it the Renderer we just created. I will leave the specifics of setting up CEGUI for a later tutorial, just know that you always have to tell CEGUI which SceneManager you are using with the last parameter to OgreCEGUIRenderer.
# Set up the CEGUI Renderer self.ceguiRenderer = CEGUI.OgreCEGUIRenderer(self.renderWindow, ogre.RENDER_QUEUE_OVERLAY, False, 3000, self.sceneManager) self.ceguiSystem = CEGUI.System(self.ceguiRenderer)
Now we need to actually show the cursor. Again, I'm not going to explain most of this code. We will revisit it in a later tutorial.
# Show the mouse cursor CEGUI.SchemeManager.getSingleton().loadScheme("TaharezLookSkin.scheme") CEGUI.MouseCursor.getSingleton().setImage("TaharezLook", "MouseArrow")
If you compile and run the code, you will see a cursor at the up-left corner of the screen, but it will not move (yet).
Introducing the MouseQueryListener
That was all that needed to be done for the application. The MouseQueryListener is the complicated portion of the code, so I will spend some time outlining what we are trying to accomplish with the application so you have an idea before we start implementing it.
- First, we want to bind the right mouse button to a "mouse look" mode. It's fairly annoying not being able to use the mouse to look around, so our first priority will be making adding mouse control back to the program (though only when we hold the right mouse button down).
- Second, we want to make it so that the camera does not pass through the Terrain. This will make it closer to how we would expect program like this to work.
- Third, we want to add entities to the scene anywhere on the terrain we left click.
- Finally, we want to be able to "drag" entities around. That is by left clicking and holding the button down we want to see the entity, and move him to where we want to place him. Letting go of the button will actually lock him into place.
Setting up the MouseQueryListener
To do this we are going to use several variables in the MouseQueryListener. Find the constructor of the class and add the following code to initialize the variables:
# Initialize variables self.raySceneQuery = None self.leftMouseDown = False self.rightMouseDown = False self.robotCount = 0 self.currentObject = None self.moveSpeed = 50 self.rotateSpeed = 1/500.0
The raySceneQuery variable will hold a copy of the RaySceneQuery we will be using to find the coordinates on the terrain. The leftMouseDown and rightMouseDown variables will track whether we have the mouse held down (IE leftMouseDown is true when the user holds down the left mouse button, false otherwise). robotCount counts the number of robots we have on screen. currentObject holds the most recently created SceneNode that we have created (we will be using this to "drag" the entity around). Note that we are also reducing the movement speed and rotation speed since the Terrain is fairly small.
For completeness sake, the existing variables in the constructor are used to reference the sceneManager, ceguiRenderer and camera that are set up in the TutorialApplication class.
In order for the MouseQueryListener to receive mouse events, we register it as a MouseListener. If any of this is unfamiliar, please consult Basic Tutorial 5. The following line is already present in the initial code:
# Register this so that we get mouse events. self.Mouse.setEventCallback(self)
Finally, in the constructor we need to create the RaySceneQuery object. This is done with a call to the SceneManager:
# Create RaySceneQuery self.raySceneQuery = self.sceneManager.createRayQuery(ogre.Ray())
Be sure you can run your code before moving on to the next section.
Adding Mouse Look
We are going to bind the mouse look mode to the right mouse button. To do this, we are going to:
- update CEGUI when the mouse is moved (so that the cursor is also moved)
- set rightMouseButton to be true when the mouse is pressed
- set rightMouseButton to be false when it is released
- change the view when the mouse is "dragged" (that is held down and moved)
- hide the mouse cursor when the mouse is dragging
Find the MouseQueryListener.mouseMoved method. We will be adding code to move the mouse cursor every time the mouse has been moved. Add this code to the function:
# Update CEGUI with the mouse motion CEGUI.System.getSingleton().injectMouseMove(evt.get_state().X.rel, evt.get_state().Y.rel)
Now find the MouseQueryListener.mousePressed method. This chunk of code hides the cursor when the right mouse button goes down, and sets the rightMouseDown variable to true.
# Check which button has been pressed if id == OIS.MB_Left: self.leftMouseDown = True elif id == OIS.MB_Right: CEGUI.MouseCursor.getSingleton().hide() self.rightMouseDown = True
Next we need to show the mouse cursor again and toggle rightMouseDown when the right button is let up. Find the mouseReleased function, and add this code:
# Check which button has been released if id == OIS.MB_Left: self.leftMouseDown = False elif id == OIS.MB_Right: CEGUI.MouseCursor.getSingleton().show() self.rightMouseDown = False
Now we have all of the prerequisite code written, we want to change the view when the mouse is moved while holding the right button down. What we are going to do is read the distance it has moved since the last time the method was called. This is done in the same way that we rotated the camera in Basic Tutorial 5. Find the MouseQueryListener.mouseMoved function and add the following code just before the return statement:
# If we are dragging the left mouse button. if self.leftMouseDown: pass # If we are dragging the right mouse button. elif self.rightMouseDown: self.camera.yaw(ogre.Degree(-evt.get_state().X.rel * self.rotateSpeed)) self.camera.pitch(ogre.Degree(-evt.get_state().Y.rel * self.rotateSpeed))
Now if you run this code you will be able to control where the camera looks by holding the right mouse button down.
Terrain Collision Detection
We are now going to make it so that when we move towards the terrain, we cannot pass through it. Since the SampleFramework FrameListener already handles moving the camera, we are not going to touch that code. Instead, after the SampleFramework FrameListener moves the camera we are going to make sure the camera is 10 units above the terrain. If it is not, we are going to move it there. Please follow this code closely. We will use the RaySceneQuery to do several other things by the time this tutorial is finished, and I will not go into as much detail after this section.
Go to the MouseQueryListener.frameStarted method and remove all of the code from the method. The first thing we are going to do is call the MouseQueryFrameListener.frameStarted method to do all of its normal functions. If it returns false, we will return false as well.
# Process the sampleframework frame listener code. Since we are going to be # manipulating the translate vector, we need this to happen first. if not sf.FrameListener.frameStarted(self, evt): return False
We do this at the top of our frameStarted function because the SampleFramework FrameListener's frameStarted member function moves the camera, and we need to perform the rest our actions in this function after this happens. Our goal is to find the camera's current position, and fire a Ray straight down it into the terrain. This is called a RaySceneQuery, and it will tell us the height of the Terrain below us. After getting the camera's current position, we need to create a Ray. A Ray takes in an origin (where the ray starts), and a direction. In this case our direction will be UNIT_NEGATIVE_Y, since we are pointing the ray straight down. Once we have created the ray, we tell the RaySceneQuery object to use it. Add the following to the frameStarted function:
# Set up the ray using the position of the camera camPos = self.camera.getPosition() updateRay = ogre.Ray() updateRay.setOrigin(camPos + ogre.Vector3(0,10,0)) updateRay.setDirection(ogre.Vector3().NEGATIVE_UNIT_Y) self.raySceneQuery.Ray = updateRay
Note that we have used a height of 10 instead of the camera's actual position. If we used the camera's Y position instead of this height we would miss the terrain entirely if the camera is under the terrain. Now we need to execute the query and get the results. The results of the query comes in the form of a list, which we will iterate through and alter the camera position if necessary:
# Perform the scene query for queryResult in self.raySceneQuery.execute():
The result of the query is basically (oversimplification here) a combined list of worldFragments (in this case the Terrain) and of movables (we will cover movables in a later tutorial). In the next demo we will have to deal with multiple return values for SceneQuerys. The following line of code ensures that the result is the terrain (item.worldFragment).
# Our ray hit a worldFragment if queryResult.worldFragment is not None:
The worldFragment struct contains the location where the Ray hit the terrain in the singleIntersection variable (which is a Vector3). Move our camera to a location above the terrain. Because we only care about the first item in the query results, we use break to stop iteration.
# If we are above terrain, set the height of the camera compared to the distance to the terrain terrain self.camera.setPosition(camPos.x, camPos.y - queryResult.distance + 40, camPos.z) break return True
Lastly, we return True to continue rendering. At this point you should test your program.
Terrain Selection
In this section we will be creating and adding objects to the screen every time you click the left mouse button. Every time you click and hold the left mouse button, an object will be created and "held" on your cursor. You can move the object around until you let go of the button, at which point it will lock into place. To do this we are going to need to change the mousePressed function to do something different when you click the left mouse button. Find the following code in the MouseQueryListener.mousePressed function. We will be adding code inside this if statement.
# if id == OIS.MB_Left: self.leftMouseDown = True
The first piece of code will look very familiar. We will be creating a Ray to use with the raySceneQuery object, and setting the Ray. Ogre provides us with Camera.getCameraToViewpointRay; a nice function that translates a click on the screen (x and y coordinates) into a Ray that can be used with a RaySceneQuery object.
# if id == OIS.MB_Left: self.leftMouseDown = True # Setup the ray scene query, use CEGUI's mouse position mousePos = CEGUI.MouseCursor.getSingleton().getPosition() mouseRay = self.camera.getCameraToViewportRay(mousePos.d_x / float(evt.get_state().width), mousePos.d_y / float(evt.get_state().height)) self.raySceneQuery.setRay(mouseRay)
Next we will execute the query and make sure it returned a result.
# Determine if we hit the terrain # Execute query result = self.raySceneQuery.execute() if len(result) > 0: item = result[0] if item.worldFragment:
Now that we have the worldFragment (and therefore the position that was clicked on), we are going to create the object and place it on that position. Our first difficulty is that each Entity and SceneNode in ogre needs a unique name. To accomplish this we are going to name each Entity "Robot1", "Robot2", "Robot3"... and each SceneNode "Robot1Node", "Robot2Node", "Robot3Node"... and so on. First we create the name. Next we create the Entity and SceneNode. Note that we use item.worldFragment.singleIntersection for our default position of the Robot. We also scale him down to 1/10th size because of how small the terrain is. Be sure to take note that we are assigning this newly created object to the member varaible self.currentObject. We will be using that in the next section.
# We have the position we clicked on, create a new object and # place it here name = "Robot" + str(self.robotCount) ent = self.sceneManager.createEntity(name, "robot.mesh") self.robotCount += 1 self.currentObject = self.sceneManager.getRootSceneNode().createChildSceneNode(name + "Node", item.worldFragment.singleIntersection) print item.worldFragment.singleIntersection self.currentObject.attachObject(ent) self.currentObject.setScale(0.1, 0.1, 0.1) self.leftMouseDown = True
Now run the demo. You can now place Robots on the scene by clicking anywhere on the Terrain. We have almost completed our program, but we need to implement object dragging before we are finished. We will be adding code inside this if statement in the mouseMoved function:
# if self.leftMouseDown: # We are dragging the left mouse button
This next chunk of code should be self explanatory now. We create a Ray based on the mouse's current location, we then execute a RaySceneQuery and move the object to the new position. Note that we don't have to check self.currentObject to see if it is valid or not, because leftMouseDown would not be true if currentObject had not been set by mousePressed.
# if self.leftMouseDown: # We are dragging the left mouse button # Drag the object if we selected one mousePos = CEGUI.MouseCursor.getSingleton().getPosition() mouseRay = self.camera.getCameraToViewportRay(mousePos.d_x / float(evt.get_state().width), mousePos.d_y / float(evt.get_state().height)) self.raySceneQuery.setRay(mouseRay) result = self.raySceneQuery.execute() if len(result) > 0: item = result[0] if item.worldFragment: self.currentObject.setPosition(item.worldFragment.singleIntersection)
Run the program. We are now finished!
Notice: You (the Ray's origin) must be over the Terrain for the RaySceneQuery to report the intersection when using the TerrainSceneManager.
Exercises for Further Study
Easy Exercises
- To keep the camera from looking through the terrain, we chose 10 units above the Terrain. This selection was arbitrary. Could we improve on this number and get closer to the Terrain without going through it? If so, make this variable a static class member and assign it there.
- We sometimes do want to pass through the terrain, especially in a SceneEditor. Create a flag which turns toggles collision detection on and off, and bind this to a key on the keyboard. Be sure you do not make a SceneQuery in frameStarted if collision detection is turned off.
Intermediate Exercises
- We are currently doing the SceneQuery every frame, regardless of whether or not the camera has actually moved. Fix this problem and only do a SceneQuery if the camera has moved. (Hint: Find the translation vector in SampleFramework FrameListener, after the function is called test it against Vector3.ZERO.)
Advanced Exercises
- Notice that there is a lot of code duplication every time we make a scene query call. Wrap all of the SceneQuery related functionality into a protected function. Be sure to handle the case where the Terrain is not intersected at all.
Exercises for Further Study
- In this tutorial we used RaySceneQueries to place objects on the Terrain. We could have used it for many other purposes. Take the code from Tutorial 1 and complete Difficult Question 1 and Expert Question 1. Then merge that code with this one so that the Robot now walks on the terrain instead of empty space.
- Add code so that every time you click on a point on the scene, the robot moves to that location.
Final version of the code
#!/usr/bin/env python # This code is Public Domain. """Python-Ogre Intermediate Tutorial 02: Final code""" import ogre.renderer.OGRE as ogre import ogre.gui.CEGUI as CEGUI import ogre.io.OIS as OIS import SampleFramework as sf class MouseQueryListener(sf.FrameListener, OIS.MouseListener): """A FrameListener class that handles basic user input.""" def __init__(self, win, cam, sc, renderer): # Subclass any Python-Ogre class and you must call its constructor. sf.FrameListener.__init__(self, win, cam, True, True) OIS.MouseListener.__init__(self) self.sceneManager = sc self.ceguiRenderer = renderer self.camera = cam # Register as MouseListener (Basic tutorial 5) self.Mouse.setEventCallback(self) # Initialize our state values self.raySceneQuery = None self.leftMouseDown = False self.rightMouseDown = False self.robotCount = 0 self.currentObject = None self.moveSpeed = 50 self.rotateSpeed = 1/500.0 self.raySceneQuery = self.sceneManager.createRayQuery(ogre.Ray()) def frameStarted(self, evt): if not sf.FrameListener.frameStarted(self, evt): return False # Find the current position, fire a Ray straight down # in order to determine the distance to the terrain # If we are too close, keep the distance to a certain amount camPos = self.camera.getPosition() updateRay = ogre.Ray() updateRay.setOrigin(camPos + ogre.Vector3(0,10,0)) updateRay.setDirection(ogre.Vector3().NEGATIVE_UNIT_Y) self.raySceneQuery.Ray = updateRay # Perform the scene query for queryResult in self.raySceneQuery.execute(): if queryResult.worldFragment is not None: self.camera.setPosition(camPos.x, camPos.y - queryResult.distance + 40, camPos.z) break return True def mouseMoved(self, evt): CEGUI.System.getSingleton().injectMouseMove(evt.get_state().X.rel, evt.get_state().Y.rel) if self.leftMouseDown: # We are dragging the left mouse button # Drag the object if we selected one mousePos = CEGUI.MouseCursor.getSingleton().getPosition() mouseRay = self.camera.getCameraToViewportRay(mousePos.d_x / float(evt.get_state().width), mousePos.d_y / float(evt.get_state().height)) self.raySceneQuery.setRay(mouseRay) result = self.raySceneQuery.execute() if len(result) > 0: item = result[0] if item.worldFragment: self.currentObject.setPosition(item.worldFragment.singleIntersection) elif self.rightMouseDown: self.camera.yaw(ogre.Degree(-evt.get_state().X.rel * self.rotateSpeed)) self.camera.pitch(ogre.Degree(-evt.get_state().Y.rel * self.rotateSpeed)) return True def mousePressed(self, evt, id): if id == OIS.MB_Left: self.leftMouseDown = True # Setup the ray scene query, use CEGUI's mouse position mousePos = CEGUI.MouseCursor.getSingleton().getPosition() mouseRay = self.camera.getCameraToViewportRay(mousePos.d_x / float(evt.get_state().width), mousePos.d_y / float(evt.get_state().height)) self.raySceneQuery.setRay(mouseRay) # Execute query result = self.raySceneQuery.execute() if len(result) > 0: item = result[0] if item.worldFragment: # We have the position we clicked on, create a new object and # place it here name = "Robot" + str(self.robotCount) ent = self.sceneManager.createEntity(name, "robot.mesh") self.robotCount += 1 self.currentObject = self.sceneManager.getRootSceneNode().createChildSceneNode(name + "Node", item.worldFragment.singleIntersection) print item.worldFragment.singleIntersection self.currentObject.attachObject(ent) self.currentObject.setScale(0.1, 0.1, 0.1) self.leftMouseDown = True elif id == OIS.MB_Right: CEGUI.MouseCursor.getSingleton().hide() self.rightMouseDown = True return True def mouseReleased(self, evt, id): if id == OIS.MB_Left: self.leftMouseDown = False elif id == OIS.MB_Right: CEGUI.MouseCursor.getSingleton().show() self.rightMouseDown = False return True class TutorialApplication(sf.Application): """Application class.""" def _chooseSceneManager(self): self.sceneManager = self.root.createSceneManager(ogre.ST_EXTERIOR_CLOSE, 'TerrainSM') def _createScene(self): # CEGUI setup (see Basic Tutorial 7 about this) self.ceguiRenderer = CEGUI.OgreCEGUIRenderer(self.renderWindow, ogre.RENDER_QUEUE_OVERLAY, False, 3000, self.sceneManager) self.ceguiSystem = CEGUI.System(self.ceguiRenderer) self.sceneManager.setAmbientLight((0.5, 0.5, 0.5)) self.sceneManager.setSkyDome(True, "Examples/CloudySky", 5, 8) # World geometry (Basic tutorial 3) self.sceneManager.setWorldGeometry("terrain.cfg") # Set camera lookpoint self.camera.setPosition(40, 100, 580) self.camera.pitch(ogre.Degree(-30)) self.camera.yaw(ogre.Degree(-45)) # Show the mouse cursor CEGUI.SchemeManager.getSingleton().loadScheme("TaharezLookSkin.scheme") CEGUI.MouseCursor.getSingleton().setImage("TaharezLook", "MouseArrow") def _createFrameListener(self): self.frameListener = MouseQueryListener(self.renderWindow, self.camera, self.sceneManager, self.ceguiRenderer ) self.root.addFrameListener(self.frameListener) self.frameListener.showDebugOverlay(True) if __name__ == '__main__': ta = TutorialApplication() ta.go()
| Python-Ogre Tutorials |
|---|
|
Python-Ogre Beginner Tutorials: Beginner 1 - Beginner 2 - Beginner 3 - Beginner 4 - Beginner 5 - Beginner 6 - Beginner 7 - Beginner 8 Intermediate Tutorials: Intermediate 1 - Intermediate 2 - Intermediate 3 - Intermediate 4 - Intermediate 5 - Intermediate 6 Advanced Tutorials: Advanced 1 See also: Artist Tutorials - Ogre Articles - Cookbook |
