Representing ODE geoms/collisions as visible geom

In an effort to learn more about ODE/Panda3D and as an assistance for debugging ODE simulation, I am writing a few Python classes that will let me visualize what is happening. Primarily, I want a way to see the ODE Geoms and since I can find no way to debug view these, I decided to start work on a Python implementation.

This, of course, probably would normally live in the C++ part of Panda3D, but I am nowhere near ready to start tackling that right now. :slight_smile:

Here’s what I got so far: A class for to generate GeomNode representations of ODE geoms.

Maybe other people will find this useful. Comments/suggestions are always welcome, as I am still learning Panda3d.

import direct.directbase.DirectStart

from pandac.PandaModules import Point3, Vec3

from pandac.PandaModules import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from pandac.PandaModules import Geom, GeomNode, GeomPoints, NodePath, GeomLinestrips

import math

"""
Note that wireprims are wire-like representations of geom, in the same manner as Ogre's debug mode.  I find this the most useful way to represent
ODE geom structures visually, as you can clearly see the orientation versus a more generic wireframe mesh.

These wireprims are rendered as linestrips.  Therefore, only vertices are required and texturing is not supported.  You can use standard render attribute changes such
as setColor in order to change the line's color.  By default it is green.

This class merely returns a NodePath to a GeomNode that is a representation of what is requested.  You can use this outside of ODE geom visualizations, obviously.

Supported are sphere, box, cylinder, capsule (aka capped cylinder), ray, and plane

to use:

sphereNodepath = wireGeom().generate ('sphere', radius=1.0)
boxNodepath = wireGeom().generate ('box', extents=(1, 1, 1))
cylinderNodepath = wireGeom().generate ('cylinder', radius=1.0, length=3.0)
rayNodepath = wireGeom().generate ('ray', length=3.0)
planeNodepath = wireGeom().generate ('plane')

"""
class wireGeom:
  
  def __init__ (self):    
    # GeomNode to hold our individual geoms
    self.gnode = GeomNode ('wirePrim')
    
    # How many times to subdivide our spheres/cylinders resulting vertices.  Keep low
    # because this is supposed to be an approximate representation
    self.subdiv = 12

  def line (self, start, end):  
    
    # since we're doing line segments, just vertices in our geom
    format = GeomVertexFormat.getV3()
    
    # build our data structure and get a handle to the vertex column
    vdata = GeomVertexData ('', format, Geom.UHStatic)
    vertices = GeomVertexWriter (vdata, 'vertex')
        
    # build a linestrip vertex buffer
    lines = GeomLinestrips (Geom.UHStatic)
    
    vertices.addData3f (start[0], start[1], start[2])
    vertices.addData3f (end[0], end[1], end[2])
    
    lines.addVertices (0, 1)
      
    lines.closePrimitive()
    
    geom = Geom (vdata)
    geom.addPrimitive (lines)
    # Add our primitive to the geomnode
    self.gnode.addGeom (geom)

  def circle (self, radius, axis, offset):  
    
    # since we're doing line segments, just vertices in our geom
    format = GeomVertexFormat.getV3()
    
    # build our data structure and get a handle to the vertex column
    vdata = GeomVertexData ('', format, Geom.UHStatic)
    vertices = GeomVertexWriter (vdata, 'vertex')
        
    # build a linestrip vertex buffer
    lines = GeomLinestrips (Geom.UHStatic)
    
    for i in range (0, self.subdiv):
      angle = i / float(self.subdiv) * 2.0 * math.pi
      ca = math.cos (angle)
      sa = math.sin (angle)
      if axis == "x":
        vertices.addData3f (0, radius * ca, radius * sa + offset)
      if axis == "y":
        vertices.addData3f (radius * ca, 0, radius * sa + offset)
      if axis == "z":
        vertices.addData3f (radius * ca, radius * sa, offset)
    
    for i in range (1, self.subdiv):
      lines.addVertices(i - 1, i)
    lines.addVertices (self.subdiv - 1, 0)
      
    lines.closePrimitive()
    
    geom = Geom (vdata)
    geom.addPrimitive (lines)
    # Add our primitive to the geomnode
    self.gnode.addGeom (geom)

  def capsule (self, radius, length, axis):
    
    # since we're doing line segments, just vertices in our geom
    format = GeomVertexFormat.getV3()
    
    # build our data structure and get a handle to the vertex column
    vdata = GeomVertexData ('', format, Geom.UHStatic)
    vertices = GeomVertexWriter (vdata, 'vertex')
        
    # build a linestrip vertex buffer
    lines = GeomLinestrips (Geom.UHStatic)
    
    # draw upper dome
    for i in range (0, self.subdiv / 2 + 1):
      angle = i / float(self.subdiv) * 2.0 * math.pi
      ca = math.cos (angle)
      sa = math.sin (angle)
      if axis == "x":
        vertices.addData3f (0, radius * ca, radius * sa + (length / 2))
      if axis == "y":
        vertices.addData3f (radius * ca, 0, radius * sa + (length / 2))

    # draw lower dome
    for i in range (0, self.subdiv / 2 + 1):
      angle = -math.pi + i / float(self.subdiv) * 2.0 * math.pi
      ca = math.cos (angle)
      sa = math.sin (angle)
      if axis == "x":
        vertices.addData3f (0, radius * ca, radius * sa - (length / 2))
      if axis == "y":
        vertices.addData3f (radius * ca, 0, radius * sa - (length / 2))
    
    for i in range (1, self.subdiv + 1):
      lines.addVertices(i - 1, i)
    lines.addVertices (self.subdiv + 1, 0)
      
    lines.closePrimitive()
    
    geom = Geom (vdata)
    geom.addPrimitive (lines)
    # Add our primitive to the geomnode
    self.gnode.addGeom (geom)

  def rect (self, width, height, axis):
    
    # since we're doing line segments, just vertices in our geom
    format = GeomVertexFormat.getV3()
    
    # build our data structure and get a handle to the vertex column
    vdata = GeomVertexData ('', format, Geom.UHStatic)
    vertices = GeomVertexWriter (vdata, 'vertex')
        
    # build a linestrip vertex buffer
    lines = GeomLinestrips (Geom.UHStatic)
    
    # draw a box
    if axis == "x":
      vertices.addData3f (0, -width, -height)
      vertices.addData3f (0, width, -height)
      vertices.addData3f (0, width, height)
      vertices.addData3f (0, -width, height)
    if axis == "y":
      vertices.addData3f (-width, 0, -height)
      vertices.addData3f (width, 0, -height)
      vertices.addData3f (width, 0, height)
      vertices.addData3f (-width, 0, height)
    if axis == "z":
      vertices.addData3f (-width, -height, 0)
      vertices.addData3f (width, -height, 0)
      vertices.addData3f (width, height, 0)
      vertices.addData3f (-width, height, 0)

    for i in range (1, 3):
      lines.addVertices(i - 1, i)
    lines.addVertices (3, 0)
      
    lines.closePrimitive()
    
    geom = Geom (vdata)
    geom.addPrimitive (lines)
    # Add our primitive to the geomnode
    self.gnode.addGeom (geom)

  def generate (self, type, radius=1.0, length=1.0, extents=Point3(1, 1, 1)):
            
    if type == 'sphere':
      # generate a simple sphere
      self.circle (radius, "x", 0)
      self.circle (radius, "y", 0)
      self.circle (radius, "z", 0)

    if type == 'capsule':
      # generate a simple capsule
      self.capsule (radius, length, "x")
      self.capsule (radius, length, "y")
      self.circle (radius, "z", -length / 2)
      self.circle (radius, "z", length / 2)

    if type == 'box':
      # generate a simple box
      self.rect (extents[1], extents[2], "x")
      self.rect (extents[0], extents[2], "y")
      self.rect (extents[0], extents[1], "z")

    if type == 'cylinder':
      # generate a simple cylinder
      self.line ((0, -radius, -length / 2), (0, -radius, length/2))
      self.line ((0, radius, -length / 2), (0, radius, length/2))
      self.line ((-radius, 0, -length / 2), (-radius, 0, length/2))
      self.line ((radius, 0, -length / 2), (radius, 0, length/2))
      self.circle (radius, "z", -length / 2)
      self.circle (radius, "z", length / 2)
    
    if type == 'ray':
      # generate a ray
      self.circle (length / 10, "x", 0)
      self.circle (length / 10, "z", 0)
      self.line ((0, 0, 0), (0, 0, length))
      self.line ((0, 0, length), (0, -length/10, length*0.9))
      self.line ((0, 0, length), (0, length/10, length*0.9))

    if type == 'plane':
      # generate a plane
      length = 3.0
      self.rect (1.0, 1.0, "z")
      self.line ((0, 0, 0), (0, 0, length))
      self.line ((0, 0, length), (0, -length/10, length*0.9))
      self.line ((0, 0, length), (0, length/10, length*0.9))
    
    # rename ourselves to wirePrimBox, etc.
    name = self.gnode.getName()
    self.gnode.setName(name + type.capitalize())
    
    NP = NodePath (self.gnode)  # Finally, make a nodepath to our geom
    NP.setColor(0.0, 1.0, 0.0)   # Set default color
    
    return NP
  
  
