MOgre VB.NET Intermediate Tutorial 3

From Ogre Wiki

Jump to: navigation, search

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

Ported to VB.NET by aeauseth

Contents

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.

Prerequisites

This tutorial will assume that you have gone through the previous tutorial. We will also assume you have downloaded and understand how to use MQuickGUI.

Getting Started

Create a VB.NET console application and paste the following code into Module1.vb:

Imports Mogre

Module Module1
    Public myKeyboard As MOIS.Keyboard
    Public myMouse As MOIS.Mouse
    Public myCamera As Camera
    Public MyWindow As RenderWindow
    Public myScene As SceneManager
    Public myTranslation As Vector3 = Vector3.ZERO
    Public Quitting As Boolean = False
    Public myRightMouseDown As Boolean = False
    Public myLeftMouseDown As Boolean = False
    Public myCurrentObject As SceneNode
    Public myRoot As Root
    Public myMouseCursor As MQuickGUI.MouseCursor
    Public myRobotMode As Boolean = True
    Public Enum QueryFlags As UInteger
        NINJA_MASK = 1 << 0
        ROBOT_MASK = 1 << 1
    End Enum

    Sub Main()

        Try

            'Creating the Root Object 
            myRoot = New Root("Plugins.cfg", "ogre.cfg", "ogre.log")

            'Defining the Resources 
            Dim cf As New ConfigFile
            cf.Load("resources.cfg", vbTab + ":=", True)
            Dim seci As ConfigFile.SectionIterator = cf.GetSectionIterator
            Dim secName As String, typeName As String, archName As String
            While (seci.MoveNext())
                secName = seci.CurrentKey
                Dim settings As ConfigFile.SettingsMultiMap = seci.Current
                For Each pair As KeyValuePair(Of String, String) In settings
                    typeName = pair.Key
                    archName = pair.Value
                    Select Case typeName
                        Case "FileSystem"
                            If Not IO.Directory.Exists(archName) Then
                                If IO.Directory.Exists("../../" & archName) Then
                                    archName = "../../" & archName
                                End If
                            End If
                        Case "Zip"
                            If Not IO.File.Exists(archName) Then
                                If IO.File.Exists("../../" & archName) Then
                                    archName = "../../" & archName
                                End If
                            End If
                    End Select
                    ResourceGroupManager.Singleton.AddResourceLocation(archName, typeName, secName)
                Next
            End While

            'Setting up the RenderSystem
            If Not myRoot.RestoreConfig Then
                If Not myRoot.ShowConfigDialog Then
                    Exit Sub
                End If
            End If

            'Creating the Render Window
            MyWindow = myRoot.Initialise(True, "Ogre RenderWindow")
            AddHandler myRoot.FrameStarted, AddressOf FrameStarted

            'Initializing Resource Groups
            TextureManager.Singleton.DefaultNumMipmaps = 5
            ResourceGroupManager.Singleton.InitialiseAllResourceGroups()

            'Creating the Scene
            myScene = myRoot.CreateSceneManager(SceneType.ST_EXTERIOR_CLOSE)

            'Set the default lighting
            myScene.AmbientLight = New ColourValue(0.5, 0.5, 0.5)
            myScene.SetSkyDome(True, "Examples/CloudySky", 5, 8)

            'Camera
            myCamera = myScene.CreateCamera("Camera")
            myCamera.SetPosition(40, 200, 580)
            myCamera.Pitch(New Degree(-30))
            myCamera.Yaw(New Degree(-45))
            myCamera.NearClipDistance = 5
            myRoot.AutoCreatedWindow.AddViewport(myCamera)

            'World geometry
            myScene.SetWorldGeometry("terrain.cfg")

            'Overlay
            Dim myPanelOverlay As Overlay = OverlayManager.Singleton.GetByName("Core/DebugOverlay")
            myPanelOverlay.Show()

            'Input handler
            InputClass.Init()

            'The Render Loop
            myRoot.StartRendering()

            'Cleanup
            MyWindow.Dispose()
            myRoot.Dispose()

        Catch ex As System.Runtime.InteropServices.SEHException
            If OgreException.IsThrown Then
                MsgBox(OgreException.LastException.FullDescription, MsgBoxStyle.Critical, _
                     "An Ogre SEHException has occured!")
            Else
                MsgBox(ex.ToString, "An error has occured")
            End If
        End Try

    End Sub

    Public Function FrameStarted(ByVal e As FrameEvent) As Boolean
        myMouse.Capture()
        myKeyboard.Capture()

        MQuickGUI.GUIManager.Singleton.injectTime(e.timeSinceLastFrame)

        'Handle player/camera movement
        InputClass.ProcessKeyboard()

        'Camera movement
        If myTranslation <> Vector3.ZERO Then

            myCamera.Position += myCamera.Orientation * myTranslation * e.timeSinceLastFrame

            'Setup the scene query
            Dim camPos As Vector3 = myCamera.Position
            Dim cameraRay As Ray = New Ray(New Vector3(camPos.x, 5000, camPos.z), Vector3.NEGATIVE_UNIT_Y)
            Dim myRaySceneQuery As RaySceneQuery = myScene.CreateRayQuery(cameraRay)
            Dim results As RaySceneQueryResult = myRaySceneQuery.Execute

            For Each result As RaySceneQueryResultEntry In results
                If Not result.worldFragment Is Nothing Then
                    Dim terrainHeight As Single = result.worldFragment.singleIntersection.y
                    If terrainHeight + 10 > camPos.y Then
                        myCamera.SetPosition(camPos.x, terrainHeight + 10, camPos.z)
                    End If
                    Exit For
                End If
            Next
            myRaySceneQuery.Dispose()
        End If

        'Now update the robot location if left mouse button is still down
        If myLeftMouseDown Then
            'Find location our mouse cursor is aiming at
            Dim mouseRay As Ray = myCamera.GetCameraToViewportRay((myMouseCursor.getPixelPosition.x + 15) / MyWindow.Width, (myMouseCursor.getPixelPosition.y + 15) / MyWindow.Height)
            Dim myRaySceneQuery As RaySceneQuery = myScene.CreateRayQuery(mouseRay)
            myRaySceneQuery.SetSortByDistance(True)
            Dim results As RaySceneQueryResult = myRaySceneQuery.Execute

            For Each result As RaySceneQueryResultEntry In results
                If Not result.worldFragment Is Nothing Then
                    myCurrentObject.Position = result.worldFragment.singleIntersection
                    Exit For
                End If
            Next
            myRaySceneQuery.Dispose()
        End If


        'Debug Overlay
        Dim myAvg As OverlayElement = OverlayManager.Singleton.GetOverlayElement("Core/AverageFps")
        Dim myCurr As OverlayElement = OverlayManager.Singleton.GetOverlayElement("Core/CurrFps")
        Dim myBest As OverlayElement = OverlayManager.Singleton.GetOverlayElement("Core/BestFps")
        Dim myWorst As OverlayElement = OverlayManager.Singleton.GetOverlayElement("Core/WorstFps")
        Dim myNumTris As OverlayElement = OverlayManager.Singleton.GetOverlayElement("Core/NumTris")
        Dim myNumBatches As OverlayElement = OverlayManager.Singleton.GetOverlayElement("Core/NumBatches")
        Dim myDebug As OverlayElement = OverlayManager.Singleton.GetOverlayElement("Core/DebugText")

        myAvg.Caption = "Average FPS: " & Mogre.StringConverter.ToString(MyWindow.AverageFPS)
        myCurr.Caption = "Current FPS: " & Mogre.StringConverter.ToString(MyWindow.LastFPS)
        myBest.Caption = "Best FPS: " & Mogre.StringConverter.ToString(MyWindow.BestFPS)
        myWorst.Caption = "Worst FPS: " & Mogre.StringConverter.ToString(MyWindow.WorstFPS)
        myNumTris.Caption = "Triangle Count: " & Mogre.StringConverter.ToString(MyWindow.TriangleCount)
        myNumBatches.Caption = "Batch Count: " & Mogre.StringConverter.ToString(MyWindow.BatchCount)

        If myRobotMode Then
            myDebug.Caption = "Robot Mode Enabled - Press Space to Toggle"
        Else
            myDebug.Caption = "Ninja Mode Enabled - Press Space to Toggle"
        End If

        Return Not Quitting
    End Function

    Public Class InputClass

        Const TRANSLATE As Single = 200
        Const ROTATE As Single = 0.003

        Shared Sub Init()
            'Keyboard
            Dim windowHnd As Integer
            MyWindow.GetCustomAttribute("WINDOW", windowHnd)
            Dim myInputManager As MOIS.InputManager = MOIS.InputManager.CreateInputSystem(windowHnd)
            myKeyboard = myInputManager.CreateInputObject(MOIS.Type.OISKeyboard, True)
            AddHandler myKeyboard.KeyPressed, AddressOf InputClass.KeyPressed
            AddHandler myKeyboard.KeyReleased, AddressOf InputClass.KeyReleased

            'Mouse
            myMouse = myInputManager.CreateInputObject(MOIS.Type.OISMouse, True)
            Dim mousestate As MOIS.MouseState_NativePtr = myMouse.MouseState
            mousestate.width = MyWindow.Width
            mousestate.height = MyWindow.Height
            AddHandler myMouse.MouseMoved, AddressOf InputClass.MouseMovedListener
            AddHandler myMouse.MousePressed, AddressOf InputClass.MousePressedListener
            AddHandler myMouse.MouseReleased, AddressOf InputClass.MouseReleasedListener
            MQuickGUI.GUIManager.Singleton._notifyWindowDimensions(MyWindow.Width, MyWindow.Height)
            myMouseCursor = MQuickGUI.GUIManager.Singleton.createMouseCursor(New Vector2(30, 30), "qgui.pointer")
            SetMouseLocaton(New Vector2(MyWindow.Width / 2, MyWindow.Height / 2))


        End Sub

        Shared Sub SetMouseLocaton(ByVal loc As Vector2)
            Dim axis As MOIS.Axis_NativePtr = myMouse.MouseState.X
            axis.abs = loc.x
            axis = myMouse.MouseState.Y
            axis.abs = loc.y

        End Sub


        Shared Function KeyPressed(ByVal e As MOIS.KeyEvent) As Boolean
            'Currently unused by this application
            If e.key = MOIS.KeyCode.KC_SPACE Then
                myRobotMode = Not myRobotMode
            End If

            Return Nothing
        End Function

        Shared Function KeyReleased(ByVal e As MOIS.KeyEvent) As Boolean

            'This function is just a placeholder
            'It is unlikely you will ever use this
            'Typically you either process unbuffered keyboard input (as in ProcessKeyboard)
            'or you process buffered Keypress

            Return Nothing
        End Function

        Shared Sub ProcessKeyboard()

            'This Sub is typically called via the FrameStarted event.

            'Clear previous translation
            myTranslation.z = 0
            myTranslation.x = 0
            myTranslation.y = 0

            If myKeyboard.IsKeyDown(MOIS.KeyCode.KC_ESCAPE) Then
                Quitting = True
            End If

            If myKeyboard.IsKeyDown(MOIS.KeyCode.KC_UP) Or _
                myKeyboard.IsKeyDown(MOIS.KeyCode.KC_W) Then
                myTranslation.z += -TRANSLATE
            End If

            If myKeyboard.IsKeyDown(MOIS.KeyCode.KC_S) Or _
                myKeyboard.IsKeyDown(MOIS.KeyCode.KC_DOWN) Then
                myTranslation.z += TRANSLATE
            End If

            If myKeyboard.IsKeyDown(MOIS.KeyCode.KC_A) Or _
                myKeyboard.IsKeyDown(MOIS.KeyCode.KC_LEFT) Then
                myTranslation.x += -TRANSLATE
            End If

            If myKeyboard.IsKeyDown(MOIS.KeyCode.KC_D) Or _
                myKeyboard.IsKeyDown(MOIS.KeyCode.KC_RIGHT) Then
                myTranslation.x += TRANSLATE
            End If

            If myKeyboard.IsKeyDown(MOIS.KeyCode.KC_Q) Or _
            myKeyboard.IsKeyDown(MOIS.KeyCode.KC_PGUP) Then
                myTranslation.y += TRANSLATE
            End If

            If myKeyboard.IsKeyDown(MOIS.KeyCode.KC_Z) Or _
                myKeyboard.IsKeyDown(MOIS.KeyCode.KC_PGDOWN) Then
                myTranslation.y += -TRANSLATE
            End If
        End Sub

        Shared Function MouseMovedListener(ByVal e As MOIS.MouseEvent) As Boolean

            'Mouse Cursor Movement
            MQuickGUI.GUIManager.Singleton.injectMousePosition(myMouse.MouseState.X.abs, myMouse.MouseState.Y.abs)

            'Camera Rotate
            If myRightMouseDown Then
                myCamera.Yaw(e.state.X.rel * -ROTATE)
                myCamera.Pitch(e.state.Y.rel * -ROTATE)
            End If

        End Function

        Shared Function MousePressedListener(ByVal e As MOIS.MouseEvent, ByVal id As MOIS.MouseButtonID) As Boolean

            If e.state.ButtonDown(MOIS.MouseButtonID.MB_Right) Then
                onRightPressed()
            End If

            If e.state.ButtonDown(MOIS.MouseButtonID.MB_Left) Then
                onLeftPressed()
            End If


        End Function

        Shared Sub onLeftPressed()
            myLeftMouseDown = True

            'Inform GUI of mouse status
            MQuickGUI.GUIManager.Singleton.injectMouseButtonDown(MOIS.MouseButtonID.MB_Left)

            'Turn off bounding box
            If Not myCurrentObject Is Nothing Then
                myCurrentObject.ShowBoundingBox = False
            End If

            'Find location our mouse cursor is aiming at
            Dim mouseRay As Ray = myCamera.GetCameraToViewportRay((myMouseCursor.getPixelPosition.x + 15) / MyWindow.Width, (myMouseCursor.getPixelPosition.y + 15) / MyWindow.Height)
            Dim myRaySceneQuery As RaySceneQuery = myScene.CreateRayQuery(mouseRay)
            myRaySceneQuery.SetSortByDistance(True)
            If myRobotMode Then
                myRaySceneQuery.QueryMask = QueryFlags.ROBOT_MASK
            Else
                myRaySceneQuery.QueryMask = QueryFlags.NINJA_MASK
            End If

            Dim results As RaySceneQueryResult = myRaySceneQuery.Execute

            For Each result As RaySceneQueryResultEntry In results

                'Movable object?
                If Not result.movable Is Nothing Then
                    If Not result.movable.Name.Contains("tile[") Then
                        myCurrentObject = result.movable.ParentSceneNode
                        Exit For
                    End If
                End If

                If Not result.worldFragment Is Nothing Then
                    CreateEntityAt(result.worldFragment.singleIntersection)
                    Exit For
                End If
            Next
            myRaySceneQuery.Dispose()

            'Turn on bounding box
            If Not myCurrentObject Is Nothing Then
                myCurrentObject.ShowBoundingBox = True
            End If

            'Hide mouse cursor while we are holding down the left mouse button
            myMouseCursor.hide()

        End Sub

        Shared Sub CreateEntityAt(ByVal Loc As Vector3)
            Static iCount As Integer = 0
            iCount += 1
            Dim entName As String
            Dim myEntity As Entity
            If myRobotMode Then
                entName = "Robot" & iCount
                myEntity = myScene.CreateEntity(entName, "robot.mesh")
                myEntity.QueryFlags = QueryFlags.ROBOT_MASK
            Else
                entName = "Ninja" & iCount
                myEntity = myScene.CreateEntity(entName, "ninja.mesh")
                myEntity.QueryFlags = QueryFlags.NINJA_MASK
            End If
            myCurrentObject = myScene.RootSceneNode.CreateChildSceneNode(entName & "Node", Loc)
            myCurrentObject.AttachObject(myEntity)
            myCurrentObject.SetScale(0.1, 0.1, 0.1)
        End Sub

        Shared Sub onRightPressed()

            myRightMouseDown = True
            myMouseCursor.hide()

            'Inform GUI of mouse status
            MQuickGUI.GUIManager.Singleton.injectMouseButtonDown(MOIS.MouseButtonID.MB_Right)

        End Sub

        Shared Function MouseReleasedListener(ByVal e As MOIS.MouseEvent, ByVal id As MOIS.MouseButtonID) As Boolean
            If myRightMouseDown And Not e.state.ButtonDown(MOIS.MouseButtonID.MB_Right) Then
                myRightMouseDown = False
                MQuickGUI.GUIManager.Singleton.injectMouseButtonUp(MOIS.MouseButtonID.MB_Left)
                SetMouseLocaton(New Vector2(MyWindow.Width / 2, MyWindow.Height / 2))
                MQuickGUI.GUIManager.Singleton.injectMousePosition(myMouse.MouseState.X.abs, myMouse.MouseState.Y.abs)
                myMouseCursor.show()
            End If

            If myLeftMouseDown And Not e.state.ButtonDown(MOIS.MouseButtonID.MB_Left) Then
                myLeftMouseDown = False
                MQuickGUI.GUIManager.Singleton.injectMouseButtonUp(MOIS.MouseButtonID.MB_Right)
                myMouseCursor.show()
            End If
        End Function

    End Class

