Hi guys, the following is a simple class to pick and drag objects with your mouse.
To create a picker, you must specify, among other parameters, a key to start picking and one to finish. The only weird thing is that you have to set a tag (which can be specified and by default is equal to “pickable”) for each object you want to pick.
Note that mouse button 3 is used in the example, but this means you have to disable the trackball (app.disable_mouse ()) to avoid bizarre behavior, so, IMHO, you should use another key to pick objects and navigate the scene comfortably with the mouse.
'''
Picker class for Panda3d.
Created on Oct 31, 2017
@author: consultit
'''
from panda3d.core import CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, \
BitMask32, LPoint3f, NodePath, CardMaker
from direct.showbase.ShowBase import ShowBase
class Picker(object):
'''
A class for picking (Panda3d) objects.
'''
def __init__(self, app, render, camera, mouseWatcher, pickKeyOn, pickKeyOff, collideMask,
pickableTag="pickable"):
self.render = render
self.mouseWatcher = mouseWatcher.node()
self.camera = camera
self.camLens = camera.node().get_lens()
self.collideMask = collideMask
self.pickableTag = pickableTag
self.taskMgr = app.task_mgr
# setup event callback for picking body
self.pickKeyOn = pickKeyOn
self.pickKeyOff = pickKeyOff
app.accept(self.pickKeyOn, self._pickBody, [self.pickKeyOn])
app.accept(self.pickKeyOff, self._pickBody, [self.pickKeyOff])
# collision data
self.collideMask = collideMask
self.cTrav = CollisionTraverser()
self.collisionHandler = CollisionHandlerQueue()
self.pickerRay = CollisionRay()
pickerNode = CollisionNode("Utilities.pickerNode")
pickerNode.add_solid(self.pickerRay)
pickerNode.set_from_collide_mask(self.collideMask)
pickerNode.set_into_collide_mask(BitMask32.all_off())
self.cTrav.add_collider(self.render.attach_new_node(pickerNode), self.collisionHandler)
# service data
self.pickedBody = None
self.oldPickingDist = 0.0
self.deltaDist = 0.0
self.dragging = False
self.updateTask = None
def _pickBody(self, event):
# handle body picking
if event == self.pickKeyOn:
# check mouse position
if self.mouseWatcher.has_mouse():
# Get to and from pos in camera coordinates
pMouse = self.mouseWatcher.get_mouse()
#
pFrom = LPoint3f()
pTo = LPoint3f()
if self.camLens.extrude(pMouse, pFrom, pTo):
# Transform to global coordinates
rayFromWorld = self.render.get_relative_point(self.camera, pFrom)
rayToWorld = self.render.get_relative_point(self.camera, pTo)
# cast a ray to detect a body
# traverse downward starting at rayOrigin
self.pickerRay.set_direction(rayToWorld - rayFromWorld)
self.pickerRay.set_origin(rayFromWorld)
self.cTrav.traverse(self.render)
if self.collisionHandler.get_num_entries() > 0:
self.collisionHandler.sort_entries()
entry0 = self.collisionHandler.get_entry(0)
hitPos = entry0.get_surface_point(self.render)
# get the first parent with name
pickedObject = entry0.get_into_node_path()
while not pickedObject.has_tag(self.pickableTag):
pickedObject = pickedObject.getParent()
if not pickedObject:
return
if pickedObject == self.render:
return
#
self.pickedBody = pickedObject
self.oldPickingDist = (hitPos - rayFromWorld).length()
self.deltaDist = (self.pickedBody.get_pos(self.render) - hitPos)
print(self.pickedBody.get_name(), hitPos)
if not self.dragging:
self.dragging = True
# create the task for updating picked body motion
self.updateTask = self.taskMgr.add(self._movePickedBody,
"_movePickedBody")
# set sort/priority
self.updateTask.set_sort(0)
self.updateTask.set_priority(0)
else:
if self.dragging:
# remove pick body motion update task
self.taskMgr.remove("_movePickedBody")
self.updateTask = None
self.dragging = False
self.pickedBody = None
def _movePickedBody(self, task):
# handle picked body if any
if self.pickedBody and self.dragging:
# check mouse position
if self.mouseWatcher.has_mouse():
# Get to and from pos in camera coordinates
pMouse = self.mouseWatcher.get_mouse()
#
pFrom = LPoint3f()
pTo = LPoint3f()
if self.camLens.extrude(pMouse, pFrom, pTo):
# Transform to global coordinates
rayFromWorld = self.render.get_relative_point(self.camera, pFrom)
rayToWorld = self.render.get_relative_point(self.camera, pTo)
# keep it at the same picking distance
direction = (rayToWorld - rayFromWorld).normalized()
direction *= self.oldPickingDist
self.pickedBody.set_pos(self.render, rayFromWorld + direction + self.deltaDist)
#
return task.cont
if __name__ == "__main__":
app = ShowBase()
# create the picker
PICKABLETAG = "pickable"
PICKKEYON = "mouse3"
PICKKEYOFF = "mouse3-up"
picker = Picker(app, app.render, app.cam, app.mouseWatcher, PICKKEYON, PICKKEYOFF,
BitMask32.all_on(), PICKABLETAG)
# some scene data
numR = 3
numC = 3
dist = 5
dimRMin = -((numR - 1) * dist) / 2.0
dimCMin = -((numC - 1) * dist) / 2.0
# ground
cm = CardMaker("ground")
left, right, bottom, top = dimCMin * 1.1, -dimCMin * 1.1, dimRMin * 1.1, -dimRMin * 1.1
cm.setFrame(left, right, bottom, top)
ground = app.render.attach_new_node(cm.generate())
ground.set_pos(0, 0, 0)
ground.set_p(-90)
ground.set_color(0.2, 0.6, 0.4, 1)
ground.set_tag(PICKABLETAG, "")
# panda
panda = app.loader.load_model("panda")
panda.reparent_to(app.render)
panda.set_pos(0, 0, 6)
panda.set_scale(0.5)
panda.set_tag(PICKABLETAG, "")
# smiley
smiley = app.loader.load_model("smiley")
for r in range(numR):
for c in range(numC):
smileyInst = NodePath("smiley_" + str(r) + "_" + str(c))
smiley.instance_to(smileyInst)
smileyInst.reparent_to(app.render)
smileyInst.set_pos(dimCMin + dist * c, dimRMin + dist * r, 3)
smileyInst.set_tag(PICKABLETAG, "")
# setup camera
# trackball = app.trackball.node()
# trackball.set_pos(0.0, max(-dimRMin * 2, -dimCMin * 2) * 2, -2.0)
# trackball.set_hpr(0.0, 25.0, 0.0)
app.disable_mouse()
app.camera.set_pos(0.0, max(dimRMin * 2, dimCMin * 2) * 3, 8.0)
app.camera.set_hpr(0.0, -5.0, 0.0)
# run
app.run()
Bye