startrcewww-data endrce

MOGRE Intermediate Tutorial 4: Volume Selection and Basic Manual Objects

Note: This tutorial converted from OGRE and work with MOGRE SDK 1.7.2.
Any problems you encounter while working with this tutorial should be posted to the MOGRE Forum.

Introduction

In this tutorial we will be covering how to do volume selection. The idea is that when you click and drag the mouse across the screen, a white rectangle will trace the area you are selecting. When you let go of the mouse, all objects within the selection area will be highlighted. In order to accomplish this we will be learning how to use two objects: ManualObject (to create the rectangle) and PlaneBoundedVolumeListSceneQuery. Note that while we will cover some basic uses of ManualObject, this is only an introduction to it, and not a tutorial on how to completely create 3D objects with it. We will only cover what we need to use.

You can find the code for this tutorial here. 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

Create a .cs file in the IDE of your choice and

[+] add the following code to it


Be sure this code compiles before continuing. Press escape to exit.

ManualObjects

A Crash Course in 3D Objects


Before we start diving directly into making a mesh, it would probably be useful to talk about what a mesh is, and what it is made up of. Though this is a gross oversimplification, a mesh consists of roughly two parts: the vertex buffer and the index buffer.

Vertex buffers define points in 3D space. Each element in the vertex buffer is defined by several attributes you can set. The only attribute you must set is the position of the vertex. Aside from that, there are many optional properties you can set, such as the color of the vertex, the texture coordinates, and so on. Which ones you will actually need to use is dependent on what you are trying to do with the mesh.

Index buffers "connect the dots" by selecting points from the vertex buffer. Every three indexes specified in the index buffer defines a single triangle to be drawn by the GPU. The order in which you select vertices in the index buffer tells the graphics card which way the triangle faces. A triangle which is drawn counter-clockwise is facing you, one drawn clockwise is facing away from you. Normally only the front of a triangle is rendered, so it is important to be sure that your triangles are setup properly.

Though all meshes have a vertex buffer, not all meshes will have an index buffer. For example, the mesh we are about to create will not have an index buffer since we want to create an empty rectangle (as opposed to a filled rectangle). Lastly, note that vertex and index buffers are usually stored in the video card's own memory, so your software can just send the card one simple, discrete set of commands to tell it to use those predefined buffers to render an entire 3D mesh in one go.

Introduction

There are two ways to create your own mesh within Ogre. The first way is to subclass the SimpleRenderable object and provide it with the vertex and index buffers directly. This is the most direct way to create one, but it's also the most cryptic. The Generating A Mesh code snippet shows an example of this. To make things easier, Ogre provides a much nicer interface called ManualObject, which allows you to use some simple functions to define a mesh instead of writing raw data to the buffer objects. Instead of dropping the position, color, and so on into a buffer, you simply call the "position" and "colour" functions.

In this tutorial we need to create a white rectangle to display when we are dragging the mouse to select objects. There really isn't a class in Ogre we could use to display a 2D rectangle. We will have to come up with a way of doing it on our own. We could use an Overlay and resize it to display the selection rectangle, but the problem with doing it this way is that the image you use for the selection rectangle could get stretched out of shape and look awkward. Instead, we will generate a very simple 2D mesh to act as our selection rectangle.

The Code

When we create the selection rectangle, we have to create it such that it will render in 2D. We also have to be sure that it will render when Ogre's Overlays render so that it sits on top of all other objects on screen. Doing this is actually very easy. Find the SelectionRectangle's constructor and add the following code:

RenderQueueGroup = (byte)RenderQueueGroupID.RENDER_QUEUE_OVERLAY; // when using this, ensure Depth Check is Off in the material
UseIdentityProjection = true;
UseIdentityView = true;
QueryFlags = 0;


The first function sets the render queue for the object to be the Overlay queue. The next two functions sets the projection and view matrices to be the identity. Projection and view matrices are used by many rendering systems (such as OpenGL and DirectX) to define where objects go in the world. Since Ogre abstracts this away for us, we won't go into any detail about what these matrices actually are or what they do. Instead what you need to know is that if you set the projection and view matrix to be the identity, as we have here, we will basically create a 2D object. When defining this object, the coordinate system changes a bit. We no longer deal with the Z axis (if you are asked for the Z axis, set the value to -1). Instead we have a new coordinate system with X and Y running from -1 to 1 inclusive. Lastly, we will set the query flags for the object to be 0, which will prevent prevent the selection rectangle from being included in the query results.

Cursor

Now that the object is set up, we need to actually build the rectangle. But in first, to show cursor we need to attach some GUI system (but no one work with MOGRE 1.7x at this time) or draw cursor manually or use OS capabilities. The last is simplest and we implement this with MOIS:
override method CreateInput to use non-exclusive and buffered modes.

[+] Add to class VolumeSelectionApplication


