Intermediate Tutorial 2         RaySceneQueries and Basic Mouse Usage (Part 1 of 2)
Print

Any problems you encounter during working with this tutorial should be posted in the Help Forum(external link).

 

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

 
Here is the code for Intermediate Tutorial 2. 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. You should also be familiar with basic STL iterators, as this tutorial uses them. (Ogre also uses a lot of STL, if you are not familiar with it, you should take the time to learn it.)

This Tutorial makes use of CEGUI, you should have completed the steps in the Basic Tutorial 7 CEGUI and Ogre To ensure that this tutorial will work as expected.

Info NOTE: If you want to try the tutorial without using CEGUI you can find the source code using the built in SdkTrays here.

Getting Started

 
First, you need to create a new project and add the following code:

ITutorial02 header
#ifndef __ITutorial02_h_
#define __ITutorial02_h_
 
#include "BaseApplication.h"
 
class ITutorial02 : public BaseApplication
{
public:
    ITutorial02(void);
    virtual ~ITutorial02(void);
 
protected:
    virtual void createScene(void);
    virtual void chooseSceneManager(void);
    virtual void createFrameListener(void);
    //frame listener    
    virtual bool frameRenderingQueued(const Ogre::FrameEvent &evt);
    //mouse listener
    virtual bool mouseMoved(const OIS::MouseEvent &arg);
    virtual bool mousePressed(const OIS::MouseEvent &arg,OIS::MouseButtonID id);
    virtual bool mouseReleased(const OIS::MouseEvent &arg,OIS::MouseButtonID id);
 
 
protected:
    Ogre::RaySceneQuery *mRaySceneQuery;// The ray scene query pointer
    bool mLMouseDown, mRMouseDown;		// True if the mouse buttons are down
    int mCount;							// The number of robots on the screen
    Ogre::SceneNode *mCurrentObject;	// The newly created object
    CEGUI::Renderer *mGUIRenderer;		// CEGUI renderer
    float mRotateSpeed;		
 
 
};
 
#endif // #ifndef __ITutorial02_h_
ITutorial02 implementation
#include <CEGUISystem.h>
#include <CEGUISchemeManager.h>
#include <RendererModules/Ogre/CEGUIOgreRenderer.h>
 
#include "ITutorial02.h"
 
//-------------------------------------------------------------------------------------
ITutorial02::ITutorial02(void)
{
}
//-------------------------------------------------------------------------------------
ITutorial02::~ITutorial02(void)
{
}
 
//-------------------------------------------------------------------------------------
void ITutorial02::createScene(void)
{}
void ITutorial02::createFrameListener(void)
{
	BaseApplication::createFrameListener();
}
void ITutorial02::chooseSceneManager(void)
{
	// Use the terrain scene manager.
	mSceneMgr = mRoot->createSceneManager(Ogre::ST_EXTERIOR_CLOSE);
}
 
bool ITutorial02::frameRenderingQueued(const Ogre::FrameEvent &evt)
{return BaseApplication::frameRenderingQueued(evt);}
bool ITutorial02::mouseMoved(const OIS::MouseEvent &arg)
{return true;}
bool ITutorial02::mousePressed(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
{return true;}
bool ITutorial02::mouseReleased(const OIS::MouseEvent &arg, OIS::MouseButtonID id)
{return true;}
 
 
 
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
#endif
 
#ifdef __cplusplus
extern "C" {
#endif
 
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
    INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
    int main(int argc, char *argv[])
#endif
    {
        // Create application object
        ITutorial02 app;
 
        try {
            app.go();
        } catch( Ogre::Exception& e ) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
            MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
            std::cerr << "An exception has occured: " <<
                e.getFullDescription().c_str() << std::endl;
#endif
        }
 
        return 0;
    }
 
#ifdef __cplusplus
}
#endif

 

Be sure this code compiles before continuing. You will most likely need to configure your project to recognise CEGUI since it is no longer packaged with Ogre as of Ogre 1.7, so remember to add to your project properties (in Visual Studio):

  • In C/C++, add the path to the CEGUI include files to Additional Include Directories,
  • In the Linker options, add the path to CEGUI lib files to Additional Library Directories and add "CEGUIOgreRenderer_d.lib" and "CEGUIBase_d.lib" to the Additional Dependencies (remove the _d for the Release configuration)
  • Add the path to the CEGUI bin directory to the Environment Variables or copy the CEGUI DLLs somewhere your project can find them.

 