# demonstration code below

allGeoms = render.attachNewNode ("allWireGeoms")

plane = wireGeom().generate ('plane')
plane.setPos (0, 0, 0)
plane.setHpr (0, 0, 0)
plane.reparentTo( allGeoms )

sphere = wireGeom().generate ('sphere', radius=1.0)
sphere.setPos (0, 3, 1)
sphere.setHpr (0, 0, 0)
sphere.reparentTo( allGeoms )

capsule = wireGeom().generate ('capsule', radius=1.0, length=3.0)
capsule.setPos (0, 6, 1)
capsule.setHpr (0, 0, 0)
capsule.reparentTo( allGeoms )

box = wireGeom().generate ('box', extents=(1, 1, 1))
box.setPos (0, 9, 1)
box.setHpr (0, 0, 0)
box.reparentTo( allGeoms )

cylinder = wireGeom().generate ('cylinder', radius=1.0, length=3.0)
cylinder.setPos (0, -3, 1)
cylinder.setHpr (0, 0, 0)
cylinder.reparentTo( allGeoms )

ray = wireGeom().generate ('ray', length=3.0)
ray.setPos (0, -6, 1)
ray.setHpr (0, 0, 0)
ray.reparentTo( allGeoms )

allGeoms.reparentTo( base.render )

