Intermediate Tutorial 4         Box Selection and Building Objects Manually

%tutorialhelp%

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 to determine what should be selected in the scene. This is only an introduction to manual objects. They can be used to generate a wide range of objects for your scene without ever needing to use 3D modeling software.

The full source for this tutorial is here.

Note: There is also source available that uses the BaseApplication framework and Ogre 1.7 here.

selection_visual.png

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.

The base code for this tutorial is here.

ManualObjects

A manual object allows us to 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 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 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.

Depending on the mode, every three indices may define a triangle to be drawn by the GPU. The order of the vertices will determine 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. In other words, it is not rendered.

All meshes have a vertex buffer, but not all of them 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 define the 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 SimpleRenderable and directly provide the vertex and index buffers to it. This method is powerful, but 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 methods like position.

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.

SelectionBox.h
#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 */

We want the selection box to render as a 2D object. We also want to make sure that it renders after our scene elements. 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:

setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
setUseIdentityProjection(true);
setUseIdentityView(true);
setQueryFlags(0);

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 details away for us, so we won't get into exactly what these functions do. The reason you're using Ogre is probabaly because you don't want to reinvent the wheel.

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 effectively no longer has a z value. Finally, we set the query flags to zero. This will exclude our selection box from being included in any scene query results.

Now we will start to build the actual rectangle. We have one issue that needs to cleared up first. 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 manual object 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 the coordinate system we need to use now, 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 setCorners method with four parameters:

left = 2 * left - 1;
right = 2 * right - 1;
top = 1 - 2 * top;
bottom = 1 - 2 * bottom;

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.

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

The very first thing we do is call clear 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 begin method to start building our mesh. Its first parameter is the name of the material you want to use for the object. The next parameter is the render operation 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 adds another point into our vertex buffer. To finalize the mesh we call end.

The last thing we will do is set the bounding box for our object. Many scene managers cull objects which move offscreen. Even though we asked Ogre to project our manual object 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 a scene node (as we're about to do), it may disappear when the node moves off camera. 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.

setBoundingBox(Ogre::AxisAlignedBox::BOX_INFINITE);

This line should be placed after the clear call. Every time we call clear the bounding box is reset.

The last thing we need to do with this class is finish the overloaded setCorners method with two vector parameters. Just make a call to the setCorners method we've already defined.

setCorners(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);

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 SelectionBox.h in our header:

BasicApp.h
#include "SelectionBox.h"

We also need to add a pointer to a SelectionBox object.

BasicApp.h
SelectionBox* mSelectionBox;

And add an initialization to the contructor:

BasicApp.cpp
mSelectionBox(0),

Now we need to create an instance of our new class and attach it to our root scene node. Then we want the scene manager to create a plane-bounded volume query for us. Add the following to the end of createScene:

mSelectionBox = new SelectionBox("SelectionBox");
mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(mSelectionBox);

mVolQuery = mSceneMgr->createPlaneBoundedVolumeQuery(Ogre::PlaneBoundedVolumeList());

As usual, we make sure to clean up in the destructor. Add the following to ~BasicApp:

mSceneMgr->destroyQuery(mVolQuery);

if(mSelectionBox)
  delete mSelectionBox;

Notice we let the scene manager clean up the queries 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 an infinitely long piece of square tubing, like we're looking through a rain gutter. Here's a simple visual:
volume_visual.png
The volume goes on infinitely in one direction because it will only be enclosed by five planes. It would require six to completely enclose. Everything that is contained within this infinite square tube will be selected when we let go of the left mouse button.

The process will start when the left mouse button is pressed down. At that point, we'll need to get a starting vector for drawing our selection box. We will also turn on the flag that says we are currently selecting objects and make the selection box visible. Add the following to the if statement for the left mouse button in mousePressed:

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

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 the user sees, so we rely on the CEGUI coordinates.

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

performSelection(mStart, mStop);
mSelecting = false;
mSelectionBox->setVisible(false);

Whenever the mouse is moved we need to update the position of the selection box. Add the following to mouseMoved:

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);
}