End Module

Be sure you can compile and run this code before continuing.

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. Note we have already added the following code that unselect the currentObject whenever we press the onLeftPressed:

       'Turn off bounding box
       If Not myCurrentObject Is Nothing Then
            myCurrentObject.ShowBoundingBox = False
       End

Just before we exit the onLeftPressed we set the bounding box to true:

       'Turn on bounding box
       If Not myCurrentObject Is Nothing Then
            myCurrentObject.ShowBoundingBox = True
       End

Now the myCurrentObject is always highlighted on the screen.

Adding Ninjas

We also added some code that creates Ninja's. Pressing the space key will toggle the mode, and we will display a message to the user which mode they are in.


The code to accomplish this is pretty straight forward so I'll spare you the details. You can always look thru the code to see what is going on.

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. (Please follow the link and look at the struct briefly.)

The RaySceneQueryResult returns an iterator of RaySceneQueryResultEntry structs. This struct 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 non-null. 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 NULL 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 struct 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.

        'Do not add this code to the program, just read along:

        Dim results As RaySceneQueryResult = myRaySceneQuery.Execute
        For Each result As RaySceneQueryResultEntry In results
            'Is this a result a WorldFragment?
            If Not result.worldFragment Is Nothing Then
                Dim location As Vector3 = result.worldFragment.singleIntersection
                Debug.WriteLine("WorldFragment: (" & location.x & ", " & location.y & ", " & location.z & ")")
            End If

            'Is this a MovableObject?
            If Not result.movable Is Nothing Then
                Debug.WriteLine("MovableObject: " & result.movable.Name)
            End If
        Next

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.