We have one small snag before we get started. We are going to be calling setCorners function with mouse locations. That is we will be given, a number between 0 and 1 for the x and y coordinates, yet we need to convert these to numbers in the range -1, 1. To make matters slightly more complicated, the y coordinate is backwards too. The mouse cursor in our case defines the top of the screen at 0, the bottom at 1. In our new coordinate system, the top of the screen is +1, the bottom is -1. Thankfully, a few quick conversions will take care of this problem. Find the setCorners function and add the following:

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


Now the positions are in the new coordinate system. Next we need to actually build the object. To do this, we first call the begin method. It takes in two parameters, the name of the material to use for this section of the object, and the render operation to use to draw it. Since we are not putting a texture on this, we will leave the material blank. The second parameter is the RenderOperation. We can render the mesh using points, lines, or triangles. We would use triangles if we were rendering a full mesh, but since we want an empty rectangle, we will use the line strip. The line strip draws a line to each vertex from the previous vertex you defined. So to create our rectangle, we will define 5 points (the first and the last point are the same to connect the entire rectangle):

Clear();
Begin("", RenderOperation.OperationTypes.OT_LINE_STRIP);
Position(left, top, -1);
Position(right, top, -1);
Position(right, bottom, -1);
Position(left, bottom, -1);
Position(left, top, -1);
End();

Note that since we will be calling this many times, we have added the clear call at the beginning to remove the previous rectangle before redrawing it. When defining a manual object, you may call begin/end multiple times to create multiple sub-meshes (which can have different materials/RenderOperations). Note we have also set the Z parameter to be -1, since we are trying to define a 2D object which will not use that axis. Setting it to be -1 will ensure that we are not on top of, or behind, the camera when rendering.

The last thing we need to do is set the bounding box for this object. Many SceneManagers cull objects which are off screen. Even though we've basically created a 2D object, Ogre is still a 3D engine, and treats our 2D object as if it sits in 3D space. This means that if we create this object and attach it to a SceneNode (as we will do in the next section), it will disappear on us 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 of it:

BoundingBox.SetInfinite();

Be sure to note that we have added this code after the clear call. Every time you call ManualObject.Clear(), the bounding box is reset, so be careful if you create another ManualObject which is cleared often because the bounding box will have to be set every time you recreate it.

That's all we have to do for the SelectionRectangle class. Be sure your code compiles before continuing, but note that no functionality has changed in the program yet.

Volume Selection

Setup

Before we can jump into the selection code, we first have to setup a few things. First of all, we need to create an instance of the SelectionRectangle class, and have the SceneManager create a volume query for us. Add the following code to the CreateScene():

mRect = new SelectionRectangle("Selection SelectionRectangle");
sceneMgr.RootSceneNode.CreateChildSceneNode().AttachObject(mRect);

Probably, we need (??) to be sure the frame listener cleans up after itself when we are done. Add to the VolumeSelectionApplication DestroyScene method or finalizer:

public override void DestroyScene()
{
    sceneMgr.DestroyQuery(mVolQuery);
    mRect.Dispose();
}

Note that we let the SceneManager clean up the query for us.

Mouse Handlers

The feature we are trying to implement is volume selection. This means that when the user clicks the mouse down and drags it across the screen a rectangle will be drawn. When they let go of the mouse, all objects within the rectangle will be selected. The first thing we will need to do is handle when the mouse is pressed. We'll need to store the starting location and set the the SelectionRectangle to be visible. Find the mousePressed function and add the following code:

switch (button)
{
    case MOIS.MouseButtonID.MB_Left:
        mStart.x = (float)e.state.X.abs / (float)e.state.width;
        mStart.y = (float)e.state.Y.abs / (float)e.state.height;
        mStop = mStart;

        bSelecting = true;
        mRect.Clear();
        mRect.Visible = true;
        break;
}


Note that in some GUI systems, x and y coordinates of displayed cursor may not be equal to OIS's mouse coordinates.

The next thing we will need to do is stop displaying the selection rectangle and perform the selection query when the user releases the mouse. Add the following code to the mouseReleased code:

switch (button)
{
    case MOIS.MouseButtonID.MB_Left:
        performSelection(mStart, mStop);
        bSelecting = false;
        mRect.Visible = false;
        break;
}

Finally, every time the mouse is moved, we need to update the rectangle to the new coordinates. Add to MouseMotion:

if (bSelecting)
        {
            mStop.x = e.state.X.abs / (float)e.state.width;
            mStop.y = e.state.Y.abs / (float)e.state.height;

            mRect.setCorners(mStart, mStop);
        }

We adjust the mStop vector every time the mouse is moved so that we can simply use it for the setCorners member function. Compile and run your application. You can now draw a rectangle using the mouse.

PlaneBoundedVolumeListSceneQuery

Now that we have the SelectionRectangle properly rendering, we need to actually perform the volume selection. Find the performSelection function and add the following code:

