Intermediate Tutorial 3

From PyWiki

Jump to: navigation, search

Contents

Intermediate Tutorial 3: Mouse Picking (3D Object Selection) and SceneQuery Masks

Introduction

In this tutorial we will continue the work on the previous tutorial. We will be covering how to select any object on the screen using the mouse, and how to restrict what is selectable.

You can find the code for this tutorial here. As you go through the demo you should be slowly adding code to your own project and watching the results as we build it.

Prerequisites

This tutorial will assume that you have gone through the previous tutorial. We will also be using STL iterators to go through multiple results of SceneQueries, so basic knowledge of them will be helpful.

Getting Started

Even though we will be editing the code from last time, some changes have been made to make the rest of the tutorial more readable. For all of the mouse events, I have created wrapper functions to handle them. Now when the user presses the left mouse button, the "onLeftPressed" function is called, when the right button is released, the "onRightReleased" function is called, and so on. You should take the time to review these changes before starting the tutorial.

Please note that cegui render setup in the TutorialApplication _createScene() may use temporarily fix from http://www.ogre3d.org/addonforums/viewtopic.php?f=3&t=11708 due to API changes from CEGUI ver. 0.6 to 0..7.

#!/usr/bin/env python 
# This code is Public Domain. 
"""Python-Ogre Intermediate Tutorial 03: 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.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()
       ray = ogre.Ray((camPos.x, 5000.0, camPos.y), ogre.Vector3().NEGATIVE_UNIT_Y)
       self.raySceneQuery.setRay(ray)
 
       # Perform the scene query
       result = self.raySceneQuery.execute()
       if len(result) > 0: # Make sure we actually hit something with our ray
           item = result[0]
 
           # Result of this query is a list of worldFragments and a list of movables
           # Find the terrain-fragment
 
           if item.worldFragment != None:
               terrainHeight = item.worldFragment.singleIntersection.y
               if (terrainHeight + 10.0) > camPos.y:
                   self.camera.setPosition(camPos.x, terrainHeight + 10.0, camPos.z)
       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.onLeftPressed(evt)
            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
 
    def onLeftPressed(self, evt):
        # 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)
                self.currentObject.attachObject(ent)
                self.currentObject.setScale(0.1, 0.1, 0.1)
 
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()


Be sure you can run this code before continuing. Despite some code changes, it should work the exact same as the last tutorial.

Showing Which Object is Selected

In this tutorial we will be making it so that you can "pick up" and move objects after you have placed them. We would like to have a way for the user to know which object she's currently manipulating. In a game, we would probably like to create a special way of highlighting the object, but for our tutorial (and for your applications before they are release-ready), you can use the showBoundingBox method to create a box around objects.

Our basic idea is to disable the bounding box on the old current object when the mouse is first clicked, then enable the bounding box as soon as we have the new object. To do this, we will add the following code to the beginning of the onLeftPressed function:

# Turn off bounding box.
       if self.currentObject:
            self.currentObject.showBoundingBox(False)

Then add the following code to the very end of the onLeftPressed function:

# Show the bounding box to highlight the selected object
       if self.currentObject:
            self.currentObject.showBoundingBox(True)

Now the currentObject is always highlighted on the screen.

Adding Ninjas

We now want to modify the code to not just support robots, but also placing and moving ninjas. We will have a "Robot Mode" and a "Ninja Mode" that will determine which object we are placing on the screen. We will make the space key be our mode switching button, and we will display a message to the user which mode they are in.

First, we will setup the MouseQueryListener to be in Robot Mode from the beginning. We need to add a variable to hold the state of the object (that is, if we are placing robots or ninjas). Add this code to the end of the MouseQueryListener constructor:

# Initalize our state values
       self.robotMode = True
       self.debugText = "Robot Mode Enabled - Press Space to Toggle"

This puts us in Robot Mode! If only it were that simple. Now we need to create either a Robot or a Ninja mesh based on the self.robotMode variable. Locate this code in onLeftPressed:

# 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")

The replacement should be straight forward. Depending on the self.robotMode state we either put out a Robot or a Ninja, and name it accordingly:

# We have the position we clicked on, create a new
                    # object and place it here
                    if self.robotMode:
                        name = "Robot" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "robot.mesh")
                    else:
                        name = "Ninja" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "ninja.mesh")
                    self.robotCount += 1

Now we are almost done. The only thing left to do is to bind the spacebar to change states. Find the following code in frameStarted:

#
        if not sf.FrameListener.frameStarted(self, evt):
           return False

And add this code after it:

# Swap modes
        if self.Keyboard.isKeyDown(OIS.KC_SPACE) and self.timeUntilNextToggle <= 0:
           self.robotMode = not self.robotMode
           self.timeUntilNextToggle = 1
           if self.robotMode:
               type = "Robot"
           else:
               type = "Ninja"
           self.debugText = type + " Mode Enabled - Press Space to Toggle"

Now we are done! Run the demo. You can now choose which object you place by using the space bar.

Selecting Objects

Now we are going to dive into the meat of this tutorial: using RaySceneQueries to select objects on the screen. Before we start making changes to the code I will first explain a RaySceneQueryResultEntry in more detail.

The RaySceneQueryResult returns a list of RaySceneQueryResultEntry objects. This object contains three variables. The distance variable tells you how far away the object is along the ray. One of the other two variables will be None, the last variable will contain an object. The movable variable will contain a MovableObject if the Ray intersected one. The worldFragment will contain a WorldFragment object if it hit a world fragment (like the terrain).

MovableObjects are basically any object you would attach to a SceneNode (such as Entities, Lights, etc). See the inheritance tree on this page to find out what type of objects would be returned. Most normal applications of RaySceneQueries will involve selecting and manipulating either the MovableObject you have clicked on, or the SceneNodes they are attached to. To get the name of the MovableObject, call the getName method. To get the SceneNode (or Node) the object is attached to, call getParentSceneNode (or getParentNode). The movable variable in a RaySceneQueryResultEntry will be equal to None if the result is not a MovableObject.

The WorldFragment is a different beast all together. When the worldFragment member of a RaySceneQueryResult is set, it means that the result is part of the world geometry created by the SceneManager. The type of world fragment that is returned is based on the SceneManager. The way this is implemented is WorldFragment object contains the fragmentType variable which specifies the type of world fragment it contains. Based on the fragmentType variable, one of the other variables will be set (singleIntersection, planes, geometry, or renderOp). Generally speaking, RaySceneQueries only return WFT_SINGLE_INTERSECTION WorldFragments. The singleIntersection variable is simply a Vector3 reporting the location of the intersection. Other types of world fragments are beyond the scope of this tutorial.

Now lets look at an example. Lets say we wanted to print out a list of results after a RaySceneQuery. The following code would do this. (Assume that the fout object is of type ofstream, and that it has already been created using the open method.)

# Do not add this code to the program, just read along:
 
result = mRaySceneQuery->execute();
for item in result:
   # Is this result a WorldFragment?
   if item.worldFragment:
      location = item.worldFragment.singleIntersection
      print "WorldFragment: (" + str(location.x) + ", " + str(location.y) + ", " + str(location.z) + ")"
 
   # Is this result a MovableObject?
   elif item.movable:
      print "MovableObject: " + str(item.movable.getName())

This would print out the names of all MovableObjects that the ray intersects, and it would print the location of where it intersected the world geometry (if it did hit it). Note that this can sometimes act in strange ways. For example, if you are using the TerrainSceneManager, the origin of the Ray you fire must be over the Terrain or the intersection query will not register it as a hit. Different scene managers implement RaySceneQueries in different ways. Be sure to experiment with it when you use it with a new SceneManager.

Now, if we look back at our RaySceneQuery code, something should jump out at you: we are not looping through all the results! In fact we are only looking at the first result, which (in the case of the TerrainSceneManager) is the world geometry. This is bad, since we cannot be sure that the TerrainSceneManager will always return the world geometry first. We need to loop through the results to make sure we are finding what we are looking for. Another thing that we want to do is to "pick up" and drag objects that have already been placed. Currently if you click on an object that has already been placed, the program ignores it and places a robot behind it. Let's fix that now.

Go to the onLeftPressed function in the code. We first want to make sure that when we click, we get the first thing along the Ray. To do that, we need to set the RaySceneQuery to sort by depth. Find this code in the onLeftPressed function:

# 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]

And change it to this:

# 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)
        self.raySceneQuery.setSortByDistance(True)
        if self.robotMode:
            self.raySceneQuery.setQueryMask(self.ROBOT_MASK)
        else:
            self.raySceneQuery.setQueryMask(self.NINJA_MASK)
 
        # Execute query
        result = self.raySceneQuery.execute()

Now that we are returning the results in order, need to update the query results code. We are going to rewrite that section, so remove this code:

#
         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
                    if self.robotMode:
                        name = "Robot" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "robot.mesh")
                    else:
                        name = "Ninja" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "ninja.mesh")
 
                    self.robotCount += 1
                self.currentObject = self.sceneManager.getRootSceneNode().createChildSceneNode(name + "Node", item.worldFragment.singleIntersection)
                self.currentObject.attachObject(ent)
                self.currentObject.setScale(0.1, 0.1, 0.1)

We want to make it so that we can select objects that are already placed on the screen. Our strategy is going to have two parts. First, if the user clicks on an object, then make self.currentObject equal to its parent SceneNode. If the user does not click on an object (if he clicks on the terrain instead), place a new Robot there like we did before. The first change is that we will be using a for loop instead of an if statement:

# Loop through all the results returned
        if len(result) > 0:
            for item in result:

First we will check if the first intersection is a MovableObject, if so we'll assign self.currentObject to be its parent SceneNode. There is a catch though. The TerrainSceneManager creates MovableObjects for the terrain itself, so we might actually be intersecting one of the tiles. In order to fix that, I check the name of the object to make sure that it does not resemble a terrain tile name; a sample tile name would be "tile[0][0,2]". Finally, notice the break statement. We only need to act on the first object, so as soon as we find a valid one we need to get out of the for loop altogether.

#
                if item.movable and item.movable.getName()[0:5] != "tile[":
                    self.currentObject = item.movable.getParentSceneNode()
                    break # We found an existing object

Next, we will check to see if the intersection returned a WorldFragment.

#
                elif item.worldFragment:
                    # We have the position we clicked on, create a new
                    # object and place it here

Now we are either going to create a Robot entity or a Ninja entity based on self.robotState. After creating the entity we will create the SceneNode and break out of the for loop.

# We have the position we clicked on, create a new
                    # object and place it here
                    if self.robotMode:
                        name = "Robot" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "robot.mesh")
                    else:
                        name = "Ninja" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "ninja.mesh")
 
                    self.robotCount += 1
                    self.currentObject = self.sceneManager.getRootSceneNode().createChildSceneNode(name + "Node", item.worldFragment.singleIntersection)
                    self.currentObject.attachObject(ent)
                    self.currentObject.setScale(0.1, 0.1, 0.1)

Believe it or not, that's all that has to be done! Compile and play with the code. Now we create the correct type of object when we click on terrain, and when we click on an object we will see the bounding box (no dragging it around right now, this comes in the next Step). One valid question is, since we only want the first intersection, and since we sorted by depth, why not just use an if statement? The main reason is we could actually have a fall through if there the first returned object is one of those pesky tiles. We have to loop until we find something other than a tile or we hit the end of the list.

Now that we are on the subject, we need to also update the RaySceneQuery code in other locations. In both the frameStarted and the onLeftDragged functions we only need to find the Terrain. There is no reason to sort the results because the terrain will be at the end of the sorted list (so we are going to turn sorting off). We still want to loop through the results though, just in case the TerrainSceneManager is changed at a future date to not return the terrain first. First, find this section of code in frameStarted:

# Perform the scene query
       result = self.raySceneQuery.execute()
       if len(result) > 0: # Make sure we actually hit something with our ray
           item = result[0]
 
           # Result of this query is a list of worldFragments and a list of movables
           # Find the terrain-fragment
 
           if item.worldFragment != None:
               terrainHeight = item.worldFragment.singleIntersection.y
               if (terrainHeight + 10.0) > camPos.y:
                   self.camera.setPosition(camPos.x, terrainHeight + 10.0, camPos.z)

Then replace it with this:

# Perform the scene query
       result = self.raySceneQuery.execute()
       for item in result:
           # Result of this query is a list of worldFragments and a list of movables
           # Find the terrain-fragment
 
           if item.worldFragment != None:
               terrainHeight = item.worldFragment.singleIntersection.y
               if (terrainHeight + 10.0) > camPos.y:
                   self.camera.setPosition(camPos.x, terrainHeight + 10.0, camPos.z)

This should be self explanatory. We add a line to turn off sorting, then we convert the if statement into a for loop, and break as soon as we have found the position we are looking for. We will do the exact same thing with the mouseMoved function. Find this section of code in the mouseMoved:

self.raySceneQuery.setRay(mouseRay)
 
            result = self.raySceneQuery.execute()
            if len(result) > 0:
                item = result[0]
                if item.worldFragment:
                    self.currentObject.setPosition(item.worldFragment.singleIntersection)

And replace it with this code:

#
            self.raySceneQuery.setRay(mouseRay)
            self.raySceneQuery.setSortByDistance(False)
 
            result = self.raySceneQuery.execute()
            for item in result:
                if item.worldFragment:
                    self.currentObject.setPosition(item.worldFragment.singleIntersection)

Run the code. There shouldn't be any noticeable difference since the last time we ran the code, but now we are doing it the correct way, and future updates to the TerrainSceneManager will not break our code.

Query Masks

Notice that no matter what mode we are in we can select either object. Our RaySceneQuery will return either Robots or Ninjas, whichever is in front. It doesn't have to be this way though. All MovableObjects allow you to set a mask value for them, and SceneQueries allow you to filter your results based on this mask. All masks are done using the binary AND operation, so if you are unfamiliar with this, you should brush up on it before continuing (See http://docs.python.org/lib/bitstring-ops.html)

The first thing we are going to do is create the mask values. Go to the very beginning of the MouseQueryListener class and add this after the class statement:

NINJA_MASK = 1 << 0
    ROBOT_MASK = 1 << 1

This creates two values, which in binary are 0001 and 0010. Now, every time we create a Robot entity, we call its "setMask" function to set the query flags to be ROBOT_MASK. Every time we create a Ninja entity we call its "setMask" function and use NINJA_MASK instead. Now, when we are in Ninja mode, we will make the RaySceneQuery only consider objects with the NINJA_MASK flag, and when we are in Robot mode we will make it only consider ROBOT_MASK.

Find this section of onLeftPressed:

#
                    if self.robotMode:
                        name = "Robot" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "robot.mesh")
                    else:
                        name = "Ninja" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "ninja.mesh")


We will add two lines to set the mask on both of them:

#
                    if self.robotMode:
                        name = "Robot" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "robot.mesh")
                        ent.setQueryFlags(self.ROBOT_MASK)
                    else:
                        name = "Ninja" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "ninja.mesh")
                        ent.setQueryFlags(self.NINJA_MASK)

We still need to make it so that when we are in a mode, we can only click and drag objects of that type. We need to set the query flags so that only the correct object type can be selected. We accomplish this by setting the query mask in the RaySceneQuery to be the ROBOT_MASK in Robot mode, and set it to NINJA_MASK in Ninja mode. In the onLeftPressed function, find this code:

self.raySceneQuery.setSortByDistance(True);

Add these lines of code after it:

#
       if self.robotMode:
            self.raySceneQuery.setQueryMask(self.ROBOT_MASK)
        else:
            self.raySceneQuery.setQueryMask(self.NINJA_MASK)

Run the tutorial. We now select only the objects we are looking for. All rays that pass through other objects go through them and hit the correct object. We are now finished working on this code. The next section will not be modifying it.

Query Type Masks

There's one more thing to consider when using scene queries. Suppose you added a billboardset or a particle system to your scene above, and you want to move it around. You will find that the query never returns the billboardset that you click on. This is because the SceneQuery has another mask, the QueryTypeMask, that limits you to selecting only the type specified as the flag. By default when you do a query, it returns only objects of entity type.

In your code, if you want your query to return BillboardSets or ParticleSystems, you'll have to do this first before executing your query:

self.raySceneQuery.setQueryTypeMask(SceneManager.FX_TYPE_MASK);

Now the query will only return BillboardSets or ParticleSystems as results.

There are 6 types of QueryTypeMask defined in the SceneManager class as static members:

WORLD_GEOMETRY_TYPE_MASK //Returns world geometry. ENTITY_TYPE_MASK //Returns entities. FX_TYPE_MASK //Returns billboardsets / particle systems. STATICGEOMETRY_TYPE_MASK //Returns static geometry. LIGHT_TYPE_MASK //Returns lights. USER_TYPE_MASK_LIMIT //User type mask limit.

The default QueryTypeMask when the property is not set manually is ENTITY_TYPE_MASK.

More on Masks

Our mask example is very simple, so I would like to go through a few more complex examples.

Setting a MovableObject's Mask

Every time we want to create a new mask, the binary representation must contain only one 1 in it. That is, these are valid masks:

00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000

And so on. We can very easily create these values by taking 1 and bitshifting them by a position value. That is:

00000001 = 1<<0 00000010 = 1<<1 00000100 = 1<<2 00001000 = 1<<3 00010000 = 1<<4 00100000 = 1<<5 01000000 = 1<<6 10000000 = 1<<7

All the way up to 1<<31. This gives us 32 distinct masks we can use for MovableObjects.

Querying for Multiple Masks

We can query for multiple masks by using the bitwise OR operator. Let say we have three different groups of objects in a game:

   FRIENDLY_CHARACTERS = 1<<0,
   ENEMY_CHARACTERS = 1<<1,
   STATIONARY_OBJECTS = 1<<2

Now, if we wanted to query for only friendly characters we could do:

self.raySceneQuery.setQueryMask(FRIENDLY_CHARACTERS);

If we want the query to return both enemy characters and stationary objects, we would use:

slef.raySceneQuery.setQueryMask(ENEMY_CHARACTERS | STATIONARY_OBJECTS);

If you use a lot of these types of queries, you might want to define this in the enum:

OBJECTS_ENEMIES = ENEMY_CHARACTERS | STATIONARY_OBJECTS

And then simply use OBJECTS_ENEMIES to query.

Querying for Everything but a Mask

You can also query for anything other than a mask using the bit inversion operator, like so:

self.raySceneQuery.setQueryMask(~FRIENDLY_CHARACTERS);

Which will return everything other than friendly characters. You can also do this for multiple masks:

self.raySceneQuery.setQueryMask(~(FRIENDLY_CHARACTERS | STATIONARY_OBJECTS));

Which would return everything other than friendly characters and stationary objects.

Selecting all Objects or No Objects

You can do some very interesting stuff with masks. The thing to remember is, if you set the query mask QM for a SceneQuery, it will match all MovableObjects that have the mask OM if QM & OM contains at least one 1. Thus, setting the query mask for a SceneQuery to 0 will make it return no MovableObjects. Setting the query mask to ~0 (0xFFFFF...) will make it return all MovableObjects that do not have a 0 query mask.

Using a query mask of 0 can be highly useful in some situations. For example, the TerrainSceneManager does not use QueryMasks when it returns a worldFragment. By doing this:

self.raySceneQuery.setQueryMask(0);

You will get ONLY the worldFragment in your RaySceneQueries for that SceneManager. This can be very useful if you have a lot of objects on screen and you do not want to waste time looping through all of them if you only need to look for the Terrain intersection.


Python version of the code

#!/usr/bin/env python 
# This code is Public Domain. 
"""Python-Ogre Intermediate Tutorial 03: 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."""
 
    NINJA_MASK = 1 << 0
    ROBOT_MASK = 1 << 1
 
    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.robotMode = True
       self.debugText = "Robot Mode Enabled - Press Space to Toggle"
 
       self.raySceneQuery = self.sceneManager.createRayQuery(ogre.Ray())
 
    def frameStarted(self, evt):
       if not sf.FrameListener.frameStarted(self, evt):
           return False
 
       if self.Keyboard.isKeyDown(OIS.KC_SPACE) and self.timeUntilNextToggle <= 0:
           self.robotMode = not self.robotMode
           self.timeUntilNextToggle = 1
           if self.robotMode:
               type = "Robot"
           else:
               type = "Ninja"
           self.debugText = type + " Mode Enabled - Press Space to Toggle"
 
       # 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()
       ray = ogre.Ray((camPos.x, 5000.0, camPos.y), ogre.Vector3().NEGATIVE_UNIT_Y)
       self.raySceneQuery.setRay(ray)
       self.raySceneQuery.setSortByDistance(False)
 
       # Perform the scene query
       result = self.raySceneQuery.execute()
       for item in result:
           # Result of this query is a list of worldFragments and a list of movables
           # Find the terrain-fragment
 
           if item.worldFragment != None:
               terrainHeight = item.worldFragment.singleIntersection.y
               if (terrainHeight + 10.0) > camPos.y:
                   self.camera.setPosition(camPos.x, terrainHeight + 10.0, camPos.z)
       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)
            self.raySceneQuery.setSortByDistance(False)
 
            result = self.raySceneQuery.execute()
            for item in result:
                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.onLeftPressed(evt)
            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
 
    def onLeftPressed(self, evt):
        if self.currentObject:
            self.currentObject.showBoundingBox(False)
 
 
        # 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)
        self.raySceneQuery.setSortByDistance(True)
        if self.robotMode:
            self.raySceneQuery.setQueryMask(self.ROBOT_MASK)
        else:
            self.raySceneQuery.setQueryMask(self.NINJA_MASK)
 
        # Execute query
        result = self.raySceneQuery.execute()
        if len(result) > 0:
            for item in result:
                if item.movable and item.movable.getName()[0:5] != "tile[":
                    self.currentObject = item.movable.getParentSceneNode()
                    break # We found an existing object
 
                elif item.worldFragment:
                    # We have the position we clicked on, create a new
                    # object and place it here
                    if self.robotMode:
                        name = "Robot" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "robot.mesh")
                        ent.setQueryFlags(self.ROBOT_MASK)
                    else:
                        name = "Ninja" + str(self.robotCount)
                        ent = self.sceneManager.createEntity(name, "ninja.mesh")
                        ent.setQueryFlags(self.NINJA_MASK)
 
                    self.robotCount += 1
                    self.currentObject = self.sceneManager.getRootSceneNode().createChildSceneNode(name + "Node", item.worldFragment.singleIntersection)
                    self.currentObject.attachObject(ent)
                    self.currentObject.setScale(0.1, 0.1, 0.1)
        if self.currentObject:
            self.currentObject.showBoundingBox(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

Personal tools