You may recall that in the previous tutoraial we just assumed that the first result was our terrain. 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. You will note the following code changes:

            'Find location our mouse cursor is aiming at
            Dim mouseRay As Ray = myCamera.GetCameraToViewportRay((myMouseCursor.getPixelPosition.x + 15) / MyWindow.Width, (myMouseCursor.getPixelPosition.y + 15) / MyWindow.Height)
            Dim myRaySceneQuery As RaySceneQuery = myScene.CreateRayQuery(mouseRay)
            myRaySceneQuery.SetSortByDistance(True)
            Dim results As RaySceneQueryResult = myRaySceneQuery.Execute

            For Each result As RaySceneQueryResultEntry In results

                'Movable object?
                If Not result.movable Is Nothing Then
                    If Not result.movable.Name.Contains("tile[") Then
                        myCurrentObject = result.movable.ParentSceneNode
                        Exit For
                    End If
                End If

                If Not result.worldFragment Is Nothing Then
                    CreateEntityAt(result.worldFragment.singleIntersection)
                    Exit For
                End If
            Next
            myRaySceneQuery.Dispose() 

First we check if the first intersection is a MovableObject, if so we'll assign mCurrentObject 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 'Exit For' 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.

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.

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.

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 public statement:

    Public Enum QueryFlags As UInteger
        NINJA_MASK = 1 << 0
        ROBOT_MASK = 1 << 1
    End Enum

