Use Ode to do ray casting and object selection

Every once in a while, I get a PM on how to use ODE for ray-casting and object selection. So I thought I would write a small example to show how I’m using ODE to do my object selection and world physics.

Why Ode?

  1. It’s WAY WAY simpler to use than bullet physics. If all you want is rigid body physics + applying simple forces, give Ode a try

  2. Panda has a fairly good implementation of all of the basic ODE functions wrapped up nicely.

  3. Ode docs are extremely easy to understand because its so simple.

Ode recognizes atomic geometric bodies such boxes, spheres, arbitrary meshes, etc… You can also stick these atomic objects into ‘spaces’ to organize them hiearchically.

The most important Ode collision functions are
dCollide - rigorous collision detection between two objects
dSpaceCollide2 - approx. collision detection between a space and object or between two spaces
dSpaceCollide - approx. collision detection for all objects with a space

dSpace* collisions are only approximate, and to check to see if you have a real collision, you have to use a callback function to call dCollide to check the putative collision.

Panda sticks dCollide and dSpaceCollide2 into the OdeUtil module, and dSpaceCollide is a public member of the space class.

Thus to do a ray check, put all of your world objects into a 'space, then use OdeUtil.collide2 to check for collisiosn between the ray and your world objects.

The only tricky part is you have to cast Panda’s space object to a Panda Geom object in python using

self.spacegeom = OdeUtil.spaceToGeom( self.space ) to use OdeUtil.collide2

#http://www.ode.org/ode-latest-userguide.html#sec_6_3_0
#http://www.panda3d.org/manual/index.php/Collision_Detection_with_ODE
#http://pyode.sourceforge.net/tutorials/tutorial1.html
#http://www.panda3d.org/manual/index.php/Collision_Detection_with_ODE

#import __Header
#import PandaHeader
import direct.directbase.DirectStart

from pandac.PandaModules import Point3, DirectionalLight, VBase4, VBase3
from pandac.PandaModules import Filename
import random
import math
import time
from pandac.PandaModules import *
#from _CameraHandler import clCameraHandler
from direct.showbase.DirectObject import DirectObject



  # // test if geom o1 and geom o2 can collide
  # cat1 = dGeomGetCategoryBits (o1);
  # cat2 = dGeomGetCategoryBits (o2);
  # col1 = dGeomGetCollideBits (o1);
  # col2 = dGeomGetCollideBits (o2);
  # if ((cat1 & col2) || (cat2 & col1)) {
    # // call the callback with o1 and o2
  # }
  # else {
    # // do nothing, o1 and o2 do not collide
# }

def MakeWall(size, vb4Color = VBase4( .6, .6, .6, 1)):
	temp = CardMaker('')
	temp.setFrame(-size, size, -size, size)
	npSquare = NodePath( temp.generate() )
	npSquare.setColor( vb4Color )
	npSquare.setTwoSided(True)
	return npSquare


class clSim(DirectObject):
	booDebugDrawMouseLine = False
	booTimeit = False
	def __init__(self, objCamera = None):
		world = OdeWorld()
		world.setGravity( 0, 0, -10.0)
