Skip to main content

History: Intermediate Tutorial 1

Preview of version: 182

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


Introduction

This first tutorial will cover how to animate an entity walking between predefined points. We will use quaternion rotation to keep the entity facing in the direction it is moving. As you read through the tutorial, you should be slowly adding code to your own project.

The full source for this tutorial is here.

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

robot_walk_visual.png

Prerequisites

This tutorial assumes that you already know how to set up an Ogre project and compile it successfully. It will also make use of the STL deque data structure. No prior knowledge of deque is required, but you should at least understand what templates are. If you are unfamiliar with the STL, then the STL Pocket Reference [ISBN 0-596-00556-3] is recommended. You can read the first part of it for free here.

Setting up the Scene

First, let's add some new variables to BasicApp.h. Add these to the Tutorial Section of the header:

Copy to clipboard
////////////////////// // Tutorial Section // ////////////////////// std::deque<Ogre::Vector3> mWalkList; Ogre::Real mDistance; Ogre::Real mWalkSpd; Ogre::Vector3 mDirection; Ogre::Vector3 mDestination; Ogre::AnimationState* mAnimationState; Ogre::Entity* mEntity; Ogre::SceneNode* mNode;

Then add these initializations to the constructor:

Copy to clipboard
mDistance(0), mWalkSpd(70.0), mDirection(Ogre::Vector3::ZERO), mDestination(Ogre::Vector3::ZERO), mAnimationState(0), mEntity(0), mNode(0)

Let's start by setting the ambient light to full so that we can clearly see the objects we put in our scene. Add the following to the beginning of createScene:

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

We will now create a robot entity. The first call creates the entity, and the second creates a scene node which will attach the entity to our scene. The last line actually attaches the entity to the scene node.

Copy to clipboard
mEntity = mSceneMgr->createEntity("robot.mesh"); mNode = mSceneMgr->getRootSceneNode()->createChildSceneNode( Ogre::Vector3(0, 0, 25.0)); mNode->attachObject(mEntity);

We will now give the robot a path to walk. This is what we'll use the deque ("deck") for. It is the STL implementation of a double-ended queue. It's like a queue, but you can efficiently add objects to either the front or back of the collection. We will be using the push_front and push_back methods to add objects to either the front or back of the deque. The front and back methods are used to return those values without removing them, and the pop_front and pop_back methods are used to remove those values from the deque (they do not return these values). Finally, the empty method returns true when the deque is...well, empty.

This code will add three vectors to our deque. These will later be used as waypoints on the robot's walking path.

Copy to clipboard
mWalkList.push_back(Ogre::Vector3(550.0, 0, 50.0)); mWalkList.push_back(Ogre::Vector3(-100.0, 0, -200.0)); mWalkList.push_back(Ogre::Vector3(0, 0, 25.0));

Next, we want to place some objects in the scene so we can see the robot's path. We make the y components of these objects negative so the robot will stand on top of them rather than walking into them.

Copy to clipboard
Ogre::Entity* ent; Ogre::SceneNode* node; ent = mSceneMgr->createEntity("knot.mesh"); node = mSceneMgr->getRootSceneNode()->createChildSceneNode( Ogre::Vector3(0, -10.0, 25.0)); node->attachObject(ent); node->setScale(0.1, 0.1, 0.1); ent = mSceneMgr->createEntity("knot.mesh"); node = mSceneMgr->getRootSceneNode()->createChildSceneNode( Ogre::Vector3(550.0, -10.0, 50.0)); node->attachObject(ent); node->setScale(0.1, 0.1, 0.1); ent = mSceneMgr->createEntity("knot.mesh"); node = mSceneMgr->getRootSceneNode()->createChildSceneNode( Ogre::Vector3(-100.0, -10.0,-200.0)); node->attachObject(ent); node->setScale(0.1, 0.1, 0.1);

Finally, we will position the camera to get a good view of the scene.

Copy to clipboard
mCamera->setPosition(90.0, 280.0, 535.0); mCamera->pitch(Ogre::Degree(-30.0)); mCamera->yaw(Ogre::Degree(-15.0));

Make sure this compiles before continuing.

Animation

We are now going to set up animation for the robot. We will get an animation state from the robot entity, set its options, and then enable it. We will then need to update the animation state based on how much time has passed since the last frame.

Add the following code right after the camera positioning in createScene:

Copy to clipboard
mAnimationState = mEntity->getAnimationState("Idle"); mAnimationState->setLoop(true); mAnimationState->setEnabled(true);

The first line gets the animation state from the entity. The second line makes the animation repeat. For certain animations, like the robot's death animation, we would set this to false. A robot should only die once, but until that day, he can walk on. The last line makes the animation state active.

You might be wondering where 'Idle' came from. Each mesh can have its own set of animations defined for it. Ogre+Meshy can be used to view Ogre meshes and their animations. Or you can create them from models from scratch by using a program like Blender and then installing the Ogre exporter.

Finally, we need to update the animation state based on the elapsed frame time. Find the frameRenderingQueued method, and add this line right after the keyboard and mouse are captured:

Copy to clipboard
mAnimationState->addTime(fe.timeSinceLastFrame);

This will get the elapsed time from the frame event reference and use it to update the animation state. Compile and run the application again. You should see a robot using its Idle animation.

Moving the Robot

We will now make the robot walk. Let's look at some of the variables we will be using. First, we will store the direction the robot is moving in a Vector3 called mDirection. We will store the robot's current destination in a Vector3 called mDestination. We will store the distance the robot has to reach its current destination in mDistance. Finally, we will store the robot's walking speed in a Real called mWalkSpd. In the constructor, we set the mWalkSpd to be 70 units per second.

Now we need to make the robot start its walking animation. But we only want it to start walking if it has a location left in its mWalkList. The nextLocation method will be used to determine this. Add the following code to frameRenderingQueued just before the addTime call:

Copy to clipboard
if (mDirection == Ogre::Vector3::ZERO) { if (nextLocation()) { mAnimationState = mEntity->getAnimationState("Walk"); mAnimationState->setLoop(true); mAnimationState->setEnabled(true); } }

If you compile and run the application now, the robot will walk in place. This is because the robot starts out with a direction vector of ZERO, and our nextLocation method always returns true. We will fix that soon.

We are going to add code to actually move the robot to its next destination. We will move the robot a small amount each frame based on how much time has elapsed since the last frame. This will ensure the robot moves at the same speed across the screen regardless of how fast your computer renders the frames. Basically, if your computer renders frames really fast, then we would want the robot to move relatively small distances each frame. Whereas, if your computer renders frames slow, then we would want to move the robot farther each frame so it could keep up.

For now, we're safe simply multiplying the elapsed time by our speed to smooth things out (try temporarily removing the multiplication to see how things change). We will also update the distance the robot has left to move by subtracting how far it moves in this frame from the total. Add the following code to frameRenderingQueued, right after the if statement and before the addTime call:

Copy to clipboard
else { Ogre::Real move = mWalkSpd * evt.timeSinceLastFrame; mDistance -= move;

Next we need to check to see if we've arrived at our destination (or moved slightly past it). If the distance left is less than or equal to 0, then we set the position of the entity's SceneNode to our destination vector, so the robot sits exactly on its destination. And then we set its current direction to be the ZERO vector, because if there is no further locations returned by nextLocation, then the robot is done walking its path.

Copy to clipboard
^ if (mDistance <= 0) { mNode->setPosition(mDestination); mDirection = Ogre::Vector3::ZERO;

If the robot has completed one leg of its journey, we need to look for another destination. If we find one, then we should rotate the robot to face its next destination. If there are no more locations in the robot's path, then we will return it to the Idle animation.

Copy to clipboard
^ if (nextLocation()) { // rotation code will go here } else { mAnimationState = mEntity->getAnimationState("Idle"); mAnimationState->setLoop(true); mAnimationState->setEnabled(true); } }

We do not need to set the walking animation again, because the robot will already be in this state. However, the robot will most likely not be facing in the right direction, so we will have to rotate it. We will come back to the rotation.

That takes care of when the robot is very close to its destination. In the else statement, we will take care of the case where the robot is still walking along the path. All we have to do is translate the robot along its current direction vector by an amount proportional to the move value we calculated.

Copy to clipboard
^ else { mNode->translate(move * mDirection); } }

The next thing we need to do is fill in the nextLocation method. This will set up the variables the robot needs to correctly follow its path. The nextLocation method will return false when there are no points left to walk to. Add the following code to nextLocation:

Copy to clipboard
if (mWalkList.empty()) return false;

Next we are going to get another destination vector from the deque. We will set the direction vector relative to our current position by subtracting the SceneNode's current position from the direction vector we pull out of the deque (remember to get a vector from X to Y, you subtract the vector X from Y).

Copy to clipboard
mDestination = mWalkList.front(); mWalkList.pop_front(); mDirection = mDestination - mNode->getPosition();

We have another problem, though. We multiply the direction vector by the move value in frameRenderingQueued. For everything to work out, we need the mDestination vector to have a length of one (a "unit vector"). The vector operation that does this is called normalise. This method also returns the vector's length before normalisation. This is very useful, because this length is exactly the distance we need for our robot's mDistance variable.

Copy to clipboard
mDistance = mDirection.normalise();

That completes the nextLocation method. You may want to read this method a few times. It is short, but a lot is going on.

You can compile and run the code. The robot is walking! But he is not turning. We will now add in the rotation code. We need to get the direction the robot is facing and then rotate it. Add the following code to where our placeholder comment was in the previous step:

Copy to clipboard
Ogre::Vector3 src = mNode->getOrientation() * Ogre::Vector3::UNIT_X; Ogre::Quaternion quat = src.getRotationTo(mDirection); mNode->rotate(quat);

Quaternions were briefly mentioned in Basic Tutorial 4, but this is the first real use of them. Quaternions can be thought of as representations of rotations in 3 dimensional space. They are used to keep track of orientation and rotation in modern 3d rendering.

In the first line we call getOrientation, which returns a Quaternion representing the robot's current orientation in space (which direction it is facing). This is still just an abstract representation. We need to multiply it by the UNIT_X vector so that it "knows" which direction we consider the robot to be facing by default. The second line builds a Quaternion that represents a rotation from the robot's current direction towards its next destination.

It's completely alright if this is confusing. Quaternions are a rather tricky subject, but you can learn how to use them without needing to understand too much about why they work. Just keep in mind that they represent orientation and rotations. That's enough for now.

There is one problem with our code. There is a special case where the rotation will fail. If we are trying to rotate the robot exactly 180 degrees, then the rotatation code will throw a divide by zero error. In order to fix that we will deal with the special case separately. Two vectors are 180 degrees apart exactly when their dot product is -1. We can use this to determine when we are attempting a 180 degree rotation. In that case, we will simply use the yaw method to manually turn the SceneNode by 180 degrees.

Copy to clipboard
Ogre::Vector3 src = mNode->getOrientation() * Ogre::Vector3::UNIT_X; if ((1.0 + src.dotProduct(mDirection)) < 0.0001) { mNode->yaw(Ogre::Degree(180)); } else { Ogre::Quaternion quat = src.getRotationTo(mDirection); mNode->rotate(quat); }

Notice that we are not directly comparing the dot product of the two vectors to zero. This is because there are inherent limitations to floating point numbers. These limitations can mean that a dot product that "should" equal 0 is actually a number very close to zero, but not zero. To account for this, we simply check to see if the sum of one and the dot product is close enough to zero for our purposes (since 1 + -1 = 0).

It should be clear by now that a minimum understanding of linear algebra will be helpful for any 3d graphics programmer. If you would like to read up more on the subject, then take a look at the Quaternion and Rotation Primer.

Compile and run the application. We should now have a robot that walks its path and faces the right direction.

Exercises

Easy

  1. Add new points to the robot's path. Also add a new knot for each new location so you can track the robot's progress.
  2. When a robot has come to the end of its journey, then it must die to make room for another generation of path-walkers. Have the robot perform its death animation when it is done walking. The animation name is 'Die'.

Intermediate

  1. The variable mWalkSpd is set once and never changed. In the name of good practice, change mWalkSpeed to a constant static class variable.
  2. It is ugly to track whether the robot is walking or not by comparing the mDirection vector to the ZERO vector. It would be better if created a boolean flag called mWalking to keep track of this.

Difficult

  1. One of the limits to our class is that points can't be added to the robot's path after we've created the object. Fix this problem by implementing a new method which takes a Vector3 and adds it to the mWalkList deque. (Hint: If the robot is still walking, then you only have to add the point to the end of the deque. If the robot has finished walking, you will need to call nextLocation to get the robot walking again.)

Advanced

  1. Another limitation of our class is that it only animates one robot. Reimplement the class so that it can control any number of robots around the path. (Hint: You should create another class that completely controls the animation of a single robot. Then store some of these robot animation objects in a STL map so that you can retreive them for animation.) Bonus points for doing this without adding any more frame listeners.
  2. If you were successful in the previous question and you created robots moving at different speeds, then you now know that robots can walk right through each other. Fix this by implementing some type of pathfinding function or by adding some collision detection to prevent them from walking through each other.

Conclusion

In this tutorial, we setup the AnimationState for our robot Entity. By getting a reference to our Entity's AnimationState, we were able to set animation options like looping, and we were able to choose between animations that were defined in our Ogre mesh.

We introduced the STL deque to represent a list of points for the robot's walking path. The C++ Standard Template Library is something you will most likely see a lot of as a c++ graphics programmer. It is worth understanding well. The deque is a double-ended queue. It has an efficient implementation of adding objects to the front and back of the collection and those objects can later be popped off from either side.

We also covered our first significant use of Quaternions for modeling rotations. Quaternions are a rich and interesting subject, but the main point to remember is that they are used in 3d rendering to represent rotations. They are similar to matrices, as we saw when we multiplied them together with vectors to get other, transformed vectors. We used Quaternions to rotate our robot as it walked along its path.

Full Source

The full source for this tutorial is here.

Next

Intermediate Tutorial 2


Alias: Intermediate_Tutorial_1

History

Information Version
Mon 30 of Mar, 2015 22:03 GMT-0000 kabbotta 196
Mon 30 of Mar, 2015 22:02 GMT-0000 kabbotta 195
Mon 30 of Mar, 2015 21:56 GMT-0000 kabbotta 194
Mon 30 of Mar, 2015 21:55 GMT-0000 kabbotta 193
Mon 30 of Mar, 2015 21:55 GMT-0000 kabbotta 192
Mon 30 of Mar, 2015 09:12 GMT-0000 kabbotta 191
Mon 30 of Mar, 2015 07:47 GMT-0000 kabbotta 190
Mon 30 of Mar, 2015 04:30 GMT-0000 kabbotta 189
Mon 30 of Mar, 2015 04:26 GMT-0000 kabbotta 188
Mon 30 of Mar, 2015 04:22 GMT-0000 kabbotta 187
Mon 30 of Mar, 2015 04:21 GMT-0000 kabbotta 186
Mon 30 of Mar, 2015 04:06 GMT-0000 kabbotta 185
Mon 30 of Mar, 2015 03:19 GMT-0000 kabbotta 184
Mon 30 of Mar, 2015 03:10 GMT-0000 kabbotta 183
Mon 30 of Mar, 2015 03:02 GMT-0000 kabbotta 182
Mon 30 of Mar, 2015 02:46 GMT-0000 kabbotta 181
Mon 30 of Mar, 2015 02:44 GMT-0000 kabbotta 180
Mon 30 of Mar, 2015 02:41 GMT-0000 kabbotta 179
Mon 30 of Mar, 2015 02:39 GMT-0000 kabbotta 178
Mon 30 of Mar, 2015 02:38 GMT-0000 kabbotta 177
Mon 30 of Mar, 2015 02:38 GMT-0000 kabbotta 176
Mon 30 of Mar, 2015 02:36 GMT-0000 kabbotta 175
Mon 30 of Mar, 2015 02:35 GMT-0000 kabbotta 174
Mon 30 of Mar, 2015 01:09 GMT-0000 kabbotta 173
Sun 29 of Mar, 2015 22:34 GMT-0000 kabbotta 172