Making animations fit to intervals

Intervals are a very powerful feature of Panda3D, which you can see in the recipe Controlling actions using intervals. In this recipe you will go one step beyond and see how to efficiently control animations when used in conjunction with intervals.

Getting ready

If you haven't already, please read how to use intervals in Controlling actions using intervals and follow the steps of that recipe.

How to do it...

This recipe consists of the following tasks:

  1. Open Application.py.
  2. Delete the line self.panda.loop("walk").
  3. Find the following part of the code:
    self.pandaWalk = Sequence(self.walkIval1, self.turnIval1, self.colorIval, self.walkIval2, self.turnIval2,
    self.colorIval)
    self.pandaWalk.loop()
    
  4. Replace it with the following code:
    self.pandaAnim = ActorInterval(self.panda, "walk", loop = 1, duration = 5)
    self.pandaMove = Sequence(self.walkIval1, self.turnIval1, self.colorIval, self.walkIval2, self.turnIval2, self.colorIval)
    self.pandaWalk = Parallel(self.pandaAnim, self.pandaMove)
    self.pandaWalk.loop()
    

How it works...

The panda is moving just like before, but now an ActorInterval we create in the highlighted line of the code controls animation playback. We set the animation to loop and to play over the duration of five seconds. Finally, the animation-controlling ActorInterval and the Sequence of transformations are made part of a Parallel that plays the two intervals at the same time.

There's more...

The actor interval constructor can take a range of optional parameters besides loop and duration that were already presented in the sample code. Let's take a look at what else you can do with ActorInterval:

Instead of duration, you can use startTime and endTime to define more precisely when to start and stop playing the animation.

The playrate parameter lets you set the animation playback speed. Also note that if playrate is a negative value, the animation is played backwards.

You can specify a sub range of the animation to be played by setting startFrame and endFrame. Also, if you want to loop an animation range, set constrainedLoop to one instead of loop.

In the following line of code, all these options have been applied to our sample code to loop the first second of the walk animation at a very low rate:

self.pandaAnim = ActorInterval(self.panda, "walk", startTime = 0, endTime = 1, playrate = 0.1, constrainedLoop = 1, duration = 5)

Making objects follow a predefined path

In this recipe we will see how to move a model along a line, typically referred to as a path, stored in Panda3D's model format. This can come in very handy for setting paths of non-player characters inside a level. Another common use of this is to attach the camera to a path and make it fly through the game world smoothly.

Getting ready

To follow this recipe you first have to set up a project as described in Setting up the game structure. In addition, you need to create a curve in Maya or Blender and export it to Panda3D's .egg file format. In case you don't have these tools or do not know how to work with them, here's a sample curve you can paste into a text editor and save it as path.egg:

<CoordinateSystem> { Z-up }
<Group> Curve {
<VertexPool> Curve {
<Vertex> 0 {
-2.66117048264 -0.964361846447 0.0 1.0
}
<Vertex> 1 {
1.8930850029 -0.948404431343 0.0 1.0
}
<Vertex> 2 {
10.3484048843 1.0 0.0 1.0
}
<Vertex> 3 {
-3.6957449913 0.0 0.0 1.0
}
}
<NURBSCurve> {
<Order> { 4 }
<Knots> { 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 }
<VertexRef> { 0 1 2 3 <Ref> { Curve }}
}
}

How to do it...

Making objects follow a predefined path can be done like the following:

  1. Copy the file containing the curve to the models directory of your project and make sure it is named path.egg.
  2. Copy the following code to Application.py:
    from direct.showbase.ShowBase import ShowBase
    from direct.actor.Actor import Actor
    from direct.directutil.Mopath import Mopath
    from direct.interval.IntervalGlobal import *
    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.smiley = self.loader.loadModel("smiley")
    self.smiley.reparentTo(render)
    self.mopath = Mopath()
    self.mopath.loadFile("path.egg")
    self.ival = MopathInterval(self.mopath, self.smiley, duration = 10)
    self.ival.loop()
    self.cam.setPos(0, -20, 0)
    
  3. Press F6 to start the program. The smiley model will follow the path.