#		world.setErp(0.8)
#		world.setCfm(1E-5)
		world.initSurfaceTable(1)
		world.setSurfaceEntry(0, 0, 150, 0.0, 9.1, 0.9, 0.00001, 0.0, 0.002)

		space = OdeQuadTreeSpace( Point3(0,0,0), VBase3( 200, 200, 200), 7 )
		space.setAutoCollideWorld(world)
		contactgroupWorld = OdeJointGroup()
		space.setAutoCollideJointGroup( contactgroupWorld )
		
		self.world = world
		self.space = space
		self.spacegeom = OdeUtil.spaceToGeom( self.space )
		self.contactgroupWorld = contactgroupWorld

		self.dictStaticObjects = {}
		self.tlist = []
		self.dictDynamicObjects = {}
		self.raygeomMouse = OdeRayGeom( 10 )
		self.idMouseRayGeom = self.raygeomMouse.getId().this
		
		self.InitBoxPrototype()
		
		self.objCamera = objCamera
		self.npCamera = base.cam if objCamera == None else objCamera.GetNpCamera()
		self.EnableMouseHit()
		taskMgr.add( self.taskSimulation, 'taskSimulation')
		
	def AddStaticTriMeshObject(self, npObj, strName = '', bitMaskCategory = BitMask32(0x0), bitMaskCollideWith = BitMask32(0x0)):
		trimeshData = OdeTriMeshData( npObj, True )
		trimeshGeom = OdeTriMeshGeom( self.space, trimeshData )
		trimeshGeom.setCollideBits( bitMaskCollideWith )
		trimeshGeom.setCategoryBits( bitMaskCategory )
		
		trimeshGeom.setPosition( npObj.getPos() )
		trimeshGeom.setQuaternion( npObj.getQuat() )
		
		id = trimeshGeom.getId().this
		self.dictStaticObjects[ id ] = ( strName, trimeshGeom )
		
	def EnableMouseHit(self):
		self.accept('mouse1', self.OnMouseBut1 )
		
	def DisableMouseHit(self):
		self.ignore('mouse1')
		
	def InitBoxPrototype(self):
		npBox = loader.loadModel("box")
		#Center the box. it starts out 0 <= x,y,z <=1
		npBox.setPos(-.5, -.5, -.5)
		npBox.flattenLight() # Apply transform
		npBox.setTextureOff()
		self.npBox = npBox
		
	def RefreshMousePointerRayGeom(self):
		pt3CamNear = Point3( 0,0,0)
		pt3CamFar = Point3(0,0,0)
		pt2Mpos = base.mouseWatcherNode.getMouse()
		self.npCamera.node().getLens().extrude( pt2Mpos, pt3CamNear, pt3CamFar)
		
		pt3CamNear = render.getRelativePoint( self.npCamera, pt3CamNear )
		pt3CamFar = render.getRelativePoint( self.npCamera, pt3CamFar )
		
		if self.booDebugDrawMouseLine:
			npLine = CreateConnectedLine( [pt3CamNear, pt3CamFar] )
			npLine.reparentTo( render )
		
		pt3Dir = pt3CamFar - pt3CamNear
		fLength = pt3Dir.length()
		self.raygeomMouse.setLength( fLength )
		self.raygeomMouse.set( pt3CamNear, pt3Dir/fLength )

	def OnMouseBut1(self):
		self.RefreshMousePointerRayGeom()
		
		OdeUtil.collide2( self.raygeomMouse, self.spacegeom, '33', self.CollideNear_Callback )
		if self.tlist != []:
			self.tlist.sort()
			print 'Mouse hit', time.time()
			self.PrintObject( self.tlist[0][1].getG1().getId().this )
			self.PrintObject( self.tlist[0][1].getG2().getId().this )
			self.tlist = []
	
	def PrintObject(self, id):
		if id != self.idMouseRayGeom:
			if id in self.dictStaticObjects:
				print self.dictStaticObjects[ id ]
			elif id in self.dictDynamicObjects:
				print self.dictDynamicObjects[ id ]
			else:
				print 'id', id, 'not recognized!'

	def CollideNear_Callback(self, data, geom1, geom2):
		contactGroup = OdeUtil.collide( geom1, geom2 )
		if contactGroup.getNumContacts() != 0:
			tupDistContact = self.FindNearestContactFromGroup( contactGroup )
			self.tlist.append( tupDistContact )

	def FindNearestContactFromGroup(self, contactGroup):
		camPos = self.npCamera.getPos()
		tlist = [ ((contactGroup[i].getPos() - camPos).length(), contactGroup[i]) for i in xrange(contactGroup.getNumContacts()) ]
		tlist.sort()
		return tlist[0]
	
	def taskSimulation(self, task):
		if self.booTimeit:
			fStarttime = time.clock()
		self.space.autoCollide() # Setup the contact joints
		# Step the simulation and set the new positions
		self.world.quickStep(globalClock.getDt())
		for id in self.dictDynamicObjects:
			np = self.dictDynamicObjects[id][3]
			body = self.dictDynamicObjects[id][2]
			np.setPosQuat(render, body.getPosition(), Quat(body.getQuaternion()))
		self.contactgroupWorld.empty() # Clear the contact joints
		if self.booTimeit:
			print time.clock() - fStarttime
		return task.cont

	def AddBox(self, strId = '', pt3Pos = Point3(0, 0, 20), vb4Color = VBase4( 1, 1, 1, 1) ):
		newbox = self.npBox.copyTo( render )
		newbox.setColor( vb4Color )
		newbox.setPos( pt3Pos )
		
		M = OdeMass()
		M.setBox(50, 1, 1, 1)
		
		boxBody = OdeBody( self.world )
		boxBody.setPosition( newbox.getPos() )
		boxBody.setQuaternion( newbox.getQuat() )
		
		boxGeom = OdeBoxGeom( self.space, 1, 1, 1 )
		boxGeom.setCollideBits( BitMask32( 3 ) )
		boxGeom.setCategoryBits( BitMask32( 2 ) )
		
		## Links the transformation matrices of the space and the world together.
		boxGeom.setBody( boxBody )
		
		self.dictDynamicObjects[ boxGeom.getId().this ] = ( strId, boxGeom, boxBody, newbox )

