Getting Object Under Mouse

What is the best way to find the node path of a model under the mouse?

The manual talks of using rays, but I hate Pandas method of doing rays.

Is there any other way?

Another way would be to have an offscreen buffer that renders every object a flat colour, every object having a different colour. Then you can render that to a RAM texture, and then access the pixel value of the texture at the pixel where the mouse cursor is, and translate that back to an object ID.

However, this will most likely be an order of magnitude slower than when using a ray. Copying textures to RAM every frame is a very slow operation.

Hmm…

Okay, I tried using the example from the manual, but I could not get it to work.

Does the modle I am testing against need special collision geoms?

IMO there should be a picking example somewhere.
There is no need to have special collision geoms for pickable objects. If you’re going to do that each frame (for some sort of drag and drop on uneven terrain) you’d run into speed problems and you’d want to use special collision geom.
For normal “Clicking on 3d objects” you’ll be fine.

Here is a working example:

from direct.directbase import DirectStart
from pandac.PandaModules import *

smiley = loader.loadModel('smiley')
smiley.reparentTo(render)
smiley.setTag('pickable', '')

pickerNode = CollisionNode('mouseRay')
pickerNP = camera.attachNewNode(pickerNode)
pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
pickerRay = CollisionRay()
pickerNode.addSolid(pickerRay)
pickerNP.show()
rayQueue = CollisionHandlerQueue()
base.cTrav = CollisionTraverser()
base.cTrav.addCollider(pickerNP, rayQueue)

def pickObject():
	if base.mouseWatcherNode.hasMouse():
		mpos = base.mouseWatcherNode.getMouse()
		pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
		if rayQueue.getNumEntries() > 0:
			rayQueue.sortEntries()
			entry = rayQueue.getEntry(0)
			pickedNP = entry.getIntoNodePath()
			if pickedNP.hasNetTag('pickable'):
				print 'Clicked on the nodepath (with tag "pickable"): %s' % pickedNP

base.accept('mouse1', pickObject)
base.disableMouse() #- Disable default camera driver
camera.setY(camera, -10) #- Move the camera back a lil bit so we can see the smiley
run()

Hope this helps,
~powerpup118

Ah, but there is one: the chess sample.

PandaOde is really good for this. You can directly convert your object using an OdeTriMesh and then collide an OdeRay against all of the objects in your screen. Let me know if you need an example of how to do this.

Sounds interesting zhao, although I’d be even more interested in hearing what kind of performance difference there is between using internal collisions and ODE for this. Assuming both use unoptimized geometry etc.

The code works great!

I just have one question:

How do I call a function on the class that the model belongs to?

For example:

This is the class that creates my model:

class Model():

    def __init__(self,path,x=0,y=0,z=0,h=0,p=0,r=0):

        #Load a model:
        self.model = main.loader.loadModel(path)

        self.model.setTag('pickable', '')

        #Set the position:
        self.model.setPos(x,y,z)

        #Reparent the model to render:
        self.model.reparentTo(render)

    def TestFunction(self):

        print "TestFunction..."

Now, when ever the model is clicked, I want to call the TestFunction function.

How would do this? (There will be a lot of instances of the Model class.)

Keep track of all of the instances in a dictionary. In the project that I am working on I keep the dict in the world class.

You will need to add a new class definition argument of “ID” and alter your tag to:

class Model():
    def __init__(self,path,x=0,y=0,z=0,h=0,p=0,r=0, ID):
        ...
        self.model.setTag('pickable', str(ID))

I have it setup so that when an object/NPC is spawned they are given a randint(0,10000) and use that as the key in the dictionary. If the ID is already in there get a new randint obviously.

Now when you get the collision:

if pickedNP.hasNetTag('pickable'):
        TestFunction(yourDict[pickedNP.getTag('pickable')]

or have it set it as a target or adapt it however you like.

Explaination:
The tag value(ID) will be used as the key in your dictionary and return the value(instance you want to click).

I’m currently just using OdeUtil to do a manual collision like this:

contactGroup = OdeUtil.collide( self.trimeshGeom, rayGeom )

On my 2.5gHZ, colliding a ray against a 10k mesh takes 0.12 msecs. The collision for ray against mesh seems to be very robust. The time doesn’t decrease if the mesh is simpler (ie., a sphere.) For my simple scenes < ~100 selectable objects, I just manually check the ray against trimesh, but for more complicated scenes you can setup an octree or BSP in Ode. I’ve never really used Panda’s internal collision system before – it always seemed a bit bulky or convoluted to me.

Someone PM’d me how to use Ode to do ray checking so here’s my example code.

import direct.directbase.DirectStart
from pandac.PandaModules import Point3, VBase4
from pandac.PandaModules import Filename
import time
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import LineSegs

#---------------------

from pandac.PandaModules import OdeWorld, OdeSimpleSpace, OdeJointGroup
from pandac.PandaModules import OdeBody, OdeMass, OdeBoxGeom, OdeTriMeshGeom, OdeTriMeshData, OdeUtil, OdeRayGeom

def GetUnitSphere( vb4Color = VBase4( 1.0, 0, 0, 1) ):
	np = loader.loadModel('smiley')
	np.setColor( vb4Color )
	return np

def CreateConnectedLine(listSegs, intThickness=1, vb4Color= VBase4(1, 1, 1, 1), parent= None):
	LS = LineSegs()
	LS.setColor(vb4Color)
	LS.setThickness(intThickness)
	LS.moveTo(listSegs[0])
	for i in range(1, len(listSegs)):
		LS.drawTo(listSegs[i])
	geomLine = LS.create()
	if parent == None:
		npt_line = render.attachNewNode(geomLine)
	else:
		npt_line = parent.attachNewNode(geomLine)
	return npt_line

def SameVec(pt1, pt2):
	return (pt1 - pt2).length() < 1E-5

def ReduceContactGroup(contactGroup):
	tlist = [ contactGroup[i] for i in xrange(contactGroup.getNumContacts()) ]
	i = 0
	while i < len(tlist):
		j = i + 1
		while j < len(tlist):
			if (SameVec(tlist[i].getPos(), tlist[j].getPos())):
				tlist.pop(j)
			else:
				j += 1
		i += 1
	return tlist
	
def DrawContact(t_contact, npRoot = None):
	vb3Pos = t_contact.getPos()
	npObj = GetUnitSphere( vb4Color = VBase4( 1.0, 0, 0, 1) )
	if npRoot == None:
		npObj.reparentTo( render )
	else:
		npObj.reparentTo( None )
	npObj.setScale( .07 )
	npObj.setPos( vb3Pos )
	return npObj
	
class CollideWithGeom(DirectObject):
	def __init__(self, npObj):
		self.npObj = npObj
		
		self.space = OdeSimpleSpace()
		
		t = time.time()
		self.trimeshData = OdeTriMeshData( self.npObj, True )
		self.trimeshGeom = OdeTriMeshGeom( self.space, self.trimeshData )
		print 'time to generate trimesh', time.time() - t
		
		self.rayGeom = OdeRayGeom( self.space, 1)
		self.rayGeom.set( Point3(0,0,0), Point3(1,0,0) )
		
		self.booDebugDrawMouseLine = True
		self.EnableMouseHit()

	def EnableMouseHit(self):
		self.accept('mouse1', self.OnMouseBut1 )
		
	def DisableMouseHit(self):
		self.ignore('mouse1')
		
	def OnMouseBut1(self):
		pt3CamNear = Point3( 0,0,0)
		pt3CamFar = Point3(0,0,0)
		pt2Mpos = base.mouseWatcherNode.getMouse()
		base.camLens.extrude( pt2Mpos, pt3CamNear, pt3CamFar)
		
		pt3CamNear = render.getRelativePoint( base.cam, pt3CamNear )
		pt3CamFar = render.getRelativePoint( base.cam, pt3CamFar )
		
		if self.booDebugDrawMouseLine:
			npLine = CreateConnectedLine( [pt3CamNear, pt3CamFar] )
			npLine.reparentTo( render )
		
		pt3Dir = pt3CamFar - pt3CamNear
		fLength = pt3Dir.length()
		self.rayGeom.setLength( fLength )
		self.rayGeom.set( pt3CamNear, pt3Dir/fLength )
				
		listContacts = self.CollideRayWithModel( self.rayGeom )
		for i in listContacts:
			DrawContact( i )
		
	def CollideRayWithModel(self, rayGeom):
		t = time.clock()
		contactGroup = OdeUtil.collide( self.trimeshGeom, rayGeom )
		print 'time', t - time.clock(), 'contacts', contactGroup.getNumContacts()

		listContacts = ReduceContactGroup( contactGroup )
		return listContacts

base.cam.setPos( 0, -20, 0 )
npModel = loader.loadModel('smiley')
npModel.reparentTo( render )
npModel.setRenderModeWireframe()

objCollider = CollideWithGeom( npModel ) 

run()

Thanks a lot for this snippet zhao. It works really great, i wish i had found it earlier. I’m having a problem though : I’m trying to do mouse ray checking on OdeSphereGeoms, and i’m getting unexpected results. What tickles me the most is that it works perfect for cubes, but collisions on spheres go crazy.

I’ve zipped here : odeMousePicker a script reproducing the problem, as well as the box and ball models i use. Press C to spawn a cube, move the mouse cursor over it and check how well it works, then press S to spawn a sphere and start the trouble.

Any hint on what could cause this problem and how to solve it would be greatly appreciated.