Intermediate Tutorial 5         Static Geometry
Tutorial Introduction
Ogre Tutorial Head

If we can let Ogre know that an object will not be moved, then it can optimize how it handles that object. This is the core idea behind static geometry objects. Static geometry is a bit of a misnomer in this case, because we can use some tricks to accomplish things like grass waving in the wind, but the general idea is that this is an object that will not be manipulated a great deal.

Good examples include rocks, trees, and buildings. Static geometry objects can hold a large number of meshes that will all be "batched" together and drawn efficiently as a group. We will also go into some more detail on the use of manual objects. We will expand on what was introduced in Intermediate Tutorial 4. This tutorial is largely based on the Grass Demo in the Ogre Samples. The source for that demo can be read for further details.

The full source for this tutorial is here.

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

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

Prerequisites

This tutorial assumes that you already know how to set up an Ogre project and compile it successfully. If you need help with this, then read Setting Up An Application. This tutorial is also part of the Intermediate Tutorials series and knowledge from the previous tutorials will be assumed.

The base code for this tutorial is here.

grass_visual.png

Setting Up the Scene

The first thing we will do is create the grass mesh we will be rendering. We will use a pattern you've probably seen to create the illusion of grass. We will render three square quads that have a grass texture applied to them. We will create one, then place another rotated 60 degrees, and then place a third rotated 120 degrees. This will create a simple illusion of 3D grass. As in the previous tutorial, we will be using a manual object to generate our mesh, but this time we will create a solid mesh instead of a 2D outline. This will require an index buffer to connect the vertices.

The first step will be to define some variables. We will define the width and height of our quad, then we will initialize a vector that will be used to define the four corners of our quad. We will again be using quaternions to handle rotations. Our plan is to use the vector to represent the orientation of the base of our quad, then we will rotate it with a quaternion and repeat. Add the following to createGrassMesh:

const float width = 25;
const float height = 30;
Ogre::Vector3 vec(width/2, 0, 0);
Ogre::ManualObject obj("GrassObject");

Ogre::Quaternion quat;
quat.FromAngleAxis(Ogre::Degree(60), Ogre::Vector3::UNIT_Y);

This should look somewhat familiar. We have created a quaternion that represents a 60 degree rotation around the y-axis.

We will now begin defining our manual object. We set the render operation to be OT_TRIANGLE_LIST. This means that after we define our vertices with the position method, we then have to let Ogre know how to set up the index buffer by giving it a list of triangles made from the vertices.

obj.begin("Examples/GrassBlades", Ogre::RenderOperation::OT_TRIANGLE_LIST);

for (int i = 0; i < 3; ++i)
{

For each quad we are going to define four vertices representing the corners. We will also specify a texture coordinates. These are normalized coordinates that tell Ogre how to map the texture on to our mesh. In our case, these coordinates are very simple since we are creating a solid square.

^ obj.position(-vec.x, height, -vec.z);
  obj.textureCoord(0, 0);
  obj.position(vec.x, height, vec.z);
  obj.textureCoord(1, 0);
  obj.position(-vec.x, 0, -vec.z);
  obj.textureCoord(0, 1);
  obj.position(vec.x, 0, vec.z);
  obj.textureCoord(1, 1);

The vector we are using starts out pointing down the x-axis with a length that is half the width of our quad. This may seem confusing at first, because you'll notice all of the z components are zero for the first quad. The vector may seem unnecessary, but once we rotate it to set up our next quad, the z values will no longer be zero. This may be a little hard to visualize. Here is a picture to help:
quad_visual.png
Remember that x and z are in the plane of the floor. So our vector keeps track of where the foundation of our quad is, we build everything from that. We've also labeled the four corners with the order they were created to help with creating the triangles. The count starts with the 0th corner.

To ensure that both triangles face the same direction, we need to provide the points in counter-clockwise order. The triangle method does not directly take positions, instead it takes three numbers that represent the order in which the points were created. This is why we labeled the four corners in our image.

^ int offset = 4 * i;
  obj.triangle(offset + 0, offset + 3, offset + 1);
  obj.triangle(offset + 0, offset + 2, offset + 3);

First, ignore the offset value and look at the numbers we are adding. They match the numbers we assigned in the image. The first triangle connects the 0th, 3rd, and 1st corners. The second triangle connects the 0th, 2nd, and 3rd corners. You can look at the image to see these are in counter-clockwise order. The purpose of the offset is because we are creating three different quads, but they are all going to be a part of one manual object. So the second quad's corners will be numbered 4, 5, 6, 7. Adding the offset accounts for this.

Now we need to rotate our vector so it can be used to make the next quad.

^ vec = quat * vec;
}

After we've created all three quads, the loop finishes and we call end to finalize the object.

obj.end();
obj.convertToMesh("GrassBladesMesh");

The last line converts our manual object into an actual mesh. Meshes are a bit more optimized than directly rendering a manual object.

We are now finished creating the grass mesh. If you create a complex mesh, then you may save it to a file instead of rebuilding the mesh each time. To do this, you would save the mesh pointer that is returned by convertToMesh. Then you would use a mesh serializer to export the mesh to a file.

Here is an example. Do not add this code to our current project.

Ogre::MeshPtr ptr = obj.convertToMesh("GrassBladesMesh");
Ogre::MeshSerializer ser;
ser.exportMesh(ptr.getPointer(), "user_grass.mesh");

Adding Static Geometry

We are now going to build our StaticGeometry object, but we need a basic scene set up first. Add the following to createScene:

mSceneMgr->setAmbientLight(Ogre::ColourValue(1.0, 1.0, 1.0));

createGrassMesh();

