ODE Collision Event Sample

Since I forgot to put this on the 1.6.0 changelog, I thought I’d make it up by providing a sample program for the new ODE event system.
Dumb simple: it just shows some spheres falling down and colliding with each other & the floor.
Plus, it makes a sound when something hits, and, it paints a dot when a ball hits the ground.

from pandac.PandaModules import loadPrcFileData
from direct.directbase import DirectStart
from pandac.PandaModules import OdeWorld, OdeSimpleSpace, OdeJointGroup
from pandac.PandaModules import OdeBody, OdeMass, OdeSphereGeom, OdePlaneGeom
from pandac.PandaModules import BitMask32, CardMaker, Vec4, Quat
from pandac.PandaModules import PNMImage, PNMPainter, PNMBrush, Texture
from random import randint, random

# Setup our physics world
world = OdeWorld()
world.setGravity(0, 0, -9.81)

# The surface table is needed for autoCollide
world.initSurfaceTable(1)
world.setSurfaceEntry(0, 0, 100, 1.0, 9.1, 0.9, 0.00001, 0.0, 0.002)

# Create a space and add a contactgroup to it to add the contact joints
space = OdeSimpleSpace()
space.setAutoCollideWorld(world)
contactgroup = OdeJointGroup()
space.setAutoCollideJointGroup(contactgroup)

# Load the ball
ball = loader.loadModel("smiley")
ball.flattenLight() # Apply transform
ball.setTextureOff()

# Add a random amount of balls
balls = []
# This 'balls' list contains tuples of nodepaths with their ode geoms
for i in range(15):
  # Setup the geometry
  ballNP = ball.copyTo(render)
  ballNP.setPos(randint(-7, 7), randint(-7, 7), 10 + random() * 5.0)
  ballNP.setColor(random(), random(), random(), 1)
  ballNP.setHpr(randint(-45, 45), randint(-45, 45), randint(-45, 45))
  # Create the body and set the mass
  ballBody = OdeBody(world)
  M = OdeMass()
  M.setSphere(50, 1)
  ballBody.setMass(M)
  ballBody.setPosition(ballNP.getPos(render))
  ballBody.setQuaternion(ballNP.getQuat(render))
  # Create a ballGeom
  ballGeom = OdeSphereGeom(space, 1)
  ballGeom.setCollideBits(BitMask32(0x00000001))
  ballGeom.setCategoryBits(BitMask32(0x00000001))
  ballGeom.setBody(ballBody)
  # Create the sound
  ballSound = loader.loadSfx("audio/sfx/GUI_rollover.wav")
  balls.append((ballNP, ballGeom, ballSound))

# Add a plane to collide with
cm = CardMaker("ground")
cm.setFrame(-20, 20, -20, 20)
cm.setUvRange((0, 1), (1, 0))
ground = render.attachNewNode(cm.generate())
ground.setPos(0, 0, 0); ground.lookAt(0, 0, -1)
groundGeom = OdePlaneGeom(space, (0, 0, 1, 0))
groundGeom.setCollideBits(BitMask32(0x00000001))
groundGeom.setCategoryBits(BitMask32(0x00000001))

# Add a texture to the ground
groundImage = PNMImage(512, 512)
groundImage.fill(1, 1, 1)
groundBrush = PNMBrush.makeSpot((0, 0, 0, 1), 8, True)
groundPainter = PNMPainter(groundImage)
groundPainter.setPen(groundBrush)
groundTexture = Texture("ground")
ground.setTexture(groundTexture)
groundImgChanged = False

# Set the camera position
base.disableMouse()
# Just to show off panda auto-converts tuples, now:
base.camera.setPos((40, 40, 20))
base.camera.lookAt((0, 0, 0))

# Setup collision event
def onCollision(entry):
  global groundImgChanged
  geom1 = entry.getGeom1()
  geom2 = entry.getGeom2()
  body1 = entry.getBody1()
  body2 = entry.getBody2()
  # Look up the NodePath to destroy it
  for np, geom, sound in balls:
    if geom == geom1 or geom == geom2:
      velocity = body1.getLinearVel().length()
      if velocity > 2.5 and sound.status != sound.PLAYING:
        sound.setVolume(velocity / 2.0)
        sound.play()
      # If we hit the ground, paint a dot.
      if groundGeom == geom1 or groundGeom == geom2:
        groundImgChanged = True
        for p in entry.getContactPoints():
          groundPainter.drawPoint((p[0] + 20.0) * 12.8, (p[1] + 20.0) * 12.8)

space.setCollisionEvent("ode-collision")
base.accept("ode-collision", onCollision)

# The task for our simulation
def simulationTask(task):
  global groundImgChanged
  space.autoCollide() # Setup the contact joints
  # Step the simulation and set the new positions
  world.quickStep(globalClock.getDt())
  for np, geom, sound in balls:
    if not np.isEmpty():
      np.setPosQuat(render, geom.getBody().getPosition(), Quat(geom.getBody().getQuaternion()))
  contactgroup.empty() # Clear the contact joints
  # Load the image into the texture
  if groundImgChanged:
    groundTexture.load(groundImage)
    groundImgChanged = False
  return task.cont

# Wait a split second, then start the simulation 
taskMgr.doMethodLater(0.5, simulationTask, "Physics Simulation")

run()

More info: panda3d.org/apiref.php?page=OdeCollisionEntry

Useful example that - been needing that capability. Thanks:-)

Quick question, as you seem to know your way around ODE - how do you cast a ray? That looks like if I drop a ray into the space I should then get collisions back from it, which I can then store and find the nearest object etc, buts that is quite involved when all I want is the object my gun is pointed at. I’m just wondering if there is a better way? Only other way I can think of is to brute force collide every object with the ray, which would be bad. What I really need is a ‘collide this object with all objects in this space’ method, but none exists, at least in the API that Panda exposes. (As far as I know - I’ve checked out every ‘collide’ method I’ve bumped into.)

Hmm, not sure what you mean. You can add an OdeRayGeom to the space:
panda3d.org/apiref.php?page=OdeRayGeom
Besides the methods it provides, you can use the event system to figure out with which objects it collides.

You can configure with which objects a geom will collide by setting the category and collide bits. You can kind of compare that to the from and into masks in Panda.

I’m talking about collision detection with ODE outside the actual collision/response loop - my code has all the collisions and collision responses running fine, that is not a problem. The problem is when you want to fire a ray off as a one off. For instance when a player presses the trigger on a gun the bullet moves so quickly you might just want to find out what its going to collide with and handle it there and then. If the ray was coming from the mouse cursor you would call this picking.

Having a ray constantly pointing out the front of the gun and listening to events seems like overkill, especially given that the trigger is not held down most of the time. (Ok, you can enable/disable, but that will delay the ray cast information being available by a frame.) The ideal solution is to construct a ray, call a method on a space and get all collisions that the ray makes with the space back. Right now the Panda ODE interface, as far as I know, only supports getting all collisions between all objects within a space, not all collision between one object and all objects in a space. ODE itself supports this, its just not exposed by the Panda interface from what I can tell - I was hoping you would tell me otherwise. (And, yes, I know that Panda’s collision system supports this, but I’ld rather run one collisions system only!)

You don’t have to put the autoCollide function in the main loop. You can call it whenever you want: create the ray, call autoCollide, destroy the ray.

However, if there is an ODE feature lacking in Panda’s implementation, I’d love to hear it and I’ll fix that.

As far as I know you can’t keep an object in two separate spaces, which is what it would take to solve this problem that way, unless you wanted to waste computation getting all collisions. Well, you could principally create a parent space, do automatic collisions in the child space and then only do collision when needed in the parent space, containing just a ray and this child space. But that would be a rather heavy way to go, and is also not currently supported by the Panda ODE interface. Or change all the collision flags before calling, but that would be nuts. As would keeping two complete copies of the scene.

The problem with the current Panda ODE interface is that a OdeSpace should be a child of a OdeGeom - that is the structure in ODE as in most places you can give a geom id you can also give a space id. Internally the c++ code for ODE inherits the space class from the geom class. Until that is the case the ODE interface is extremely limited - it doesn’t support hierarchies, it doesn’t properly support ray casting, it doesn’t really support not using the autoCollide, which is not what you always want. I’m afraid the original design and implementation of the Panda ODE interface is inherently flawed, and I have no idea how much work it would take to fix.

I’m pretty sure pro-rsoft had implemented a way to do the collisions yourself as per how it is done in PyODE. I’m not sure about the other thing about heirarchies though.

Ah! I think I see what you mean. Try OdeUtil.collide, you can specify two geoms and it returns an OdeContactCollection containing the contacts.

The problem with that is I have to call that on every geom in the scene - I have over 30 for just my test scene. (I like building stacks of cans. Even if they are actually cuboids, and fall over before it even loads due to that dam jitter.) It is using that function where one of the geoms is actually a scene that needs to be enabled to be able to do things the right way, hence why spaces should inherit from geoms, just like they do in ode, and indeed in PyOde. For both of them a space is a valid input to that function, for panda it is not.

Only way I can do it all right now is to drop the rays into the space and use callbacks, which is a horribly messy and expensive way to do what would be a one liner if the inheritance was correct.

(I’m currently trying to build a simple fps demo, so I can play around with fps AI, though I intend to stick it online once I have the basics done as it should be of interest to a few people. With the current system shooting - by both enemies and player, plus AI visibility tests are going to be very expensive, not to mention messy.)

Ok, here is something far more simple, and directly related to your above example - is there anything unique to a specific OdeGeom? A unique number I can stick in a dictionary or some ability to store user specified data? I don’t see one. I know that you can get an associated body, which can have user specified data, but not all geoms have bodies, and I certainly don’t want the level geometry, for instance, to have a body just for that. I need to react differently depending on which geom it is in the callback method, and unlike your above code with a small number of objects I don’t think brute forcing a list is sensible in my case, not for every single collision. I guess I can hack in bodies for the task, but that is hardly ideal.

About OdeSpace inheriting from OdeGeom: I’ll get back to you about that.

For your other question: Since the OdeGeom class itself is nothing but a pointer to the underlying dGeomID (just like NodePath is to PandaNode), and in Python you’re actually using a pointer to that OdeGeom, why not just store an OdeGeom pointer in that dictionary?
If you really need the underlying dGeomID, you could do str(geom) and extract the id out of that string, but that would be ugly.

Yeah, that was my first thought to try, but it didn’t seem to work - id(geom) seems to change and using the geom directly works fine for equality but not in a dictionary. Didn’t think of str(geom), but as you say that would be ugly, though I’ll keep it in mind as it might be the best option for now.

Thanks for considering the inheritance issue - I don’t imagine that being an easy one to fix, but you won’t get the full power of ODE until it is - I only highlighted one problem it causes here, but it has other consequences - composite objects are impossible, as are user constructed hierarchies for optimisation purposes. Really large environments where you split 'em into multiple spaces would also be problematic.

There is a getId method, but it’s not exposed to Python, and I don’t think it should be either. str(geom) and extracting the id would be the closest thing to that.
Actually, using strings as unique identifiers would be less ugly - you could just store str(geom) as dictionary key instead of extracting the id from the string.

Under OS X.5 I get a lot these:

:audio(error): _channel->setVolume(): The specified channel has been reused to play another sound. 

Other then that it seems to be working.

Hmm, yeah, that happens when too much sounds are playing at the same time.

I am enhancing the demomaster with this new ode event.

I would like to have a registration and callback mechanism for the collision event. This will need a dictionary look up for the OdeGeom object to match the objects the caller is interested in.

In the onCollision event handler, I need to look up this dictionary by the
entry.getGeom1(), entry.getGeom2()

But I need to get the id of OdeGeom to be the key of the dictionary.
(cannot use directly the OdeGeom object from entry.getGeom1() and entry.getGeom2())

In short, how can I get the geom id from a OdeGeom efficiently ?

In addition to above question, I have another issue to integrate the ode event with an ODE car program.

I am interested to know the event that the car hit the wall or a building, but not the wheel hitting the floor. If I turn on the ode event:
space.setCollisionEvent(“odeevent”), there are too much collision information fed to the event handler. The program is running significantly slow. In current version, how can I make this more efficient ?

Currently, the only way to do that is:

id = int(str(ode_object).split(" ")[-1].rstrip(")"), 16)

where ode_object is your OdeGeom/Joint/whatever.

Good question. Any ideas how I can make it more efficient? I realize that passing events to Python code is just very slow.

It would be nice if future version of panda can have a get id method for Ode related objects.

I have not fully consider the cases.

Some initial thoughts for a car race game, I think most likely the subjects of interests are one or two geoms. e.g. I would only want to know if a car body is colliding with some objects (walls, other cars) . For wheels, and other un-related collisions, I don’t care about the details, but I would like to know the timing (e.g. play a sound for some moving object collisions).

For other cases an application may be interested to know “real” collisions of many objects (e.g. a pool game), that is when the object is not stationary.

And, I believe the current event method is not very useful as it calls too many times per cycle. My demos all hanging up when there are more than 30-50 objects.

May I know if I can tell panda to stop sending the ode event after calling space.setCollisionEvent(“ode-collision”) ?
Since it is too slow in general, I would like to turn it on for specific demo and switch off after using that.

Hmm, I’m not sure. I’ll consider looking into it.

It works for most cases. I experienced no real performance loss using the event system. Are you sure you aren’t doing slow stuff in your event code? Does calling setCollisionEvent(“blah”) make a performance difference already (even when no DirectObject is accepting “blah”?)

This should work:

space.setCollisionEvent("")

Still, I haven’t experienced such a large slowdown. Happen to have some sample code that shows that it’s very slow?
Also, you should only call setCollisionEvent on the OdeSpaces that you really want an event from.