panda3d's ode

based on a question in irc from chozabu (thanks for the hint) i updated the sample to include immovable’s (like a terrain using a trimesh):

# -----
# example for the integrated ode of panda3d
# -----
# by reto spoerri (Hypnos)
# rspoerri@@@nouser...org
# -----
# panda3d can be crashed using ode, this should hopefully be fixed some time
# -----
# license: you must! modify change and do with this script whatever you want.
# no requirement, but it would be nice if you have good idea's to put samples of it on the forums.
# -----
# possible sources of problems:
# - self.odeWorld.initSurfaceTable( x )
#   i dont know what x should be, but if you dont call this function and use
#   odeSpace.autoCollide() it will crash
# - self.odeContactGroup.empty()
#   panda will add up collisions every frame, it will slow down until nothing
#   works anymore
# - self.odeWorld.step( dt ) crashed for me, quickStep never did
#   (might not be true accurate)
# -----
# usage:
# w/a/s/d/space:   move to bottom block
# -----

import sys, random

from pandac.PandaModules import *
from direct.showbase.DirectObject import DirectObject

Infinity = 1e10000
NaN = Infinity / Infinity

class odeObj( NodePath ):
  # a ode controlled object
  def __init__( self ):
    NodePath.__init__( self, 'odeObj' )
  
  def step( self ):
    pos = self.odeBody.getPosition()
    quat = Quat(self.odeBody.getQuaternion())
    if pos.length() == Infinity or pos.length() == NaN:
      # object got out of control, might happen if something goes wrong with
      # the collision (or a bug)
      self.stop()
      # define a reset pos and rotation for the object
      pos = Vec3(0,0,0)
      quat = Quat(0,0,0,0)
    self.setPos( render, pos )
    self.setQuat( render, quat )
  
  def stop( self ):
    # stop a objects motion and rotation
    # position
    self.odeBody.setLinearVel( Vec3(0,0,0) )
    self.odeBody.setForce( Vec3(0,0,0) )
    # rotation
    self.odeBody.setAngularVel( Vec3(0,0,0) )
    self.odeBody.setTorque( Vec3(0,0,0) )
  
  def setPos( self, *args ):
    NodePath.setPos( self, *args )
    self.updateOde()
  
  def setHpr( self, *args ):
    NodePath.setHpr( self, *args )
    self.updateOde()
  
  def updateOde( self ):
    # moves the ode object, accordingly to the panda object
    # usually the ode object moves the panda object
    # this is not useful if you want to move a object which should collide
    # during the moving time, you will need to apply forces to do that
    self.odeBody.setPosition( self.getPos( render ) )
    self.odeBody.setQuaternion( self.getQuat( render ) )

class odeGeom( odeObj ):
  # a simple ode box object
  def __init__( self, _odeSpace, _odeWorld ):
    odeObj.__init__( self )
    self.model = loader.loadModel( 'models/misc/rgbCube.bam' )
    
    self.odeMass = OdeMass()
    self.odeMass.setBox( 1, Vec3(1,1,1) )
    
    self.odeBody = OdeBody( _odeWorld )
    self.odeBody.setMass( self.odeMass )
    
    # adding a geom to the space here _and_ later (using space.add(geom)) will crash panda
    self.odeGeom = OdeBoxGeom( _odeSpace, Vec3(1,1,1) ) 
    self.odeGeom.setBody( self.odeBody )
    
    self.model.reparentTo( self )

class odeJointBox( odeObj ):
  # a ode box conntected to attachTo with a joint
  def __init__( self, _odeSpace, _odeWorld, attachTo ):
    odeObj.__init__( self )
    self.model = loader.loadModel( 'models/misc/rgbCube.bam' )
    
    self.odeMass = OdeMass()
    self.odeMass.setBox( 1, Vec3(1,1,1) )
    
    self.odeBody = OdeBody( _odeWorld )
    self.odeBody.setMass( self.odeMass )
    
    # adding a geom to the space here _and_ later (using space.add(geom)) will crash panda
    self.odeGeom = OdeBoxGeom( _odeSpace, Vec3(1,1,1) ) 
    self.odeGeom.setBody( self.odeBody )
    
    self.model.reparentTo( self )
    
    # Connect body1 with the static environment
    self.joint = OdeBallJoint( _odeWorld )
    self.joint.attach( self.odeBody, attachTo )
    self.joint.setAnchor( Vec3(2,2,0) )

class odeTrimesh( odeObj ):
  def __init__( self, _odeSpace, _odeWorld ):
    odeObj.__init__( self )
    self.model = loader.loadModel( 'models/panda-model.egg' )
    self.model.setScale( 0.01 )
    
    self.odeMass = OdeMass()
    self.odeMass.setBox( 1, Vec3(1,1,1) )
    
    self.odeBody = OdeBody( _odeWorld )
    self.odeBody.setMass( self.odeMass )
    
    # if a model is scaled you need to call flattenstrong on the data
    # else the collision object will stay the same size as before
    # (at least that's what i suspect)
    self.model.flattenStrong()
    # get the models polygons
    trimeshData = OdeTriMeshData( self.model, True )
    # crete a trimesh object from the trimeshData
    self.odeGeom = OdeTriMeshGeom( _odeSpace, trimeshData )
    
    if False: # checking the data
      print "self.odeGeom.getNumTriangles()", self.odeGeom.getNumTriangles()
      for i in xrange(self.odeGeom.getNumTriangles()):
        x = Point3(0)
        y = Point3(0)
        z = Point3(0)
        self.odeGeom.getTriangle( i, x, y, z )
        print x,y,z
    self.odeGeom.setBody( self.odeBody )
    
    self.model.reparentTo( self )

class odeTerrain( odeObj ):
  # a object that does not move (like a terrain)
  def __init__( self, _odeSpace, _odeWorld ):
    odeObj.__init__( self )
    self.model = loader.loadModel( 'models/panda-model.egg' )
    self.model.setScale( 0.1 )
    #self.model.setPos( Vec3(0,0,-20) )
    
    self.model.flattenStrong()
    
    # get the models polygons
    trimeshData = OdeTriMeshData( self.model, True )
    # crete a trimesh object from the trimeshData
    self.odeGeom = OdeTriMeshGeom( _odeSpace, trimeshData )
    
    self.model.reparentTo( self )
  
  def updateOde( self ):
    # moves the ode object, accordingly to the panda object
    # usually the ode object moves the panda object
    # this is not useful if you want to move a object which should collide
    # during the moving time, you will need to apply forces to do that
    self.odeGeom.setPosition( self.getPos( render ) )
    self.odeGeom.setQuaternion( self.getQuat( render ) )

class mainClass( DirectObject ):
  def __init__( self ):
    self.odeWorld = OdeWorld()
    self.odeWorld.setGravity( 0, 0, -9.81 )
    self.odeWorld.setCfm(0.01)
    self.odeWorld.setErp(0.01)
    self.odeWorld.setContactSurfaceLayer(.0001)
    
    self.odeSpace = OdeSimpleSpace()
    self.odeSpace.enable()
    
    self.floor = OdePlaneGeom(self.odeSpace, 0, 0, 1, 0)
    # visual object for cardmaker
    cm = CardMaker('card')
    cm.setFrame( -1000, 1000, -1000, 1000 )
    card = render.attachNewNode(cm.generate())
    card.setHpr(0,-90,0)
    
    self.objects = list()
    for i in xrange(5):
      obj = odeGeom( self.odeSpace, self.odeWorld )
      obj.reparentTo( render )
      obj.setPos( Vec3(0,5,i*2+2) )
      self.objects.append( obj )
      self.odeWorld.addBodyDampening( obj.odeBody, 0 )
    
    # 2 objects connected by a joint
    obj = odeGeom( self.odeSpace, self.odeWorld )
    obj.reparentTo( render )
    obj.setPos( Vec3(2,5,20))
    self.objects.append( obj )
    obj = odeJointBox( self.odeSpace, self.odeWorld, obj.odeBody )
    obj.reparentTo( render )
    obj.setPos( Vec3(4,5,20))
    self.objects.append( obj )
    
    # a trimesh object
    obj = odeTrimesh( self.odeSpace, self.odeWorld )
    obj.reparentTo( render )
    obj.setPos( Vec3(20,20,10))
    self.objects.append( obj )
    
    # im not sure how to use this settings actually
    # if initSurfaceTable is not defined panda will segfault
    # but already a initSurfaceTable(0) will work
    self.odeWorld.initSurfaceTable(self.odeSpace.getNumGeoms())
    for i in xrange(self.odeSpace.getNumGeoms()):
      for j in xrange(self.odeSpace.getNumGeoms()):
        #  pos1, pos2, mu, bounce, bounce_vel, soft_erp, soft_cfm, slip, dampen
        self.odeWorld.setSurfaceEntry( i, j, 0.1, 0.1, 0, 0.01, 0.01, 0, 0 )
    
    obj = odeTerrain( self.odeSpace, self.odeWorld )
    obj.setPos( Vec3(20,20,0) )
    obj.reparentTo( render )
    
    # create a joint ground for the collision joints
    self.odeSpace.setAutoCollideWorld( self.odeWorld )
    self.odeContactGroup = OdeJointGroup()
    self.odeSpace.setAutoCollideJointGroup( self.odeContactGroup )
    
    self.accept( 'a', self.left )
    self.accept( 'd', self.right )
    self.accept( 'w', self.front )
    self.accept( 's', self.back )
    self.accept( 'e', self.up )
    self.accept( 'u', self.push )
    self.accept( 'j', self.pull )
    self.accept( 'space', self.random )
    self.accept( 'escape', sys.exit )
  
  def step( self ):
    # is required, else the collisions will increase until you have 0fps
    self.odeContactGroup.empty()
    
    # panda's internal method to handle collisions
    contactCount = self.odeSpace.autoCollide()
    
    dt = globalClock.getDt()
    # the first frame of panda3d usually has a pretty large lag, causing objects
    # to fall trough the ground
    if dt > 0.2:
      dt = 0.001
    self.odeWorld.quickStep( dt )
    
    for obj in self.objects:
      obj.step()
  
  def random( self ):
    for obj in self.objects:
      r = random.randint(0,1000)-500
      obj.odeBody.addForce( Vec3(r,r,r) )
  def pull( self ):
    for obj in self.objects:
      force = -obj.getPos( render ) * 50 + Vec3(0,0,50)
      obj.odeBody.addForce( force )
  def push( self ):
    for obj in self.objects:
      obj.odeBody.addForce( obj.getPos( render ) * 50 )
  
  def up( self ):
    obj = self.objects[-1]
    obj.odeBody.addForce( Vec3(0,0,1000) )
  def front( self ):
    obj = self.objects[-1]
    obj.odeBody.addForce( Vec3(0,500,0) )
  def back( self ):
    obj = self.objects[-1]
    obj.odeBody.addForce( Vec3(0,-500,0) )
  def left( self ):
    obj = self.objects[-1]
    obj.odeBody.addForce( Vec3(-500,0,0) )
  def right( self ):
    obj = self.objects[-1]
    obj.odeBody.addForce( Vec3(500,0,0) )

if __name__ == '__main__':
  import direct.directbase.DirectStart
  main = mainClass()
  while True:
    main.step()
    taskMgr.step()