Intermediate Tutorial 2: RaySceneQueries and Basic Mouse Usage

Original version by Clay Culver

Initial .NET C# port by DigitalCyborg

Mogre port by lionceaudor

Note: This was written for Mogre 1.7.1 and is known to compile against it. Any problems you encounter while working with this tutorial should be posted to the OgreDotNet Forum.

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. Using the mouse to select x and y coordinates on the terrain

Prerequisites

This tutorial will assume that you already know how to set up an Mogre project and make it compile successfully. Knowledge of basic Mogre objects (SceneNodes, Entities, etc) is assumed.

Familiarity with STL iterators also helps since the cpp version of the tutorial uses them and we'll introduce how IEnumerator works in a similar fashion. Clay says "(Ogre also uses a lot of STL, if you are not familiar with it, you should take the time to learn it.)" which to us, means take the to learn the how the functionality IEnumator defines is intended to be used.

This tutorial use the Mogre.Demo.ExampleApplication.Example class which is in the samples of Mogre SDK.

Getting Started

First, you need to create a new project for the demo. Add a file called "MouseQuery.cs" to the project, and add this to it:

using System;
using System.Drawing;
using System.Text;
using Mogre.Demo.ExampleApplication;
using Mogre;

namespace MogreTutorial
{
  class IntermediateTutorial2 : Example
  {
    protected RaySceneQuery mRaySceneQuery = null;      // The ray scene query pointer
    protected int mCount = 0;                           // The number of robots on the screen
    protected SceneNode mCurrentObject = null;          // The newly created object

    public override void CreateInput()
    {
      base.CreateInput();
      inputMouse.MousePressed += new MOIS.MouseListener.MousePressedHandler(MousePressed);
      inputMouse.MouseMoved += new MOIS.MouseListener.MouseMovedHandler(MouseMotion);
    }

    protected bool MouseMotion(MOIS.MouseEvent arg)
    {
      return true;
    }

    protected bool MousePressed(MOIS.MouseEvent arg, MOIS.MouseButtonID id)
    {
      return true;
    }

    public override void CreateFrameListener()
    {
      base.CreateFrameListener();
      Root.Singleton.FrameStarted += FrameStarted;
    }

    bool FrameStarted(FrameEvent evt)
    {
      return true;
    }

    public override void CreateScene()
    {
    }

    public IntermediateTutorial2()
      : base()
    {
    }

    public override void DestroyScene()
    {
      base.DestroyScene();
    }
  }
}

namespace Mogre.Demo.CameraTrack
{
  using MogreTutorial;

  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        IntermediateTutorial2 app = new IntermediateTutorial2();
        app.Go();
      }
      catch (System.Runtime.InteropServices.SEHException)
      {
        // Check if it's an Ogre Exception
        if (OgreException.IsThrown)
          ExampleApplication.Example.ShowOgreException();
        else
          throw;
      }
    }
  }

}

Be sure this code compiles before continuing.

Setting up the Scene

Go to the CreateScene method. The following code should all be familiar. If you do not know what something does, please consult the Ogre API reference before continuing. Add this to CreateScene

// Set ambient light 
      sceneMgr.AmbientLight=new ColourValue(0.5f, 0.5f, 0.5f);

      // World geometry & Sky
      sceneMgr.SetSkyDome(true, "Examples/CloudySky", 5, 8);
      sceneMgr.SetWorldGeometry("terrain.cfg");

      // Set camera look point           
      camera.Position=new Vector3(40, 100, 280);
      camera.Pitch(new Radian(new Degree(-30)));
      camera.Yaw(new Radian(new Degree(-45)));

Also, since we are using terrian, we need to setup a SceneManager which can use it. Add this method:

public override void ChooseSceneManager()
    {
      // Get the SceneManager, in this case a specific for the terrain
      sceneMgr = root.CreateSceneManager(SceneType.ST_EXTERIOR_CLOSE, "SceneMgr");
    }

I think we already covered why we need it in an earlier tutorial.

Now that we have the basic world geometry set up, we need to turn on the cursor. So we need to attach some GUI system (but no one work with MOGRE 1.7x at this time) or draw cursor manually or use OS capabilities. The last is simplest and we implement this with MOIS. Just change your CreateInput function for this one:

MOIS.ParamList pl = new MOIS.ParamList();
      IntPtr windowHnd;
      window.GetCustomAttribute("WINDOW", out windowHnd);
      pl.Insert("WINDOW", windowHnd.ToString());
      //Non-exclusive input, show OS cursor
      //If you want to see the mouse cursor and be able to move it outside your OGRE window and use the keyboard outside the running application 
      pl.Insert("w32_mouse", "DISCL_FOREGROUND");
      pl.Insert("w32_mouse", "DISCL_NONEXCLUSIVE");
      pl.Insert("w32_keyboard", "DISCL_FOREGROUND");
      pl.Insert("w32_keyboard", "DISCL_NONEXCLUSIVE");
      inputManager = MOIS.InputManager.CreateInputSystem(pl);

      // Create all devices (except joystick, as most people have Keyboard/Mouse) using buffered input.
      inputKeyboard = (MOIS.Keyboard)inputManager.CreateInputObject(MOIS.Type.OISKeyboard, true);
      inputMouse = (MOIS.Mouse)inputManager.CreateInputObject(MOIS.Type.OISMouse, true);

      MOIS.MouseState_NativePtr mouseState = inputMouse.MouseState;
      mouseState.width = viewport.ActualWidth; //! update after resize window
      mouseState.height = viewport.ActualHeight;

      //base.CreateInput();
      inputMouse.MousePressed += new MOIS.MouseListener.MousePressedHandler(MousePressed);
      inputMouse.MouseReleased += new MOIS.MouseListener.MouseReleasedHandler(MouseReleased);
      inputMouse.MouseMoved += new MOIS.MouseListener.MouseMovedHandler(MouseMotion);

If you compile and run the code, you will see your cursor moving with the camera. The moves of the camera come from our super class. I admit that it's an awful control for a game or other programs, but for this tutorial we will simply use this for some basic moves and keep our code focuses on the new functionalities.

Introducing the EventHandler

That was all that needed to be done for the application. The EventHandler is the complicated portion of the code, so I will spend some time outlining what we are trying to accomplish with the application so you have an idea before we start implementing it.

  • First, we want to make it so that the camera does not pass through the Terrain. This will make it closer to how we would expect program like this to work.
  • Second, we want to add entities to the scene anywhere on the terrain we left click.
  • Finally, we want to be able to "drag" entities around. That is by left clicking and holding the button down we want to see the entity, and move him to where we want to place him. Letting go of the button will actually lock him into place.


To do this we are going to use several protected variables (these are already added to the class):

protected RaySceneQuery mRaySceneQuery = null;      // The ray scene query pointer
        protected int mCount = 0;                           // The number of robots on the screen
        protected SceneNode mCurrentObject = null;          // The newly created object

The mRaySceneQuery variable holds a copy of the RaySceneQuery we will be using to find the coordinates on the terrain. And the mCount variable counts the number of entities we have on screen. mCurrentObject holds a pointer to the most recently created SceneNode that we have created (we will be using this to "drag" the entity around).

Setting up the EventHandler

Remember that, in order for the EventHandler to recieve events, it be must be subscribed to the events. We already did this in our CreateInput method and we already covered it in an earlier tutorial I won't go into it again here. I think the CreateInput function is an ideal place the create the RaySceneQuery... so add this line to the bottom of CreateInput:

// Create RaySceneQuery
            mRaySceneQuery = sceneMgr.CreateRayQuery(new Ray());

This is all we do to create the SceneQuery, but if we create a RaySceneQuery, we must later dispose of it since it implements IDisposable. Go to the Applications DestroyScene() method and add the following line above what is already there.

mRaySceneQuery.Dispose();

Be sure you can compile your code before moving on to the next section.

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 base class ExampleApplication already handles moving the camera, we are not going to touch that code. Instead, after the EventHandler moves the camera we are going to make sure the camera is 10 units above the terrain. If it is not, we are going to move it there. Please follow this code closely. We will use the RaySceneQuery to do several other things by the time this tutorial is finished, and I will not go into as much detail after this time.

Now the camera has been moved. Our trick 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 NegativeUnitY, since we are pointing the ray straight down. Once we have created the ray, we tell the RaySceneQuery object to use it. Add the following code to your FrameStarted method:

// Setup the scene query
      Vector3 camPos = camera.Position;
      Ray cameraRay = new Ray(new Vector3(camPos.x, 5000.0f, camPos.z),
          Vector3.NEGATIVE_UNIT_Y);
      mRaySceneQuery.Ray=cameraRay;

Next we need to execute the query and get the results. The results of the query come in the form of an RaySceneQueryResultEnumerator, which I will breifly describe. First thing to know is that it implements IEnumerator which means there are some functions that are guaranteed (by the compiler) to be implemented. Let's add the code to perform the query, then we'll talk about it a little more.

// Perform the scene query;
      RaySceneQueryResult result = mRaySceneQuery.Execute();
      RaySceneQueryResult.Enumerator itr = (RaySceneQueryResult.Enumerator)(result.GetEnumerator());

The result of the query is basically (oversimplification here) a list of worldFragments (in this case the Terrain) and a list of movables (we will cover movables in a later tutorial). If you are not familiar with IEnumerator, to get the enumerator we have to call GetEnumerator (one of the methods that all classes that implement IEnumerator must have). We also have to call MoveNext to actually go to the first element. MoveNext() returns false, then there were no results returned. 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. The following line of code ensures that the query returned at least one result ( itr != null && itr.MoveNext ), and that the result is the terrain (itr.Current.getWWorldFragment).

// Get the results, set the camera height
            if((itr != null) && itr.MoveNext()){

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 we actually move the camera up by 10 units. This ensures that we can't see through the Terrain by being too close to it.

float terrainHeight = itr.Current.worldFragment.singleIntersection.y;

        if ((terrainHeight + 10.0f) > camPos.y)
          camera.SetPosition(camPos.x, terrainHeight + 10.0f, camPos.z);
      }
      return true;

Lastly, we return true to continue rendering. At this point you should compile and test your program.

Terrain Selection

In this section we will be creating and adding objects to the screen every time you click the left mouse button. Every time you click and hold the left mouse button, an object will be created and "held" on your cursor. You can move the object around until you let go of the button, at which point it will lock into place. To do this we are going to need to change the MousePressed function to do something different when you click the left mouse button. Add the following code to the MousePressed function:

// Setup the ray scene query
        float screenX = arg.state.X.abs / (float)arg.state.width;
        float screenY = arg.state.Y.abs / (float)arg.state.height;
        Ray mouseRay = camera.GetCameraToViewportRay(screenX, screenY);
        mRaySceneQuery.Ray=mouseRay;
 
        // Execute query
        RaySceneQueryResult result = mRaySceneQuery.Execute();
        RaySceneQueryResult.Enumerator itr = (RaySceneQueryResult.Enumerator)(result.GetEnumerator());
 
        // Get results, create a node/entity on the position
        if ( itr != null && itr.MoveNext() )
        {

The first piece of code will look very familiar. We will be creating a Ray to use with the mRaySceneQuery object, and setting the Ray. Mogre provides us with Camera.GetCameraToViewpointRay; a nice function that translates a click on the screen (x and y coordinates) into a Ray that can be used with a RaySceneQuery object.

Now that we have the worldFragment (and therefore the position that was clicked on), we are going to create the object and place it on that position. Our first difficulty is that each Entity and SceneNode in ogre needs a unique name. To accomplish this we are going to name each Entity "Robot1", "Robot2", "Robot3"... and each SceneNode "RobotNode1", "RobotNode2", "RobotNode3"... and so on. To do this we are going to make use of the way strings are handled in C# (namely that the + operation concatenates 2 strings) and the ToString method. We'll be forming the strings Robot1 and RobotNode1 like this:

"Robot" + mCount.ToString()
              "RobotNode" + mCount.ToString()

Next we create the Entity and SceneNode. Note that we use itr.Current.GetWorldFragment().GetSingleIntersection() for our default position of the Robot. We also scale him down to 1/10th size because of how small the terrain is. Be sure to take note that we are assigning this newly created object to the member variable mCurrentObject. We will be using that in the next section.

Entity ent = sceneMgr.CreateEntity(
                            "Robot" + mCount.ToString(), "robot.mesh");

          mCurrentObject = sceneMgr.RootSceneNode.CreateChildSceneNode(
              "RobotNode" + mCount.ToString(),
              itr.Current.worldFragment.singleIntersection);

          mCount++;
          mCurrentObject.AttachObject(ent);
          mCurrentObject.SetScale(0.1f, 0.1f, 0.1f);
        }

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. Go to the MouseMotion function. We will be adding code inside this if statement:

// If we are dragging the left mouse button.
      if (arg.state.ButtonDown(MOIS.MouseButtonID.MB_Left))
      {
      } // if

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

// If we are dragging the left mouse button.
      if (arg.state.ButtonDown(MOIS.MouseButtonID.MB_Left))
      {
        float screenX = arg.state.X.abs / (float)arg.state.width;
        float screenY = arg.state.Y.abs / (float)arg.state.height;
        Ray mouseRay = camera.GetCameraToViewportRay(screenX, screenY);
        mRaySceneQuery.Ray = mouseRay;

        RaySceneQueryResult result = mRaySceneQuery.Execute();
        RaySceneQueryResult.Enumerator itr = (RaySceneQueryResult.Enumerator)(result.GetEnumerator());
 
        if ((itr != null) && itr.MoveNext())
        {
            mCurrentObject.Position=itr.Current.worldFragment.singleIntersection;
        }
      }

Compile and run the program. We are now finished! Your result should look something like this, after some strategic clicking: http://www.idleengineer.net/images/tutorial_02.png

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.

Credits

Thanks Clay Culver for providing the original tutorial and DigitalCyborg for the initial .NET C# port.