MOgre VB.NET Intermediate Tutorial 2

From Ogre Wiki

Jump to: navigation, search

Intermediate Tutorial 2: RaySceneQueries and Basic Mouse Usage

Ported to VB.NET by Aeauseth


Contents

Introduction

In this tutorial we will create the beginnings of a basic Scene Editor. During this process, we will cover:

  1. How to use RaySceneQueries to keep the camera from falling through the terrain
  2. How to use the MouseListener and MouseMotionListener interfaces
  3. How to select x and y coordinates on the terrain based on your camera view

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 will assume that you already know how to set up an Ogre project and make it compile successfully. Knowledge of basic Ogre objects (SceneNodes, Entities, etc) is assumed. We will gloss over the MOIS keyboard & mouse sections so you may want to review Tutorial 4.

Getting Started

First, you need to create a new VB.NET console Application for the demo. Make the necessary MOgre changes as described in Basic Tutorial 0. Replace the contents of Module1.vb with:

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 myRotating As Boolean = False
    Public myLeftMouseDown As Boolean = False
    Public myCurrentObject As SceneNode
    Public myRoot As Root

    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()

        'Handle player/camera movement
        InputClass.ProcessKeyboard()

        'Camera movement
        If myTranslation <> Vector3.ZERO Then

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

            'Setup the scene query

        End If

        'Now update the robot location if left mouse button is still down

        '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)



        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, False)
            AddHandler myKeyboard.KeyPressed, AddressOf InputClass.KeyPressed
            AddHandler myKeyboard.KeyReleased, AddressOf InputClass.KeyReleased

            'Mouse
            myMouse = myInputManager.CreateInputObject(MOIS.Type.OISMouse, True)
            AddHandler myMouse.MouseMoved, AddressOf InputClass.MouseMovedListener
            AddHandler myMouse.MousePressed, AddressOf InputClass.MousePressedListener
            AddHandler myMouse.MouseReleased, AddressOf InputClass.MouseReleasedListener

        End Sub

        Shared Function KeyPressed(ByVal e As MOIS.KeyEvent) As Boolean
            'Currently unused by this application

            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) Or _
            myKeyboard.IsKeyDown(MOIS.KeyCode.KC_SPACE) 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
            Static myLastX As Integer = e.state.X.abs
            Static myLastY As Integer = e.state.Y.abs

            If myRotating 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
                myRotating = True
            End If

        End Function

        Shared Function MouseReleasedListener(ByVal e As MOIS.MouseEvent, ByVal id As MOIS.MouseButtonID) As Boolean
            If Not e.state.ButtonDown(MOIS.MouseButtonID.MB_Right) Then
                myRotating = False
            End If

            If Not e.state.ButtonDown(MOIS.MouseButtonID.MB_Left) Then
                myLeftMouseDown = False
            End If
        End Function

    End Class

End Module

Be sure this code compiles before continuing.

Setting up the Scene

The scene has already been setup for you. Keyboard AWSD and mousemovement is also working.

You may have noticed that the mouse cursor is missing. The C++ version of this tutorial gives an introduction to CEGUI which is a GUI addon to Ogre. Unfortunatly CEGUI is not working properly with MOgre 1.4.6 SDK (Reference: Mogre CEGUI). The mouse cursor isn't really an important piece to this tutorial, so we will just ignore the lack of a mouse cursor for now.

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 BaseFrameListener already handles moving the camera, we going to add a bit of code to that section.

Go to the FrameStarted Function and find the Camera movement section. 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 NEGATIVE_UNIT_Y, since we are pointing the ray straight down. Once we have created the ray, we tell the RaySceneQuery object to use it.

        '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 

Note that we have used a height of 5000.0f 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 result of the query is basically (oversimplification here) a collection of worldFragments (in this case the Terrain) and a list 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. For now, we'll just do some hand waving and move through it. We make sure we have at least two values and pick the 1st one (our terrain).

            'There should be at least 2 results, 1st one is the terrain.
            If results.Count >= 2 Then 

