Using a picking ray in aspect2d

I’ve redone the starmap in my game so it loads faster. I’ve got a DirectScrolledFrame as before. The lines you see in the picture are lineTo’s, the grey fields are CardMakers and the words are OnscreenTexts.

Now, I want the grey fields to be pickables. The pick, i.e. the mouse click, should return the name of the star system aka. the words you see in the screen. Each of the grey fields (cardmaker) are their own nodes, all of them got a tag applied with the name “stobname”. For testing also the words (onscreentexts) have this tag applied.

The node hierarchy:

Canvas
+ LinesNode
+-- LineSeg
+-- LineSeg
+-- ...
+ FieldNode
+-- CardMaker
+-- CardMaker
+-- ...
+ TextNode
+-- OnscreenText
+-- OnscreenText
+-- ...

I use the tried and proven code from the manual for the click handler:

if self.rayHandler.getNumEntries() > 0:
				self.rayHandler.sortEntries()
				pickedObj = self.rayHandler.getEntry(0).getIntoNodePath()
				print pickedObj.getTag("stobname")

When I now click on something I do not get the name of something, but an empty line. So how do I fetch the tag? How do I even find out what I clicked on? How do I limit the clicking to the DirectGUI elements, because right now I can also just click besides the starmap. Using getNetTag() and related gives me errors.

Thx

Hmm, it seems like a bit of a heavy hammer to use the picking ray for your GUI objects. The picking ray is needed for the 3-d scene, because that’s a complicated space; but picking an object from the 2-d scene is much easier and doesn’t require the picking ray.

Just use DirectLabel instead of CardMaker. If you set state = DGG.NORMAL in the constructor, then you can detect clicks on the DirectLabel with bind:

myLabel.bind(DGG.B1CLICK, self.myFunction)

David

The problem is that the map was loading too slow with DirectButtons. This goes months back in my questions thread: the initialisation time for the elements was too long. Time went down from using DirectButtons instead of DirectRadioButtons, but it’s still too long. I assume it would be even less with DirectLabel. But the starmap is something the player needs so often it should be very responsive.

So the picking issue is still there. Another thing:

StarmapText.setPos(StarmapText.getPos() *0.8)

does nothing. StarmapText being a OnscreenText. With other elements it works of course, like the CardMakers.

Same function, just the line above, changing the scale works with:

StarmapText.setScale(StarmapText.getScale() * 8/9)

I’ve been working on a similar problem. (My setup looks a lot like yours actually.)

The method I’m currently working on is a bit heavy handed, but seems promising.

I’m also using a picker ray. I plan to use a dictionary in which the keys are tuples containing (x,y,z) locations and values are objects. (As soon as the objects are created, they add themselves to the dictionary). Since the collision node is at the same location as the object, getting the location of the collision sphere from the dictionary gets the object.

I had similar ideas but discarded them. Originally when I had DirectButtons the BoundingSpheres of the buttons would overlap. So you would click on one button but get the value of another. I dont know how to set the geometry of the button itself as the collision geometry; irrelevant anyway since I dont want to use DirectGUI for speed reasons.

treeform said on the IRC channel I should take the coords of the mouse, and make a distance check against the buttons. I’m sceptical. Lets say I have 100 buttons, then I had to make a (Vector2 - Vector1).getLength() a hundred times. Then a sort() on the resulting list. And then what? How would I relate the result to the name of the stellar?
What about precision? You zoom out the starmap then the buttons are very close together. Maybe float precision gets you here. And doing this kind of length check … wouldn’t that again be like a spherical collision area around the buttons?

Dont know about your approach. Can one use tuples for keys? Btw, if your setup looks similar I’d like to see it.
I mean I’m pretty happy with my solution. It’s fast and no crashes, just no returned tag.

A TextNode (and the OnscreenText object that wraps it) has no pickable geometry. You can make it pickable by setting mayChange = False to the OnscreenText constructor, which tells it to create raw geometry instead of keeping around a TextNode.

David

Nuh I already had the mayChange attribute at False. Changing it to True still prints me empty lines.

Well I think I figured it out. The CardMaker “buttons” in my starmap never had a collision solid. I dont want to use Spheres or Boxes, but the actual CardMaker geometry.

Now the manual doesnt mention how to add anything but a CollisionSolid, Astelix’ tutorials dont mention it and a forum search doesnt help.

I assume that CardMakers, unlike a NodePath returned by loader.loadModel, dont get a collision solid based on their geometry by default. So how do I add it? Something like this (pseudocode):

stobartnode.attachNewNode(CollisionNode("buttoncnode"))
stobardnode.node().addSolid(stobartnode.getGeometry())

?

Right, the CardMaker creates visible geometry, not collision geometry. But you can use visible geometry the same way you use collision geometry (it’s just a lot more expensive). To do this, you just set the appropriate collide mask on the visible geometry, e.g. card.setCollideMask(BitMask32(1)).

David

Nope, tried that, gives me:

'libpanda.GeomNode' object has no attribute 'setCollideMask'

Besides, I got a mouseTrav in the starmap and a general base.cTrav in the Environment for the ships. Last time I tried they interfered each other. I got a error like:

Collision not defined: CollisionSolid Sphere into CollisionSolid Ray.

Which is nonsense, the ray has only the from mask:

self.pickerNode.setFromCollideMask(BitMask32(0)

At this point I did not have any buttons yet, the ray was colliding with the coll spheres underneath, here: from the star ships.

setCollideMask() is a method on NodePath. If you have a GeomNode instead, you can call the lower-level setIntoCollideMask().

That is indeed nonsense, so it must not be true. :slight_smile: Either your ray had a different mask than you thought for some reason, or there was a different CollisionRay in the scene graph that you weren’t aware of.

David

Well I still got that error when I set

bla.setIntoCollideMask(BitMask32(1))

for the buttons and

self.pickerNode.setFromCollideMask(BitMask32(0))

. I now know that this comes from the star ships flying “underneath” the star map. If I dont give the ships a collision solid, the Ray->Sphere not allowed error does not come up. The ships are also into objects.

Can this be prevented by setting a BitMask? Can you please give me the Mask values for these constellations, I will read up later on the subject?

from -> into (my guess):
Projectiles (01) -> Ships (00)
Ray (11) -> Buttons (10)
other constellations get ignored, especially Ray -> Ships.

Is it healthy to have two CollisionTraversers? Like, one in the map, one in the world? Or should the map use the one from the world?

EDIT:
With self.pickerNode.setIntoCollideMask(BitMask32.allOff()) at least the error goes away. But still, print self.rayHandler.getNumEntries() prints 0.

EDIT2:
Well I need this solved, its a very basic mechanism of my game. I will paste some code, propably someone can come up with observations.

def buildStarmap()
	self.mouseTrav = CollisionTraverser()
	self.rayHandler = CollisionHandlerQueue()
	self.pickerNode = CollisionNode("mouseRay")
	pickerNP = camera.attachNewNode(self.pickerNode)
	self.pickerNode.setIntoCollideMask(BitMask32.allOff())
	self.pickerNode.setFromCollideMask(BitMask32(0))
	self.pickerRay = CollisionRay()
	self.pickerNode.addSolid(self.pickerRay)
	self.mouseTrav.addCollider(pickerNP,self.rayHandler)
		
def buildStobArt(self, stobname, buttonposx, buttonposy, indexno):
	stobartcm = CardMaker("stobart")
	stobartcm.setFrame(-1,1,-1,1)
	stobart = stobartcm.generate()
	stobart.setIntoCollideMask(BitMask32(1))
	stobart.setTag("stobname", stobname)
	stobartnode = self.StarmapArtNode.attachNewNode(stobart)
	stobartnode.setPos(buttonposx*0.8*self.scalefactor/480,0,buttonposy*0.5*self.scalefactor/300)
	stobartnode.setColor(0.1,0.1,0.1,1.0)
	stobartnode.setScale(0.075,1.0,0.02)
	stobartnode.setTag("stobname", stobname)
	stobartnode.setShaderOff()
	return stobartnode
	
def buildStobText(self, stobname, buttonposx, buttonposy):
	stobtext = OnscreenText(text=stobname, fg=self.findStobPointColor(StarmapData[stobname][6]),
	pos=(buttonposx*0.8*self.scalefactor/480,buttonposy*0.5*self.scalefactor/300-0.01),
	scale=(0.03), align=TextNode.ACenter, sort=1, font=loader.loadFont("cmss12"),
	mayChange=True)
	stobtext.node().setIntoCollideMask(BitMask32(1))
	stobtext.setTag("stobname", stobname)
	stobtext.reparentTo(self.StarmapTextNode)
	return stobtext
	
def processClick(self):
	if base.mouseWatcherNode.hasMouse():
		mousepos = base.mouseWatcherNode.getMouse()
		self.pickerRay.setFromLens(base.camNode, mousepos.getX(), mousepos.getX())
		self.mouseTrav.traverse(aspect2d)
		print self.rayHandler.getNumEntries()
		print self.rayHandler.getEntries()
		if self.rayHandler.getNumEntries() > 0:
			bla ...

Right now the prints at the end just print 0 and [].

I reworked what I had. The dictionary method was becoming too unwieldy for large numbers of objects. I’m currently using one collision traverser with multiple collision nodes and solids. The 2d component is liberally taken from this thread: discourse.panda3d.org/viewtopic … 4dcccef37a

class mouseCollisionClass:
    def __init__(self):
        base.accept('mouse1',self.onClick)
        self.draggedObj = None
        self.setupCollision()
        self.setupCollision3()
        taskMgr.add( self.mouseMoverTask, 'mouseMoverTask' )

        self.mx, self.my = 0,0

    def setupCollision( self ):
    # Initialise the collision ray used to detect 2d objects.
        base.ctrav = CollisionTraverser()
        self.queue = CollisionHandlerQueue()
        cn = CollisionNode('')
        cn.addSolid( CollisionRay(0,-100,0, 0,1,0) ) #CollisionRay(0,-100,0, 0,1,0)
        cn.setFromCollideMask(dragMask)
        cn.setIntoCollideMask(BitMask32.allOff())
        self.cnp = aspect2d.attachNewNode(cn)
        base.ctrav.addCollider(self.cnp,self.queue)
        self.cnp.show() # Need this so that dragged elements display

    def setupCollision3( self ):
    # Initialize the collision ray used to detect 3d objects.
        cn = CollisionNode('')
        cnp = base.camera.attachNewNode(cn)
        self.pickerRay = CollisionRay()
        cn.addSolid(self.pickerRay)
        base.ctrav.addCollider(cnp, self.queue)

    def mouseMoverTask( self, task ):
        if base.mouseWatcherNode.hasMouse():
            mpos = base.mouseWatcherNode.getMouse()
            self.mx, self.my = mpos.getX(), mpos.getY()
            self.cnp.setPos(render2d, self.mx, 0, self.my)
            self.pickerRay.setFromLens(base.camNode, self.mx, self.my)
        return task.cont

    def collisionCheck( self, collisionNp=aspect2d ):
        base.ctrav.traverse(collisionNp)
        self.queue.sortEntries()
        ...

onClick() calls collisionCheck() if the mouse is within the 2d interface area and collisionCheck(render) if it’s not. Something similar might work for you.

Edit:

Here’s a variation that makes the setup for the 2d collider more like the setup for the 2d collider.

class mouseCollisionClass:
    def __init__(self):
        base.accept("escape",sys.exit)
        base.accept('mouse1',self.onPress)
        base.accept('mouse1-up',self.onRelease)
        self.draggedObj = None

        base.ctrav = CollisionTraverser()
        self.queue = CollisionHandlerQueue()

        self.setupCollision2d()
        self.setupCollision3d()

        self.mnp = base.aspect2d.attachNewNode('')
        self.mnp.show()
        self.mx, self.my = 0,0

        taskMgr.add( self.mouseMoverTask, 'mouseMoverTask' )

    def setupCollision2d( self ):
    # Initialise the collision ray used to detect 2d objects.
        cn = CollisionNode('')
        cn.setFromCollideMask(dragMask)
        cn.setIntoCollideMask(BitMask32.allOff())
        cnp = base.camera2d.attachNewNode(cn)
        self.pickerRay2d = CollisionRay()
        cn.addSolid(self.pickerRay2d)
        base.ctrav.addCollider(cnp, self.queue)

    def setupCollision3d( self ):
    # Initialize the collision ray used to detect 3d objects.
        cn = CollisionNode('')
        cnp = base.camera.attachNewNode(cn)
        self.pickerRay = CollisionRay()
        cn.addSolid(self.pickerRay)
        base.ctrav.addCollider(cnp, self.queue)

    def mouseMoverTask( self, task ):
        if base.mouseWatcherNode.hasMouse():
            mpos = base.mouseWatcherNode.getMouse()
            self.mx, self.my = mpos.getX(), mpos.getY()
            self.mnp.setPos(render, self.mx, 0, self.my)
            self.pickerRay2d.setFromLens(base.cam2dNode, self.mx, self.my)
            self.pickerRay.setFromLens(base.camNode, self.mx, self.my)
        return task.cont

    def collisionCheck( self, collisionNp=aspect2d ):
        base.ctrav.traverse(collisionNp)
        self.queue.sortEntries()
        ...

Well, thanks for your efforts, but I don’t really see what’s different compared to my code. I use the setFromLens method which I guess is the way it’s supposed to be. Now I am even more frustrated seeing a demo where someone successfully picks a CardMaker card.

Anyway, in my code I did a:

print self.pickerRay.getDirection(), self.pickerRay.getOrigin()

and I get: Point2(-0.211111, -0.08) for the mouse pos and:
Vec3(-21082.3, 99863.4, -4993.17) Point3(-0.211111, 1, -0.05)

Are these rather large values normal? Just the ray going in a infinite direction? Do the values make sense, I mean does the ray go from the cam to the mouse?

EDIT:
Another oddity. Setting the camera and the ray I use:

self.pickerNP = base.cam.attachNewNode(self.pickerNode)

later, in the collision check function I use:

self.pickerRay.setFromLens(base.camNode, mousepos.getX(), mousepos.getY())

Am I actually using the same camera here? That was an issue in another thread.

Here is the full code of the starmap, maybe someone could proof-read it? pastebin.com/3VzJ2LDE

Here is the code as a standalone: megaupload.com/?d=GX4SB8Q4

Well, Astelix helped me … it seems either he is right and all you guys are wrong (and then I am screwed) or he doesnt see the picture and I can be helped.

Either way, you grab the stand alone code from the megaupload link above. That one doesnt work. But if you take Astelix’ code from here: fabiushouse.com/p3d/fellas/b … starmap.py
it suddenly throws collisions. Only thing he changed is:

  1. reparent all GUI stuff to render by parenting the main DirectFrame to render.
self.StarmapInvisFrame = DirectFrame(parent=render,
  1. let self.mouseTrav point to base.cTrav. WHY DOES THIS EVEN MATTER?!
#self.mouseTrav = CollisionTraverser()
    base.cTrav = CollisionTraverser()
    self.mouseTrav = base.cTrav

So someone please tell me how to solve this. And thanks to Astelix by the way.

base.cTrav is automatically traversed() by its own task. If this makes a difference, it means you are not call traverse() on your self.mouseTrav.

In general, if you create your own CollisionTraverser, you are responsible for calling traverse() on it once a frame in order to make it do its thing. Otherwise it does nothing.

David

Well, I call traverse() on click. That should suffice, right?

What about the general problem? Why do collisions in 2D (it seems) not work?

Collisions do work in 2d if you get the transforms right. I haven’t analyzed your code thoroughly. Perhaps you weren’t getting the transforms right?

David

Transforms of what? The ray goes from the cam to the mouse, where they should hit the geometry. Don’t see what could go wrong here. And why would the transforms be wrong in 2D but work in 3D (see Astelix’ reparenting).

I believe you when you say collisions should work in 2D. But why, if I visualize collisions in my code via:

self.mouseTrav.showCollisions(render)

dont I even see the ray shoting from the cam into infinity? Am I the only one using collisions in 2D?

Would the Plane class be a solution to my problems?
How come CardMaker cards in aspect2d worked here? discourse.panda3d.org/viewtopic … 3366#43366

  1. your pickerNode is parented to base.cam, which is parented to render as top node
  2. your into nodes are all parented to render2d as top node

Collisions only happen under the same top/root node.
So, parent your pickerNode to base.cam2d, and setFromLens(base.cam2d.node(), …)