float left = first.x, right = second.x,
            top = first.y, bottom = second.y;

            if (left > right) swap(ref left, ref right);
            if (top > bottom) swap(ref top, ref bottom);

In this code section, we have assigned the vector parameters into the left, right, top, and bottom variables. The if statements ensure that we actually have the lowest value in left and top. (If the rectangle is drawn "backwards", meaning bottom-right to top-left, then we have to perform this swap.)

Next, we have to check and see how big of an area the rectangle actually makes. If the rectangle is too small, our method of creating a plane bound volumes will fail and we will end up selecting too many or too few objects. If the rectangle is less than a certain percentage of the screen, we will simply return and not perform the selection. I have arbitrarily selected 0.0001 as the threshold for canceling the query, though you should probably play around with this value for your own program. Also, in a real application you should probably find the center of the rectangle and perform a standard RaySceneQuery (as explained in previous tutorial) instead of doing nothing:

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

Now we are at the meat of the function, and we need to perform the query itself. PlaneBoundedVolumeQueries use planes to enclose an area, then select any objects inside that area. For this example we will build an area enclosed by five planes which face inward. To create these planes out of our rectangle, we will create 4 rays, one for each corner of the rectangle. Once we have these four rays, we will grab points along the rays to create the planes:

Ray topLeft = camera.GetCameraToViewportRay(left, top);
Ray topRight = camera.GetCameraToViewportRay(right, top);
Ray bottomLeft = camera.GetCameraToViewportRay(left, bottom);
Ray bottomRight = camera.GetCameraToViewportRay(right, bottom);

Now we will create the planes. Note that we are grabbing a point 100 units along the ray. This was chosen fairly arbitrarily. We could have chosen 2 instead of 100. The only point which matters here is the front plane, which we are starting 3 units in front of the Camera.

PlaneBoundedVolume vol=new PlaneBoundedVolume();
vol.planes.Add(new Plane(topLeft.GetPoint(3), topRight.GetPoint(3), bottomRight.GetPoint(3)));    // front plane
vol.planes.Add(new Plane(topLeft.Origin, topLeft.GetPoint(100), topRight.GetPoint(100)));         // top plane
vol.planes.Add(new Plane(topLeft.Origin, bottomLeft.GetPoint(100), topLeft.GetPoint(100)));       // left plane
vol.planes.Add(new Plane(bottomLeft.Origin, bottomRight.GetPoint(100), bottomLeft.GetPoint(100)));// bottom plane
vol.planes.Add(new Plane(topRight.Origin, topRight.GetPoint(100), bottomRight.GetPoint(100)));    // right plane

These planes have now defined an "open box" which extends to infinity in front of the camera. You can think of the rectangle we drew with the mouse as being the termination point of the box just in front of the camera. Now that we have created the planes, we need to execute the query:

PlaneBoundedVolumeList volList = new PlaneBoundedVolumeList();
volList.Add(vol);
if(mVolQuery!=null)
{
mVolQuery.SetVolumes(volList);
}
else
{
mVolQuery = sceneMgr.CreatePlaneBoundedVolumeQuery(volList);
}
SceneQueryResult result = mVolQuery.Execute();

Finally we need to handle the results of the query. First we will deselect all previously selected objects, then we will select all objects which were found by the query. The deselectObjects and selectObject functions have already been filled in for you since we have covered everything in those functions in a previous tutorial:

foreach (var obj in result.movables) selectObject(obj);

That's all we need to do for the query. Note we can also use query flags on volume queries, even though we have not done so in this tutorial. See the previous tutorial for more information on query flags.

Compile and run the application. You can now volume select objects in the scene!

A Final Note About Bounding Boxes

As you may have noticed from this tutorial and the previous two tutorials, selection in Ogre relies on the bounding box of the objects in question and not on the mesh itself. This means that a RaySceneQuery and PlaneBoundedVolumeQuery will always be too accepting in what is actually hit by the query. There are ways of performing pixel perfect ray selection (such as what you would need to do to see if a gunshot laser beam in an FPS actually hit its target) and volume selection which will give you perfectly accurate results for a trade-off in speed. Unfortunately, this is beyond the scope of this tutorial. Take a look at Raycasting to the polygon level for more information on how to do this in pure Ogre. If you have integrated Ogre with a physics library, such as OgreNewt, they should also provide methods to do this for you.

You did not learn all of this about ray queries and volume queries for nothing though. Doing a mesh based selection is very time consuming and can quickly kill your framerate if you try to check everything in a scene. In fact, the most common way to do "true" mouse selection is to first perform an Ogre query (such as a RaySceneQuery), then individually test each intersection returned from the query with a physics engine that will actually check the mesh's geometry to see if you actually hit it or just came very close.


Alias: MOGRE_Intermediate_Tutorial_4