Simple Picker Class

Hello Pandaites!

I tried to find the mysterious cited in these forums, and without luck, I decided to write my own. It’s not much, but this straightforward mouse picking class has helped me quite a bit with 3d guis and in-game interaction because it’s so simple to use.

I use a node path instead of collision bits to define the “pickables” I want to search, and create one traverser per picker to keep things separate. Works well for my simple uses.

It can be adjusted to return more information than just node picked and surface point (play with pick()).

Test it with “ppython” if you like.

Generic 3d object picker class for Panda3d.

from pandac.PandaModules import *

class Picker(object):
    Generic object picker class for Panda3d. Given a top Node Path to search, 
    it finds the closest collision object under the mouse pointer.
    Picker takes a topNode to test for mouse ray collisions.
    the pick() method returns (NodePathPicked, 3dPosition, rawNode) underneath the mouse position. 
    If no collision was detected, it returns None, None, None.
    'NodePathPicked' is the deepest NAMED node path that was collided with, this is
    usually what you want. rawNode is the deep node (such as geom) if you want to
    play with that. 3dPosition is where the mouse ray touched the surface.
    The picker object uses to collide, so if you have a custom camera,
    well, sorry bout that.
    pseudo code:
    p = Picker(mycollisionTopNode)
    thingPicked, positionPicked, rawNode = p.pick()
    if thingPicked:
        # do something here like

    def __init__(self, topNode, cameraObject = None):
        self.traverser = CollisionTraverser()
        self.handler = CollisionHandlerQueue()
        self.topNode = topNode = cameraObject
        pickerNode  = CollisionNode('MouseRay')
        #NEEDS to be set to global camera. boo hoo
        self.pickerNP =
        # this seems to enter the bowels of the node graph, making it
        # difficult to perform logic on
        self.pickRay = CollisionRay()
        self.traverser.addCollider(self.pickerNP, self.handler)

    def setTopNode(self, topNode):
        """set the topmost node to traverse when detecting collisions"""
        self.topNode = topNode
    def destroy(self):
        """clean up my stuff."""
        # remove colliders, subnodes and such
    def pick(self):
        pick closest object under the mouse if available.
        returns ( NodePathPicked, surfacePoint, rawNode )
        or (None, None None)
        if not self.topNode:
            return None, None, None
        if base.mouseWatcherNode.hasMouse():
            mpos = base.mouseWatcherNode.getMouse()

            self.pickRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())

            if self.handler.getNumEntries() > 0:
                picked = self.handler.getEntry(0).getIntoNodePath()
                thepoint = self.handler.getEntry(0).getSurfacePoint(self.topNode)
                return self.getFirstParentWithName(picked), thepoint, picked

        return None, None, None
    def getFirstParentWithName(self, pickedObject):
        return first named object up the node chain from the picked node. This
        helps remove drudgery when you just want to find a simple object to
        work with. Normally, you wouldn't use this method directly.
        name = pickedObject.getName()
        parent = pickedObject
        while not name:
            parent = parent.getParent()
            if not parent:
                raise Exception("Node '%s' needs a parent with a name to accept clicks." % (str(pickedObject)))
            name = parent.getName()
        if parent == self.topNode:
            raise Exception("Collision parent '%s' is top Node, surely you wanted to click something beneath it..." % (str(parent)))
        return parent

if __name__ == "__main__":
    # test code
    # use the pview-style camera controls to wander about, then click
    # happy spheres.
    # most of this code is about setting up the scene;
    # skip to the end for the four picker lines
    from pandac.PandaModules import *
    import direct.directbase.DirectStart
    def breedOnClick():
        """this is called when mouse1 is pressed."""
        global picker
        namedNode, thePoint, rawNode = picker.pick()
        if namedNode:
            # breed smilies for fun (well, 'fun' if you're slap happy)
            name = namedNode.getName()
            p = namedNode.getParent() # the visible smiley
            pos = p.getPos()
            # stick a smiley clone on this smiley
            # at the click point
            clonePos = (thePoint - pos)*2 + pos
            newguy = model.copyTo(render)
            newguy.find("**/smileyCollide").setName("the clone of %s" % (name))
            print namedNode.getName()
            print "Collision Point: ", thePoint
    def rolloverTask(task):
        an alternate way to use the picker.
        Handle the mouse continuously
        global rollover
        obj, point, raw = rollover.pick()

        if obj:
            dt = globalClock.getDt()
            p = obj.getParent() # obj is collision sphere, so rotate model as well
            p.setH(p.getH() + dt*20)

        return task.cont
    # mkay lets create some smiley clickables
    model = loader.loadModel("models/smiley")

    # set up the collision body
    min,macks= model.getTightBounds()
    radius = max([macks.getY() - min.getY(), macks.getX() - min.getX()])/2

    cs = CollisionSphere(0,0,0, radius)
    csNode = model.attachNewNode(CollisionNode("smileyCollide"))
    # create smiley first-strike army
    for x in range(0, 9):
        for y in range(0,9):
            nodep = model.copyTo(render)
            realx, realy = (-50 + x*10, -50 + y * 10 )
            colliderNP = nodep.find("**/smileyCollide")
            colliderNP.setName("the happy dude at %i, %i" % (realx, realy))
            nodep.setPos(realx, realy, 0 )

    # remove the ugly
    light = DirectionalLight("prettify")
    lnp = render.attachNewNode(light)
    # wait for it... WAIT FOR IT...
    picker = Picker(render)
    base.accept("mouse1", breedOnClick)
    rollover = Picker(render)   
    taskMgr.add(rolloverTask, 'rollover')

Hey, this was exactly what I needed. I have a question though.

when the code in my application is:

self.mouseClick = Picker(render)

Everything works fine.

However, I don’t want to use the top node of render, my node tree is set up that I have the following node paths:


I need the player collision geometry that’s slightly in front of the camera to not be picked up by this. None of the following worked:

self.mouseClick = Picker("render/worldCollision")
worldCollision = render.find("worldCollision")
self.mouseClick = Picker(worldCollision)

Even though when I print worldCollision, I get “render/worldCollision”

What would be the proper way to modify this example to work with another node?