pyODE attract and explode example

Just a guide for doing ode simulation :
[1] create a body. Any types of simulation calculations is performed on a body, including position & orientation changes, and applied forces.
[2] create a geom. Geom is the shape on which the collision detection is performed. Dynamic objects have bodies, while static objects do not. Geom may be one of the basic ode primitives (box, sphere, cappedCylinder, etc.) or GeomTriMesh (polygonal custom shape, which is defined by passing vertices list and vertices index into ode).
[3] at runtime :
a. make the needed changes : change position, rotation, apply forces, etc.
b. run the simulation timestep
c. synchronize all visible geometries associated with the bodies

It sounds like a question for the devs.
If it’s for me, then I have a question for you : how easy do you want it to be ?
Alright, I’ve simplified it for more, but it’s just a quick & dirty one. I simply splitted the only one class into 2 classes : World and ODEsim. Class ODEsim consists of methods directly used for doing simulation (sim setting, collision detection & response, vertices passing, etc).

[EDIT]
enhanced setTriMesh :

  • custom object’s transformation is set before calling setTriMesh, so in setTriMesh, everything is correctly transformed. Thus, it’s more “integrated” ??.
  • more efficiency. No more space for unnecessary duplicated vertex data sent to ODE, compare the print out for yourself

[EDIT 3/18/07]
brought it to object oriented version

Here it is :

[code]from pandac.PandaModules import *
import direct.directbase.DirectStart
from direct.gui.DirectGui import OnscreenText
from direct.showbase.DirectObject import DirectObject
from direct.interval.IntervalGlobal import *
from direct.task import Task
import sys, random
from math import degrees, sin, cos, pi
from direct.showbase import PythonUtil as PU
import ode as ode

def genLabelText(text, i):
return OnscreenText(text = text, pos = (-1.3, .95-.05*i), fg=(1,1,1,1), align = TextNode.ALeft, scale = .05, mayChange=1)

class ODEobject:
‘’’ the base class of ODE boxes, spheres, and TriMeshes used here ‘’’

def storeProps(self, realObj, density, friction, noColl_ID=None):
    self.realObj=realObj
    self.density=density
    self.geom.friction=friction
    # randomize geom's collision ID, if not supplied
    if noColl_ID==None:
       if realObj:
          noColl_ID=id(realObj)
       else:
          noColl_ID=random.random()*random.random()*1000
    self.geom.noColl=noColl_ID

def destroy(self):
    if hasattr(self,'body'):
       del self.body
    self.geom.getSpace().remove(self.geom)
    del self.geom
    if hasattr(self,'visualizer'):
       self.visualizer.removeNode()
    self.realObj.removeNode()

def getOBB(self,collObj):
    ''' get the Oriented Bounding Box '''
    # save object's parent and transformation
    parent=collObj.getParent()
    trans=collObj.getTransform()
    # ODE need everything in world's coordinate space,
    # so bring the object directly under render, but keep the transformation
    collObj.wrtReparentTo(render)
    # get the tight bounds before any rotation
    collObj.setHpr(0,0,0)
    bounds=collObj.getTightBounds()
    # bring object to it's parent and restore it's transformation
    collObj.reparentTo(parent)
    collObj.setTransform(trans)
    # (max - min) bounds
    box=bounds[1]-bounds[0]
    return [box[0],box[1],box[2]]

class ODEbox(ODEobject):
‘’’ An easy setup for creating ode.GeomBox (based on the tight bounding box),
and it’s body.

    If you have a geometry you'd like to use to define GeomBox size precisely,
    pass it as "collObj", or otherwise
    the "realObj" will be used to define GeomBox size.

    You can also set the object's mass density and friction coefficient (mu),
    set density=0 to set the object as a static object, or
    setting it larger than 0 will automatically set the object as a dynamic one.

    "noColl_ID" is an arbitrary number, use it when you need to build
    a larger collision structure from several geoms, which might overlap each other.
    Geoms with the same "noColl_ID" will NOT be tested for collision.
    In such case, don't pass your "realObj" to every geoms, or else
    your "realObj" transformation will be updated based on your every geoms.
    Pass it only when creating the main geom.
