bug in OdeUniversalJoint (solved)

i think i’ve discovered a bug in the OdeUniversalJoint implementation:

in http://panda3d.cvs.sourceforge.net/viewvc/panda3d/panda/src/ode/odeUniversalJoint.I?revision=1.5&view=markup

   98 
   99 INLINE void OdeUniversalJoint::
  100 set_param_lo_stop(int axis, dReal val) {
  101   nassertv( _id != 0 );
  102   nassertv( 0 <= axis && axis <= 1 );
  103   if ( axis == 0 ) {
  104     dJointSetUniversalParam(_id, dParamLoStop, val);
  105   } else if ( axis == 1 ) {
  106     dJointSetUniversalParam(_id, dParamLoStop, val);
  107   }
  108 }
  109 
  110 INLINE void OdeUniversalJoint::
  111 set_param_hi_stop(int axis, dReal val) {
  112   nassertv( _id != 0 );
  113   nassertv( 0 <= axis && axis <= 1 );
  114   if ( axis == 0 ) {
  115     dJointSetUniversalParam(_id, dParamHiStop, val);
  116   } else if ( axis == 1 ) {
  117     dJointSetUniversalParam(_id, dParamHiStop, val);
  118   }
  119 }

when the axis is 1, the dParamHiStop and dParamLoStop should be replaced by dParamHiStop2 and dParamLoStop2.

when i understand it correctly all other parameters that work on axis == 1 should be modified to use paramName2 instead of paramName as well.

i’ve discovered it because of a pyode snipplet that looks like this when defining both axis’s:

        joint.setAxis1((0,1,0))
        joint.setAxis2((1,0,0))
        joint.setParam(ode.ParamLoStop,lo_stop1)
        joint.setParam(ode.ParamHiStop,hi_stop1)
        joint.setParam(ode.ParamLoStop2,lo_stop2)
        joint.setParam(ode.ParamHiStop2,hi_stop2)        

finally i got a idea why my tests creating a ragdoll failed miserably until now…

edit: it’s OdeUniversalJoint not the hingeJoint

tnx hatjosh

(a fix has been checked in)

Sorry about that. Looks like all of the functions in odeUniversalJoint.I suffered from the same problem. I’ve fixed them and it compiles ok. Unfortunately I don’t have good way of testing it to make sure it really works as expected.

Would you happen to have a simple self-contained test case I could use? Or if you compile your own Panda, you could just as easily try it out now.

unfortunately i havent got a small sample that shows the bug clearly. but you can try this code i’ve been working on and check if the legs and arms move as one would expect. i’ve been experiencing that the bones were stuck in one direction or did move freely (in one axis-rotation) with the previous version. (i hope the parameters correspond to my workaround).

models and “normal” code:
http://www.nouser.org/WD/projects/sparetime/humanModel/ragdoll_2009-08-21-v002.zip

replace the ragdoll_v005.py contents with:

import sys, os, random, time, math

from pandac.PandaModules import *

loadPrcFileData("", "basic-shaders-only #f") 
loadPrcFileData("", "show-frame-rate-meter 1")
loadPrcFileData("", "vsync #f")

import direct.directbase.DirectStart 
from direct.showbase.DirectObject import * 
from direct.actor.Actor import Actor
from direct.task import Task


dt = 0.03
loopFlag = True
world = OdeWorld()
world.setGravity((0,0,-9))
world.setQuickStepNumIterations(600)
# The surface table is needed for autoCollide
world.initSurfaceTable(1)
# pos1, pos2, mu, bounce, bounce_vel, soft_erp, soft_cfm, slip, dampen
# soft_erp (0.2 default)
# soft_cfm (0 is hard, larger = soft)
# dampen (0.01 default)
world.setSurfaceEntry(0, 0, 100, 0.0, 0.0, 0.8, 0.0, 0.0, 0.01)
space = OdeSimpleSpace()
space.setAutoCollideWorld(world) 
contactgroup = OdeJointGroup()
space.setAutoCollideJointGroup(contactgroup)
space.setCollisionEvent("ode-collision")

render.setShaderAuto(1)