scale = 20
#objCamera = clCameraHandler( pt3CameraPos = Point3(0, -scale*4, scale*4 ), tupNearFar = (0.05, scale*20 ) )
objCamera = None
base.cam.setPos( Point3(0, -scale*4, scale*4 ) )
base.cam.lookAt( Point3(0, 0, 0 ) )

ground = MakeWall( scale )
left = MakeWall( scale, vb4Color = VBase4( .6, 0, 0, 1) )
right = MakeWall( scale, vb4Color = VBase4( .6, 0, 0, 1) )
front = MakeWall( scale, vb4Color = VBase4( 0, 0.6, 0, 1) )
#back = MakeWall( scale, vb4Color = VBase4( 0, 0, 0.6, .2) )

for i in [ground, left, right, front]:
	i.reparentTo( render )

ground.setHpr( 0, -90, 0)
right.setPos( scale, 0, scale )
right.setHpr( 90, 0, 0 )
left.setPos( -scale, 0, scale)
left.setHpr( 90, 0, 0 )
front.setPos( 0, scale, scale )
#back.setPos( 0, -scale, scale )
#back.setTransparency( True )


global nBoxes
global objSim
global nStartTime

objSim = clSim( objCamera )
objSim.AddStaticTriMeshObject( ground, 'ground', bitMaskCategory = BitMask32( 1 ), bitMaskCollideWith = BitMask32( 2 ) )
objSim.AddStaticTriMeshObject( left, 'left', bitMaskCategory = BitMask32( 1 ), bitMaskCollideWith = BitMask32( 2 ) )
objSim.AddStaticTriMeshObject( right, 'right', bitMaskCategory = BitMask32( 1 ), bitMaskCollideWith = BitMask32( 2 ) )
objSim.AddStaticTriMeshObject( front, 'front', bitMaskCategory = BitMask32( 1 ), bitMaskCollideWith = BitMask32( 2 ) )
#objSim.AddStaticTriMeshObject( back, 'front', bitMaskCategory = BitMask32( 1 ), bitMaskCollideWith = BitMask32( 2 ) )

objSim.AddBox( strId = 'red', vb4Color = (0, 0.6, 0, 1) )
nBoxes = 0
nStartTime = int(time.time())

def taskAddBall(task):
	global nBoxes
	global objSim
	global nStartTime
	if nBoxes < 50:
		if ((time.time() - nStartTime) > 1):
			objSim.AddBox( strId = str( 'box '+ str( nBoxes )) , vb4Color = ( 1.*nBoxes/50, 0.6 ,0, 1) )
			nBoxes += 1
			nStartTime = time.time()
		return task.cont
	return task.done

taskMgr.add( taskAddBall, 'addball' )

run()