'''
def __init__(self, world, space, realObj=None, collObj=None,
                   density=0, friction=0, noColl_ID=None):
    if realObj==None:
       obj=collObj
    else:
       obj=realObj
       if collObj==None:
          collObj=realObj

    boundingBox=self.getOBB(collObj)

    self.geom = ode.GeomBox(space, lengths=boundingBox)
    if density:  # create body if the object is dynamic, otherwise don't
       self.body = ode.Body(world)
       M = ode.Mass()
       M.setBox(density, *boundingBox)
       self.body.setMass(M)
       self.geom.setBody(self.body)
    # synchronize ODE geom's transformation according to the real object's
    self.geom.setPosition(obj.getPos(render))
    self.geom.setQuaternion(obj.getQuat(render))
    # store object's properties
    self.storeProps(realObj, density, friction, noColl_ID)

class ODEsphere(ODEobject):
‘’’ An easy setup for creating ode.GeomSphere (based on the tight bounding box),
and it’s body.

    If you have a geometry you'd like to use to define GeomSphere size precisely,
    pass it as "collObj", or otherwise
    the "realObj" will be used to define GeomSphere size.

    You can also set the object's mass density and friction coefficient (mu),
    set density=0 to set the object as a static object, or
    setting it larger than 0 will automatically set the object as a dynamic one.

    "noColl_ID" is an arbitrary number, use it when you need to build
    a larger collision structure from several geoms, which might overlap each other.
    Geoms with the same "noColl_ID" will NOT be tested for collision.
    In such case, don't pass your "realObj" to every geoms, or else
    your "realObj" transformation will be updated based on your every geoms.
    Pass it only when creating the main geom.
'''
def __init__(self, world, space, realObj=None, collObj=None,
                   density=0, friction=0, noColl_ID=None):
    if realObj==None:
       obj=collObj
    else:
       obj=realObj
       if collObj==None:
          collObj=realObj

    boundingBox=self.getOBB(collObj)
    radius=.5*max(*boundingBox)

    self.geom = ode.GeomSphere(space, radius)
    if density:  # create body if the object is dynamic, otherwise don't
       self.body = ode.Body(world)
       M = ode.Mass()
       M.setSphere(density, radius)
       self.body.setMass(M)
       self.geom.setBody(self.body)
    # synchronize ODE geom's transformation according to the real object's
    self.geom.setPosition(obj.getPos(render))
    self.geom.setQuaternion(obj.getQuat(render))
    # store object's properties
    self.storeProps(realObj, density, friction, noColl_ID)

class ODEtrimesh(ODEobject):
‘’’ An easy setup for creating ode.GeomTriMesh and it’s body.

    Pass any nodepath as "collObj", all geometries' polygons under that node will be sent to ODE,
    including the current transformation (position, rotation, scale), except shear.
    If not given, your "realObj" will be used.

    You can also set the object's mass density and friction coefficient (mu),
    set density=0 to set the object as a static object, or
    setting it larger than 0 will automatically set the object as a dynamic one.
