History: Intermediate Tutorial 1
Preview of version: 122
- «
- »
Any problems you encounter during working with this tutorial should be posted in the Help Forum
.
Table of contents
Intermediate Tutorials Introduction
For the intermediate tutorial series we will be leaving behind the BaseApplication framework. Now that we have a basic understanding of Ogre from the first tutorial series, we are going to want to be able to manipulate many of the working pieces in our application. We will now use a single source file that is a slightly simplified version of the basic tutorial framework.
You should not organize your projects like this when you begin making your own Ogre applications. Having everything in one enormous class is terrible object-oriented programming. This is just for the purpose of the tutorials.
Here is base code you should start each tutorial with. It might look overwhelming, but that is just because we were hiding some things in BaseApplication before.
Note: During these tutorials, when a piece of code is broken up over more than one code block, a caret (upwards arrow) will be used to force the wiki to indent it correctly and to serve as a reminder that the code is continuing a previous example. This ^ should not be included in your code. It is soley for formatting.
An example:
if (skyIsBlue) { sunshineOnMyFace = true;
Later...
^ if (seeCloud) { enjoyItWhileICan(); } }
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 the code to your own project.
The finished source code for this tutorial is here.
Note: There is also source code using the BaseApplication framework available here.
Prerequisites
This tutorial assumes that you already know how to set up an Ogre project and compile it successfully. It also makes 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 "STL Pocket Reference" here
Setting up the Scene
First, let's add some new variables to BasicApp.h. Add these to the Tutorial Section of the header:
Ogre::Real mDistance; Ogre::Real mWalkSpd; Ogre::Vector3 mDirection; Ogre::Vector3 mDestination; Ogre::AnimationState* mAnimationState; Ogre::Entity* mEntity; Ogre::SceneNode* mNode; std::deque<Ogre::Vector3> mWalkList;
Then add these initializations to the end of the constructor's initalization list:
mDistance(0), mWalkSpd(70.0), mDirection(Ogre::Vector3::ZERO), mDestination(Ogre::Vector3::ZERO), mAnimationState(0), mEntity(0), mNode(0)
Let's set the ambient light to full so that we can clearly see the objects we put on screen. Add the following code to createScene:
mSceneMgr->setAmbientLight(Ogre::ColourValue(1.0, 1.0, 1.0));
We will now create a robot entity. The first line creates the Entity, and the second line creates a SceneNode to attach it to the scene. The last line attaches the Entity to the SceneNode.
mEntity = mSceneMgr->createEntity("Robot", "robot.mesh"); mNode = mSceneMgr->getRootSceneNode()->createChildSceneNode( "RobotNode", 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 for. The deque ("deck") 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 the values without removing them, and the pop_front and pop_back methods are used to remove 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 two vectors to the deque, which will later be used as waypoints on the robot's walking path.
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)); mWalkList.push_back(Ogre::Vector3(550.0, 0, 50.0));
Next, we want to place some objects in the scene so we can see the robot's path. Notice the y component of their positions is negative. This makes it so the robot will stand on top of them, instead of walking into them.
Ogre::Entity* ent; Ogre::SceneNode* node; ent = mSceneMgr->createEntity("Knot1", "knot.mesh"); node = mSceneMgr->getRootSceneNode()->createChildSceneNode( "Knot1Node", Ogre::Vector3(0, -10.0, 25.0)); node->attachObject(ent); node->setScale(0.1, 0.1, 0.1); ent = mSceneMgr->createEntity("Knot2", "knot.mesh"); node = mSceneMgr->getRootSceneNode()->createChildSceneNode( "Knot2Node", Ogre::Vector3(550.0, -10.0, 50.0)); node->attachObject(ent); node->setScale(0.1, 0.1, 0.1); ent = mSceneMgr->createEntity("Knot3", "knot.mesh"); node = mSceneMgr->getRootSceneNode()->createChildSceneNode( "Knot3Node", 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.
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 setup animation for the robot. We will get an AnimationState from the robot Entity, set its options, and then enable it. We will then need to update the AnimationState based on how much time has passed since the last frame.
Add the following code right after the camera positioning in createScene:
mAnimationState = mEntity->getAnimationState("Idle"); mAnimationState->setLoop(true); mAnimationState->setEnabled(true);
The first line gets the AnimationState 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 AnimationState 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 Ogre3d meshes and their Animations.
Finally, we need to update the AnimationState based on the elapsed frame time. Find the frameRenderingQueued method, and add this line of code right after the keyboard and mouse are captured:
mAnimationState->addTime(evt.timeSinceLastFrame);
This will get the elapsed time from the FrameEvent reference and use it to update the AnimationState. 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 an 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:
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:
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.
^ 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 before we start it walking again. If there are no more locations in the robot's path, then we will return it to the Idle animation.
^ 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.
^ else { mNode->translate(move * mDirection); } }
We are almost done. The last 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:
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).
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 destination 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 here, because this length is exactly the distance we need for our robot's mDistance variable.
mDistance = mDistance.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:
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 rotate 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.
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 one plus 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 these subjects, 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.
Additional Information
Correctly Walking on Terrain
The robot's rotation code will not work properly if the robot is moving vertically (i.e. walking along a bumpy terrain). You might see unexpected roll and pitch. This can be fixed using ideas and code from the Quaternion and Rotation Primer:
Vector3 mDestination = mWalkList.front( ); Vector3 mDirection = mDestination - mNode->getPosition(); Vector3 src = mNode->getOrientation() * Vector3::UNIT_X; // ignore vertical y components src.y = 0; mDirection.y = 0; // normalise both vectors again src.normalise(); Real mDistance = mDirection.normalise( ); Quaternion quat = src.getRotationTo(mDirection); mNode->rotate(quat);
This should look familiar to our approach in the tutorial. Except the y components of the vectors are being set to zero, and the vectors are being renormalised (adjusted to length one, keeping direction constant).
My Robot is Not Facing the Right Direction.
The robot is turned when it arrives a waypoint. To properly turn the robot in all cases you best put the rotation code to a new function:
void rotateRobotToDirection();
void IntermediateTutorial1::rotateRobotToDirection() { Ogre::Vector3 src = mNode->getOrientation() * Ogre::Vector3::UNIT_X; // remove the y components src.y = 0; mDirection.y = 0; // normalise both vectors again src.normalise(); Ogre::Real mDistance = mDirection.normalise( ); Ogre::Quaternion quat = src.getRotationTo(mDirection); mNode->rotate(quat); }
Now you can use this function where we placed the rotation code in frameRenderingQueued. And you can add this function here to make sure the robot begins its journey facing the correct direction.
if (mDirection == Ogre::Vector3::ZERO) { if (nextLocation()) { // Set walking animation mAnimationState = mEntity->getAnimationState("Walk"); mAnimationState->setLoop(true); mAnimationState->setEnabled(true); rotateRobotToDirection(); } } else //...
Exercises for Further Study
Easy Questions
- 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.
- 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 Questions
- The variable mWalkSpd is set once and never changed. In the name of good practice, change mWalkSpeed to a constant static class variable.
- 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 Questions
- 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.)
Expert Questions
- 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.
- 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
Here is the full source for this tutorial: IntermediateTutorial1Source.
Next
Proceed to Intermediate Tutorial 2 : RaySceneQueries and Basic Mouse Usage
Alias: Intermediate_Tutorial_1