Procedural character skeleton hierarchy generation?

I’m trying to generate a complete actor, including procedural textures via pixel shaders, generated mesh, generated skeleton hierarchy and dynamic animation feed in Panda3D using Python scripts only (ie. no C++ and no external files) and without changing the existing Panda3D code.

Ignoring the actual animation for now, I’m currently facing problems with generating the skeleton hierarchy (bones / joints). Essentially I want to generate a standard skeleton hierarchy with vertex references (ie. one that would be compatible with eg. loadAnim() and the LerpAnimInterval class as long as the animations match the skeleton). Is there any way to do this without resorting to using the egg loader? If not, can I do this without resorting to generating egg syntax (eg. using the Egg classes)?

I can easily generate a mesh with procedural textures using setShader() and the Geom related classes (GeomVertexData et al). The resulting mesh can be attached to an actor using loadModel(), but it won’t contain any useful skeleton data (and a warning message is printed indicating this). If I understand the code in Actor.py correctly, then joint information will only be honoured if the provided model is or contains a Character object.

I’ve tried creating a Character object directly, but python simply won’t let me; it complains that the class is constant. I’m relatively new to Python, but I’m guessing this is part of the metadata for the exported C++ class and not overridable from Python code (and if so, then presumably there’s a good reason for this constraint).

Loading an egg file that contains a character will result in a Character object parented to the returned object. So evidently the egg loader can create those. So far I’ve found no other way to create Character objects.

According to this page you can generate egg data dynamically and load it without actually creating any egg file, by using EggData() and its subnodes. There is an isJoint() member of the EggNode class, which suggests a joint in the egg domain is either an EggNode object or an object of a class that directly or indirectly inherits it, but I can’t seem to find a way to create such joints using these API functions.

My last resort, then, is to generate a string containing egg syntax and have it loaded from a StringStream object. This approach has been confirmed to work with egg data in general, and I see no reason why I wouldn’t be able to include joints this way.

Looking at the syntax description on this page it is apparent that this would have some rather awkward consequences, even beyond being relatively ineffective since it obviously requires string parsing and probably a few additional layers of data restructuring.

For a node to be effective, it needs to contains nodes. Each node has a list of vertex indices and a node pointing to a vertex pool.

If I have to generate egg syntax, I would much prefer to stop at that point. This would require that I have the ability to create a named vertex pool programmatically and somehow map it to the generated vertex list. I’ve found no way to do so (the closest thing I’ve found is the EggVertexPool class, which appears to be used only during loading, and in any case it seems to have no exposed functionality related to GeomVertexData objects).

Furthermore, the egg loader complains about the pool not being defined unless I include a node in the egg syntax. If I do this, the egg loader complains about any vertices not defined using nodes. In other words, I seem to be forced to include the entire model in the egg syntax, instead of generating the mesh with the Geom classes. This is still workable as long as I can map my shaders to egg materials, but extremely awkward.

Did I miss anything? Is there a better way to do this?

Wow, what a long and dark path you’re about to walk down. The animation structures within Panda (or any 3-D graphics engine) are necessarily fairly complicated, substantially more complicated than ordinary, static geometry. But it can be done.

Let’s unwind your proposals, starting from the last one first.

It would be perfectly doable to generate your egg syntax as a string, and feed that via StringStream to the egg loader. You would, as you’ve observed, have to define your entire model, or at least the animated parts of it, via this egg syntax; you can’t just create a Geom and attach it to the joints separately. There’s a very good reason for this: an animatable Geom has a lot of additional data, in addition to the plain vertices, that make it animatable. Unless you’re prepared to create all of this additional data yourself, you need to let the egg loader create it for you.

Secondly, it would also be possible, and a little bit faster, to just create the egg structures directly. The egg structures map 1:1 to syntax you see in the egg file, so for the most part any egg file you can generate in a text string, you could also just generate the corresponding egg structures for directly. This does require knowing a little bit about how the egg syntax corresponds to egg structures, but for the most part it’s pretty straightforward; and we can help you with the initial difficulties (like creating vertex refs to joints and so on).

Finally, it ought to be possible just to create the Character node and all of the necessary joints and the associated TransformTables and the animated vertex data by hand, without messing with the egg loader at all. There might be a few oopses in there that make it impossible to do this from Python at the moment; for instance, the fact that we did not publish the Character constructor will make things a little tricky (oops!). But even if we fixed all of these oopses and exposed all of the necessary constructors, you’ll have a lot of code to write, unless the characters you’re trying to create are very simple.

