Intermediate Tutorial 1

From PyWiki

Jump to: navigation, search

Contents

Intermediate Tutorial 1: Animation, Walking Between Points, and Basic Quaternions

Introduction

In this tutorial we will be covering how to take an Entity, animate it, and have it walk between predefined points. This will also cover the basics of Quaternion rotation by showing how to keep the Entity facing the direction it is moving. As you go through the demo you should be slowly adding code to your own project and watching the results as we build it.

Prerequisites

This tutorial assumes you have knowledge of Python programming and you have already installed Python-Ogre. This tutorial builds on the material covered in previous tutorials, and it assumes you have read them.

Getting Started

As with the previous tutorials, we will be using a pre-constructed code base as our starting point. Create a file named intermediate_1.py:

#!/usr/bin/env python 
# This code is Public Domain. 
"""Python-Ogre Intermediate Tutorial 01: Animation, Walking Between Points, and Basic Quaternions.""" 
 
 import ogre.renderer.OGRE as ogre 
 import SampleFramework as sf
 
 class TutorialFrameListener(sf.FrameListener):
    """A FrameListener class that handles basic user input."""
 
    def __init__(self, win, cam, sc, ent, walk):
       # Subclass any Python-Ogre class and you must call its constructor.
       sf.FrameListener.__init__(self, win, cam)
 
       self.entity = ent # The entity we are animating
       self.node = sc # The SceneNode that the Entity is attached to
       self.walklist = walk # The list of points we are walking to
 
    def nextLocation(self):
       return True
 
    def frameStarted(self, evt):
       return sf.FrameListener.frameStarted(self, evt)  
 
 class TutorialApplication(sf.Application): 
    """Application class.""" 
 
    def _createScene(self):
       sceneManager = self.sceneManager 
 
    def _createCamera(self):
       self.camera =  self.sceneManager.createCamera('PlayerCam')
 
    def _createFrameListener(self):
       self.frameListener = TutorialFrameListener(self.renderWindow,
                                                    self.camera,
                                                    self.node,
                                                    self.entity,
                                                    self.walklist
                                                    )
       self.root.addFrameListener(self.frameListener)
       self.frameListener.showDebugOverlay(True)
 
 if __name__ == '__main__': 
    ta = TutorialApplication() 
    ta.go()

Note that this program won't run yet, we have to add a few more lines of code before it will work.

Setting up the Scene

Before we begin, notice that we have 3 extra variables in TutorialApplication that we wish to pass to our Frame Listener, self.entity will hold the entity we create, self.node will hold the node we create, and self.walkList will contain all the points we wish the object to walk to.

Go to the TutorialApplication._createScene method and add the following code to it. First we are going to set the ambient light to full so that we can see objects we put on the screen.

# Setup the ambient light. 
sceneManager.ambientLight = (1.0, 1.0, 1.0)

Next we will create a Robot on the screen so that we can play with him. To do this we will create the entity for the Robot, then create a SceneNode for him to dangle from.

# Create the entity
self.entity = sceneManager.createEntity('Robot', 'robot.mesh') 
 
# Create the scene node and attach the entity to it
self.node = sceneManager.getRootSceneNode().createChildSceneNode('RobotNode', (0, 0, 25)) 
self.node.attachObject(self.entity)

This all should be very basic, so I will not go into detail about any of it. In the next chunk of code, we are going to tell the robot where he needs to be moved to. To store the waypoints for the robot we will be using a Python Array. This code adds two Vectors to the array, which we will later make the robot move to.

# Create the walking list
self.walklist = []
self.walklist.append(ogre.Vector3(550, 0, 50))
self.walklist.append(ogre.Vector3(-100, 0, -200))

Next, we want to place some objects on the scene to show where the robot is supposed to be moving to. This will allow us to see the robot moving with respect to other objects on the screen. Notice the negative Y component to their position. This puts the objects under where the robot is moving to, and he will stand on top of them when he gets to the right spot.

# Add some objects so we can see movement
knotEnt = sceneManager.createEntity('Knot1', 'knot.mesh')
knotNode = sceneManager.getRootSceneNode().createChildSceneNode('Knot1Node', (0, -10, 25))
knotNode.attachObject(knotEnt)
knotNode.scale = (.1, .1, .1)
 
