Skip to main content

History: Intermediate Tutorial 4

Source of version: 95

Copy to clipboard
            %tutorialhelp%
{maketoc}
!Introduction
This tutorial will cover the creation of a box selection mechanism. This is the familiar process of dragging out a rectangle on the screen to select everything within it. It is another one of the features that a basic scene editor would have.

To accomplish this we will be introducing two new objects. We will use a ManualObject to create the box on screen, and we will use a PlaneBoundedVolumeListSceneQuery determine what should be selected in the scene. This is only an introduction to ManualObject. The class has many more capabilities that can be used to generate solid objects in your scene.

The full source for this tutorial ((IntermediateTutorial4SourceCurrent|here)).

__Note:__ There is also source available that uses the BaseApplication framework and Ogre 1.7 ((IntermediateTutorial4Source|here)).

{img fileId="2257" rel="box[g]"}
!Prerequisites
This tutorial assumes that you already know how to set up an Ogre project and compile it successfully. Knowledge of the topics from previous tutorials is also assumed.

!ManualObjects
A ManualObject allows us to actually construct a mesh without having to design it beforehand in 3D modeling software like Blender. To understand how we can do this, it will be helpful to introduce a few basic concepts related to how 3D objects are represented in a graphics library like Ogre.

A mesh in Ogre can be thought of as consisting of two pieces. They are referred to as ''vertex buffers'' and ''index buffers''. If you have any experience with OpenGl, then this might sound somewhat familiar.

A vertex buffer defines a series of points in 3D space. Each element in a vertex buffer is defined by a several attributes. The only required attribute is the position of the vertex. The other attributes allow you to change things like the color or texture that will be applied to the vertex when it is rendered.

An index buffer connects the vertices. Every three indexes define a single triangle to be drawn by the GPU. The order of the vertices determines which way the triangle will face. A triangle which is drawn counter-clockwise is facing the camera. A triangle that is drawn clockwise is facing away from the camera. This is important because often the back face of a triangle is "culled" or not drawn by the GPU.

