Skip to main content

History: Intermediate Tutorial 2

Preview of version: 21

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

WARNING!
As of OGRE 1.8, this tutorial is not compatible, due to the removal of the Terrain Scene Manager!


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.


  • Changes needed for Ogre 1.8 and above:

- The Terrain Scene Manager is obsolete and has been replaced by the terrain component system. Please go through Basic Tutorial 3 to learn how to use it.
- For now, use the terrain generated in Basic Tutorial 3 for this tutorial. There will be glitches (such as the camera being able to go through parts of the terrain if you follow this tutorial's code).
- This tutorial will ask you to use RaySceneQueries to find objects of type worldFragment. General-purpose queries are no longer used for this. Instead, call the rayIntersects() function in your TerrainGroup object. It takes an Ogre::Ray as an argument (and a distance limit as an optional second argument, of type Real) and returns an Ogre::TerrainGroup::RayResult object (ex. Ogre::TerrainGroup::RayResult result = terrain_group->rayIntersects(mouse_ray)). RayResult has three public members: bool hit, true if the given ray hit the TerrainGroup object, Ogre::Vector3 position, the x, y and z coordinates of the point of intersection and Ogre::Terrain terrain, the terrain object that was hit.

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
Copy to clipboard
#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
Copy to clipboard
#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:

Copy to clipboard
// 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.

Copy to clipboard
// 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.

Copy to clipboard
// 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:

Copy to clipboard
[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):

Copy to clipboard
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.

Copy to clipboard
// 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:

Copy to clipboard
// 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:

Copy to clipboard
// 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:

Copy to clipboard
// 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.

Copy to clipboard
// 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:

Copy to clipboard
// 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:

Copy to clipboard
// 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.

Copy to clipboard
// 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.

Copy to clipboard
// 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.

Copy to clipboard
// 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).

Copy to clipboard
// 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.

Copy to clipboard
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.

Copy to clipboard
// 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.

Copy to clipboard
// 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.

Copy to clipboard
// 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).

Copy to clipboard
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.

Copy to clipboard
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:

Copy to clipboard
// 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.

Copy to clipboard
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

History

Information Version
Fri 16 of Oct, 2020 19:13 GMT-0000 sercero 177
Mon 30 of Mar, 2015 09:13 GMT-0000 kabbotta 176
Mon 30 of Mar, 2015 07:47 GMT-0000 kabbotta 175
Mon 30 of Mar, 2015 05:14 GMT-0000 kabbotta 174
Mon 30 of Mar, 2015 05:14 GMT-0000 kabbotta 173
Mon 30 of Mar, 2015 05:12 GMT-0000 kabbotta 172
Mon 30 of Mar, 2015 05:00 GMT-0000 kabbotta 171
Mon 30 of Mar, 2015 04:59 GMT-0000 kabbotta 170
Mon 30 of Mar, 2015 04:51 GMT-0000 kabbotta 169
Mon 30 of Mar, 2015 04:48 GMT-0000 kabbotta 168
Mon 30 of Mar, 2015 04:44 GMT-0000 kabbotta 167
Mon 30 of Mar, 2015 04:42 GMT-0000 kabbotta 166
Mon 30 of Mar, 2015 04:40 GMT-0000 kabbotta 165
Mon 30 of Mar, 2015 04:38 GMT-0000 kabbotta 164
Mon 30 of Mar, 2015 04:36 GMT-0000 kabbotta 163
Mon 30 of Mar, 2015 04:32 GMT-0000 kabbotta 162
Mon 30 of Mar, 2015 01:10 GMT-0000 kabbotta 161
Sun 29 of Mar, 2015 22:33 GMT-0000 kabbotta 160
Sun 29 of Mar, 2015 22:33 GMT-0000 kabbotta 159
Sun 29 of Mar, 2015 22:32 GMT-0000 kabbotta 158
Sun 29 of Mar, 2015 22:32 GMT-0000 kabbotta 157
Sun 29 of Mar, 2015 22:32 GMT-0000 kabbotta 156
Sun 29 of Mar, 2015 22:32 GMT-0000 kabbotta 155
Sun 29 of Mar, 2015 22:31 GMT-0000 kabbotta 154
Sun 29 of Mar, 2015 22:31 GMT-0000 kabbotta 153