This creates an enum with 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.

            If myRobotMode Then
                entName = "Robot" & iCount
                myEntity = myScene.CreateEntity(entName, "robot.mesh")
                myEntity.QueryFlags = QueryFlags.ROBOT_MASK
            Else
                entName = "Ninja" & iCount
                myEntity = myScene.CreateEntity(entName, "ninja.mesh")
                myEntity.QueryFlags = QueryFlags.NINJA_MASK
            End If

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.

            If myRobotMode Then
                myRaySceneQuery.QueryMask = QueryFlags.ROBOT_MASK
            Else
                myRaySceneQuery.QueryMask = QueryFlags.NINJA_MASK
            End If

Compile and 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:

myRaySceneQuery.QueryTypeMask = 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:

Enum QueryFlags As Integer
     FRIENDLY_CHARACTERS = 1 << 0
     ENEMY_CHARACTERS = 1 << 1
     STATIONARY_OBJECTS = 1 << 2
End Enum

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

myRaySceneQuery.QueryTypeMask = QueryFlags.FRIENDLY_CHARACTERS

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

  myRaySceneQuery.QueryTypeMask = QueryFlags.ENEMY_CHARACTERS Or QueryFlags.STATIONARY_OBJECTS

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

OBJECTS_ENEMIES = QueryFlags.ENEMY_CHARACTERS Or QueryFlags.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:

  myRaySceneQuery.QueryTypeMask = Not QueryFlags.FRIENDLY_CHARACTERS 

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