Similarly, add these to your project (in Eclipse):

Right click project -> Properties -> C/C++ Build -> Settings
-> GCC C++ Compiler -> Includes

and add the full path : /usr/local/include/CEGUI (on Linux)
-> GCC C++ Linker -> Linker -> Libraries

and this to Libraries (-l): CEGUIOgreRenderer

Setting up the Scene

 
Go to the ITutorial02::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
        mSceneMgr->setAmbientLight(Ogre::ColourValue(0.5, 0.5, 0.5));
        mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8);
 
        // World geometry
        mSceneMgr->setWorldGeometry("terrain.cfg");
 
        // Set camera look point
        mCamera->setPosition(40, 100, 580);
        mCamera->pitch(Ogre::Degree(-30));
        mCamera->yaw(Ogre::Degree(-45));

 
Now that we have the basic world geometry set up, we need to turn on the cursor. We do this using some CEGUI function calls. Before we can do that, however, we need to start up CEGUI. This is now very easy- the bootstrapSystem() method will do all the required setup for us. Note that this also makes CEGUI use Ogre's resource management system.

// CEGUI setup
        mGUIRenderer = &CEGUI::OgreRenderer::bootstrapSystem();

 
Now we need to actually show the cursor. Again, I'm not going to explain most of this code. We will revisit it in a later tutorial.

// Mouse
        CEGUI::SchemeManager::getSingleton().create((CEGUI::utf8*)"TaharezLook.scheme");
        CEGUI::MouseCursor::getSingleton().setImage("TaharezLook", "MouseArrow");

 

If you compile and run the code, you will see a cursor at the center of the screen, but it will not move (yet).

It is likely that you will also need to tell the Ogre resource manager about the CEGUI resources. If you get exceptions at runtime, try adding the following to resources.cfg:

[CEGUI]
 FileSystem=/usr/share/CEGUI/schemes
 FileSystem=/usr/share/CEGUI/fonts
 FileSystem=/usr/share/CEGUI/imagesets
 FileSystem=/usr/share/CEGUI/layouts
 FileSystem=/usr/share/CEGUI/looknfeel
 FileSystem=/usr/share/CEGUI/lua_scripts
 FileSystem=/usr/share/CEGUI/schemes
 FileSystem=/usr/share/CEGUI/xml_schemas

 

Introducing the FrameListener

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

  • First, we want to bind the right mouse button to a "mouse look" mode. It's fairly annoying not being able to use the mouse to look around, so our first priority will be adding mouse control back to the program (though only when we hold the right mouse button down). NOTE: the tutorial framework already handles camera control thanks to the sdkCameraMan class from OgreBites but for the sake of learning we will be implementing camera control from scratch.
  • Second, we want to make it so that the camera does not pass through the Terrain. This will make it closer to how we would expect a program like this to work.
  • Third, we want to add entities to the scene anywhere on the terrain we left click.
  • Finally, we want to be able to "drag" entities around; that is, by left clicking and holding the button down we want to see the entity, and move it to where we want to place it. Letting go of the button will actually lock it in place.

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

Ogre::RaySceneQuery *mRaySceneQuery;     // The ray scene query pointer
     bool mLMouseDown, mRMouseDown;     // True if the mouse buttons are down
     int mCount;                        // The number of robots on the screen
     Ogre::SceneNode *mCurrentObject;         // The newly created object
     CEGUI::Renderer *mGUIRenderer;     // cegui renderer
     float mRotateSpeed;

 
The mRaySceneQuery variable holds a copy of the RaySceneQuery we will be using to find the coordinates on the terrain. The mLMouseDown and mRMouseDown variables will track whether we have the mouse held down (IE mLMouseDown is true when the user holds down the left mouse button, false otherwise). mCount counts the number of entities we have on screen. mCurrentObject holds a pointer to the most recently created SceneNode (we will be using this to "drag" the entity around). Finally, mGUIRenderer holds a pointer to the CEGUI Renderer, which we will be using to update CEGUI.

Also note that there are many functions related to Mouse listeners that we are overriding to provide camera control

Setting up the FrameListener

 
Go to the createFrameListener method, and add the following initialization code after the call to BaseApplication::createFrameListener(). Note that we are also reducing rotation speed of the camera since the Terrain is fairly small.

// Setup default variables
         mCount = 0;
         mCurrentObject = NULL;
         mLMouseDown = false;
         mRMouseDown = false;
 
         // Reduce rotate speed
         mRotateSpeed =.1;

 
Finally, we need to create the RaySceneQuery object. This is done with a call to the SceneManager:

// Create RaySceneQuery
         mRaySceneQuery = mSceneMgr->createRayQuery(Ogre::Ray());

 
This is all we need for createFrameListener(), but if we create a RaySceneQuery, we must later destroy it. Go to the ITutorial02 destructor (~ITutorial02) and add the following line:

// We created the query, and we are also responsible for deleting it.
         mSceneMgr->destroyQuery(mRaySceneQuery);

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

Adding Mouse Look

 
We are going to bind the mouse look mode to the right mouse button. To do this, we are going to:

  • update CEGUI when the mouse is moved (so that the cursor is also moved)
  • set mRMouseButton to true when the right mouse button is pressed
  • set mRMouseButton to false when it is released
  • change the view when the mouse is "dragged" (that is, when a button is held down as the mouse moves)
  • hide the mouse cursor when the mouse is dragging

 
Find the ITutorial02::mouseMoved method. We will be adding code to move the mouse cursor every time the mouse has been moved. Add this code to the function:

// Update CEGUI with the mouse motion
        CEGUI::System::getSingleton().injectMouseMove(arg.state.X.rel, arg.state.Y.rel);

 
Now find the ITutorial02::mousePressed method. This chunk of code hides the cursor when the right mouse button goes down, and sets the mRMouseDown variable to true.

// Left mouse button down
        if (id == OIS::MB_Left)
        {
            mLMouseDown = true;
        } // if
 
        // Right mouse button down
        else if (id == OIS::MB_Right)
        {
            CEGUI::MouseCursor::getSingleton().hide();
            mRMouseDown = true;
        } // else if

 
Next we need to show the mouse cursor again and toggle mRMouseDown when the right button is let up. Find the mouseReleased function, and add this code:

// Left mouse button up
        if (id == OIS::MB_Left)
        {
            mLMouseDown = false;
        } // if
 
        // Right mouse button up
        else if (id == OIS::MB_Right)
        {
            CEGUI::MouseCursor::getSingleton().show();
            mRMouseDown = false;
        } // else if

 
Now we have all of the prerequisite code written, we want to change the view when the mouse is moved while holding the right button down. What we are going to do is read the distance it has moved since the last time the method was called. This is done in the same way that we rotated the camera in Basic Tutorial 5. Find the ITutorial::mouseMoved function and add the following code just before the return statement:

// If we are dragging the left mouse button.
        if (mLMouseDown)
        {
        } // if
 
        // If we are dragging the right mouse button.
        else if (mRMouseDown)
        {
            mCamera->yaw(Ogre::Degree(-arg.state.X.rel * mRotateSpeed));
            mCamera->pitch(Ogre::Degree(-arg.state.Y.rel * mRotateSpeed));
        } // else if

 
Now if you compile and run this code you will be able to control where the camera looks by holding the right mouse button down.

Terrain Collision Detection

 
We are now going to make it so that when we move towards the terrain, we cannot pass through it. Since BaseApplication::createFrameListener() already handles the camera movement, we are not going to touch that code. Instead, after BaseApplication::createFrameListener() moves the camera we are going to make sure the camera is 10 units above the terrain. If it is not, we are going to move it there. Please follow this code closely. We will use the RaySceneQuery to do several other things by the time this tutorial is finished, and I will not go into as much detail after this section.

Go to the ITutorial02::frameRenderingQueued() method and remove its contents. The first thing we are going to do is call the BaseApplication::frameRenderingQueued method to do all of its normal functions. If it returns false, we will return false as well.

// Process the base frame listener code.  Since we are going to be
         // manipulating the translate vector, we need this to happen first.
         if (!BaseApplication::frameRenderingQueued(evt))
             return false;

 
We do this at the top of our frameRenderingQueued function because the BaseApplication::frameRenderingQueued member function handles the updating of the TrayManager window from OgreBites (the FPS window and Ogre logo) and we need to perform the rest of our actions in this function after this happens. Our goal is to find the camera's current position, and fire a Ray straight down 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.

// Setup the scene query
        Ogre::Vector3 camPos = mCamera->getPosition();
        Ogre::Ray cameraRay(Ogre::Vector3(camPos.x, 5000.0f, camPos.z), Ogre::Vector3::NEGATIVE_UNIT_Y);
        mRaySceneQuery->setRay(cameraRay);

 
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 were under it. Now we need to execute the query and get the results. The results of the query come in the form of an std::iterator, which I will briefly describe.

// Perform the scene query
         Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
         Ogre::RaySceneQueryResult::iterator itr = result.begin();

 
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 STL iterators, just know that to get the first element of the iterator, call the begin method. If the result.begin() == result.end(), then there were no results to return. In the next tutorial 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 != result.end() ), and that the result is the terrain (itr->worldFragment).

// Get the results, set the camera height
         if (itr != result.end() && itr->worldFragment)
         {

 
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.

Ogre::Real terrainHeight = itr->worldFragment->singleIntersection.y;
             if ((terrainHeight + 10.0f) > camPos.y)
                 mCamera->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. Find the following code in the ITutorial02::mousePressed function. We will be adding code inside this if statement.

// Left mouse button down
        if (id == OIS::MB_Left)
        {
            mLMouseDown = true;
        } // if

 
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. Ogre provides us with Camera::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.

// Left mouse button down
            if (id == OIS::MB_Left)
            {
                // Setup the ray scene query, use CEGUI's mouse position
                CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
                Ogre::Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width), mousePos.d_y/float(arg.state.height));
                mRaySceneQuery->setRay(mouseRay);

 
Next we will execute the query and make sure it returned a result.

// Execute query
                Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
                Ogre::RaySceneQueryResult::iterator itr = result.begin( );
 
                // Get results, create a node/entity on the position
                if (itr != result.end() && itr->worldFragment)
                {

 
Now that we have the worldFragment (and therefore the position that was clicked on), we are going to create the object and place it on that position. Our first difficulty is that each Entity and SceneNode in ogre needs a unique name. To accomplish this we are going to name each Entity "Robot1", "Robot2", "Robot3"... and each SceneNode "Robot1Node", "Robot2Node", "Robot3Node"... and so on. First we create the name (consult a reference on C for more information on sprintf).

char name[16];
                sprintf( name, "Robot%d", mCount++ );

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

Ogre::Entity *ent = mSceneMgr->createEntity(name, "robot.mesh");
                    mCurrentObject = mSceneMgr->getRootSceneNode()->createChildSceneNode(std::string(name) + "Node", itr->worldFragment->singleIntersection);
                    mCurrentObject->attachObject(ent);
                    mCurrentObject->setScale(0.1f, 0.1f, 0.1f);
                } // if
 
                mLMouseDown = true;
            } // if

 
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. We will be adding code inside this if statement:

// If we are dragging the left mouse button.
 	if (mLMouseDown)
 	{
 	} // if

 
This next chunk of code should now be self explanatory. We create a Ray based on the mouse's current location, 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's the latest object or not, because mLMouseDown and mCurrentObject are both set in the mousePressed() function: mLMouseDown is set to true, and mCurrentObject is set to the most recently created SceneNode.

if (mLMouseDown)
        {
            CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
            Ogre::Ray mouseRay = mCamera->getCameraToViewportRay(mousePos.d_x/float(arg.state.width),mousePos.d_y/float(arg.state.height));
            mRaySceneQuery->setRay(mouseRay);
 
            Ogre::RaySceneQueryResult &result = mRaySceneQuery->execute();
            Ogre::RaySceneQueryResult::iterator itr = result.begin();
 
            if (itr != result.end() && itr->worldFragment)
                mCurrentObject->setPosition(itr->worldFragment->singleIntersection);
        } // if

 
Compile and run the program. We are now finished!

Info Note: You (= the Ray's origin) must be over the Terrain for the RaySceneQuery to report the intersection when using the TerrainSceneManager.

Info Note: If you are using your own framework, make sure your scene query has access to the frame listener, e.g. your frameStarted() method. Otherwise, if you use it in an init() function you may get no results.

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 Intermediate Tutorial 3 Mouse Picking (3D Object Selection) and SceneQuery Masks


Alias: Intermediate_Tutorial_2


Contributors to this page: Flateno13 points  , Spacegaier3733 points  , qcha0s53 points  , makism130 points  , Latin11 points  , jubei254 points  , jacmoe111451 points  , Dan True81 points  and atomr181 points  .
Page last modified on Friday 23 of September, 2011 14:46:32 GMT by Flateno13 points .


The content on this page is licensed under the terms of the Creative Commons Attribution-ShareAlike License.
As an exception, any source code contributed within the content is released into the Public Domain.