All meshes have a vertex buffer, but not all have an index buffer. The mesh we will be creating for this tutorial will not have an index buffer, because we are creating an "empty" rectangle. A solid rectangle would require an index buffer to create the two triangles that would make up its face.
!Creating a Mesh
There are two ways to create our own mesh within Ogre. The first way is to build a class that inherits from [http://www.ogre3d.org/docs/api/html/classOgre_1_1SimpleRenderable.html|SimpleRenderable] and directly provide the vertex and index buffers to it. This method is a bit cryptic. An example is given in ((Generating A Mesh)). We will use the simpler method of creating a ((ManualObject)). Instead of setting all of the attributes and indices by hand, we will simply call ManualObject methods like {MONO()}position{MONO}.

We want to generate a rectangular outline on the screen for selection purposes. We could use CEGUI or the builtin overlay system, but instead we are going to generate a simple 2D mesh using the ManualObject class.
!The SelectionBox Class
To keep things clean we are going to put our selection box functionality into its own class. Create a new class in your project called SelectionBox that inherits from Ogre::ManualObject.
{CODE(caption="SelectionBox.h", wrap="1", colors="c++")}
#ifndef SELECTIONBOX_H
#define SELECTIONBOX_H

#include <OgreManualObject.h>

class SelectionBox : public Ogre::ManualObject
{
public:
  SelectionBox(const Ogre::String& name);
  virtual ~SelectionBox();

  void setCorners(float left, float top, float right, float bottom);
  void setCorners(const Ogre::Vector2& topLeft, const Ogre::Vector2& bottomRight);
};

#endif /* SELECTIONBOX_H */
{CODE}
We want the SelectionBox to render as a 2D object. We also want to make sure that it renders after our 3D scene objects. This is so it doesn't get covered up by the objects we are trying to select. Add the following to the body of the SelectionBox constructor:
{CODE(wrap="1",colors="c++")}
setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
setUseIdentityProjection(true);
setUseIdentityView(true);
setQueryFlags(0);
{CODE}
The first function makes sure the mesh will be rendered with the overlay. The next two functions set the projection and view matrices for our object. Ogre abstracts all of these gorey details away for us, so we won't get into exactly what these functions do. The important part to know is that using the indentity matrix for both translates into rendering our rectangle as a 2D object. The other important thing to notice is that we really must treat the object like a 2D object in some cases. For instance, if a function asks for the z value of our object we will use -1, since our object has been projected into the 2D x-y plane of the screen and no effectively no longer has a z value. Finally, we set the query flags to zero. This will exclude our SelectionBox from being included in any SceneQuery results. 

Now we will start to build the actual rectangle. We have one issue that needs to cleared up first, though. We will be using the location of the cursor in our functions. The problem is that the normalized mouse positions run from [0, 1] for the x and y axes, but the ManualObject functions are expecting coordinates that run from [-1, 1]. To make things a little more complicated, the y coordinate also runs in the other direction. CEGUI defines the top of the screen as y = 0, just like Ogre does, but in our new coordinate system, the top of the screen is y = 1 and the bottom is y = -1. This can be fixed by transforming the coordinates before we use them. Add the following to the {MONO()}setCorners{MONO} method with four parameters:
{CODE(wrap="1", colors="c++")}
left = 2 * left - 1;
right = 2 * right - 1;
top = 1 - 2 * top;
bottom = 1 - 2 * bottom;
{CODE}
If you stare at that for a little bit, you can probably convince yourself it does exactly what we need it to. Now that we have the correctly transformed coordinates, we can actually build the rectangle.
{CODE(wrap="1", colors="c++")}
clear();
begin("Examples/KnotTexture", Ogre::RenderOperation::OT_LINE_STRIP);
position(left, top, -1);
position(right, top, -1);
position(left, bottom, -1);
position(left, top, -1);
end();
{CODE}
The very first thing we do is call {MONO()}clear{MONO} since we will be calling this a number of times, and we want the rectangles drawn in previous frames to disappear. Next we need to call the {MONO()}begin{MONO} method to start building our mesh. Its first parameter is the name of the material you want to use for the current section of the object. The next parameter is the RenderOperation we want to use to build our mesh. We have chosen the line strip method, which connects all positions with a straight line. Next we start building the mesh. Each call to position essentially adds another point into our vertex buffer. To finalize the mesh we call {MONO()}end{MONO}.

The last thing we will do is set the bounding box for our object. Many SceneManagers cull objects which move offscreen. Even though we asked Ogre to project our ManualObject as if it were essentially a 2D object, it is still really a 3D object in our scene. This means that if we attach the object to SceneNode (as we're about to do), it will disappear when we look away. To fix this we will set the bounding box of the object to be infinite so that the camera will always be inside it and will never cull the object.
{CODE(wrap="1", colors="c++")}
setBoundingBox(Ogre::AxisAlignedBox::BOX_INFINITE);
{CODE}
This line should be placed ''after'' the {MONO()}clear{MONO} call. Every time we call {MONO()}clear{MONO}, the bounding box is reset.

The last thing we need to do with this class is finish the overloaded {MONO()}setCorners{MONO} method. Add the following:
{CODE(wrap="1", colors="c++")}
setCorners(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
{CODE}
This overload will allow us to use vectors instead of providing all four corners of the box separately. The SelectionBox class is now complete. Compile and run your application to make sure it works. The functionality should not have changed yet.
!Box Selection
Now we will implement the actual selection code. First, we have to set up a few things. Make sure to include {MONO()}SelectionBox.h{MONO}:
{CODE(wrap="1",colors="c++")} 
#include "SelectionBox.h"
{CODE}
We also need to add a pointer to a SelectionBox to our header.
{CODE(caption="BasicApp.h", wrap="1", colors="c++")}
SelectionBox* mSelectionBox;
{CODE}
Add an initialization to the contructor:
{CODE(caption="BasicApp.cpp", wrap="1", colors="c++")}
mSelectionBox(0),
{CODE}
Now we need to create an instance of our new class and attach it to our root SceneNode. Then we want the SceneManager to create a volume query for us. Add the following to the end of {MONO()}createScene{MONO}:
{CODE(wrap="1",colors="c++")}
mSelectionBox = new SelectionBox("SelectionBox");
mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(mSelectionBox);

mVolQuery = mSceneMgr->createPlaneBoundedVolumeQuery(Ogre::PlaneBoundedVolumeList());
{CODE}
Now that it is all set up we need to create an instance of the SelectionBox class, and have the SceneManager create a volume query for us. Add the following code at the end of createScene(), so we know for sure that the SceneManager has been initialized:

As usual, we also must make sure we clean up in the destructor. Add the following to {MONO()}~BasicApp{MONO}:
{CODE(wrap="1",colors="c++")}        
mSceneMgr->destroyQuery(mVolQuery);

if(mSelectionBox)
  delete mSelectionBox;
{CODE}
Notice we let the SceneManager clean up the query for us.
!Box Selection is Volume Selection
Really what we're trying to do is select everything that is contained within a volume in our scene. The rectangle we draw on the screen can be thought of like the very edge of the opening of a long piece of square tubing, like we're looking through a rain gutter. Everything that is contained within this long square tube should be selected when we let go of the left mouse button.

The first thing we need to do is start with the left mouse button being pressed down. At that point, we'll need to getting a starting vector position for drawing our selection box. We will also turn on the flag that says we are currently selecting objects, and we will make the SelectionBox visible. Add the following to the if statement for the left mouse button in {MONO()}mousePressed{MONO}:

{CODE(wrap="1", colors="c++")}
CEGUI::MouseCursor* mouse = &context.getMouseCursor();
mStart.x = mouse->getPosition().d_x / (float)arg.state.width;
mStart.y = mouse->getPosition().d_y / (float)arg.state.height;
mStop = mStart;
 
mSelecting = true;
mSelectionBox->clear();
mSelectionBox->setVisible(true);
{CODE}

One important thing to notice is that we're using the CEGUI mouse position and not the position from OIS. This is because OIS sometimes thinks the mouse is somewhere different than where CEGUI is displaying it. We want our application to sync with what theuser sees, so we rely on the CEGUI coordinates.

The next thing we need to do is hide the SelectionBox and perform the selection when the user releases the mouse button. We will fill in {MONO()}performSelection{MONO} shortly. Add the following to the if statement for the left mouse button in {MONO()}mouseReleased{MONO}:

{CODE(wrap="1",colors="c++")}
performSelection(mStart, mStop);
mSelecting = false;
mSelectionBox->setVisible(false);
{CODE}

Whenever the mouse is moved we need to update the position of the SelectionBox. Add the following to {MONO()}mouseMoved{MONO}:

{CODE(wrap="1",colors="c++")}
if (mSelecting)
{
  CEGUI::MouseCursor* mouse = &context.getMouseCursor();
  mStop.x = mouse->getPosition().d_x / (float)me.state.width;
  mStop.y = mouse->getPosition().d_y / (float)me.state.height;
 
  mSelectionBox->setCorners(mStart, mStop);
}
{CODE}

First, we calculate the stop vector for our SelectionBox, and then we pass both the start and stop vectors to our overloaded {MONO()}setCorners{MONO} method. 

Compile and run your application. You can now draw a rectangle using the mouse. Cool.

!PlaneBoundedVolumeListSceneQuery
Now that we have the SelectionBox working, we are going to set up our volume selection. First, we'll quickly fill out our {MONO()}swap{MONO} method.
{CODE(wrap="1", colors="c++")}
void BasicApp::swap(float& x, float& y)
{
  float temp = x;
  x = y;
  y = temp;
}
{CODE}
Now we'll begin writing up the selection code. Add the following to {MONO()}performSelection{MONO}:
{CODE(wrap="1", colors="c++")}
float left = first.x, right = second.x;
float top = first.y, bottom = second.y;
 
if (left > right)
  swap(left, right);
 
if (top > bottom)
  swap(top, bottom);
{CODE}
The first thing we do is unpack the first and second vectors into four float values representing the four corners of our selection box. Then we make sure the rectangle rectangle is oriented correctly. Our selection rectangle could be drawn "backwards" if the user clicks and then moves the mouse to the left. For our purposes, we always want the points organized so that the lowest values are top and left.

After that we want to check our selection rectangle's area. Our current selection method will fail if the rectangle is too small.
{CODE(wrap="1",colors="c++")}
if ((right - left) * (bottom - top) < 0.0001)
  return;
{CODE}
This determines the two side lengths of our rectangle and then uses them to determine if the area is below 0.0001. In your own projects, it would be better to perform a standard RaySceneQuery instead of simply returning. This would effectively overcome the limitations of our current method.

We will now perform the query itself. A PlaneBoundedVolumeQuery uses a series of planes to enclose an area, and it returns any objects inside of that volume. We will create five planes to build our selection volume. To do this, we will create four Rays that come straight out from the plane of the viewport. This can be difficult to visualize, so here is a simple image to help clarify things:
{img fileId="2234" rel="box[g]"}
You can probably see how these Rays can be used to form the volume we're looking for now. The first thing we'll do is create the Rays.
{CODE(wrap="1",colors="c++")}
Ogre::Ray topLeft = mCamera->getCameraToViewportRay(left, top);
Ogre::Ray topRight = mCamera->getCameraToViewportRay(right, top);
Ogre::Ray bottomLeft = mCamera->getCameraToViewportRay(left, bottom);
Ogre::Ray bottomRight = mCamera->getCameraToViewportRay(right, bottom);
{CODE}
It could be argued that the {MONO()}getCameraToViewportRay{MONO} has a somewhat confusing name. You might have thought of the camera as being at a single point, but for the purposes of this method the camera is modeled as the whole plane of the screen. Therefore, the method returns rays that are all perpendicular to the screen and not rays that shoot out from a single point like cone.

The next we do is create the five planes we are going to use.
{CODE(wrap="1",colors="c++")}

Ogre::Plane frontPlane, topPlane, leftPlane, bottomPlane, rightPlane;

frontPlane = Ogre::Plane(
  topLeft.getOrigin(),
  topRight.getOrigin(),
  bottomRight.getOrigin());

topPlane = Ogre::Plane(
  topLeft.getOrigin(),
  topLeft.getPoint(10),
  topRight.getPoint(10));

leftPlane = Ogre::Plane(
  topLeft.getOrigin(),
  bottomLeft.getPoint(10),
  topLeft.getPoint(10));

bottomPlane = Ogre::Plane(
  bottomLeft.getOrigin(),
  bottomRight.getPoint(10),
  bottomLeft.getPoint(10));

rightPlane = Ogre::Plane(
  topRight.getOrigin(),
  topRight.getPoint(10),
  bottomRight.getPoint(10));
{CODE}
As I'm sure you remember from your school days, three points in space uniquely define a plane. So to form the front plane we take the origins of the topLeft, topRight, and bottomRight Rays as our points. Notice the order determines which way the plane faces. The order we've used makes sure the front plane faces away from the camera. We want all of the planes facing towards the inside of our volume. Using the points 10 units down the Rays to define the other planes is arbitrary. The only thing that matters is that they are all the same distance down the Ray. We could have used a point 1 unit down the Ray or a point 10000 units down the Ray. They all would have defined the same infinite plane.

Next we need to put our planes into a PlaneBoundedVolume and then place that into a PlaneBoundedVolumeList so that they can be used by our query.
{CODE(wrap="1",colors="c++")}
Ogre::PlaneBoundedVolume vol;

vol.planes.push_back(frontPlane);
vol.planes.push_back(topPlane);
vol.planes.push_back(leftPlane);
vol.planes.push_back(bottomPlane);
vol.planes.push_back(rightPlane);

Ogre::PlaneBoundedVolumeList volList;
volList.push_back(vol);
{CODE}
We are now ready to execute the actual query.
{CODE(wrap="1",colors="c++")}
mVolQuery->setVolume(volList);
Ogre::SceneQueryResult result = mVolQuery->execute();
{CODE}
First, we pass our PlaneBoundedVolumeList to our query, and then we call {MONO()}execute{MONO}. Now we will iterate through the results, and select any valid movables we've found. But first we need to call {MONO()}deselectObjects{MONO}, which we will write in just a moment.
{CODE(wrap="1",colors="c++")}
deselectObjects();

Ogre::SceneQueryResultMovableList::iterator it;
for (it = result.movables.begin(); it != result.movables.end(); ++it)
  selectObject(*iter);
{CODE}
That's the entire {MONO()}performSelection{MONO} method. Note that you can also use QueryFlags with volume queries. Now let's fill in our two missing methods. Add the following to your implementation:
{CODE(wrap="1",colors="c++")}
void BasicApp::deselectObjects()
{
  std::list<Ogre::MovableObject*>::iterator it;

  for (it = mSelected.begin(); it != mSelected.end(); ++it)
    (*it)->getParentSceneNode()->showBoundingBox(false);
}
{CODE}
This iterates through our list of selected objects and turns off their bounding boxes. Notice the important parenthesis around the dereference of {MONO()}it{MONO}, without them we would be be dereferencing the parent SceneNode pointer instead of the MovableObject pointer. Lastly, we will fill in our {MONO()}selectObject{MONO} method.
{CODE(wrap="1",colors="c++")}
void BasicApp::selectObject(Ogre::MovableObject* obj)
{
  obj->getParentSceneNode()->showBoundingBox(true);
  mSelected.push_back(obj);
}
{CODE}
Compile and run the application. You can now box select objects in your scene.
!A Final Note About Selection
You have probably noticed that selection relies on the bounding box of the objects in our scene and not on the mesh itself. This means that SceneQuerys will always be too accepting in what they consider a hit. Don't worry, there are ways of performing pixel perfect raycasts, but they involve tradeoffs in performance. You can read ((Raycasting to the polygon level)) for more information on implementing this feature with Ogre. If you are integrating a physics library like OgreNewt into your application, then it should also provide methods for performing these more accurate raycasts.

Learning the techniques in these tutorials was not a waste, though. Pixel perfect raycasting is very performance intensive, so it should not be used everywhere. One of the more common techniques actually involves performing an Ogre query like we've learned, and then after discovering the general area of intersection, performing a second, more accurate raycast. You will find many examples of these two-tiered approaches to graphics programming. It is an important design pattern to begin to think about. Very similar methods are used to get better performance when dealing with complicated path-finding problems.

!Exercises
!!Easy
# Add ninjas to your robot army. Then make it so your volume query will select either ninjas or robots depending on the current mode.
!!Intermediate
# Create an interface with CEGUI that displays an icon for each selected entity.
# Allow the user to command the selected units to walk in place. Have everyone else Idle.
!!Difficult
# Try to change the shape of the SelectionBox and implement the corresponding PlaneBoundedVolumeQuery (i.e. change the SelectionBox into a SelectionTriangle).
!!Advanced
# Further extend your interface from the Intermediate exercise to allow the user to select subgroups of entities by clicking on the GUI instead of box selecting units in the scene. Make it so they can select multiple entities by holding down shift while clicking on either the interface or the unit in the scene.
!Conclusion
The first part of this tutorial introduced the concept of a ManualObject. This is one of the simpler ways to manually create a mesh in Ogre. We created a new class that inherited from ManualObject and used it to create our selection rectangle. We also mentioned how to set the projection and view matrices of our object so that it was displayed as a 2D object.

The second part of the tutorial focused on setting up and running a PlaneBoundedVolumeQuery. This involved defining a series of planes that would create an enclosed volume in our scene based on the placement of our selection rectangle - a process sometimes referred to as box selection.

!Full Source
The full source for this tutorial is ((IntermediateTutorial4SourceCurrent|here)).

!Next
((Intermediate Tutorial 5))

---
Alias: (alias(Intermediate_Tutorial_4))

        

History

Information Version
Sat 17 of Oct, 2020 21:52 GMT-0000 sercero 113
Sun 19 of Apr, 2015 02:25 GMT-0000 kabbotta 112
Mon 30 of Mar, 2015 09:12 GMT-0000 kabbotta 111
Mon 30 of Mar, 2015 09:11 GMT-0000 kabbotta 110
Mon 30 of Mar, 2015 09:10 GMT-0000 kabbotta 109
Mon 30 of Mar, 2015 09:06 GMT-0000 kabbotta 108
Mon 30 of Mar, 2015 08:53 GMT-0000 kabbotta 107
Mon 30 of Mar, 2015 08:51 GMT-0000 kabbotta 106
Mon 30 of Mar, 2015 08:45 GMT-0000 kabbotta 105
Mon 30 of Mar, 2015 08:35 GMT-0000 kabbotta 104
Mon 30 of Mar, 2015 08:14 GMT-0000 kabbotta 103
Mon 30 of Mar, 2015 08:12 GMT-0000 kabbotta 102
Mon 30 of Mar, 2015 08:08 GMT-0000 kabbotta 101
Mon 30 of Mar, 2015 07:58 GMT-0000 kabbotta 100
Mon 30 of Mar, 2015 07:57 GMT-0000 kabbotta 99
Mon 30 of Mar, 2015 07:56 GMT-0000 kabbotta 98
Mon 30 of Mar, 2015 07:46 GMT-0000 kabbotta 97
Mon 30 of Mar, 2015 07:45 GMT-0000 kabbotta 96
Mon 30 of Mar, 2015 07:41 GMT-0000 kabbotta 95
Mon 30 of Mar, 2015 02:01 GMT-0000 kabbotta 94
Mon 30 of Mar, 2015 02:00 GMT-0000 kabbotta 93
Mon 30 of Mar, 2015 02:00 GMT-0000 kabbotta 92
Mon 30 of Mar, 2015 01:59 GMT-0000 kabbotta 91
Mon 30 of Mar, 2015 01:14 GMT-0000 kabbotta 90
Sun 29 of Mar, 2015 05:25 GMT-0000 kabbotta 89