'''
def __init__(self, world, space, realObj, collObj=None, density=0, friction=0, showCollObj=0, noColl_ID=None):
    if collObj==None:
       collObj=realObj
    # find any geomnode under the given node
    geomList=collObj.findAllMatches('**/+GeomNode').asList()
    if not geomList:
       geomList.append(collObj)

    # get the node's scale to preserve it
    meshScale=collObj.getScale(render)
    collNumVtx=0
    collnumface=0
    # dictionary to keep the overall vertex data length before appending the new data
    vDataStart={}
    # list for the vertices
    collVertices=[]
    # list for the vertices index
    collFaces=[]

    # append all vertex data into a complete list
    for collGeom in geomList:
        for gIdx in range(collGeom.node().getNumGeoms()):
            currentGeom=collGeom.node().getGeom(gIdx)
            collVtxData=currentGeom.getVertexData()
            numVtx=collVtxData.getNumRows()
            # append the current vertex data, IF it hasn't been added to the list,
            # otherwise, don't add it again, to avoid duplicated vertex data,
            # which may be shared by several geoms
            if not vDataStart.has_key(collVtxData):
               # store the number of the collected vertices so far,
               # to mark the index start for the next vertex data
               vDataStart[collVtxData]=collNumVtx
               # create vertex reader
               collvtxreader=GeomVertexReader(collVtxData)
               # set the start position for reading the vertices,
               # begin reading at column 0, which is the vertex position
               collvtxreader.setColumn(0)
               # begin reading at vertex 0
               collvtxreader.setRow(0)
               for i in range(numVtx):
                   # respect each geomNode's transformation which may be exist
                   vtx=collObj.getRelativePoint(collGeom,collvtxreader.getData3f())
                   # synchronize TriMesh to the current scale of the collision mesh
                   vtx1=vtx[0]*meshScale[0]
                   vtx2=vtx[1]*meshScale[1]
                   vtx3=vtx[2]*meshScale[2]
                   collVertices.append((vtx1,vtx2,vtx3))
               # add the current collected vertices count
               collNumVtx+=numVtx

    # storing the vertices index
    for collGeom in geomList:
        for gIdx in range(collGeom.node().getNumGeoms()):
            geom=collGeom.node().getGeom(gIdx)
            collVtxData=geom.getVertexData()
            vDataBegin=vDataStart[collVtxData]
            for prim in range(geom.getNumPrimitives()):
                # store the vertices index for each triangle
                collFaceData=geom.getPrimitive(prim).decompose()
                # get the triangle counts
                numface=collFaceData.getNumFaces()
                # get the start index of current primitive at geom's vertex data
                s = collFaceData.getPrimitiveStart(prim)
                for i in range(numface):
                    # refer to the vertex data length list created earlier
                    vtx1=vDataBegin+collFaceData.getVertex(s+i*3)
                    vtx2=vDataBegin+collFaceData.getVertex(s+i*3+1)
                    vtx3=vDataBegin+collFaceData.getVertex(s+i*3+2)
                    collFaces.append((vtx1,vtx2,vtx3))
                collnumface+=numface

    meshdata=ode.TriMeshData()
    # create TriMesh data
    meshdata.build(collVertices,collFaces)
    # and pass it to ODE
    self.geom = ode.GeomTriMesh(meshdata,space)

    # create body if the object is dynamic, otherwise don't
    if density:
       M2 = ode.Mass()
       M2.setBox(density, 1,1,1)
       self.body = ode.Body(world)
       self.body.setMass(M2)
       self.body.setGravityMode(1)
       self.geom.setBody(self.body)
    # store object's properties
    self.storeProps(realObj, density, friction, noColl_ID)

    print '###############################'
    print collObj
    collObj.analyze()
    print '--- sent to ODE ---'
    print 'Vertices : ',collNumVtx
    print 'Faces    : ',collnumface
    print '###############################\n'

    # originally only to debug the transfer method
    self.visualize(collnumface)

    # synchronize TriMesh to the current transformation (position & rotation) of the collision mesh
    self.geom.setPosition(collObj.getPos(render))
    self.geom.setQuaternion(collObj.getQuat(render))

    # hide the collision geometry, if it's not the visible object
    if not showCollObj and collObj!=realObj:
       collObj.hide()

def visualize(self,collnumface):
    ''' this method creates the actual geometry succesfully sent to ODE,
        originally only to debug the transfer method '''

    #(step.1) create GeomVertexData and add vertex information
    format=GeomVertexFormat.getV3()
    vdata=GeomVertexData('vertices', format, Geom.UHStatic)
    vertexWriter=GeomVertexWriter(vdata, 'vertex')
    tris=GeomTriangles(Geom.UHStatic)

    for i in range(collnumface):
      vtx1,vtx2,vtx3=self.geom.getTriangle(i)
      vertexWriter.addData3f(*vtx1)
      vertexWriter.addData3f(*vtx2)
      vertexWriter.addData3f(*vtx3)
      #(step.2) make primitives and assign vertices to them
      tris.addConsecutiveVertices(i*3,3)
      #indicates that we have finished adding vertices for the triangle
      tris.closePrimitive()

    #(step.3) make a Geom object to hold the primitives
    collmeshGeom=Geom(vdata)
    collmeshGeom.addPrimitive(tris)
    #(step.4) now put geom in a GeomNode
    collmeshGN=GeomNode('')
    collmeshGN.addGeom(collmeshGeom)

    self.visualizer = self.realObj.attachNewNode(collmeshGN)
    self.visualizer.setRenderModeWireframe(1)
    self.visualizer.setColor(1,1,1,1)
    scale=self.realObj.getScale()
    self.visualizer.setScale(1./scale[0],1./scale[1],1./scale[2])
    self.visualizer.setLightOff()
    # put the axis at object's origin (represents it's center of gravity)
    axis = loader.loadModelCopy('zup-axis')
    axis.reparentTo(self.visualizer)
    axis.setScale(.25)

class ODEsim:
‘’’ this class consists of methods associated with
ODE simulation setting & loop ‘’’

def initODE(self):
    self.ode_WORLD = ode.World()
    self.ode_WORLD.setGravity((0, 0, -9.81))
    self.ode_WORLD.setCFM(0)

self.ode_WORLD.setContactMaxCorrectingVel(1)

self.ode_WORLD.setERP(0.8)

self.ode_WORLD.setContactSurfaceLayer(.001)

    self.ode_SPACE = ode.Space(type=1)
    self.floor = ode.GeomPlane(self.ode_SPACE, (0, 0, 1),0)
    self.ode_CONTACTgroup = ode.JointGroup()
    self.ODEdt = 1.0 / 40.0

def scalp (self, vec, scal):
    vec[0] *= scal
    vec[1] *= scal
    vec[2] *= scal

def near_callback(self, args, geom1, geom2):
    '''Callback function for the collide() method.

    This function checks if the given geoms do collide and
    creates contact joints if they do.
    '''

    # don't check for collision IF both geoms build a larger collision geom,
    # because they probably overlap each other
    noColl1=101010
    noColl2=-noColl1
    if hasattr(geom1, 'noColl'):
       noColl1=geom1.noColl
    if hasattr(geom2, 'noColl'):
       noColl2=geom2.noColl
    if noColl1==noColl2:
       return

    # Check if the objects do collide
    contacts = ode.collide(geom1, geom2)
    if not len(contacts):
       return

    # averaging the friction coefficient of the two colliding objects
    friction1=friction2=0
    if hasattr(geom1, 'friction'):
       friction1=geom1.friction
    if hasattr(geom2, 'friction'):
       friction2=geom2.friction
    averageFrictionCoef=PU.average(friction1, friction2)

print ‘AVG :’,geom1, geom2, averageFrictionCoef

    # looking for TriMesh-anything collision
    geom1_is_TriMesh=type(geom1).__name__=='GeomTriMesh'
    geom2_is_TriMesh=type(geom2).__name__=='GeomTriMesh'
    bothTriMesh=geom1_is_TriMesh and geom2_is_TriMesh
    if bothTriMesh:
       g1Vel=g2Vel=Vec3(0,0,0)
       g1body=geom1.getBody()
       if g1body:
          g1Vel=Vec3(*g1body.getLinearVel())
       g2body=geom2.getBody()
       if g2body:
          g2Vel=Vec3(*g2body.getLinearVel())

       diff=40.0/(g1Vel-g2Vel).length()
       diff=min(20.0,diff)
       bouncePerContact=diff/len(contacts)

print diff,bouncePerContact

    if ( geom1_is_TriMesh or geom2_is_TriMesh ) and self.contactVis:
       LSpoint=LineSegs()
       LSpoint.setColor(1,1,0)
       LSpoint.setThickness(8)
       LSnormal=LineSegs()
       LSnormal.setColor(1,.5,.2)
       LSnormal.setThickness(2)
       TriMeshCollision=1
    else:
       TriMeshCollision=0

    # Create contact joints
    self.ode_WORLD, self.ode_CONTACTgroup = args
    for c in contacts:
        # if any TriMesh collision, collect the contact points
        if TriMeshCollision:
           pos, normal, depth, g1, g2 = c.getContactGeomParams()
           # create a single point, call moveTo only
           pos=Point3(pos[0],pos[1],pos[2])
           LSpoint.moveTo(pos)
           # create a line
           LSnormal.moveTo(pos)
           LSnormal.drawTo(pos+Point3(normal[0],normal[1],normal[2]))

        # add more bounce if both objects are TriMesh
        if bothTriMesh:
           c.setBounce(bouncePerContact)
        else:
           c.setBounce(.2)
        c.setMu(averageFrictionCoef)
        j = ode.ContactJoint(self.ode_WORLD, self.ode_CONTACTgroup, c)
        j.attach(geom1.getBody(), geom2.getBody())

    # render TriMesh contacts
    if TriMeshCollision:
       contactVisualizer=render.attachNewNode('')
       contactVisualizer.attachNewNode(LSpoint.create())
       contactVisualizer.attachNewNode(LSnormal.create())
       contactVisualizer.setLightOff()
       # remove the visualized TriMesh contacts, after the given time
       taskMgr.doMethodLater(.01,self.removeContactVis,'removeContact',[contactVisualizer])

def removeContactVis(self,vis):
    vis.removeNode()

def simulate(self):
    # synchronize the position and rotation of the visible object to it's body
    for o in self.simObjects:
        if o.density and o.realObj!=None:
           body = o.body
           o.realObj.setPos(render,*body.getPosition())
           o.realObj.setQuat(render,Quat(*body.getQuaternion()))

    self.ode_SPACE.collide((self.ode_WORLD,self.ode_CONTACTgroup), self.near_callback)
    self.ode_WORLD.quickStep(self.ODEdt)
    ''' you can switch to standard world step, for more accuracy,
    but on most cases, it causes simulation instability '''
    #self.ode_WORLD.step(self.ODEdt)
    self.ode_CONTACTgroup.empty()

class World(DirectObject,ODEsim):
‘’’ ODE demo main class ‘’’

def __init__(self):
    '''
    self.ode_WORLD   and   self.ode_SPACE   are
    ode.World        and   ode.Space      , respectively
    '''

    # initialize class ODEsim, to prepare ODE simulation,
    # create world, space, and some simulation tuning) '''
    self.initODE()

    base.setFrameRateMeter(1)
    render.setTransparency(TransparencyAttrib.MDual)
    self.setupLights()

    # list for ODE simulation objects
    self.simObjects = []
    # base scale for the primitive objects
    self.scale = .2
    # TriMesh object visualizer state (on/off)
    self.trimeshVis=1
    # TriMesh contacts visualizer state (on/off)
    self.contactVis=0

    OnscreenText(text='Panda3D: PyODE integration',style=1, fg=(1,1,1,1),pos=(0.8,-0.95), scale = .07)
    genLabelText('the XYZ axis represents the center of gravity', 0)
    genLabelText('[ w ] : BOOOM!', 2)
    genLabelText('[ s ] : magnetism!', 3)

    self.trimeshVisText='[ NUM + ] : toggle TRIMESH VISUALIZER  '
    self.contactVisText='[ NUM - ] : toggle CONTACTS VISUALIZER  '
    self.onOff=['OFF','ON']

    self.trimeshVisLabel = genLabelText(self.trimeshVisText+'[%s]' %self.onOff[self.trimeshVis],4)
    self.contactVisLabel = genLabelText(self.contactVisText+'[%s]' %self.onOff[self.contactVis],5)

    genLabelText('[ 0 ] : scene 0', 7)
    genLabelText('[ 1 ] : scene 1', 8)
    genLabelText('[ 2 ] : scene 2', 9)

    self.keys = {'explode' : 0, 'pull': 0}

    self.accept('w', self.setKey, ['explode', 1])
    self.accept('w-up', self.setKey, ['explode', 0])
    self.accept('s', self.setKey, ['pull', 1])
    self.accept('s-up', self.setKey, ['pull', 0])
    self.accept('+', self.toggleVis)
    self.accept('-', self.toggleContactsVis)
    self.accept('0', self.startScene0)
    self.accept('1', self.startScene1)
    self.accept('2', self.startScene2)
    self.accept('escape', sys.exit)

    self.startScene1()
    taskMgr.add(self.gameLoop, 'runSim')