class World(DirectObject):
  def __init__(self):
    # create plane
    plane = loader.loadModel('plane')
    plane.setScale(2) 
    plane.reparentTo(render)
    self.plane = OdePlaneGeom(space, 0, 0, 1, 0)
    self.plane.setCollideBits(BitMask32(0x00000001))
    self.plane.setCategoryBits(BitMask32(0x00000001))
    
    self.rag1 = Ragdoll() #make an instance of Ragdoll class
    
    c = 0.7
    self.charLight = Spotlight('slight')
    self.charLight.setColor(VBase4(c,c,c, 1))
    #self.charLight.setExponent(32.0)
    try:
      self.charLight.setShadowCaster(True, 2048, 2048)
      pass
    except:
      pass
    #lens = PerspectiveLens()
    #lens.setFov(45)
    #self.charLight.setLens(lens)
    self.charLightNp = render.attachNewNode(self.charLight)
    self.charLightNp.setPos(2, 2, 20)
    self.charLightNp.lookAt(1, 1, 1)
    render.setLight(self.charLightNp)
    
    c = 0.3
    self.alight = AmbientLight('alight')
    self.alight.setColor(VBase4(c,c,c, 1))
    self.alnp = render.attachNewNode(self.alight)
    render.setLight(self.alnp)
    
    f = 3
    base.accept('e', self.addForce, [Vec3(0,0,f)])
    base.accept('q', self.addForce, [Vec3(0,0,-f)])
    base.accept('a', self.addForce, [Vec3(-f,0,0)])
    base.accept('d', self.addForce, [Vec3(f,0,0)])
    base.accept('w', self.addForce, [Vec3(0,f,0)])
    base.accept('s', self.addForce, [Vec3(0,-f,0)])
    base.accept('r', self.reset)
    self.doSimulate = False
    base.accept('space', self.toggleSimulate)
    
    #base.camera.setPos(20,20,30)
    
    self.simulate()
    taskMgr.add(self.simulate, "runSim")
  
  def reset(self):
    print "reset"
    for i in xrange(len(self.rag1.bones)):
      body = self.rag1.bones[i].body
      print i,self.rag1.bones[i].body
      body.setTorque(0,0,0)
      body.setAngularVel(0,0,0)
      body.setLinearVel(0,0,0)
      body.setForce(0,0,0)
      self.rag1.bones[i].reset()
      #body.setRotation(Mat3.identMat())
      print body.getAngularVel()
    print
  
  def toggleSimulate(self):
    self.doSimulate = not self.doSimulate
    if self.doSimulate:
      self.reset()
  
  def addForce(self, force):
    for i in xrange(len(self.rag1.bones)):
      self.rag1.bones[i].body.addForce(force * (random.random()* 0.9 + 0.2))
    #self.rag1.boneDict['neck'].body.addForce(force)
  
  def simulate(self,task=None):
    if self.doSimulate:
      for i in xrange(len(self.rag1.bones)):
        self.rag1.bones[i].updateSolid()
        # artificial slowdown
        #bone.body.setAngularVel( bone.body.getAngularVel() * 0.9 )
        #bone.body.setLinearVel( bone.body.getLinearVel() * 0.9 )
      space.autoCollide()
      world.quickStep(dt)
      contactgroup.empty()
    #base.camera.lookAt(self.rag1.bones[0].solid.getPos(render))
    
    #self.charLightNp.lookAt(self.rag1.bones[0].solid.getPos(render))
    return Task.cont

base.accept('p', base.toggleWireframe)
base.accept('escape', sys.exit)

class Ragdoll():
  def __init__(self):
    
    self.actor = Actor('char-20-v004.egg')
    self.actor.reparentTo(render)
    #self.actor.setRenderModeWireframe()
    # keep this if you ever want to modify the actor again...
    #print "getActorInfo", playerActor.getActorInfo()
    #print "getAnimNames", playerActor.getAnimNames()
    #print "listJoints", self.actor.listJoints()
    
    # expose the joints of the model
    jointConnections = list()
    actorArmature = self.actor.getJoints(jointName='*')
    exposedJoints = dict()
    exposedParents = dict()
    for parentJoint in actorArmature:
      # dont expose fingers and toes, it's too much detail
      if 'finger' in parentJoint.getName() or 'toe' in parentJoint.getName():
        continue
      else:
        for i in xrange(parentJoint.getNumChildren()):
          childJoint = parentJoint.getChild(i)
          jointConnections.append([parentJoint, childJoint])
          exposedParents[childJoint.getName()] = parentJoint.getName()
        exposedJoints[parentJoint.getName()] = self.actor.controlJoint(None,"modelRoot",parentJoint.getName())
    
    # we need to restore the parents of the nodes
    for childJointName, parentJointName in exposedParents.items():
      if childJointName in exposedJoints:
        print "reparent", childJointName, "to", parentJointName
        exposedJoints[childJointName].reparentTo(exposedJoints[parentJointName])
    exposedJoints['pelvis'].reparentTo(self.actor)
    
    # create the bones
    boneDict = dict()
    for pJ, cJ in jointConnections:
      pJName = pJ.getName()
      cJName = cJ.getName()
      if pJName in exposedJoints and cJName in exposedJoints:
        
        startJoint = exposedJoints[pJName]
        endJoint = exposedJoints[cJName]
        
        pos1 = startJoint.getPos(render)
        pos2 = endJoint.getPos(render)
        radius = (pos1-pos2).length() / 7.0
        length = (pos1-pos2).length() * 0.25
        
        print "bone", pJName, cJName, pos1, pos2, radius
        
        bone = Bone(exposedJoints[cJName], pos1, pos2, radius, length)
        boneDict[cJName] = bone
    startJoint = exposedJoints['pelvis']
    endJoint = exposedJoints['pelvis']
    pos1 = startJoint.getPos(render)
    pos2 = endJoint.getPos(render)
    bone = Bone(exposedJoints['pelvis'], pos1, pos2, radius, radius)
    boneDict['pelvis'] = bone
    self.bones = boneDict.values()[:]
    self.boneDict = boneDict
    
    self.exposedJoints = exposedJoints
    joints = list()
    p = math.pi
    p34 = math.pi*3/4 * 0.75
    p2 = math.pi/2 * 0.75
    p4 = math.pi/4 * 0.75
    p8 = math.pi/8 * 0.75
    # when creating a new model (modifying the existing)
    # define all jointConnectors as ['fix', []] first, and modifiy each invidually
    jointConnectors = {
      ('pelvis', 'L.upper-leg')     : ['fix', []], #['uni', [(1,0,0),0,p34, (0,1,0),0,p4]],
      ('pelvis', 'R.upper-leg')     : ['fix', []], #['uni', [(1,0,0),0,p34, (0,1,0),0,p4]],
      ('pelvis', 'back')            : ['fix', []], #['uni', [(1,0,0),-p8,p8, (0,1,0),-p8,p8]],
      ('L.upper-leg', 'L.lower-leg'): ['fix', []], #['hin', [(1,0,0), -p34,0]],
      ('L.lower-leg', 'L.footbase') : ['uni', [(1,0,0),-p4,p4, (0,1,0),-p4,p4]],
      ('R.upper-leg', 'R.lower-leg'): ['fix', []], #['hin', [(1,0,0), -p34,0]],
      ('R.lower-leg', 'R.footbase') : ['uni', [(1,0,0),-p4,p4, (0,1,0),-p4,p4]],
      ('back', 'L.shoulder')        : ['fix', []], #['uni', [(1,0,0),-p8,p8, (0,1,0),-p8,p8]],
      ('back', 'R.shoulder')        : ['fix', []], #['uni', [(0,0,1),-p8,p8, (0,1,0),-p8,p8]],
      ('back', 'neck')              : ['fix', []], #['uni', [(0,0,1),-p8,p8, (0,1,0),-p8,p8]],
      ('L.shoulder', 'L.upper-arm') : ['uni', [(0,0,1),0,p2, (0,1,0),-p2,p2]], # good
      ('L.upper-arm', 'L.lower-arm'): ['fix', []], #['hin', [(0,0,1), 0,p34]], # good
      ('L.lower-arm', 'L.hand')     : ['fix', []], #['uni', [(0,0,1),-p4,p4, (0,1,0),-p4,p4]],
      ('R.shoulder', 'R.upper-arm') : ['uni', [(0,0,1),-p2,0, (0,1,0),-p2,p2]], # good
      ('R.upper-arm', 'R.lower-arm'): ['fix', []], #['hin', [(0,0,1), -p34,0]], # good
      ('R.lower-arm', 'R.hand')     : ['fix', []], #['uni', [(0,0,1),-p4,p4, (0,1,0),-p4,p4]],
      ('neck', 'head')              : ['fix', []], #['uni', [(1,0,0),-p8,p8, (0,1,0),-p8,p8]],
    }
    
    for pJ, cJ in jointConnections:
      pJName = pJ.getName()
      cJName = cJ.getName()
      if pJName in boneDict and cJName in boneDict:
        pBone = boneDict[pJName]
        cBone = boneDict[cJName]
        jType, jArgs = jointConnectors[(pJName, cJName)]
        if jType == 'fix':
          joint = self.fixedJoint(pBone, cBone)
        elif jType == 'uni':
          joint = self.universalJoint(pBone, cBone, jArgs)
        elif jType == 'hin':
          joint = self.hingeJoint(pBone, cBone, jArgs)
        joints.append(joint)
        #print "('%s', '%s'): []," % (pJName, cJName)
    print "create done"
    
    if 0:
      # attach head to the world
      headJoint = OdeBallJoint(world)
      headJoint.attach(boneDict['head'].body, None)
      headJoint.setAnchor((boneDict['head'].body.getPosition())) #+Vec3(0,0,10)))
  
  def universalJoint(self,obj1,obj2,jArgs):
    axis1, loStop1, hiStop1, axis2, loStop2, hiStop2 = jArgs
    joint = OdeUniversalJoint(world)
    joint.attach(obj1.body,obj2.body)
    joint.setAnchor((obj2.body.getPosition()))
    joint.setAxis1(axis1)
    joint.setParamLoStop(0,loStop1)
    joint.setParamHiStop(0,hiStop1)
    joint.setAxis2(axis2)
    joint.setParamLoStop(1,loStop2)
    joint.setParamHiStop(1,hiStop2)
    return joint
  
  def hingeJoint(self,obj1,obj2,jArgs):
    axis, loStop, hiStop = jArgs
    #for elbows and khees
    joint = OdeHingeJoint(world)
    joint.attach(obj1.body,obj2.body)
    joint.setAnchor((obj2.body.getPosition()))
    joint.setAxis(axis)
    joint.setParamLoStop(loStop)
    joint.setParamHiStop(hiStop)
    return joint
  def ballJoint(self,obj1,obj2):
    #not in use for now
    joint = OdeBallJoint(world)
    joint.attach(obj1.body,obj2.body)
    joint.setAnchor((obj2.body.getPosition()))        
    return joint
  def fixedJoint(self,obj1,obj2):
    joint = OdeFixedJoint(world)
    joint.attach(obj1.body,obj2.body)
    joint.set()
    return joint

class Bone():
  def __init__(self,parent,startPos,endPos,radius,length):
    # create panda node to visualisation
    s2 = loader.loadModel('RGB_CUBE.egg')
    s2.reparentTo(parent)
    s2.setScale(render, radius, length, radius)
    s2.setPos(render, endPos)
    
    # create mass object to represent body mass 
    self.M = OdeMass() 
    self.M.setSphere(5, 0.1)
    #now we will create ode body
    self.body = OdeBody(world)
    self.body.setMass(self.M)
    
    # create geometry to collisions
    self.geom1 = OdeBoxGeom(space, radius, length, radius)
    self.geom1.setBody(self.body)
    
    # ajust position of ode body node
    #self.body.setQuaternion(s2.getQuat(render))
    #self.body.setPosition(s2.getPos(render))
    
    self.startQuat = s2.getQuat(render)
    self.startPos = s2.getPos(render)
    
    self.solid = parent
    
    self.reset()
    self.updateSolid()
  
  def updateSolid(self):
    # dont change this, it's correct
    self.solid.setPosQuat(render, self.geom1.getPosition(), Quat(self.geom1.getQuaternion()))
  
  def reset(self):
    self.body.setQuaternion(self.startQuat)
    self.body.setPosition(self.startPos)

w = World()
run()      

press to start the physics,
q: down
e: up
w: forward
s: backward
a: left
d: right
r: restart/reset
zoom out with the mouse first, it’s the default controls

you could play around with the jointConnectors vectors if the rotations seem wrong as well. i have fixed some of the joints so it’s more clearly visible if it works fine.

the arms can move to the back of the character when i run it with my panda version, which shoudnt be possible.

thanks for the fix :slight_smile: