Animating in Panda3D


Panda3D 1.6 Game Engine Beginner's Guide

Panda3D 1.6 Game Engine Beginner's Guide

Create your own computer game with this 3D rendering and game development framework

  • The first and only guide to building a finished game using Panda3D
  • Learn about tasks that can be used to handle changes over time
  • Respond to events like keyboard key presses, mouse clicks, and more
  • Take advantage of Panda3D's built-in shaders and filters to decorate objects with gloss, glow, and bump effects
  • Follow a step-by-step, tutorial-focused process that matches the development process of the game with plenty of screenshots and thoroughly explained code for easy pick up
        Read more about this book      

(For more resources on Panda3D, see here.)

Actors and Animations

An Actor is a kind of object in Panda3D that adds more functionality to a static model. Actors can include joints within them. These joints have parts of the model tied to them and are rotated and repositioned by animations to make the model move and change. Actors are stored in .egg and .bam files, just like models.

Animation files include information on the position and rotation of joints at specific frames in the animation. They tell the Actor how to posture itself over the course of the animation. These files are also stored in .egg and .bam files.

Time for action – loading Actors and Animations

Let's load up an Actor with an animation and start it playing to get a feel for how this works:

  1. Open a blank document in NotePad++ and save it as in the Chapter09 folder.
  2. We need a few imports to start with. Put these lines at the top of the file:
    import direct.directbase.DirectStart
    from pandac.PandaModules import *
    from import Actor
  3. We won't need a lot of code for our class' __init__ method so let's just plow through it here :

    class World:
    def __init__(self):
    base.disableMouse(), -5, 1)
    self.kid = Actor("../Models/Kid.egg",
    {"Walk" : "../Animations/Walk.egg"})

  4. The next thing we want to do is steal our setupLight() method from the Track class and paste it into this class:

    def setupLight(self):
    primeL = DirectionalLight("prime")
    self.dirLight = render.attachNewNode(primeL)
    ambL = AmbientLight("amb")
    self.ambLight = render.attachNewNode(ambL)

  5. Lastly, we need to instantiate the World class and call the run() method.
    w = World()
  6. Save the file and run it from the command prompt to see our loaded model with an animation playing on it, as depicted in the following screenshot:

    Animating in Panda3D

What just happened?

Now, we have an animated Actor in our scene, slowly looping through a walk animation. We made that happen with only three lines of code:

self.kid = Actor("../Models/Kid.egg",
{"Walk" : "../Animations/Walk.egg"})

The first line creates an instance of the Actor class. Unlike with models, we don't need to use a method of loader. The Actor class constructor takes two arguments: the first is the filename for the model that will be loaded. This file may or may not contain animations in it. The second argument is for loading additional animations from separate files. It's a dictionary of animation names and the files that they are contained in. The names in the dictionary don't need to correspond to anything; they can be any string.

myActor = Actor( modelPath,
{NameForAnim1 : Anim1Path, NameForAnim2 : Anim2Path, etc})

The names we give animations when the Actor is created are important because we use those names to control the animations. For instance, the last line calls the method loop() with the name of the walking animation as its argument.

If the reference to the Actor is removed, the animations will be lost. Make sure not to remove the reference to the Actor until both the Actor and its animations are no longer needed.

Controlling animations

Since we're talking about the loop() method, let's start discussing some of the different controls for playing and stopping animations. There are four basic methods we can use:

  • play("AnimName"): This method plays the animation once from beginning to end.
  • loop("AnimName"): This method is similar to play, but the animation doesn't stop when it's over; it replays again from the beginning.
  • stop() or stop("AnimName"): This method, if called without an argument, stops all the animations currently playing on the Actor. If called with an argument, it only stops the named animation.

    Note that the Actor will remain in whatever posture they are in when this method is called.

  • pose("AnimName", FrameNumber): Places the Actor in the pose dictated by the supplied frame without playing the animation.

We have some more advanced options as well. Firstly, we can provide option fromFrame and toFrame arguments to play or loop to restrict the animation to specific frames."AnimName", fromFrame = FromFrame, toFrame = toFrame)

We can provide both the arguments, or just one of them. For the loop() method, there is also the optional argument restart, which can be set to 0 or 1. It defaults to 1, which means to restart the animation from the beginning. If given a 0, it will start looping from the current frame.

We can also use the getNumFrames("AnimName") and getCurrentFrame("AnimName") methods to get more information about a given animation. The getCurrentAnim() method will return a string that tells us which animation is currently playing on the Actor.

The final method we have in our list of basic animation controls sets the speed of the animation.

myActor.setPlayRate(1.5, "AnimName")

The setPlayRate() method takes two arguments. The first is the new play rate, and it should be expressed as a multiplier of the original frame rate. If we feed in .5, the animation will play half as fast. If we feed in 2, the animation will play twice as fast. If we feed in -1, the animation will play at its normal speed, but it will play in reverse.

Have a go hero – basic animation controls

Experiment with the various animation control methods we've discussed to get a feel for how they work. Load the Stand and Thoughtful animations from the animations folder as well, and use player input or delayed tasks to switch between animations and change frame rates. Once we're comfortable with what we've gone over so far, we'll move on.

Animation blending

Actors aren't limited to playing a single animation at a time. Panda3D is advanced enough to offer us a very handy functionality, called blending. To explain blending, it's important to understand that an animation is really a series of offsets to the basic pose of the model. They aren't absolutes; they are changes from the original. With blending turned on, Panda3D can combine these offsets.

Time for action – blending two animations

We'll blend two animations together to see how this works.

  1. Open in the Chapter09 folder.
  2. We need to load a second animation to be able to blend. Change the line where we create our Actor to look like the following code:

    self.kid = Actor("../Models/Kid.egg",
    {"Walk" : "../Animations/Walk.egg",
    "Thoughtful" : "../Animations/Thoughtful.egg"})

  3. Now, we just need to add this code above the line where we start looping the Walk animation:
    self.kid.setControlEffect("Walk", 1)
    self.kid.setControlEffect("Thoughtful", 1)
  4. Resave the file as and run it from the command prompt.

    Animating in Panda3D

What just happened?

Our Actor is now performing both animations to their full extent at the same time. This is possible because we made the call to the self.kid.enableBlend() method and then set the amount of effect each animation would have on the model with the self.kid.setControlEffect() method. We can turn off blending later on by using the self.kid.disableBlend() method, which will return the Actor to the state where playing or looping a new animation will stop any previous animations. Using the setControlEffect method, we can alter how much each animation controls the model. The numeric argument we pass to setControlEffect() represents a percentage of the animation's offset that will be applied, with 1 being 100%, 0.5 being 50%, and so on. When blending animations together, the look of the final result depends a great deal on the model and animations being used. Much of the work needed to achieve a good result depends on the artist.

Blending works well for transitioning between animations. In this case, it can be handy to use Tasks to dynamically alter the effect animations have on the model over time.

Honestly, though, the result we got with blending is pretty unpleasant. Our model is hardly walking at all, and he looks like he has a nervous twitch or something. This is because both animations are affecting the entire model at full strength, so the Walk and Thoughtful animations are fighting for control over the arms, legs, and everything else, and what we end up with is a combination of both animation's offsets.

Furthermore, it's important to understand that when blending is enabled, every animation with a control effect higher than 0 will always be affecting the model, even if the animation isn't currently playing. The only way to remove an animation's influence is to set the control effect to 0.

This obviously can cause problems when we want to play an animation that moves the character's legs and another animation that moves his arms at the same time, without having them screw with each other. For that, we have to use subparts.



        Read more about this book      

(For more resources on Panda3D, see here.)

Creating and using Actor subparts

A subpart is a subsection of the joints in an Actor. Think of it like a grouping of some of the joints of the Actor that excludes the rest of the joints. Creating subparts doesn't actually change the Actor at all; it just creates a grouping that we can use when we want an animation to only affect a specific subset of joints. To create a subpart, we just need to call the makeSubpart() method of the Actor:

myActor.makeSubpart("SubpartName", [Included], [Excluded])

We pass three arguments into the makeSubpart() method:

  • The first is a name for the subpart that we can use to identify it later
  • The second is a list of joint names for the joints we want to include in the subpart
  • The third is a list of joint names for the joints we want to exclude from the model

We can find these joint names if we open Kid.egg and scroll down to the bottom. Here's a joint entry for a joint called Chest, for reference:

<Joint> Chest {
<Transform> {
<Matrix4> {
0.993033 0 0.117839 0
0.117839 0 -0.993033 0
0 1 0 0
0.212127 0 0 1

The makeSubpart() method automatically includes or excludes the children of any joint it is supplied.

When making Actor subparts, exclusion always overrides inclusion.

Let's take a look at some of the joints in our Actor and their hierarchy.

Animating in Panda3D

This diagram shows some of our joints, the circles, and their names. It also shows the hierarchy through the connecting arrows. Parents point to their children.

Looking at this, we can tell that the joint named Root is at the top of our joint hierarchy. If we pass Root as an included joint, that will include every joint in the model. If we also pass in Waist as an excluded joint, we'll override part of that inclusion and exclude the Waist joint and its children. Therefore, our subpart will only include Root, R_Hip and its children, and L_Hip and its children, which means only the pelvis and legs.

Time for action – playing animations on subparts

To better understand subparts, we should put them into action.

  1. Open in the Chapter09 folder.
  2. Remove the code that enables blending, sets control effects, and loops the two animations.
  3. Add this code in place of the code we just removed:
    self.kid.makeSubpart("Bottom", ["Root"], ["Waist"])
    self.kid.makeSubpart("Top", ["Waist"])
    self.kid.loop("Thoughtful", partName = "Top")
    self.kid.loop("Walk", partName = "Bottom")
  4. Resave the file as and run it from the command prompt.

    Animating in Panda3D

What just happened?

We've got a much nicer combination of the two animations now because on the legs of the Actor we're only playing the Walk animation, and on the torso of the Actor we're only playing the Thoughtful animation.

Note that if we want to play an animation only on a subpart of the Actor, we need to use the partName optional argument for play or loop.

Exposing joints

We can do more with Actors than just play animations on them. We can also attach other NodePaths to specific joints in an Actor so that they will follow that joint wherever it moves. This technique is often used to put objects in a character's hands. For our game, we'll be using it to attach the discs and turret to our cycle.

This technique works well for us because we want to change the rotation of the discs and turret. For that to work correctly, the models need to have the points they will rotate around at (0, 0, 0) in their own coordinate system. That means we can't reposition them in the modeling software, as we did with the static parts of the cycle such as the power plant.

Instead, we'll load an Actor that only contains three joints. The joints are located at the positions where we want to place our discs and turret. If we attach the Actor to the cycle's model, we can use the joints as mounting points for the discs and turret.

Without the exposeJoint() method of Actor, this technique wouldn't be possible. This method creates a NodePath and attaches it to a joint in the Actor, such that wherever that joint moves, or however it rotates, the NodePath will mimic it exactly. The method exposeJoint() takes three arguments:

  • The first is an optional Node that is usually set to None
  • The second is the name of the subpart the joint is a part of, or modelRoot if no subparts contain the joint
  • The last argument is the name of the joint to expose:
    jointNP = myActor.exposeJoint(None,
    subpartName or "modelRoot", JointName)

Once we've used exposeJoint() to create NodePaths, we can use reparentTo to attach things to them, just like any other NodePath.

Time for action – animating our cycles

We're going to add two kinds of animation to our cycle. We'll make the discs at the front and rear of the cycle rotate in accordance with the throttle setting, and we'll make the cycle lean left and right when it turns. Because we don't want the CollisionRays attached to the cycle to lean as well, we'll have to make accommodations for that.

  1. Open in the Chapter09 folder.
  2. Add this line to our import statements to give us access to Actors:
    from import Actor
  3. Scroll down to the setupVarsNPs() method and look for the section where we use if statements to position self.root and load the correct model. That section looks like the following code:

    if(startPos == 1):
    self.cycle = loader.loadModel("../Models/RedCycle.bam")
    elif(startPos == 2):
    self.cycle = loader.loadModel("../Models/BlueCycle.bam")
    elif(startPos == 3):
    self.cycle = loader.loadModel("../Models/GreenCycle.bam")
    elif(startPos == 4):
    self.cycle = loader.loadModel("../Models/YellowCycle.bam")

  4. Delete that section and replace it with the following code:

    self.cycle = self.root.attachNewNode("Cycle")
    if(startPos == 1):
    self.model = loader.loadModel("../Models/RedCycle.bam")
    self.turret = loader.loadModel("../Models/RedTurr.bam")
    elif(startPos == 2):
    self.model = loader.loadModel("../Models/BlueCycle.bam")
    self.turret = loader.loadModel("../Models/BlueTurr.bam")
    elif(startPos == 3):
    self.model = loader.loadModel("../Models/GreenCycle.bam")
    self.turret = loader.loadModel("../Models/GreenTurr.bam")
    elif(startPos == 4):
    self.model = loader.loadModel("../Models/YellowCycle.bam")
    self.turret = loader.loadModel("../Models/YellowTurr.bam")
    self.mounts = Actor("../Models/Mounts.egg")

    turretMount = self.mounts.exposeJoint(None,
    "modelRoot", "Turret")
    fdMount = self.mounts.exposeJoint(None,
    "modelRoot", "FrontDisc")
    rdMount = self.mounts.exposeJoint(None,
    "modelRoot", "RearDisc")
    self.fd = loader.loadModel("../Models/Disc.bam")
    self.rd = loader.loadModel("../Models/Disc.bam")


  5. Scroll down a little to where we set the cycle's speed, shield, and other variables. Right beneath that there is a line that attaches self.cycle to self.root. Delete that line and put the following two lines in its place:
    self.turning = None
    self.lean = 0
  6. Keep moving down until we're in setupCollisions. Look for the following line:
    self.shieldCNP = self.cycle.attachNewNode(self.shieldCN)
  7. Change that line from self.cycle to self.model so it looks like this:
    self.shieldCNP = self.model.attachNewNode(self.shieldCN)
  8. Head down to the cycleControl() method and find the short block of code that turns the cycle according to user input. It looks like the following code:

    if(self.inputManager.keyMap["right"] == True):
    self.turn("r", dt)
    elif(self.inputManager.keyMap["left"] == True):
    self.turn("l", dt)

  9. Edit that code so it looks like the following code:

    if(self.inputManager.keyMap["right"] == True):
    self.turn("r", dt)
    self.turning = "r"
    elif(self.inputManager.keyMap["left"] == True):
    self.turn("l", dt)
    self.turning = "l"
    self.turning = None

  10. Now, skip down to the move() method. Add the following code to the bottom of the method, right above the return statement:

    currentLean = self.model.getR()

    if(self.turning == "r"):
    self.lean += 2.5
    if(self.lean > 25): self.lean = 25
    (self.lean - currentLean) * dt * 5)

    elif(self.turning == "l"):
    self.lean -= 2.5
    if(self.lean < -25): self.lean = -25
    (self.lean - currentLean) * dt * 5)

    self.lean = 0
    (self.lean - currentLean) * dt * 5)

    self.fd.setH(self.fd, 5 + (20 * self.throttle))
    self.rd.setH(self.rd, -5 + (-20 * self.throttle))

  11. We've got one more change to make to this file. Go all the way down to the bottom of the file to the destroy() method and add these five lines right underneath the call to self.cycle.removeNode():
  12. Resave the file as and run from the command prompt.

    Animating in Panda3D

What just happened?

That brings some new life to our cycle! There's one thing we should talk about in this example that we haven't really discussed yet: the reason why we made self.cycle an empty NodePath and changed the model to be self.model. Well, when we lean the cycle during a turn, we don't want those CollisionRays to lean with it, but the CollisionRays still have to turn with the cycle so that the front ray stays in front and the back ray stays at the back. For all that to work out, we need to separate the turning from the leaning, and that's why we are using two NodePaths now. Since we want the CollisionSpheres for the shield to lean with the cycle, we made them children of self.model instead of self.cycle.


In this article, we talked about loading Actor and animations and how to control an Actor's animations. We also discussed how we can use blending and subparts to further refine our animation control, and we touched on how to include other objects into our Actor's animations by binding them to joints.

Further resources on this subject:

You've been reading an excerpt of:

Panda3D 1.6 Game Engine Beginner's Guide

Explore Title