ODE Joints and Panda Joints

Hey, I’m pretty new to panda and 3D game development in general, so hopefully this has a straightforward answer.

The plan is to do more with this, but, to strip a problem down to a “simple” example, let’s say I want to make a ragdoll. This ragdoll works wonderfully in ODE, but I want to display it in Panda. Now, I have seen an example where the bones are drawn in Panda, but my specifications require support of a skinned mesh.

The most natural way to do this (as apparent to me) is to get position and rotational data from the ODE joint and apply it to a panda joint. However, this is a bit challenging.

So, one question is, between position and rotation, are there any discrepancies between absolute and relative positioning in Panda and ODE? I think I’m missing something here, because in one test I set all of my joint positions to (0,0,0) and my model didn’t implode like I thought it would.

I suspect that the joint rotation will be a big mathematical pain, but that’s just a hunch…

Any help is really appreciated. If my background helps you frame your answer, I’m going on junior year in college, and I’ve been programming for about 5 years now. I’ve done 3D modeling and graphics, but not down to the low level I’ve been required to on this project. I’ve got a very strong math background, and I’ve been brushing up on concepts like quaternions and whatnot, so that’s not completely foreign to me.

Thanks a lot, let me know any information I have left out.

When you set the joints of a skinned model in Panda, you are always setting relative transforms. If you really need to set absolute transforms on a joint, you can either use Panda’s scene graph to convert absolute to relative transforms at runtime (not difficult, but you need to expose all of the joints to do this), or you can use egg-optchar with a lot of “-p jointname,” parameters to reparent all of your actor’s joints to the actor root (so that absolute and relative transforms are the same thing).

David

Okay, so I set the positions of all the joints in absolute space, using get and setMat with respect to render. It still is far from correct, though. I know I will have to do orientations, as well, but I’m trying to start simple with position only. The thing is, I am fairly certain that I am putting the joints in the right place - I ask the joint for its absolute position and put a sphere there and it’s where I expect it to be, despite the fact that the piece of the model attached to it is not.

So I ran another test. I’m controlling a single joint of my model. I am also drawing a sphere where the joint claims it is (getMat(render)). If I ask it to go to (0,0,0) it works fine, as it does along y axis translations. But if, say, I tell my joint to move to (10, 0, 0), then the sphere will indeed be there, but the model will appear as though the joint were at (0, 0, 10)… The axes for it are rotated 90 degrees about positive y. I find the discrepancy between where it claims the joint is and where it behaves the joint is disturbing. Also note: it works for the root joint, the joint I described is the first child. Any advice? Thanks.

A few clarifications: when I said “absolute space” in the above, I really meant “in the space of the model’s root”. This is only the same thing as render if your overall model is positioned at (0, 0, 0) with no rotation or scale.

But that’s probably not your problem, since you say it works well with the root joint. The problem, then, is that you also have to reconstruct the joint hierarchy within Panda. When you expose a joint, Panda creates a node that it parents directly to the actor root, and every frame it copies the “absolute” (relative to the model root) transform of that joint onto this node.

This means that if you expose all of your joints, you end up with a linear list of nodes, each of them with the “absolute” transform of its corresponding joint.

You need better than that. You need the full hierarchy of nodes, so you can calculate relative transforms. You need a hierarchy of nodes that exactly matches the hierarchy of joints, and each one should have the relative transform of its corresponding joint.

Panda can do this too, but it’s not as automatic. To make it work, you’ll have to first create the hierarchy of nodes yourself. If you don’t already know the hierarchy, you can use part = actor.getPartBundle(‘modelRoot’), then part.getNumChildren()/part.getChild() to walk through the joints recursively.

Then when you call actor.exposeJoint(), pass in the corresponding node as the first parameter, and also pass localTransform = True. This tells Panda to use that node, and to apply the local transform instead of its “absolute” transform.

Or you can try the egg-optchar trick, which might be easier.

David

Thanks for the help so far, you’ve certainly removed a lot of confusion I had with regards to the relative and absolute coordinates. But I’d still like to use egg-optchar…but I can’t seem to figure out the command line syntax for -p :blush: … so could you help me out, or tell me where I can get this syntax? Thanks a lot.

From egg-optchar -h:

Thus, to reparent all of your joints to the root, you’ll run a command like:

egg-optchar -d newdir -p joint1, -p joint2, -p joint_knee, -p joint_elbow, foo.egg

Where joint1, joint2, joint_knee, and so on name all of the joints in your model.

David

Perfect, it works for rotation and everything now. Thanks for the fast reply, I posted it, went to a code design meeting, and returned to an answer. Hopefully we’ve got an interesting project to show for all of this at the end of the summer, take care.

So I’m back again with related problems. At this point, my goal is to get the joints of a panda model to follow ODE joints, and rotate reasonably. At this point, I am keeping track of the rotation of the body as a whole, and I just want to uniformly rotate everything by that rotation. So basically, if the person I have falls down face first without bending everything should look fine. I have something which almost works, but I’m not sure if the error lies in my math or if it’s some sort of rounding error. Here’s a sample of what I’m doing right now (even though I optchar’d before, this model doesn’t, just co clarify):

		
		#This first part sets the root joint have the same orientation as the central bone
		root = self.controlJointsDict['root']
		q = Quat(-1,0,0,1)
		root.setQuat(q.multiply(self.abdomen.solid.getQuat()))
		print "root quat:", root.getQuat()
		for i in range(len(self.joints)):
			odeJoint = self.joints[i].joint #ODE Joint
			control_joint = self.joints[i].control_joint #Control joint (relative to parent..?)
			expose_joint = self.joints[i].expose_joint # Expose joint (relative to actor..?)
        	
			parent_joint = self.joints[i].parent_joint # Exposed version of the parent joint
				
			ode_position = Point3()
			ode_position.setX(odeJoint.getAnchor()[0])
			ode_position.setY(odeJoint.getAnchor()[1])
			ode_position.setZ(odeJoint.getAnchor()[2])
			#This should calculate the required translation in non-rotated space
			desired_transform = ode_position - self.actor.getPos() - parent_joint.getPos()
			parentQuat = parent_joint.getQuat()
			parentQuat.invertInPlace()
			#This should convert the translation to the rotated space of its parent
			new_desired_transform = parentQuat.xform(desired_transform)
			control_joint.setPos(new_desired_transform)

What happens is that it gets CLOSE. You can clearly see that my model is trying to line up properly, but it is extremely jittery. Following up this code with some prints, here is a typical snapshot of what’s going on:

name leg_right_knee
control_joint.getPos() Point3(-0.435378, 0.0122387, -5.54713)
expose_joint.getPos() Point3(-1.40879, 13.1019, 0.594597)
parent_joint.getPos() Point3(-1.2903, 7.51297, 0.873527)
desired_transform VBase3(-0.334286, 5.54355, -0.343031)
#Original, not inverted, parentQuat used
parentQuat.xform(new_desired_transform) VBase3(-0.334286, 5.54355, -0.34303) #This should be the same as desired_transform, and appears to be so
parentQuat.xform(new_desired_transform) + parent_joint.getPos() VBase3(-1.62458, 13.0565, 0.530496) #This should be the same as ode_joint, and it is
ode_joint.getPos() Point3(-1.62458, 13.0565, 0.530496) #This is the desired position.  It is the same as the previous print, which is good, but it is NOT the same as expose_joint.getPos(), which I expected

So does anyone have any insight as to whether I made a mistake or if this is an error of floating point precision or what, I don’t really know. I’ll gladly clarify any ambiguities in this explanation, just ask.

I’m not buying this part:

         #This should calculate the required translation in non-rotated space
         desired_transform = ode_position - self.actor.getPos() - parent_joint.getPos()
         parentQuat = parent_joint.getQuat()
         parentQuat.invertInPlace()
         #This should convert the translation to the rotated space of its parent
         new_desired_transform = parentQuat.xform(desired_transform)
         control_joint.setPos(new_desired_transform)

As a general rule of thumb, anytime you compute relative transforms by subtracting getPos() values, you’re probably doing it wrong. The math involved is almost always more complicated than that. Fortunately, Panda can do that math for you automatically, if you structure your nodes in a hierarchy that exactly match the joint hierarchy, and then use Panda’s relative NodePath operations.

To set this up properly is not trivial, but it is not difficult. It does require some understanding of the relationship between nodes and joints, and a good working understanding of Panda’s relative NodePath operations. I glossed over the proper way to do this a few posts up, but to explain it in detail would take a lot of space.

It’s impossible to tell from the code snippet whether you have done this, but I suspect you haven’t, since you appear to be trying to do the math yourself.

No problem. It’s easier to use egg-optchar as we talked about, and doing this completely eliminates the need to do the relative math at all.

There may be good reasons why you don’t want to do the egg-optchar trick, of course. One reason is that it negatively impacts animation blending and animation compression to separate out your joint hierarchy in this way. But for ragdoll physics, neither of these would seem to be an issue.

So, is there any reason you’re not doing the egg-optchar trick now? Should I try to go into detail to explain how to set up a hierarchy to accept relative transforms without doing the egg-optchar trick?

David

So egg-optchar actually worked fine for the problem presented, I just couldn’t figure out how to do the next stage of the problem with it: that is, also taking into account ODE joint angles. For instance, to calculate the orientation of the elbow joint, I believe that, to look right, I would need both the ODE joint info and its parent’s rotation info, say, the shoulder. Also, ragdoll is just step one, our objective is to have physically modeled animations, so that animation files don’t actually play, but the necessary forces are calculated via a controller which then tries to achieve a particular pose so that animations can be more dynamic (we have the physics down already, we just ultimately need skinned models to behaved).

So, to be clear, are you suggesting that I model the ODE joints in a panda node hierarchy and then the relative transforms should be taken care of more easily? I’m just not sure which types of objects I should make into a hierarchy. ODE joint positions are absolute to begin with, so this just seems to me that, as I create the hierarchy, I’m just moving the problem somewhere else and not eliminating it. So yes, I would be very grateful to receive some clarifications. Thank you so much.

Just an update. This probably isn’t what you had in mind with the node stuff, but I tried something to see if my math is wrong or what. So, here’s what I tried:

parent_joint : pandaNode representing the joint’s parent, obtained by exposeJoint
control_joint: current joint found by controlJoint
ode_position: position in absolute space of the joint I want

			desired_node = NodePath.anyPath(PandaNode("dummynode"))
			desired_node.setPos(ode_position)
			desired_node.setQuat(parent_joint.getQuat())
			new_desired_transform = desired_node.getTransform(parent_joint)
			control_joint.setTransform(new_desired_transform)

So I thought that this would put the joint where I wanted, but I got the same result as when I did the calculations by hand, I get absolute positions like:

(1.69048, -0.895712, 1.73836)

when I want

(1.44536, -0.921794, 1.79122)

I know this isn’t the most elegant thing in the world, but I’m wondering what misunderstandings I have since I see no reason why this shouldn’t work. Thanks again for bearing with me, I know I have a lot of questions.
[/code]

Maybe some sample code will help. Dumb-simple physics, but I think this illustrates what I had in mind. Note the hierarchical structure set up in __walkJointHierarchy(). With this in place, the transform is applied in one statement per joint:

np.setPosQuat(render, pos, quat)

which means to set the NodePath np to the indicated pos and rotation (quat) in world space (that is, relative to render).

The code (uses the Ralph model from the feature tutorials):

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

class RalphWorld:
    def __init__(self):
        # Initialize ODE stuff
        self.world = OdeWorld()
        self.space = OdeSimpleSpace()
        self.contactgroup = OdeJointGroup()
    
        self.FPS = 90.0
        self.DTAStep = 1.0 / self.FPS

        self.world.setAutoDisableFlag(0)
        self.world.setAutoDisableLinearThreshold(0.15)
        self.world.setAutoDisableAngularThreshold(0.15)
        self.world.setAutoDisableSteps(2)
        self.world.setGravity(0,0,-15)
        self.world.initSurfaceTable(1)
        self.world.setSurfaceEntry(0,0, 150, 0.0, 9.1, 0.9, 0.00001, 0.0, 0.002)

        self.space.setAutoCollideWorld(self.world)
        self.space.setAutoCollideJointGroup(self.contactgroup)
        self.world.setQuickStepNumIterations(8)
        self.DTA = 0.0

        # Create a plane for Ralph to fall onto.
        self.floor = OdePlaneGeom(self.space, Vec4(0.0, 0.0, 1.0, 0.0))
        self.floor.setCollideBits(BitMask32(0x00000001))
        self.floor.setCategoryBits(BitMask32(0x00000002))

        # Now set up Ralph.
        self.ralph = Actor('ralph.egg')
        self.ralph.reparentTo(render)

        # Put him up in the air.  Ralph's due for a fall.
        self.ralph.setPos(0, 0, 20)
        self.ralph.setHpr(random.uniform(0, 360), random.uniform(-90, 90), random.uniform(0, 180))

        # If any part of Ralph is visible, draw all of him.  This is
        # necessary because we might be animating some of his joints
        # outside of his original bounding volume.
        self.ralph.node().setFinal(1)

        # Set up the joints as OdeBody objects.
        self.joints = []
        self.setupRagdoll(self.ralph)

        # Set the camera back a bit.
        base.trackball.node().setPos(0, 50, -10)

        # Start the simulation running, after a brief pause.
        taskMgr.doMethodLater(3, self.__simulationTask, "simulation task")

    def __walkJointHierarchy(self, actor, part, parentNode, parentBody):
        if isinstance(part, CharacterJoint):

            # Create a node that we can control to animate the
            # corresponding joint.  Make sure it is a child of the
            # parent joint's node.
            np = parentNode.attachNewNode(part.getName())
            actor.controlJoint(np, 'modelRoot', part.getName())

            # Ensure the node's original transform is the same as the
            # joint's original transform.
            np.setMat(part.getInitialValue())

            # Create an OdeBody object to apply physics to that node.
            body = self.__makeJointBody(np)

            # Create an OdeUniversalJoint to connect this node to its parent.
            bj = OdeUniversalJoint(self.world)
            bj.attachBodies(parentBody, body)
            bj.setAnchor(np.getPos(render))

            parentNode = np
            parentBody = body
            
        for i in range(part.getNumChildren()):
            self.__walkJointHierarchy(actor, part.getChild(i), parentNode, parentBody)

    def __makeJointBody(self, np):
        density = 1
        radius = 1

        body = OdeBody(self.world)
        M = OdeMass()
        M.setSphere(density, radius)
        body.setMass(M)
        body.setPosition(np.getPos(render))
        body.setQuaternion(np.getQuat(render))

        geom = OdeSphereGeom(self.space, radius)
        geom.setCollideBits(BitMask32(0x00000002))
        geom.setCategoryBits(BitMask32(0x00000001))
        geom.setBody(body)

        self.joints.append((np, body))

        return body

    def setupRagdoll(self, actor):
        """ Recursively create a hierarchy of NodePaths, parented to
        the actor, matching the hierarchy of joints.  Each NodePath
        can be used to control its corresponding joint (as in
        actor.controlJoint()). """
        
        bundle = actor.getPartBundle('modelRoot')
        # A body for the overall actor.
        body = self.__makeJointBody(actor)
        self.__walkJointHierarchy(actor, bundle, actor, body)
        
    def __simulationTask(self, task):
        # Run the appropriate number of ODE steps according to the
        # elapsed time since the last time this task was run.
        self.DTA += globalClock.getDt()
        while self.DTA >= self.DTAStep:
            self.DTA -= self.DTAStep
            self.__simulateStep()

        # Apply ODE transforms to the Panda joints.
        for np, body in self.joints:
            pos = body.getPosition()
            # body.getQuaternion() should return a Quat, but it
            # returns a VBase4 instead.  A nuisance.
            quat = Quat(body.getQuaternion())
            
            np.setPosQuat(render, pos, quat)

        return task.cont
        
    def __simulateStep(self):
        self.space.autoCollide() # Detect collisions and create contact joints
        self.world.quickStep(self.DTAStep) # Simulate
        
        self.contactgroup.empty() # Remove all contact joints

rw = RalphWorld()

Wow, that’s perfect! I was easily able to integrate this approach into our own so that we have colliding bones and whatnot. To be honest, I’m still not sure why my other approaches didn’t work, but I have other things to worry about so I’ll let it slide for now. Really, thank you so much. I’ll be sure to let you guys see the finished product.

Sorry for the thread resurrection, but how does the surface table work? Hypno’s ODE code supposes that its based on how many geoms there are in the collision space but this example doesn’t seem to bear that out.

My sample was more or less based on trial and error. It did work with that early version of ode integration that was available back then (which seems to have changed already). So this sample will definately be a better reference.

A note about drwr’s sample code: CharacterJoint.getInitialValue() does no longer exist in 1.6.0, it has been renamed to getDefaultValue(), in case anyone tries this code and it doesn’t work. (No idea if this change was intended to break code)

It wasn’t intended to break any code; it was just one of those stupid things: “Hey, this function meaning is now broader than just the initial value. It should be called the default value instead.”

For some reason I was thinking that no one ever called this method directly from Python. I really should have known better than that, especially considering that even I had posted sample code that did exactly that. :confused:

David