The worldFragment struct contains the location where the Ray hit the terrain in the singleIntersection variable (which is a Vector3). We are going to get the height of the terrain by assigning the y value of this vector to a local variable. Once we have the height, we are going to see if the camera is below the height, and if so we are going to move the camera up to that height. Note that the camera NearClipDistance is 5, so 10 just makes sure we don't clip the terrain.

                Dim result As RaySceneQueryResultEntry = results.Item(0)
                Dim terrainHeight As Single = result.worldFragment.singleIntersection.y
                If terrainHeight + 10 > camPos.y Then
                    myCamera.SetPosition(camPos.x, terrainHeight + 10, camPos.z)
                End If
            End If
            myRaySceneQuery.Dispose()
        End If

At this point you should compile and test your program. Try to fly thru the terrain. Fly to the edge and try going under the terrain.

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 InputClass.MousePressedListener function to do something different when you click the left mouse button. Replace the MousePressedListener code with the following:

            If e.state.ButtonDown(MOIS.MouseButtonID.MB_Right) Then
                myRotating = True
            End If

            If e.state.ButtonDown(MOIS.MouseButtonID.MB_Left) Then
                myLeftMouseDown = True

                'Find location we are aiming at
                Dim mouseRay As Ray = myCamera.GetCameraToViewportRay(0.5, 0.5)
                Dim myRaySceneQuery As RaySceneQuery = myScene.CreateRayQuery(mouseRay)

                Dim results As RaySceneQueryResult = myRaySceneQuery.Execute


                'There should be at least 2 results, 1st one is terrain
                If results.Count >= 2 Then
                    Static intRobot As Integer = 0
                    intRobot += 1
                    Dim RobotName As String = "Robot" & intRobot
                    Dim result As RaySceneQueryResultEntry = results.Item(0)
                    Dim myEntity As Entity = myScene.CreateEntity(RobotName, "robot.mesh")
                    myEntity.GetAnimationState("Idle").Loop = True
                    myEntity.GetAnimationState("Idle").Enabled = True
                    myCurrentObject = myScene.RootSceneNode.CreateChildSceneNode(RobotName & "Node", result.worldFragment.singleIntersection)
                    myCurrentObject.AttachObject(myEntity)
                    myCurrentObject.SetScale(0.1, 0.1, 0.1)
                Else
                    Stop
                End If
                myRaySceneQuery.Dispose()

            End If

The first piece of code will look very familiar. We will be creating a Ray to use with the myRaySceneQuery object, and setting the Ray. Ogre provides us with getCameraToViewportRay; 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. In our case we don't have a mouse cursor we we just pick the center of the screen for a ray trace; the GetCameraToViewportRay(0.5, 0.5) is the center of the screen. If we had a mouse cursor then it would be GetCameraToViewportRay(Mouse.X/Screen.Width, Mouse.Y/Screen.Height)

Remember that each entity needs a unique name, so we make one. About the only thing of note here is the public variable MyCurrentObject; which will be used later along with myLeftMouseDown.

Now compile and 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.

This next chunk of code should be self explanatory now. We create a Ray based on center of the screen, we then execute a RaySceneQuery and move the object to the new position. Note that we don't have to check mCurrentObject to see if it is valid or not, because myLMouseDown would not be true if mCurrentObject had not been set by mousePressed.

        'Now update the robot location if left mouse button is still down
        If myLeftMouseDown Then
            'Find location we are aiming at
            Dim mouseRay As Ray = myCamera.GetCameraToViewportRay(0.5, 0.5)
            Dim myRaySceneQuery As RaySceneQuery = myScene.CreateRayQuery(mouseRay)

            Dim results As RaySceneQueryResult = myRaySceneQuery.Execute

            'There should be only 2 result, the terrain.sss
            'Other results might be less, for example fell off terrain
            If results.Count >= 2 Then
                myCurrentObject.Position = results.Item(0).worldFragment.singleIntersection
            End If
            myRaySceneQuery.Dispose()
        End If

Compile and run the program. We are now finished! Your result should look something like this, after some strategic clicking: Image:MOgre_VB_Intermediate_Tutorial2a.jpg

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

  1. 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.
  2. 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

  1. 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 ExampleFrameListener, after the function is called test it against Vector3::ZERO.)

Advanced Exercises

  1. 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

  1. 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.
  2. Add code so that every time you click on a point on the scene, the robot moves to that location.
Proceed to MOgre VB.NET Intermediate Tutorial 3 Mouse Picking (3D Object Selection) and SceneQuery Masks
Personal tools
administration