myRaySceneQuery.QueryTypeMask = Not (QueryFlags.FRIENDLY_CHARACTERS Or  _
   QueryFlags.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:

myRaySceneQuery.QueryTypeMask = 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.

Exercises

Easy Exercises

  1. The TerrainSceneManager creates tiles with a default mask of ~0 (all queries select it). We fixed this problem by testing to see if the name of the movable object equaled "tile[0][0,2]". Even though it's not implemented yet, the TerrainSceneManager supports multiple pages, and if there were more things than just "tile[0][0,2]" this would cause our code to break down. Instead of making the test in the loop, fix the problem properly by setting all of the tile objects created by the TerrainSceneManager to have a unique mask. (Hint: The TerrainSceneManager creates a SceneNode called "Terrain" which contains all of these tiles. Loop through them and set the attached object's masks to something of your choosing.)

Intermediate Exercises

  1. Our program delt with two things, Robots and Ninjas. If we were going to implement a scene editor, we would want to place any number of different object types. Generalize this code to allow the placement of any type of object from a predefined list. Create an overlay with the list of objects you want the editor to have (such as Ninjas, Robots, Knots, Ships, etc), and have the SceneQueries only select that type of object.
  2. Since we are using multiple types of objects now, use the Factory Pattern to properly create the SceneNodes and Entities.

Advanced Exercises

  1. Generalize the previous exercises to read in all of the meshes that Ogre knows about (IE everything that was parsed in from the Media directory), and give the ability to place them. Note that there should not be a limit as to how many types of objects ogre can place. Since you only have 32 unique query masks to use, you may need to come up with a way to quickly change all of the query flags for objects on the screen.
  2. You might have noticed that when you click on an object, the object is "lifted" from the bottom of the bounding box. To see this, click on the top of any character and move him. He will be transported instantly elsewhere. Modify the program to fix this problem.

Exercises for Further Study

  1. Add a way to select multiple objects to the program such that when you hold the Ctrl key and click multiple objects are highlighted. When you move these objects, move all of them as a group.
  2. Many scene editing programs allow you to group objects so that they are always moved together. Implement this in the program.
Proceed to MOgre VB.NET Intermediate Tutorial 4 Volume Selection and Basic Manual Objects
Personal tools
administration