We calculate the stop vector for our selection box, and then we pass both the start and stop vectors to our overloaded setCorners method.

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

PlaneBoundedVolumeListSceneQuery

Now that we have the selection box working, we are going to set up our volume selection. First, we'll quickly fill in our swap method.

void BasicApp::swap(float& x, float& y)
{
  float temp = x;
  x = y;
  y = temp;
}

Now we'll begin writing up the selection code. Add the following to performSelection:

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

We unpack the first and second vectors into four float values representing the four corners of our selection box. Then we make sure the rectangle is oriented correctly. Our selection rectangle could be drawn in reverse order if the user clicks and then moves the mouse up or 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.

if ((right - left) * (bottom - top) < 0.0001)
  return;

This determines the two side lengths of our rectangle and then uses them to check if the area is below 0.0001. In your own projects, it would be better to perform a standard ray scene query instead of simply returning. This would effectively overcome the limitations of our current method.

We will now perform the query itself. A plane-bounded volume list query 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 camera. This can be difficult to visualize, so here is a simple image to help clarify things:
ray_visual.png
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.

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

It could be argued that the getCameraToViewportRay has a somewhat confusing name. You might have thought of the camera as being at a single point, but the camera is actually modeled as a frustum (a pyramid with a flat top). Therefore, the method returns rays that are all perpendicular to the screen and not rays that shoot out from a single point like cone. This actually turns out to be what you want the majority of the time. That's what makes it a good model.

The next thing we do is create the five planes we are going to use.

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

As I'm sure you remember from your school days, three points in space uniquely define an infinite plane. 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 just like it does with the vertices in an index buffer. 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 rays or a point 1000000 units down the ray. They all would have defined the same infinite plane.

Next we need to put our planes into a PlaneBoundedVolume, then place that into a PlaneBoundedVolumeList so that they can be used by our query.

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

This may seem like overkill, but that is because this system is designed to handle much more complicated volume queries than the simple one we are constructing. We are now ready to execute the actual query.

mVolQuery->setVolumes(volList);
Ogre::SceneQueryResult result = mVolQuery->execute();

We pass our PlaneBoundedVolumeList to our query, and then we call execute. Now we will iterate through the results and select any valid movables we've found. But first we need to call deselectObjects, which we will write in just a moment.

deselectObjects();

Ogre::SceneQueryResultMovableList::iterator it;
for (it = result.movables.begin(); it != result.movables.end(); ++it)
  selectObject(*iter);

That's the entire performSelection method. You can also use query flags with volume queries. Now let's fill in our two missing methods. Add the following to your implementation:

void BasicApp::deselectObjects()
{
  std::list<Ogre::MovableObject*>::iterator it;

  for (it = mSelected.begin(); it != mSelected.end(); ++it)
    (*it)->getParentSceneNode()->showBoundingBox(false);
}

This iterates through our list of selected objects and turns off their bounding boxes. Notice the important parenthesis around the dereference of it, without them we would be be dereferencing the parent scene node pointer instead of the movable object pointer. Lastly, we will fill in our selectObject method.

void BasicApp::selectObject(Ogre::MovableObject* obj)
{
  obj->getParentSceneNode()->showBoundingBox(true);
  mSelected.push_back(obj);
}

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 scene querys 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 within Ogre. If you are integrating a physics library like Newton, 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 sparingly. One of the more common techniques actually involves performing a bounding box query like we've learned, and then performing a second, more accurate raycast to determine exactly where the hit was. You will find many examples of these multi-tiered approaches to programming simulations. 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.

Conclusion

The first part of this tutorial introduced the concept of a manual object. 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 plane-bounded volume query. 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.

Exercises

Easy

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

  1. Create an interface with CEGUI that displays an icon for each selected entity.
  2. Allow the user to command the selected units to walk in place. Have everyone else Idle.

Difficult

  1. Try to change the shape of the SelectionBox and implement the corresponding PlaneBoundedVolumeQuery (i.e. change the SelectionBox into a SelectionTriangle).

Advanced

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

Full Source

The full source for this tutorial is here.

Next

Intermediate Tutorial 5


Alias: Intermediate_Tutorial_4