knotEnt = sceneManager.createEntity('Knot2', 'knot.mesh')
knotNode = sceneManager.getRootSceneNode().createChildSceneNode('Knot2Node', (550, -10, 50))
knotNode.attachObject(knotEnt)
knotNode.scale = (.1, .1, .1)
 
knotEnt = sceneManager.createEntity('Knot3', 'knot.mesh')
knotNode = sceneManager.getRootSceneNode().createChildSceneNode('Knot3Node', (-100, -10, -200))
knotNode.attachObject(knotEnt)
knotNode.scale = (.1, .1, .1)

Finally, we want to set the camera to a good viewing point to see this from. We will move the camera to get a better position. Add the following lines to the end of the _createCamera method:

# Set the camera to look at our handiwork
self.camera.position = (90, 280, 535)
self.camera.pitch(ogre.Degree(-30))
self.camera.yaw(ogre.Degree(-15))

Now run the program. You should see something like this:

tutorial_01.png

Before continuing to the next section, make note of the constructor of TutorialFrameListener, as invoked in the first line of the TutorialApplication._createFrameListener method. We are passing in the SceneNode, Entity, and walklist array along with the standard BaseFrameListener arguments.

Animation

We are now going to setup some basic animation. Animation in Ogre is very simple. To do this, you need to get the AnimationState from the Entity object, set its options, and enable it. This will make the animation active, but you will also need to add time to it after each frame in order for the animation to run. We'll take this one step at a time. First, go to TutorialFrameListener's constructor and add the following code:

# Set idle animation
self.animationState = ent.getAnimationState('Idle')
self.animationState.setLoop(True)
self.animationState.setEnabled(True)

The second line gets the AnimationState out of the entity. In the third line we call setLoop(True), which makes the animation loop over and over. For some animations (like the death animation), we would want to set this to false instead. The fourth line actually enables the Animation. But wait...where did we get €œIdle from? How did this magic constant slip in there? Every mesh has their own set of Animations defined for them. In order to see all of the Animations for the particular mesh you are working on, you need to download the OgreMeshViewer and view the mesh from there.

Now, if we run the demo we see...nothing has changed. This is because we need to update the animation state with a time every frame. Find the TutorialFrameListener._frameStarted method, and add this line of code at the beginning of the function:

self.animationState.addTime(evt.timeSinceLastFrame)

Now run the application. You should see a robot performing his idle animation standing in place.

Moving the Robot

Now we are going to perform the tricky task of making the robot walk from point to point. Before we begin I would like to describe the variables that we are storing in the TutorialFrameListener class. We are going to use 4 variables to accomplish the task of moving the robot. First of all, we are going to store the direction the robot is moving in self.direction. We will store the current destination the Robot is traveling to in self.destination. We will store the distance the robot has left to travel in self.distance. Finally, we will store the robot's moving speed in self.walkSpeed.

The first thing we need to do is to set up the class's variables. We'll set the walk speed to 35 units per second. There is one big thing to note here. We are explicitly setting self.direction to be the ZERO vector because later we will use this to determine if we are moving the Robot or not. Add these lines to TutorialFrameListener.__init__ method:

# Set default values for variables
self.walkSpeed = 35.0 # The speed at which the object is moving
self.direction = ogre.Vector3().ZERO # The direction the object is moving

Now that this is done, we need to set the robot in motion. To make the robot move, we simply tell it to change animations. However, we only want to start the robot moving if there is another location to move to. For this reason we call the nextLocation method. Add this code to the top of the TutorialFrameListner._frameStarted method just before the animationState.addTime call:

if self.direction == ogre.Vector3().ZERO:
   if self.nextLocation():
      # Set walking animation
      self.animationState = self.entity.getAnimationState('Walk')
      self.animationState.setLoop(True)
      self.animationState.setEnabled(True)

If you run the program right now, the robot will walk in place. This is because the robot starts out with a direction of ZERO and our nextLocation method always returns true. In later steps we will be adding a bit more intelligence to the nextLocation method.

Now we are going to actually move the robot in the scene. To do this we need to have him move a small bit every frame. Go to the TutorialFrameListener._frameStarted method. We will be adding the following code just after our previous if statement and just above the animationState.addTime call. This code will handle the case when the robot is actually moving; self.direction != ogre.Vector3.ZERO.

else:
   move = self.walkSpeed * evt.timeSinceLastFrame;
   self.distance -= move

Now, we need to check and see if we are going to "overshoot" the target position. That is, if self.distance is now less than zero, we need to "jump" to the point and set up the move to the next point. Note that we are setting self.direction to the ZERO vector. If the nextLocation method does not change self.direction (IE there is nowhere left to go) then we no longer have to move around.

if self.distance <= 0.0:
   self.node.setPosition(self.destination)
   self.direction = ogre.Vector3().ZERO

Now that we have moved to the point, we need to setup the motion to the next point. Once we know if we need to move to another point or not, we can set the appropriate animation; walking if there is another point to go to and idle if there are no more destination points. This is a simple matter of setting the Idle animation if there are no more locations.

if not self.nextLocation():
   # Set Idle animation
   self.animationState = self.entity.getAnimationState('Idle')
   self.animationState.setLoop(True)
   self.animationState.setEnabled(True)

Note that we have no need to set the walking animation again if there are more points in the queue to walk to. Since the robot is already walking there is no reason to tell him to do so again. However, if the robot needs to go to another point, then we need to rotate him to face that point.

This takes care of when we are very close to the target position. Now we need to handle the normal case, when we are just on the way to the position but we're not there yet. To do that we will translate the robot in the direction we are travelling, and move it by the amount specified by the move variable. This is accomplished by adding the following code:

else:
   self.node.translate(self.direction * move)

We are almost done. Our code now does everything except set up the variables required for movement. If we can properly set the movement variables our Robot will move like he is supposed to. Find the TutorialFrameListener.nextLocation method. This method returns false when we run out of points to go to. This will be the first line of our method. (Note you should leave the return true statement at the bottom of the method.)

if len(self.walklist) == 0:
   return False

Now we need to set the variables. First we will pop the destination vector from the walklist array. We will set the direction vector by subtracting the SceneNode's current position from the destination. We have a problem though. Remember how we multiplied self.direction by the move amount in frameStarted? If we do this, we need the direction vector to be a unit vector (that is, it's length equals one). The normalise function does this for us, and returns the old length of the vector. Handy that, since we need to also set the distance to the destination.

self.destination = self.walklist.pop()
self.direction = self.destination - self.node.getPosition()
self.distance = self.direction.normalise()

Now run the program. It works! Sorta. The robot now walks to all the points, but he is always facing the Vector3::UNIT_X direction (his default). We will need to change the direction he is facing when he is moving towards points.

What we need to do is get the direction the Robot is facing, and use rotate function to rotate the object in the right position. Insert the following code before the return True line in the nextLocation method. This will rotate the robot to face the right away after a new location is selected. The first line gets the direction the Robot is facing. The second line builds a Quaternion representing the rotation from the current direction to the destination direction. The third line actually rotates the Robot.

src = self.node.getOrientation() * ogre.Vector3().UNIT_X
quat = src.getRotationTo(self.direction)
self.node.rotate(quat)

We briefly mentioned Quaternions in Basic Tutorial 4, but this is the first real use we have had for them. Basically speaking, Quaternions are representations of rotations in 3 dimensional space. They are used to keep track of how the object is positioned in space, and may be used to rotate objects in Ogre. In the first line we call the getOrientation method, which returns a Quaternion representing the way the Robot is oriented in space. Since Ogre has no idea which side of the Robot is the "front" of the robot, we must multiply this orientation by the UNIT_X vector (which is the direction the robot "naturally" faces) to we obtain the direction the robot is currently facing. We store this direction in the src variable. In the second line, the getRotationTo method gives us a Quaternion that represents the rotation from the direction the Robot is facing to the direction we want him to be facing. In the third line, we rotate the node so that it faces the new orientation.