# Set the camera position
base.disableMouse()
base.camera.setPos(40, 0, 0)
base.camera.lookAt(0, 0, 0)

# Spin all of the wireGeoms around so you can see 'em
i = allGeoms.hprInterval (4.0, Vec3(360, 360, 0))
i.loop()

run()

Hey, cool! Thanks for sharing, I can really use this.

No problem, glad you can use it. I am still puzzling out ODE, and working on an organization scheme for all these ODE geoms, masses, and bodies. Toss in spaces and you’re looking at duplicating a mini-scene graph along with Pandas, so I am trying to find efficient hooks between these concepts.

I’v been playing around with your code and it works great and is very helpful except for one small issue im having. I have set up a simple box drop test to play around with. I make a box geom and a wireframe but the wireframe is twice as big as my boxgeom. If i set the wireframe to half the size of the geom then it looks perfect. Am I just doing something dumb?

 # Create a BoxGeom
        self.boxGeom = OdeBoxGeom(space, 2, 2, 2)
        self.boxGeom.setCollideBits(BitMask32(0x00000002))
        self.boxGeom.setCategoryBits(BitMask32(0x00000002))
        self.boxGeom.setBody(self.boxBody)
    
        #Create a Wireframe of the box geom
        self.odebox = wireGeom().generate ('box', extents=(1, 1, 1)) 
        self.odebox.reparentTo( self ) 

First off, I have to say thank you, this is fantastic. Ive been on the forum just using the search features and never saying anything, but this was so perfect for what I needed it actually compelled me to post.

secondly @Mcadieux
From what I gathered out of it, ODE when you specify you want a box that is 1,1,1 you get a box that is 1 long, 1 deep, and 1 high

for these functions it is specifying you want a box that spans from -1,-1,-1 to 1,1,1

I just modified the function to divide whats passed in by 2 for boxes, since i preferred to be able to use the same dimensions to draw that I am passing to ODE to create the geometry

I also found it useful to modify the generate function to take in RGB values rather than making everything green, made it much easier to see things (or just generate a random RGB value if none was passed in)

This is the modified version I am using now

def generate (self, type, radius=1.0, length=1.0, extents=Point3(1, 1, 1), R=-1, G=-1, B=-1):
    if R==-1:
        R=random.uniform(0,1)
    if G==-1:
        G=random.uniform(0,1)
    if B==-1:
        B=random.uniform(0,1)

    if type == 'sphere':
      # generate a simple sphere
      self.circle (radius, "x", 0)
      self.circle (radius, "y", 0)
      self.circle (radius, "z", 0)

    if type == 'capsule':
      # generate a simple capsule
      self.capsule (radius, length, "x")
      self.capsule (radius, length, "y")
      self.circle (radius, "z", -length / 2)
      self.circle (radius, "z", length / 2)

    if type == 'box':
      # generate a simple box
      self.rect (extents[1]/2, extents[2]/2, "x")
      self.rect (extents[0]/2, extents[2]/2, "y")
      self.rect (extents[0]/2, extents[1]/2, "z")

    if type == 'cylinder':
      # generate a simple cylinder
      self.line ((0, -radius, -length / 2), (0, -radius, length/2))
      self.line ((0, radius, -length / 2), (0, radius, length/2))
      self.line ((-radius, 0, -length / 2), (-radius, 0, length/2))
      self.line ((radius, 0, -length / 2), (radius, 0, length/2))
      self.circle (radius, "z", -length / 2)
      self.circle (radius, "z", length / 2)

    if type == 'ray':
      # generate a ray
      self.circle (length / 10, "x", 0)
      self.circle (length / 10, "z", 0)
      self.line ((0, 0, 0), (0, 0, length))
      self.line ((0, 0, length), (0, -length/10, length*0.9))
      self.line ((0, 0, length), (0, length/10, length*0.9))

    if type == 'plane':
      # generate a plane
      length = 3.0
      self.rect (1.0, 1.0, "z")
      self.line ((0, 0, 0), (0, 0, length))
      self.line ((0, 0, length), (0, -length/10, length*0.9))
      self.line ((0, 0, length), (0, length/10, length*0.9))

    # rename ourselves to wirePrimBox, etc.
    name = self.gnode.getName()
    self.gnode.setName(name + type.capitalize())

    NP = NodePath (self.gnode)  # Finally, make a nodepath to our geom
    NP.setColor(R, G, B)   # Set default color

    return NP

I modified the box function the same way. I just wanted to see if I was missing something. This code has helped me out so much so far. It needs to be added to the manual. When I included it in the manual examples it showed that the box model’s center is off, so the box and ode don’t line up. Before this I couldn’t see why the box sometimes went halfway through the floor.

Oops! Sorry about the scale mismatch, I misinterpreted the ODE documentation. (I was only used Trimeshes, capsules, and spheres in my test code, so didn’t notice that.)

And good idea on the color!

Hmm, this should really be inside of Panda3d’s C++ code, as part of its bounds rendering debug stuff. However, until I can gain enough confidence to tackle writing my own additions to that part of the engine, I think this will work for the short term.

Also, I haven’t taken a stab yet with editing the manual, and I am unsure of its conventions. But if either you know how to do it, feel free to toss it in.

I’ve discussed about integrating ODE better into panda with pro-rsoft on several occasions and he agrees that it would be a good thing to do. AFAIK he hasn’t received a go ahead from other devs so the whole thing has been stuck for months. So, uh, yeah, it would be better to have it on the C++ side, but atm it’s a bit of a ‘no can do’ :cry:

I can understand that. Frankly, I suspected as such and that is part of why I decided to skip ODE and use Panda’s built in physics for my first Panda project. If the API was still in flux and ODE support was WIP, I’d rather just wait.

Anyway, I didn’t need physics that complicated! Built-in Panda physics has been working just fine.

But I am certainly looking forward to further enhancements from either you!

Hehheh, I doubt I’ll be doing any contributions, my programming skills aren’t quite up to task. Personally, I MUST use ODE even in it’s current clunky implementation (notice how complicated defining ODE collision object is compared to the native ones?), the Panda collision system hasn’t been fast enough for my needs so far. I may need to do more tests on the matter though… :unamused:

EDIT: Oh yeah, I don’t know if you’ve noticed but I heard there’s a PhysX implementation of sorts in the CVS too. Doesn’t work very well yet it seems, but it might give an excuse to do more of an abstract collision system that’s less dependent on the actual collision system used beneath the stuff visible to the average user… :smiling_imp:

thanks alot for that piece of code, it helps me alot tracking down errors :slight_smile:

greetz

Blaz

Beautiful code! I love it! Something like this should be integrated into panda, this is amazing! :open_mouth:

also thanks ALOT. this was so educational.