My recommendation is to go with option B: generate the required egg structures, and pass it to the egg loader.

David

Thanks for the surprisingly quick response. For now I’ll be prototyping my code using egg strings and worry about optimizing later, since the egg syntax is easy for me to follow without additional study. I’ll get back to you on the usage of the Egg classes or perhaps lower level stuff if/when that time comes.

I don’t mind writing lots of code, though, so if the Python interface becomes sorted out in a future release of Panda3D, then eventually I’ll probably be looking to write an abstraction layer for simplifying the generation of skeleton data from Python scripts.

I spent some time looking further into this, and writing examples of solving this problem in each of the three approaches.

First, consider this simple example, with an extremely simple character. This character has just four vertices and three joints (only two of which animate). Three vertices are hard-assigned to each of the three joints, and the fourth vertex is soft-assigned to two different joints.

This egg file contains both the model and its associated anim table, but they might just as easily have come from separate files instead.

from direct.directbase.DirectStart import *
from pandac.PandaModules import *
from direct.actor.Actor import Actor

eggtext = """
<CoordinateSystem> { Z-Up }

<Group> simplechar {
  <Dart> { 1 }

  <VertexPool> vpool {
    <Vertex> 0 {
      0 0 0
    }
    <Vertex> 1 {
      10 0 0
    }
    <Vertex> 2 {
      10 0 10
    }
    <Vertex> 3 {
      8 0 2
    }
  }
  <Group> card {
    <Polygon> {
      <VertexRef> { 0 1 2 3 <Ref> { vpool } }
    }
  }
  <Joint> root {
    <VertexRef> {
      0 <Ref> { vpool }
    }
    <Joint> hjoint {
      <Transform> {
        <Translate> { 10 0 0 }
      }
      <VertexRef> {
        1 <Ref> { vpool }
      }
      <VertexRef> {
        3 <Scalar> membership { 0.7 } <Ref> { vpool }
      }
      <Joint> vjoint {
        <Transform> {
          <Translate> { 0 0 10 }
        }
        <VertexRef> {
          2 <Ref> { vpool }
        }
        <VertexRef> {
          3 <Scalar> membership { 0.3 } <Ref> { vpool }
        }
      }
    }
  }
}

<Table> wiggle {
  <Bundle> simplechar {
    <Table> "<skeleton>" {
      <Table> root {
        <Xfm$Anim_S$> xform {
          <Char*> order { sphrt }
          <Scalar> fps { 5 }
        }
        <Table> hjoint {
          <Xfm$Anim_S$> xform {
            <Char*> order { sphrt }
            <Scalar> fps { 5 }
            <S$Anim> x { <V> { 10 11 12 13 14 15 14 13 12 11 } }
          }
          <Table> vjoint {
            <Xfm$Anim_S$> xform {
              <Char*> order { sphrt }
              <Scalar> fps { 5 }
              <S$Anim> z { <V> { 10 9 8 7 6 5 6 7 8 9 } }
            }
          }
        }
      }
    }
  }
}
"""

data = EggData()
data.read(StringStream(eggtext))
np = NodePath(loadEggData(data))

a = Actor(np)
a.reparentTo(render)
a.setPos(0, 50, 0)
a.loop('simplechar')

David

Now, here’s the exact same solution, with the same simple character, created using egg structures instead of literal egg text:

from direct.directbase.DirectStart import *
from pandac.PandaModules import *
from direct.actor.Actor import Actor

data = EggData()
data.setCoordinateSystem(CSZupRight)

# First, build the character model.

simplechar = EggGroup('simplechar')
data.addChild(simplechar)
simplechar.setDartType(simplechar.DTDefault)

vpool = EggVertexPool('vpool')
simplechar.addChild(vpool)
v0 = vpool.makeNewVertex(Point3D(0, 0, 0))
v1 = vpool.makeNewVertex(Point3D(10, 0, 0))
v2 = vpool.makeNewVertex(Point3D(10, 0, 10))
v3 = vpool.makeNewVertex(Point3D(8, 0, 2))

card = EggGroup('card')
simplechar.addChild(card)

p1 = EggPolygon()
card.addChild(p1)
p1.addVertex(v0)
p1.addVertex(v1)
p1.addVertex(v2)
p1.addVertex(v3)