There is only one problem with the code we have created. There is a special case where SceneNode.rotate will fail. If we are trying to turn the robot 180 degrees, the rotate code will bomb with a divide by zero error. In order to fix that, we will test to see if we are performing a 180 degree rotation. If so, we will simply yaw the robot by 180 degrees instead of using rotate. To do this, delete the three lines we just put in and replace them with this:

src = self.node.getOrientation() * ogre.Vector3().UNIT_X
if 1.0 + src.dotProduct(self.direction) < 0.0001:
   self.node.yaw(ogre.Degree(180))
else:
   quat = src.getRotationTo(self.direction)
   self.node.rotate(quat)

All of this should now be self explanatory except for what is wrapped in that if statement. If two unit vectors oppose each other (that is, the angle between them is 180 degrees), then their dot product will be -1. So, if we dotProduct the two vectors together and the result equals -1.0f, then we need to yaw by 180 degrees, otherwise we use rotate instead. Why do I add 1.0f and check to see if it is less than 0.0001f? Don't forget about floating point rounding error. You should never directly compare two floating point numbers. Finally, note that in this case the dot product of these two vectors will fall in the range [-1, 1]. In case it is not abundantly clear, you need to know at minimum basic linear algebra to do graphics programming! At the very least you should review the Quaternion and Rotation Primer and consult a book on basic vector and matrix operations.

Now our code is complete! Compile and run the demo to see the Robot walk the points he was given.

Exercises for Further Study

Easy Questions

  1. Add more points to the robot's path. Be sure to also add more knots that sit under his position so you can track where he is supposed to go.
  2. Robots who have outlived their usefulness should not continue existing! When the robot has finished walking, have him perform the death animation instead of idle. The animation for death is "Die".

Intermediate Questions

  1. There is something wrong with self.walkSpeed. Did you notice this when going through the tutorial? We only set the value once, and never change it. This should be a constant static class variable. Change the variable so that it is.
  2. The code does something very hacky, and that's track whether or not the Robot is walking by looking at the self.direction vector and comparing it to Vector3.ZERO. It would have been better if we instead had a boolean variable called self.walking that kept track of whether or not the robot is moving. Implement this change.

Difficult Questions

  1. One of the limitations of this class is that you cannot add points to the robot's walking path after you have created the object. Fix this problem by implementing a new method which takes in a Vector3 and adds it to the self.walkList array. (Hint, if the robot has not finished walking you will only need to add the point to the end of the array. If the robot has finished, you will need to make him start walking again, and call nextLocation to start him walking again.)

Expert Questions

  1. Another major limitation of this class is that it only tracks one object. Reimplement this class so that it can move and animate any number of objects independently of each other. (Hint, you should create another class that contains everything that needs to be known to animate one object completely. Store this in a Python dictionary object so that you can retrieve data later based on a key.) You get bonus points if you can do this without registering any additional frame listeners.
  2. After making the previous change, you might have noticed that Robots can now collide with each other. Fix this by either creating a smart path finding function, or detecting when robots collide and stopping them from passing through each other.

Complete Source Code Example

#!/usr/bin/env python 
# This code is Public Domain. 
"""Python-Ogre Intermediate Tutorial 01: Animation, Walking Between Points, and Basic Quaternions.""" 
 
import ogre.renderer.OGRE as ogre 
import SampleFramework as sf
 
class TutorialFrameListener(sf.FrameListener):
   """A FrameListener class that handles basic user input."""
 
   def __init__(self, win, cam, sc, ent, walk):
      # Subclass any Python-Ogre class and you must call its constructor.
      sf.FrameListener.__init__(self, win, cam)
 
      self.entity = ent # The entity we are animating
      self.node = sc # The SceneNode that the Entity is attached to
      self.walklist = walk # The list of points we are walking to
 
      # The current animation state of the object
      self.animationState = ent.getAnimationState('Idle')
      self.animationState.setLoop(True)
      self.animationState.setEnabled(True)
 
      # Set default values for variables
      self.walkSpeed = 35.0 # The speed at which the object is moving
      self.direction = ogre.Vector3().ZERO # The direction the object is moving
      self.distance = 0.0 # The distance the object has left to travel
 
   def nextLocation(self):
      if len(self.walklist) == 0:
         return False
      self.destination = self.walklist.pop()
      self.direction = self.destination - self.node.getPosition()
      self.distance = self.direction.normalise()
 
      src = self.node.getOrientation() * ogre.Vector3().UNIT_X
      if 1.0 + src.dotProduct(self.direction) < 0.0001:
         self.node.yaw(ogre.Degree(180))
      else:
         quat = src.getRotationTo(self.direction)
         self.node.rotate(quat)
 
      return True
 
   def frameStarted(self, evt):
      if self.direction == ogre.Vector3().ZERO:
         if self.nextLocation():
            # Set walking animation
            self.animationState = self.entity.getAnimationState('Walk')
            self.animationState.setLoop(True)
            self.animationState.setEnabled(True)
      else:
         move = self.walkSpeed * evt.timeSinceLastFrame;
         self.distance -= move
         if self.distance <= 0.0:
            self.node.setPosition(self.destination)
            self.direction = ogre.Vector3().ZERO
            if not self.nextLocation():
               # Set Idle animation
               self.animationState = self.entity.getAnimationState('Idle')
               self.animationState.setLoop(True)
               self.animationState.setEnabled(True)
         else:
            self.node.translate(self.direction * move)
      self.animationState.addTime(evt.timeSinceLastFrame)
      return sf.FrameListener.frameStarted(self, evt)  
 
class TutorialApplication(sf.Application): 
   """Application class.""" 
 
   def _createScene(self):        
      # Setup the ambient light. 
      sceneManager = self.sceneManager 
      sceneManager.ambientLight = (1.0, 1.0, 1.0) 
 
      # Create the entity
      self.entity = sceneManager.createEntity('Robot', 'robot.mesh') 
 
      # Create the scene node and attach the entity to it
      self.node = sceneManager.getRootSceneNode().createChildSceneNode('RobotNode', (0, 0, 25)) 
      self.node.attachObject(self.entity) 
 
      # Add some objects so we can see movement
      knotEnt = sceneManager.createEntity('Knot1', 'knot.mesh')
      knotNode = sceneManager.getRootSceneNode().createChildSceneNode('Knot1Node', (0, -10, 25))
      knotNode.attachObject(knotEnt)
      knotNode.scale = (.1, .1, .1)
 
      knotEnt = sceneManager.createEntity('Knot2', 'knot.mesh')
      knotNode = sceneManager.getRootSceneNode().createChildSceneNode('Knot2Node', (550, -10, 50))
      knotNode.attachObject(knotEnt)
      knotNode.scale = (.1, .1, .1)
 
      knotEnt = sceneManager.createEntity('Knot3', 'knot.mesh')
      knotNode = sceneManager.getRootSceneNode().createChildSceneNode('Knot3Node', (-100, -10, -200))
      knotNode.attachObject(knotEnt)
      knotNode.scale = (.1, .1, .1)
 
      # Create the walking list
      self.walklist = []
      self.walklist.append(ogre.Vector3(550, 0, 50))
      self.walklist.append(ogre.Vector3(-100, 0, -200))
 
   def _createCamera(self):
      self.camera =  self.sceneManager.createCamera('PlayerCam')
      self.camera.position = (90, 280, 535)
      self.camera.pitch(ogre.Degree(-30))
      self.camera.yaw(ogre.Degree(-15))
 
   def _createFrameListener(self):
      self.frameListener = TutorialFrameListener(self.renderWindow,
                                                   self.camera,
                                                   self.node,
                                                   self.entity,
                                                   self.walklist
                                                   )
      self.root.addFrameListener(self.frameListener)
      self.frameListener.showDebugOverlay(True)
 
if __name__ == '__main__': 
   ta = TutorialApplication() 
   ta.go()
Python-Ogre Tutorials

Python-Ogre Beginner Tutorials: Beginner 1 - Beginner 2 - Beginner 3 - Beginner 4 - Beginner 5 - Beginner 6 - Beginner 7 - Beginner 8

Intermediate Tutorials: Intermediate 1 - Intermediate 2 - Intermediate 3 - Intermediate 4 - Intermediate 5 - Intermediate 6

Advanced Tutorials: Advanced 1

See also: Artist Tutorials - Ogre Articles - Cookbook

Personal tools