Debug snippet: coordinate crosses

Hi all,

Here is a piece of code I wrote that may help in debugging object placement and orientation in complex scenes.

The code generates coordinate crosses that show the placement of the origin (w.r.t. render) and the directions (w.r.t. render) of the local +x, +y and +z axes of any desired NodePath. The color coding of the crosses is as in Blender3D, namely red = +x, green = +y, blue = +z.

The code is used via a manager class that combines all created instances of coordinate crosses into one Geom by using Panda’s RigidBodyCombiner, minimizing the performance hit.

The NodePaths for data and visualization scenes can be different (using e.g. the render-into-quad approach from [url]Bloom filter ignores deactivated clear color?]). This allows for selective application of render effects and filters. For example, in the application I was debugging with this, the main scene is toon-shaded and postprocessed, while the debug visualization scene is not.

Be aware that this code knows nothing about your scene setup. Hence, if using separate scenes for data and visualization, the cameras must be manually synced. Personally, I have used a somewhat hacky solution calling

camera_hud.setMat( camera_render.getMat() )

in my frame update task - there are probably better ways. In the case where sceneVis and sceneData are the same scene, there is no need to do any manual updating.

Here is the code:

from panda3d.core import Geom, GeomVertexFormat, GeomVertexData, GeomVertexWriter, GeomLinestrips, GeomNode, ColorAttrib, DepthTestAttrib, RenderAttrib

class CoordinateCross:
    # To understand procedural model creation in Panda3D,
    # see the tutorial starting at
    #
    # https://www.panda3d.org/manual/index.php/Procedurally_Generating_3D_Models
    #
    # For specific topics, also see the links given below (not necessarily linked from the tutorial)
    # plus the Python API reference of Panda3D.
    #
    def __init__(self, sceneData, sceneVis, attachTo, scale, isDynamic):
        # Create a coordinate cross showing the orientation (and scale) of the axes of
        # the NodePath "attachTo", applying a scaling multiplier of "scale".
        #
        # The nodepath "attachTo" must exist in the scene "sceneData" (NodePath, scene root).
        #
        # The coordinate cross will be parented to the scene sceneVis (which may be the same as
        # or different from sceneData).
        #
        # isDynamic: if True, this CoordinateCross can be updated by calling update().
        #            if False, this CoordinateCross remains where it was initially placed
        #                      even if the object attachTo moves from its initial position.
        #
        # Colour coding of visualization: red = +x, green = +y, blue = +z (as in Blender3D).

        self.sceneData  = sceneData
        self.sceneVis   = sceneVis
        self.attachedTo = attachTo
        self.scale      = scale
        self.isDynamic  = isDynamic

        # Initialize vertex format and vertex data writer.
        #
        # For vertex formats, see
        #    https://www.panda3d.org/manual/index.php/Pre-defined_vertex_formats
        #    https://www.panda3d.org/manual/index.php/Defining_your_own_GeomVertexFormat
        #    https://www.panda3d.org/manual/index.php/GeomVertexFormat
        #
        # For writers, see the tutorial and
        #    https://www.panda3d.org/manual/index.php/GeomVertexData
        #
        # Old page (Panda3D 1.1), some parts may still be useful:
        # https://www.panda3d.org/manual/index.php/Vertices_in_Panda3D
        #
        # Maybe useful:
        # https://www.panda3d.org/manual/index.php?title=Reading_existing_geometry_data
        #
        vformat = GeomVertexFormat.getV3c4()  # vertex coordinates (V3), RGBA vertex color (c4)
        vdata = GeomVertexData('coordinate_axes_vdata_for_%s' % self.attachedTo.getName(),
                               vformat, Geom.UHStatic)
        vdata.setNumRows(6)  # origin (three times), +x, +y, +z

        vertex = GeomVertexWriter(vdata, 'vertex')
        color  = GeomVertexWriter(vdata, 'color')

        # Fill in vertex geometry
        #
        l0, lx, ly, lz = self._getPoints()

        # +x axis
        vertex.addData3f(l0)
        color.addData4f(1, 0, 0, 1)  # RGBA (OpenGL)
        vertex.addData3f(lx)
        color.addData4f(1, 0, 0, 1)  # RGBA (OpenGL)

        # +y axis
        vertex.addData3f(l0)
        color.addData4f(0, 1, 0, 1)  # RGBA (OpenGL)
        vertex.addData3f(ly)
        color.addData4f(0, 1, 0, 1)  # RGBA (OpenGL)

        # +z axis
        vertex.addData3f(l0)
        color.addData4f(0, 0, 1, 1)  # RGBA (OpenGL)
        vertex.addData3f(lz)
        color.addData4f(0, 0, 1, 1)  # RGBA (OpenGL)

        # Create geometry primitives by connecting vertices.
        #
        # Here UHStatic means that vertex *indices* do not change (even if vertices move).
        #
        # See the tutorial and
        #     https://www.panda3d.org/manual/index.php/GeomPrimitive
        #
        prim = GeomLinestrips(Geom.UHStatic)
        for k in xrange(3):
            prim.addConsecutiveVertices( start=k*2, num_vertices=2 )
            prim.closePrimitive()  # this strip is done; next add*() call starts a new one

        # Create geometry object from geometry primitives
        #
        # Note: if desired, the same Geom can be used to hold multiple instances
        # of the *same* type of geometry primitives.
        #
        geom = Geom(vdata)
        geom.addPrimitive(prim)

        # Insert geometry into scene graph for render
        #
        node = GeomNode('coordinate_axes_node_for_%s' % self.attachedTo.getName())
        node.addGeom(geom)

        nodePath = sceneVis.attachNewNode(node)
        nodePath.setRenderModeThickness(2)  # line thickness as pixels

        # make this NodePath use the vertex colors for coloring (this is off by default)
        nodePath.setAttrib(ColorAttrib.makeVertex())

        # force this node to show through ("x-ray") to be more useful as a debug visualization
        nodePath.setBin("fixed", 40)
        nodePath.setDepthWrite(False)
        nodePath.setDepthTest(False)

        self.vdata    = vdata
        self.nodePath = nodePath

        # HAX HAX: force RigidBodyCombiner to understand this node as a "moving object".
        # In order for later transform updates to have any effect (for a node inside a RigidBodyCombiner),
        # the node must initially have a non-identity transform before rbc.collect() is called.
        #
        # CoordinateCrossManager will later correct the transform state to what it should actually be
        # by calling our update() after it has called rbc.collect().
        #
        # See
        #     https://www.panda3d.org/dox/python/html/classpanda3d_1_1core_1_1RigidBodyCombiner.html
        #
        self.nodePath.setScale(10.0)  # any nonsensical (but non-identity) transform will do

    def _getPoints(self):
        # These are the points in local coordinates - we will transform these later.
        l0 = Point3(0, 0, 0)
        lx = Point3(self.scale, 0, 0)
        ly = Point3(0, self.scale, 0)
        lz = Point3(0, 0, self.scale)
        return (l0, lx, ly, lz)

    def update(self):
        transform_state = self.attachedTo.getNetTransform()
        self.nodePath.setTransform(transform_state)


from panda3d.core import RigidBodyCombiner
class CoordinateCrossManager:
    def __init__(self, taskMgr, sceneData, sceneVis):
        self.taskMgr   = taskMgr
        self.sceneData = sceneData
        self.sceneVis  = sceneVis

        self.managed_objects_static  = []
        self.managed_objects_dynamic = []

        # We use the RigidBodyCombiner to reduce the number of Geoms sent to the graphics card.
        #
        self.rbc   = RigidBodyCombiner("CoordinateCrossManagerRBC")
        self.rbcNP = NodePath(self.rbc)
        self.rbcNP.reparentTo(self.sceneVis)

        # update any dynamic coordinate crosses once per frame
        f = lambda task: self.update(task)
        self.taskMgr.add(f, 'CoordinateCrossManagerUpdateTask')

    def update(self, task):
        for o in self.managed_objects_dynamic:
            o.update()

        return task.cont

    def addCross(self, attachTo, scale, isDynamic=True):
        o = CoordinateCross(self.sceneData, self.rbcNP, attachTo, scale, isDynamic)
        if isDynamic:
            self.managed_objects_dynamic.append(o)
        else:
            self.managed_objects_static.append(o)

    # Upload the managed geometry to RigidBodyCombiner.
    #
    # Call this after all coordinate crosses have been added (or after adding another batch).
    #
    def finalize(self):
        self.rbc.collect()

        # update the correct transforms
        for o in self.managed_objects_static:  # this is the only time we update the static crosses.
            o.update()
        for o in self.managed_objects_dynamic:
            o.update()

Usage example:

# Somewhere inside your main class deriving from DirectObject...
ccm = CoordinateCrossManager(taskMgr, sceneData=render, sceneVis=render)

ccm.addCross(render, scale=1.0, isDynamic=False)  # global axes (render)
ccm.addCross(myActor, scale=0.5, isDynamic=True)  # myActor's local axes
ccm.addCross(myExposedJoint, scale=0.2, isDynamic=True)  # myExposedJoint's local axes
ccm.addCross(myControlledJoint, scale=0.2, isDynamic=True)  # myControlledJoint's local axes
ccm.finalize()

Finally, about the controlled joint case, be aware of the caveat mentioned by drwr in [url]global coordinates of a joint]: by default, the NodePath corresponding to a controlled joint sees only its local transform.