root = EggGroup('root')
simplechar.addChild(root)
root.setGroupType(root.GTJoint)
root.refVertex(v0)

hjoint = EggGroup('hjoint')
root.addChild(hjoint)
hjoint.setGroupType(hjoint.GTJoint)
hjoint.addTranslate3d(Vec3D(10, 0, 0))
hjoint.refVertex(v1)
hjoint.refVertex(v3, membership = 0.7)

vjoint = EggGroup('vjoint')
hjoint.addChild(vjoint)
vjoint.setGroupType(vjoint.GTJoint)
vjoint.addTranslate3d(Vec3D(0, 0, 10))
vjoint.refVertex(v2)
vjoint.refVertex(v3, membership = 0.3)


# Now build the animation tables.

wiggle = EggTable('wiggle')
data.addChild(wiggle)
simplechar = EggTable('simplechar')
wiggle.addChild(simplechar)
simplechar.setTableType(simplechar.TTBundle)
skeleton = EggTable('<skeleton>')
simplechar.addChild(skeleton)

root = EggTable('root')
skeleton.addChild(root)
xform = EggXfmSAnim('xform')
root.addChild(xform)
xform.setOrder('sphrt')
xform.setFps(5)

hjoint = EggTable('hjoint')
root.addChild(hjoint)
xform = EggXfmSAnim('xform')
hjoint.addChild(xform)
xform.setOrder('sphrt')
xform.setFps(5)
for x in [10, 11, 12, 13, 14, 15, 14, 13, 12, 11]:
    xform.addComponentData('x', x)

vjoint = EggTable('vjoint')
hjoint.addChild(vjoint)
xform = EggXfmSAnim('xform')
vjoint.addChild(xform)
xform.setOrder('sphrt')
xform.setFps(5)
for z in [10, 9, 8, 7, 6, 5, 6, 7, 8, 9]:
    xform.addComponentData('z', z)

# Format the egg data for output, so we can visually see that we
# constructed it correctly.
data.write(ostream, 0)

# Finally, load the egg data and create an Actor.
np = NodePath(loadEggData(data))
a = Actor(np)
a.reparentTo(render)
a.setPos(0, 50, 0)
a.loop('simplechar')

David

And finally, here’s the same approach, eschewing the egg loader altogether. The same character, but now we’re creating the low-level Geom structures directly. It’s not too bad in this case, but it would get more complex with a more complex character.

This solution requires the Panda changes that I just committed, which expose the necessary structures (like Character) to Python. These fixes are already available in the CVS version of Panda, and hopefully we’ll be able to pick up these fixes for the upcoming 1.5.4 release, but if not, they’ll certainly be included in some future release of Panda.

from direct.directbase.DirectStart import *
from pandac.PandaModules import *
from direct.actor.Actor import Actor

# Create a character.
ch = Character('simplechar')
bundle = ch.getBundle(0)
skeleton = PartGroup(bundle, '<skeleton>')

# Create the joint hierarchy.
root = CharacterJoint(ch, bundle, skeleton, 'root',
                      Mat4.identMat())
hjoint = CharacterJoint(ch, bundle, root, 'hjoint',
                        Mat4.translateMat(Vec3(10, 0, 0)))
vjoint = CharacterJoint(ch, bundle, hjoint, 'vjoint',
                        Mat4.translateMat(Vec3(0, 0, 10)))

# Create a TransformBlendTable, listing all the different combinations
# of joint assignments we will require for our vertices.
root_trans = JointVertexTransform(root)
hjoint_trans = JointVertexTransform(hjoint)
vjoint_trans = JointVertexTransform(vjoint)

tbtable = TransformBlendTable()
t0 = tbtable.addBlend(TransformBlend())
t1 = tbtable.addBlend(TransformBlend(root_trans, 1.0))
t2 = tbtable.addBlend(TransformBlend(hjoint_trans, 1.0))
t3 = tbtable.addBlend(TransformBlend(vjoint_trans, 1.0))
t4 = tbtable.addBlend(TransformBlend(hjoint_trans, 0.7, vjoint_trans, 0.3))

# Create a GeomVertexFormat to represent the vertices.  We can store
# the regular vertex data in the first array, but we also need a
# second array to hold the transform blend index, which associates
# each vertex with one row in the above tbtable, to give the joint
# assignments for that vertex.
array1 = GeomVertexArrayFormat()
array1.addColumn(InternalName.make('vertex'),
                3, Geom.NTFloat32, Geom.CPoint)
array2 = GeomVertexArrayFormat()
array2.addColumn(InternalName.make('transform_blend'),
                 1, Geom.NTUint16, Geom.CIndex)
format = GeomVertexFormat()
format.addArray(array1)
format.addArray(array2)
aspec = GeomVertexAnimationSpec()
aspec.setPanda()
format.setAnimation(aspec)
format = GeomVertexFormat.registerFormat(format)

# Create a GeomVertexData and populate it with vertices.
vdata = GeomVertexData('vdata', format, Geom.UHStatic)
vdata.setTransformBlendTable(tbtable)
vwriter = GeomVertexWriter(vdata, 'vertex')
twriter = GeomVertexWriter(vdata, 'transform_blend')

vwriter.addData3f(0, 0, 0)
twriter.addData1i(t1)

vwriter.addData3f(10, 0, 0)
twriter.addData1i(t2)

vwriter.addData3f(10, 0, 10)
twriter.addData1i(t3)

vwriter.addData3f(8, 0, 2)
twriter.addData1i(t4)

# Be sure to tell the tbtable which of those vertices it will be
# animating (in this example, all of them).
tbtable.setRows(SparseArray.lowerOn(vdata.getNumRows()))

# Create a GeomTriangles to render the geometry
tris = GeomTriangles(Geom.UHStatic)
tris.addVertices(2, 3, 1)
tris.closePrimitive()
tris.addVertices(1, 3, 0)
tris.closePrimitive()

# Create a Geom and a GeomNode to store that in the scene graph.
geom = Geom(vdata)
geom.addPrimitive(tris)
gnode = GeomNode('gnode')
gnode.addGeom(geom)
ch.addChild(gnode)

# Now create the animation tables.  (We could also load just this part
# from an egg file, if we already have a compatible table ready.)
bundle = AnimBundle('simplechar', 5.0, 10)
skeleton = AnimGroup(bundle, '<skeleton>')
root = AnimChannelMatrixXfmTable(skeleton, 'root')

hjoint = AnimChannelMatrixXfmTable(root, 'hjoint')
table = [10, 11, 12, 13, 14, 15, 14, 13, 12, 11]
data = PTAFloat.emptyArray(len(table))
for i in range(len(table)):
    data.setElement(i, table[i])
hjoint.setTable(ord('x'), CPTAFloat(data))

vjoint = AnimChannelMatrixXfmTable(hjoint, 'vjoint')
table = [10, 9, 8, 7, 6, 5, 6, 7, 8, 9]
data = PTAFloat.emptyArray(len(table))
for i in range(len(table)):
    data.setElement(i, table[i])
vjoint.setTable(ord('z'), CPTAFloat(data))

wiggle = AnimBundleNode('wiggle', bundle)

# Finally, wrap the whole thing in a NodePath and pass it to the
# Actor.
np = NodePath(ch)
anim = NodePath(wiggle)
a = Actor(np, {'simplechar' : anim})
a.reparentTo(render)
a.setPos(0, 50, 0)
a.loop('simplechar')

David

1 Like

Hi drwr,

the code needs a run() at the end to display the animation…

very cool example

Thank you very much, drwr, for these detailed examples. As you might have guessed I’m most interested in the last one. I guess I’ll be checking out and compiling the CVS tree next. :smiley:

Some questions:

  • Is it a requirement that the transform blend indices be in a separate array, or did you do this just for clarity?

  • Is the table entry referred to by “t0” there for a specific reason, or did it just sneak in while you weren’t looking? (It doesn’t appear to be used by any vertices)

There is no requirement for this–it could be in the same array with the rest of the data–but it is a slight performance optimization to keep it separate. (There is no reason to send this data to the graphics card, but Panda will have to send it along anyway if you bundle it up in the same array.)

Oops, no, there’s no reason for this table. The egg loader creates an identity transform in slot 0 of every TransformBlendTable, just in case it needs it, but in my example none of the vertices need it, so I could have eliminated this entry. There’s not much cost to having it there, though.

When you edit and run Python code interactively, you don’t necessarily want the code to call run() as soon as you import it. I’d rather call run() via my editor keyboard commands.

David