def cameraLook(self, time,lookPoint=Point3(0,0,0)):
    base.cam.lookAt(lookPoint)

def setKey(self, key, val):
    self.keys[key] = val

def setupLights(self):
    ambientLight = AmbientLight( 'ambientLight' )
    ambientLight.setColor( Vec4(0.4, 0.4, 0.4, 1) )
    self.ambientLight=render.attachNewNode( ambientLight.upcastToPandaNode() )

    directionalLight = DirectionalLight( 'directionalLight1' )
    directionalLight.setDirection( Vec3( 1, -.5,-1 ) )
    directionalLight.setColor( Vec4( .6, .6, .6, 1 ) )
    self.directionalLight=render.attachNewNode( directionalLight.upcastToPandaNode() )

    render.setLight(self.ambientLight)
    render.setLight(self.directionalLight)

def drawCage(self):
    self.cage=render.attachNewNode('')
    size=80
    height=10
    scale=Vec3(size,size,5)
    obj = loader.loadModelCopy('misc/rgbCube')
    obj.reparentTo(self.cage)
    obj.setPos(0,0,-scale[2]*.5)
    obj.setScale(scale)
    obj.setColor(0,0,0,1)
    self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                    realObj=obj, density=0, friction=5) )

    scale=Vec3(1,size,height)
    for side in range(2):
        obj = loader.loadModelCopy('misc/rgbCube')
        obj.reparentTo(self.cage)
        obj.setPos(size*(side-.5),0,height*.5)
        obj.setScale(scale)
        obj.setTransparency(1)
        obj.setAlphaScale(.7)
        self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                        realObj=obj, density=0, friction=5) )

    scale=Vec3(size,1,height)
    for side in range(2):
        obj = loader.loadModelCopy('misc/rgbCube')
        obj.reparentTo(self.cage)
        obj.setPos(0,size*(side-.5),height*.5)
        obj.setScale(scale)
        obj.setTransparency(1)
        obj.setAlphaScale(.7)
        self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                        realObj=obj, density=0, friction=5) )

    print 'drawCage done...'


def drawBodies(self,numBodies):
    for i in range(numBodies):
        dynamic=i%2
        if dynamic:
           scale=self.scale*1.2
        else:
           scale=self.scale

        theta = random.uniform(0, 2*pi)
        rX=random.gauss(0,2)
        rY=random.gauss(0,2)
        rX += 2.*rX/abs(rX)
        rY += 2.*rY/abs(rY)
        pos=Point3(rX,rY,20*dynamic)
        if random.randint(0,1):    # sphere shape model
           if dynamic:
              model='smiley'
              hpr=Vec3(theta,theta,theta)
           else:
              model='jack'
              hpr=Vec3(180,0,0)
              scale*=2.5
           obj = loader.loadModelCopy(model)
           obj.reparentTo(render)
           obj.setPosHprScale(pos, hpr, Vec3(scale,scale,scale))
           self.simObjects.append( ODEsphere( world=self.ode_WORLD, space=self.ode_SPACE,
                                              realObj=obj, density=250*dynamic, friction=1000) )
        else:                      # box shape model
           if dynamic:
              hpr=Vec3(theta,theta,theta)
              scale=Vec3(scale, scale, scale)
           else:
              hpr=Vec3(0,0,0)
              scale=Vec3(scale*3, scale, scale*10)
           obj = loader.loadModelCopy('misc/rgbCube')
           obj.reparentTo(render)
           obj.setPosHprScale(pos, hpr, scale)
           self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                           realObj=obj, density=1200*dynamic, friction=1000) )

    print 'drawBodies done...'


def drawTriMesh(self):

self.realObject5=loader.loadModelCopy(‘camera’)

self.realObject5.reparentTo(render)

self.realObject5.setPos(-4,2,30)

self.realObject5.setScale(1.5)

self.simObjects.append( ODEtrimesh( world=self.ode_WORLD, space=self.ode_SPACE,

realObj=self.realObject5, density=40, friction=10 ) )

    self.realObject6=loader.loadModelCopy('misc/objectHandles')
    self.realObject6.reparentTo(render)
    self.realObject6.setPos(4,2,20)
    self.realObject6.setScale(1.4)
    self.simObjects.append( ODEtrimesh( world=self.ode_WORLD, space=self.ode_SPACE,
                                        realObj=self.realObject6, density=30, friction=20 ) )

    self.realObject7=loader.loadModelCopy('misc/Pointlight')
    self.realObject7.reparentTo(render)
    self.realObject7.setPos(-4,-1,25)
    self.realObject7.setScale(1.3)
    self.simObjects.append( ODEtrimesh( world=self.ode_WORLD, space=self.ode_SPACE,
                                        realObj=self.realObject7, density=30, friction=10 ) )

self.realObject8=loader.loadModelCopy(‘misc/Dirlight’)

self.realObject8.reparentTo(render)

self.realObject8.setPos(-4,-5,20)

self.realObject8.setScale(.6)

self.simObjects.append( ODEtrimesh( world=self.ode_WORLD, space=self.ode_SPACE,

realObj=self.realObject8, density=30, friction=15 ) )

self.realObject9=loader.loadModelCopy(‘misc/Spotlight’)

self.realObject9.reparentTo(render)

self.realObject9.setPos(4,3,25)

self.realObject9.setScale(.6)

self.simObjects.append( ODEtrimesh( world=self.ode_WORLD, space=self.ode_SPACE,

realObj=self.realObject9, density=50, friction=15 ) )

self.realObject10=loader.loadModelCopy(‘panda-model’)

self.realObject10.reparentTo(render)

self.realObject10.setPos(-4,-4,25)

self.realObject10.setScale(.005)

self.simObjects.append( ODEtrimesh( world=self.ode_WORLD, space=self.ode_SPACE,

realObj=self.realObject10, density=50, friction=10 ) )

self.realObject11=loader.loadModelCopy(‘panda’)

self.realObject11.reparentTo(render)

self.realObject11.setPos(5,-2,25)

self.realObject11.setScale(.3)

self.simObjects.append( ODEtrimesh( world=self.ode_WORLD, space=self.ode_SPACE,

realObj=self.realObject11, density=30, friction=2 ) )

def buildBoxesTower(self):
    size=3
    height=1.2
    columnScale=(.55,.55,height)
    floorThick=.15
    floorScale=(size*1.12,size*1.12,floorThick)
    deviation=.28
    for floor in range(9):
        pos=( 0,0,(height+floorThick)*(floor+1)-floorThick*.5 )
        obj = loader.loadModelCopy('misc/rgbCube')
        obj.reparentTo(render)
        obj.setPos(*pos)
        obj.setScale(floorScale[0]-floor*deviation,floorScale[1]-floor*deviation,floorScale[2])
        obj.setLightOff()
        obj.setPythonTag('posZ',pos[2])
        # exclude object from receiving explode & pull forces
        obj.setTag('excluded','')
        self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                        realObj=obj, density=75, friction=5000) )

        for column in range(4):
            pos=( (size-floor*deviation)*((column%2)-.5), .5*(size-floor*deviation)*(column-column%2-1),(height+floorThick)*floor+height*.5 )
            scale=Vec3(columnScale[0]-floor*.02,columnScale[1]-floor*.02,columnScale[2])
            obj = loader.loadModelCopy('misc/rgbCube')
            obj.reparentTo(render)
            obj.setPos(*pos)
            obj.setScale(scale)
            obj.setLightOff()
            obj.setPythonTag('posZ',pos[2])
            # exclude object from receiving explode & pull forces
            obj.setTag('excluded','')
            self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                            realObj=obj, density=75, friction=5000) )

def buildChainReaction(self):
    radius=10
    center=(-radius,16.1)
    height=3
    objGap=.27
    scale=Vec3(height*.5,.5,height)
    for i in range(30):
        rad=i*(objGap+i*i*.0002)
        x=cos(rad)*(radius-i*objGap) + center[0]
        y=sin(rad)*(radius-i*objGap) + center[1]
        obj = loader.loadModelCopy('misc/rgbCube')
        obj.reparentTo(render)
        obj.setPosHprScale(Point3( x, y, height*.5), Vec3(degrees(rad),0,0), scale)
        obj.setLightOff()
        # exclude object from receiving explode & pull forces
        obj.setTag('excluded','')
        self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                        realObj=obj, density=30, friction=200) )

    for i in range(2):
        height=8
        pos=Point3(-2*(i-.5)*3,3.5,height*.5)
        scale=Vec3(25,.2,height)
        deg=2*(i-.5)*78
        obj = loader.loadModelCopy('misc/rgbCube')
        obj.reparentTo(render)
        obj.setPosHprScale(pos,Vec3(deg,0,0),scale)
        obj.setColor(1,1,1,.5)
        obj.setLightOff()
        # exclude object from receiving explode & pull forces
        obj.setTag('excluded','')
        self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                        realObj=obj, density=0, friction=0) )

    ramp = loader.loadModelCopy('misc/rgbCube')
    ramp.reparentTo(render)
    ramp.setPosHprScale(0,12.8,.5, 0,210,90, .2,7,2.5)
    ramp.setLightOff()
    self.simObjects.append( ODEbox( world=self.ode_WORLD, space=self.ode_SPACE,
                                    realObj=ramp, density=0, friction=0) )

    self.arrow = loader.loadModelCopy('misc/Dirlight')
    self.arrow.reparentTo(ramp)
    self.arrow.setH(180)
    self.arrow.setScale(.2)
    arrowBW=LerpPosInterval(self.arrow,.4,Point3(.5,1.5,0),startPos=Point3(.5,1.2,0),blendType='easeOut')
    arrowFW=self.arrow.posInterval(.4,Point3(.5,1.2,0))
    self.arrowAnim = Sequence(
                       Parallel(arrowBW,
                                LerpColorInterval(self.arrow,.4,Vec4(0,0,0,0),Vec4(0,0,0,1)) ),
                       Parallel(arrowFW,
                                LerpColorInterval(self.arrow,.4,Vec4(0,0,0,1)) ) )
    self.arrowAnim.loop()

def explo(self):
    for o in self.simObjects:
        if o.density and not o.realObj.hasTag('excluded'):
           l=o.body.getPosition ()
           d=Vec3(*l).length()
           if not d: d=1
           a = max(0, 2000*(1.0-0.2*d*d))
           l = [l[0] / 10, l[1]/10, l[2]/1.5]
           self.scalp (l, a / d)
           o.body.addForce(l)

def pull(self):
    for o in self.simObjects:
        if o.density and not o.realObj.hasTag('excluded'):
           l=list (o.body.getPosition ())
           d=Vec3(*l).length()
           if not d: d=1
           self.scalp (l, -300 / d)
           o.body.addForce(l)

def gameLoop(self, task):
    if self.keys['explode']:
       self.explo()
    if self.keys['pull']:
       self.pull()

    self.simulate()
    # check if each object is already collapsed (structurally failed),
    # degrade it's color if it does
    self.isCollapsed()

    return Task.cont

def isCollapsed(self):
    if self.scene==1:
       for o in self.simObjects:
           if o.realObj.hasTag('excluded'):
              z=o.realObj.getPythonTag('posZ')
              if ( Vec3(*o.body.getLinearVel()).length() > z*.7 or
                   Vec3(*o.body.getAngularVel()).length() > z*.7 ):
                   o.realObj.setColorScale(.2,.2,.2,1)
                   o.realObj.clearTag('excluded')
    elif self.scene==2:
       numExclusion=0
       for o in self.simObjects:
           if o.realObj.hasTag('excluded'):
              numExclusion+=1
              if abs(o.realObj.getP()) > 60 :
                 o.realObj.setColorScale(.2,.2,.2,1)
                 o.realObj.clearTag('excluded')
       if numExclusion==2 and self.arrowAnim:
          self.arrowAnim.pause()
          self.arrowAnim=None
          self.arrow.removeNode()

def toggleVis(self):
    self.trimeshVis=not self.trimeshVis
    self.trimeshVisLabel.setText(self.trimeshVisText+'[%s]' %self.onOff[self.trimeshVis])
    for o in self.simObjects:
        if hasattr(o,'visualizer'):
           if self.trimeshVis:
              o.visualizer.show()
           else:
              o.visualizer.hide()

def toggleContactsVis(self):
    self.contactVis=not self.contactVis
    self.contactVisLabel.setText(self.contactVisText+'[%s]' %self.onOff[self.contactVis])


def sceneSetup(self,numBodies=60):
    if self.ode_SPACE.getNumGeoms()>1:
       for o in self.simObjects:
           o.destroy()
       if self.cameraInterval:
          self.cameraInterval.pause()
          self.cameraInterval=None
       self.simObjects = []

    self.drawCage()
    self.drawBodies(numBodies)
    self.drawTriMesh()
    for o in self.simObjects:
        if not self.trimeshVis and hasattr(o,'visualizer'):
           o.visualizer.hide()

def startScene0(self):
    self.scene=0
    self.sceneSetup(numBodies=60)
    base.mouseInterfaceNode.reset()
    self.cameraInterval=Parallel(
         LerpPosInterval(base.cam, 3, Point3(0,-20,8), startPos=Point3(0,0,90), blendType='easeOut'),
         LerpFunc(self.cameraLook, duration=3) )
    self.cameraInterval.start()

def startScene1(self):
    self.scene=1
    self.sceneSetup(numBodies=40)
    self.buildBoxesTower()
    base.mouseInterfaceNode.reset()
    self.cameraInterval=Parallel(
         LerpPosInterval(base.cam, 3, Point3(0,-28,10), startPos=Point3(60,20,90), blendType='easeOut'),
         LerpFunc(self.cameraLook, duration=3, extraArgs=[Point3(0,10,4)]) )
    self.cameraInterval.start()

def startScene2(self):
    self.scene=2
    self.sceneSetup(numBodies=40)
    self.buildChainReaction()
    base.mouseInterfaceNode.reset()
    self.cameraInterval=Parallel(
         LerpPosInterval(base.cam, 4, Point3(0,-40,30), startPos=Point3(60,20,90), blendType='easeOut'),
         LerpFunc(self.cameraLook, duration=4, extraArgs=[Point3(0,10,0)]) )
    self.cameraInterval.start()

World()
run()[/code]