How it works...

All it takes for you to use predefined paths is to create a Mopath object and load the file containing the curve you want your object to follow. Then you pass the Mopath object to a MopathInterval that is responsible for interpolating the model position along the path over the specified duration. That's it—it's that easy!

Making the camera smoothly follow an object

In this recipe you will learn how to program a simple camera system that follows an object smoothly, without giving the impression of being glued to the back of the target.

Getting ready

See the recipe Setting up the game structure to create the basic application framework for the following steps.

How to do it...

Let's build a third person camera system:

  1. Add this code to Application.py:
    from direct.showbase.ShowBase import ShowBase
    from direct.actor.Actor import Actor
    from panda3d.core import Vec3
    from direct.interval.IntervalGlobal import *
    from FollowCam import FollowCam
    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.world = loader.loadModel("environment")
    self.world.reparentTo(render)
    self.world.setScale(0.5)
    self.world.setPos(-8, 80, 0)
    self.panda = Actor("panda", {"walk": "panda-walk"})
    self.panda.reparentTo(render)
    self.panda.setHpr(270, 0, 0)
    self.panda.loop("walk")
    self.walkIval1 = self.panda.posInterval(2, Vec3(-8, -8, 0), startPos = Vec3(8, -8, 0))
    self.walkIval2 = self.panda.posInterval(2, Vec3(-8, 8, 0), startPos = Vec3(-8, -8, 0))
    self.walkIval3 = self.panda.posInterval(2, Vec3(8, 8, 0), startPos = Vec3(-8, 8, 0))
    self.walkIval4 = self.panda.posInterval(2, Vec3(8, -8, 0), startPos = Vec3(8, 8, 0))
    self.turnIval1 = self.panda.hprInterval(0.5, Vec3(180, 0, 0), startHpr = Vec3(270, 0, 0))
    self.turnIval2 = self.panda.hprInterval(0.5, Vec3(90, 0, 0), startHpr = Vec3(180, 0, 0))
    self.turnIval3 = self.panda.hprInterval(0.5, Vec3(0, 0, 0), startHpr = Vec3(90, 0, 0))
    self.turnIval4 = self.panda.hprInterval(0.5, Vec3(-90, 0, 0), startHpr = Vec3(0, 0, 0))
    self.pandaWalk = Sequence(self.walkIval1, self.turnIval1,
    self.walkIval2, self.turnIval2,
    self.walkIval3, self.turnIval3,
    self.walkIval4, self.turnIval4)
    self.pandaWalk.loop()
    self.followCam = FollowCam(self.cam, self.panda)
    
  2. Add a new file to the project. Call it FollowCam.py.
  3. Copy the following code to the file you just created:
    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import Vec3
    class FollowCam():
    def __init__(self, camera, target):
    self.dummy = render.attachNewNode("cam" + target.getName())
    self.turnRate = 2.2
    self.camera = camera
    self.target = target
    taskMgr.add(self.updateCamera, "updateCamera" + target.getName())
    def updateCamera(self, task):
    self.dummy.setPos(self.target.getPos())
    heading = self.clampAngle(self.dummy.getH())
    turnDiff = self.target.getH() - heading
    turnDiff = self.clampAngle(turnDiff)
    dt = globalClock.getDt()
    turn = turnDiff * dt
    self.dummy.setH(heading + turn * self.turnRate)
    self.camera.setPos(self.dummy.getPos())
    self.camera.setY(self.dummy, 40)
    self.camera.setZ(self.dummy, 10)
    self.camera.lookAt(self.target.getPos() + Vec3(0, 0, 7))
    return task.cont
    def clampAngle(self, angle):
    while angle < -180:
    angle = angle + 360
    while angle > 180:
    angle = angle - 360
    return angle
    
  4. Press F6 to start the application. You should be able to see a panda walking in circles while the camera follows it:
How to do it...

How it works...

We use the constructor of our Application class to set up the scene containing the walking panda and the background scenery. In the last line we create a new instance of our FollowCam, which contains the camera tracking code that is the core of this recipe.

To make the FollowCam work correctly and to be able to have multiple cameras follow different objects, we have to pass the camera we want to be updated and its target to the constructor, where we set up a few things we need for updating the camera. For example, we add a task that will call the updateCamera() method each frame. Additionally, the target's name is appended to both the dummy object's and the task's names to avoid name clashes in the case where we need to use more than one FollowCam instance. The dummy object is an invisible helper object that will help us to position the camera, as you will see in the following paragraphs.

The updateCamera() method is where all the work is happening: We move the dummy to our target's current position and get its current heading. The heading angle (in degrees) is clamped to the range of values from 180 to 180. We do this to avoid the camera getting stuck or continuing to turn because of the ambiguous nature of angles.

In the next steps, we calculate the difference between the target's heading and that of our dummy object, which is also clamped to avoid the undesired results described in the previous paragraph. In the following lines we can find the explanation for the camera's smooth turning—every frame, the dummy object's heading converges towards the heading of the camera target just a little bit. This is intentional; as it is multiplied by the time it took to complete the last frame. Additionally, we can also influence how fast the camera turns by adjusting turnRate.

In the final steps, the camera is first moved to the position of the dummy and then pushed away again along the dummy's local axis to its final position. After setting the camera's lookAt() target, we are done!

There's more...

In this version, the camera only supports smooth turning for objects that only change their heading. Other rotation axes can be added rather easily, as they work exactly the same as the one presented in this recipe!

Generating geometry at runtime

In some cases, Panda3D's model loading capabilities might not be enough for your needs. Maybe you want to procedurally generate new geometry at runtime or maybe you decided to drop the native file model file format of Panda3D in favor of your own custom data file layout. For all these cases where you need to glue together vertices by hand in order to form a model, the engine provides an API that you will learn about in this recipe.

Getting ready

As a prerequisite to the following steps, please create a new project as described in the recipe Setting up the game structure. This recipe can be found in the Chapter 1.

You will also need a texture image. Preferably it should be rectangular and in the best case be in a 2n format (64x64, 128x128, 256x256, and so on.). This recipe will use a crate texture in PNG format.

Lastly, add a directory called textures to your project and be sure it is in Panda3D's search path. This works analogous to what you did for the models and sounds directories.

How to do it...

Follow these steps to learn how to create geometry on the fly:

  1. Copy your texture image to the textures directory. Name it crate.png.
  2. Open Application.py. Insert the following code:
    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *
    vertices = [Vec3(1, 0, 1), Vec3(-1, 0, 1), Vec3(-1, 0, -1), Vec3(1, 0, -1)]
    texcoords = [Vec2(1, 1), Vec2(0, 1), Vec2(0, 0), Vec2(1, 0)]
    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    format = GeomVertexFormat.getV3t2()
    geomData = GeomVertexData("box", format, Geom.UHStatic)
    vertexWriter = GeomVertexWriter(geomData, "vertex")
    uvWriter = GeomVertexWriter(geomData, "texcoord")
    for pos, tex in zip(vertices, texcoords):
    vertexWriter.addData3f(pos)
    uvWriter.addData2f(tex)
    triangles = GeomTriangles(Geom.UHStatic)
    triangles.addVertices(0, 1, 2)
    triangles.closePrimitive()
    triangles.addVertices(2, 3, 0)
    triangles.closePrimitive()
    geom = Geom(geomData)
    geom.addPrimitive(triangles)
    node = GeomNode("box")
    node.addGeom(geom)
    box = render.attachNewNode(node)
    texture = loader.loadTexture("crate.png")
    box.setTexture(texture)
    self.cam.setPos(0, -5, 0)
    
  3. Start the program. A quad with your texture on it should be rendered to the Panda3D window:
How to do it...

How it works...

Let's take a look at what this code is doing! We begin by creating a format descriptor for one of Panda3D's built in vertex data layouts. There are several of these layouts, which also can be completely customized, which describe what kind of data will be stored for each point in space that forms the mesh. In this particular case, we are using the getV3t2() method to get a descriptor for a vertex layout that stores the vertex position in space using three floating point values and the texture coordinate using two float values.

We then move on to create a GeomVertexData object, which uses the format we just requested. We also pass the Geom.UHStatic flag, which signals the underlying rendering systems that the vertex data will not change, which allows them to enable some optimizations. Additionally, we create two GeomVertexWriter objects—one for writing to the "vertex" channel, which stores the positions of the points that form the mesh, and the other one for writing to the "texcoord" channel of the point data we are adding to geomData in the loop that follows.

What we have so far is a cloud of seemingly random points—at least to the engine. To correct this issue, we need to connect the points to form primitives, which in this case are triangles. We create a new instance of GeomTriangles, using the Geom.UHStatic flag again to hint that the primitives will not be changed after they are defined. Then we create two triangles by passing indices to the proper points in the vertices list. After each triangle, we need to call the closePrimitive() method to mark the primitive as complete and start a new one.

At this point we have a collection of points in space, stored in a GeomVertexData object and a GeomTriangles primitive that holds the information necessary to connect the dots and form a mesh. To get the model to the screen, we need to create a new Geom, and add the point data and the triangle primitives. Because a model can in fact consist of multiple Geom objects, which also can't be added directly to the scene graph, we add it to a GeomNode. Finally, we attach the GeomNode to the scene graph, load and apply the texture and set the camera a bit back to be able to see our creation.

There's more...

There's a lot more to say about Panda3D's procedural geometry feature than what you just saw, so take your time and keep on reading to discover what else you can do to generate geometry at runtime.

Built in vertex formats

You already saw the built in GeomVertexFormat.getV3t2() format, but there are several more ready to use formats available:

  • getV3(): Vertices store position only
  • getV3c4(): Vertex position and a RGBA color value
  • getV3c4t2(): Position, color, and texture coordinates
  • getV3n3(): Position and normal vector
  • getV3n3c4(): Position, normal, and RGBA color
  • getV3n3c4t2(): This is the most extensive format. Contains position, normal, color, and texture coordinates
  • getV3n3t2(): Position, normal vector, and texture coordinates.

There's also a packed color format, which you can use by replacing c4 in the previous methods with cp. In this format, colors are stored into one 32-bit integer value. The best way to define color values for this format is in hexadecimal, because it lets you easily recognize the RGBA components of the color. For example, the value 0xFF0000FF is full red.

Custom vertex formats

Apart from the built-in vertex formats, Panda3D allows you to be much more flexible by defining your own custom formats. The key parts to this are the GeomVertexArrayFormat and GeomVertexFormat classes, which are used in the following way:

arrayFmt = GeomVertexArrayFormat()
arrayFmt.addColumn(InternalName.make("normal"), 3, Geom.NTFloat32, Geom.CVector)
fmt = GeomVertexFormat()
fmt.addArray(arrayFmt)
fmt = GeomVertexFormat.registerFormat(fmt)

In the beginning, you need to describe your vertex array data layout by adding columns. The first parameter is the channel that Panda3D is going to use the data for. Very commonly used channels are "vertex", "color", "normal", "texcoord", "tangent", and "binormal".

The second and third parameters are the number of components and data type the channel is using. In this sample, the normal data is composed out of three 32-bit floating point values. Legal values for the third parameter include NTFloat32, NTUint*, where * is one of 8, 16, or 32, describing an unsigned integer of the according bit width as well as NTPackedDcba and NTPackedDabc, used for packed index and color data.

The third parameter influences how the data is going to be treated internally—for example, if and how it will be transformed if a matrix is applied to the data in the column. Possible values include:

  • CPoint: Point data in 3D space, most often used for the "vertex" channel.
  • CVector: A vector giving a direction in space. Use this for normals, tangents, and binormals.
  • CTexcoord: The data in the column contains the coordinates of texture sample points.
  • CColor: The data is to be treated as color values.
  • CIndex: The column contains table indices.
  • COther: Arbitrary data values.

Points and texture coordinates are transformed as points, resulting in new positions if they are involved in a matrix multiplication. Vectors of course are following different rules when being transformed, because they describe directions, not positions! It would go too far to go into the details here, but lots of material on vector math and linear algebra are freely available on Wikipedia and other websites.

Primitive types

Panda3D supports all the standard primitive types commonly known in computer graphics: Triangles, triangle strips, triangle fans, lines, line strips, and points. The according classes used to describe these primitives are GeomTriangles, GeomTristrips, GeomTrifans, GeomLines, GeomLinestrips, and GeomPoints.

See also

Loading models and actors, Modifying the scene graph.

Loading data asynchronously

Panda3D really makes it very easy to load and use assets like models, actors, textures, and sounds. But there is a problem with the default behavior of the asset loader—it blocks the execution of the engine.

This is not a problem if all data is loaded before the player is allowed to see the game world, but if models and other assets are to be loaded while the game is running, we are facing a serious problem because the frame rate will drop dramatically for a moment. This will cause game execution to stop for a short moment in a sudden and unpredictable way that breaks gameplay.

To avoid getting into such problems, Panda3D offers the ability to load data through a background thread. This is a very useful feature if game assets are loaded on the fly, such as the popular use case with seamless streaming in game worlds. It is also a great way to reduce initial loading times. The main level geometry and everything visible from the starting position is loaded before the player enters the world and the rest of it is loaded afterwards, often depending on the position in the game world.

Getting ready

For this recipe you will need to set up the basic framework described in Setting up the game structure.

How to do it...

Follow these steps to create a sample application that demonstrates asynchronous data loading:

  1. Add the following code to Application.py:
    from direct.showbase.ShowBase import ShowBase
    from direct.actor.Actor import Actor
    from panda3d.core import Vec3
    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.cam.setPos(0, -30, 6)
    taskMgr.doMethodLater(3, self.load, "load", extraArgs = ["teapot", Vec3(-5, 0, 0), self.modelLoaded])
    taskMgr.doMethodLater(5, self.load, "load", extraArgs = ["panda", Vec3(5, 0, 0), self.actorLoaded])
    def load(self, name, pos, cb):
    loader.loadModel(name, callback = cb, extraArgs = [pos])
    def modelLoaded(self, model, pos):
    model.reparentTo(render)
    model.setPos(pos)
    def actorLoaded(self, model, pos):
    self.panda = Actor(model, {"walk": "panda-walk"})
    self.panda.reparentTo(render)
    self.panda.setPos(pos)
    self.panda.loop("walk")
    
  2. Press F6 to run the application. You will see a delay before the teapot and the panda appear in the window.

How it works...

The previous code enqueues calls to the load() method using doMethodLater() so you can see the objects pop up as soon as they are loaded. The list passed to the extraArgs parameter will be used as parameters for the call to load().

The call to loadModel() within the load method is very important, because instead of just passing the name of the model to load, you also set the callback parameter to one of the modelLoaded() and actorLoaded() methods, depending on what the cb parameter of load() contains.

As soon as a call to loadModel() uses the callback parameter, the request to load the data is handed off to a background thread. When the required asset has finished loading, the callback function is called and the loaded asset is passed as the first parameter, as you can see in the modelLoaded() and actorLoaded() methods.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset