Simple Picker Class

Hello Pandaites!

I tried to find the mysterious Picker.py 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 Picker.py” 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 base.camera 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
        thingPicked.ls()

    """ 
    def __init__(self, topNode, cameraObject = None):
        self.traverser = CollisionTraverser()
        self.handler = CollisionHandlerQueue()
        self.topNode = topNode
        self.cam = cameraObject
        
        pickerNode  = CollisionNode('MouseRay')
        
        #NEEDS to be set to global camera. boo hoo
        self.pickerNP = base.camera.attachNewNode(pickerNode)
        
        # this seems to enter the bowels of the node graph, making it
        # difficult to perform logic on
        #pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
        
        self.pickRay = CollisionRay()
        pickerNode.addSolid(self.pickRay)
        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."""
        self.ignoreAll()
        # remove colliders, subnodes and such
        self.pickerNP.remove()
        self.traverser.clearColliders()
        
    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())
            self.traverser.traverse(self.topNode)

            if self.handler.getNumEntries() > 0:
                self.handler.sortEntries()
                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.setPos(clonePos)
            newguy.find("**/smileyCollide").setName("the clone of %s" % (name))
            
            print namedNode.getName()
            print "Collision Point: ", thePoint
    
            namedNode.ls()
    
    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"))
    csNode.node().addSolid(cs)
    
    # 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))
            # colliderNP.show()
            nodep.setPos(realx, realy, 0 )

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

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:

render/worldCollision
render/playerCollision

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?