mCamera->setPosition(150, 50, 150);
mCamera->lookAt(0, 0, 0);

Ogre::Entity* robot = mSceneMgr->createEntity("robot", "robot.mesh");
mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(robot);

Ogre::Plane plane;
plane.normal = Ogre::Vector3::UNIT_Y;
plane.d = 0;

Ogre::MeshManager::getSingleton().createPlane(
  "floor",
  Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
  plane,
  450.0, 450.0,
  10, 10, true, 1,
  50.0, 50.0,
  Ogre::Vector3::UNIT_Z);

Ogre::Entity* planeEntity = mSceneMgr->createEntity("floor"); planeEntity->setMaterialName("Examples/GrassFloor");
planeEntity->setCastShadows(false);
mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(planeEntity);

We've covered all of this before. Look into the previous tutorials if you are confused by any of this (notice we've also called createGrassMesh in our setup.

Now we get to the creation of our static geometry. The first thing we do is create an entity from the grass mesh we constructed, then we ask the scene manager to give us a pointer to a new StaticGeometry object.

Ogre::Entity* grass = mSceneMgr->createEntity("GrassBladesMesh");
Ogre::StaticGeometry* sg = mSceneMgr->createStaticGeometry("GrassArea");

Then we define two constants. The first will be used as the size of our static geometry region, and the second will be the number of grass patches we will create in that region.

const int size = 375;
const int amount = 20;

Now we'll set a few options.

sg->setRegionDimensions(Ogre::Vector3(size, size, size));
sg->setOrigin(Ogre::Vector3(-size/2, 0, -size/2));

This sets the bounding volume for our object and defines the origin of our region to be the top-left point.

Now we will prepare the actual build. We are going to loop through points on the floor of our region and place a grass patch with a random offset at each point.

for (int x = -size/2; x < size/2; x += (size / amount))
{
  for (int z = -size/2; z < size/2; z += (size / amount))
  {
    Ogre::Real offset = size / (float)amount / 2;

    Ogre::Vector3 pos(
      x + Ogre::Math::RangeRandom(-offset, offset),
      0,
      z + Ogre::Math::RangeRandom(-offset, offset));

    Ogre::Vector3 scale(1, Ogre::Math::RangeRandom(0.9, 1.1), 1);

We've partitioned the floor of our region into enough sections to fit all of our grass patches. The first thing we do is calculate an offset that will be used to randomly nudge each grass patch. This will help it look a little more natural. We use this offset to define a position vector for our object. To do this, we use the RangeRandom method from the Ogre::Math namespace. This method returns a random number that lies between its two parameters. We then use the same method to create a randomized scale vector to add some more variety to our grass. Always remember to play around with numbers like these to create surprising effects in your scene. Some great game mechanics have been discovered by doing exactly this.

^   Ogre::Quaternion quat;
    quat.FromAngleAxis(
      Ogre::Degree(Ogre::Math::RangeRandom(0, 359)),
      Ogre::Vector3::UNIT_Y);

This is the same process we used to rotate our walking robot, except we are providing a random angle between 0 and 359 degrees. Our three quads were already rotated 60 degrees from each other, but now we are randomly rotating the entire grass patch as a whole. All of these little things help the scene look a bit more natural.

^   sg->addEntity(grass, pos, quat, scale);
  }
}

The last thing we do is add a new grass entity to our scene using all of the information we've just set up. We then call the build method to construct our StaticGeometry object.

sg->build();

When defining static geometry, you will either use addEntity or addSceneNode. The addSceneNode method adds all of the entities attached to that scene node to the static geometry. It uses the positions, orientations, and scales of the child nodes instead of requiring you to specify them manually. When using addSceneNode, be sure to remove the scene node from its previous parent. If you do not, Ogre will render them both.

Compile and run your application. You should now see a robot standing in a small patch of grass.

Modifying StaticGeometry

Once you have created a StaticGeometry object, you are not supposed to do much more with it. After all, that's the entire point behind static geometry - it's supposed to be static. As mentioned before, you can use certain tricks to do things like grass waving in the wind. If you are interested in how to do this, then take a look at the Grass Demo that comes with Ogre. The GrassListener::waveGrass method adds wave-like behavior to our grass meshes.

Advanced Object Batching

This is just the beginning of the batching technique. StaticGeometry objects are useful for grouping together many things that will not move. But if you're trying to create something more massive, like a forest or a large terrain covered in grass, then you should look into more advanced batching techniques. A good place to start is the PagedGeometry Engine.

Conclusion

This tutorial introduced the concept of batching through the StaticGeometry object. If we have a collection of entities in our scene that will not be moved, then Ogre can perform an optimization by rendering them in "batches". Modern GPUs are designed to render an enormous amount of triangles at once. We are able to take advantage of this through the batching techniques used by a StaticGeometry object. Much more advanced batching techniques have also been developed to further optimize much larger collections of static objects. These techniques even allow for essentially infinite worlds.

Exercises

Easy

  1. Get your robot animated and walking around in our new patch of grass.

Intermediate

  1. Use CEGUI to allow the user to reload a new StaticGeometry object with different values used to create it (i.e. let them increase the height of the grass). Remember, you won't be able to directly change the StaticGeometry, but you can remove it from the scene and replace it with a new one after the user chooses a new grass height.

Difficult

  1. Read through the source for Ogre's Grass Demo, and use it to implement the waving grass feature.

Advanced

  1. Try to implement a system that fills a terrain with these grass patches.
  2. Extend the previous exercise by having the grass patches only added to the scene when they are within a certain distance of the camera, and then remove them when the camera moves away.

Full Source

The full source for this tutorial is here.

Next

Intermediate Tutorial 6


Alias: Intermediate_Tutorial_5