%fn-again-%in collision not functioning properly

Hello! I am currently working on selecting 3d items with my mouse and have successfully done so using CollisionHandlerQueue and kind of using CollisionHandlerEvent.

My issue lies with CollisionHandlerEvent. I want to create a tile based layout where you can edit 3d terrain in a 2d manner. So essentially like placing roads on the ground. I would like to have a hover graphic so the tile changes colour when the mouse is over top of it, and then goes back to its original colour when the mouse leaves it.

I was able to get the first part with CollisionHandlerQueue but the second part seemed like the perfect job for CollisionHandlerEvent. So I tried it. I was able to get the first part with a function for mouseRay-into-tile but once I implemented the colour change back, it stopped working.

Thankfully I found what was wrong quite quickly, but I do not know how to fix it. When I simplified the functions for the mouseIn and mouseOut to just print statements (with mouseRay-agian-tile just using the mouseIn function) I got the following output:

In!
In!
Out!

So from my interpretation, it registers that I’ve collided going in, and that I’m still colliding once, but it does not register the continuation of the collision and thinks I’ve stopped colliding on the third call. All relevant code is below.

Upon testing the code below to make sure it still ran and had the same error (I hade some code to move the camera around so I could make sure models were loaded properly) I can also see that the collision will continue through objects. While for my top down view this is unimportant (and might actually be useful in some cases) would just accepting the message once for every time the hoverMouse task is called be the solution to that?

As an aside my tags for each tile seem to not be working as the tag is just Tile.obj rather than the tag I gave it, so if you see an easy fix for that I’d appreciate it greatly.

 class MyApp(base):
    def __init__(self):
        base.__init__(self)
        base.disableMouse(self)

        tileMap = NodePath("map")
        tileMap.reparentTo(self.render)
        for x in range(2):
            for y in range(2):
                tile = loader.loadModel("./Models/Tile.obj")
                tile.setTag('tile', f"{x}{y}")
                tile.setScale(0.5, 0.5, 0.5)
                tile.setColor(0.6,1.0,0.2,1)
                tile.setPos(10 + 10*x,-20 + 10*y,0)
                tile.copyTo(tileMap)
        tileMap.flattenStrong()
        
        self.myTraverser = CollisionTraverser('baseTraverser')
        base.cTrav = self.myTraverser
        
        self.myHandler = CollisionHandlerEvent()
        self.myHandler.addInPattern('%fn-into-%in')
        self.myHandler.addInPattern('%fn-again-%in')
        self.myHandler.addInPattern('%fn-out-%in')
        
        pickerNode = CollisionNode('mouseRay')
        pickerNP = self.cam.attachNewNode(pickerNode)
        pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
        self.pickerRay = CollisionRay()
        pickerNode.addSolid(self.pickerRay)
        self.myTraverser.addCollider(pickerNP, self.myHandler)
        
        self.accept("mouseRay-into-Tile.obj", self.mouseRayIn)
        self.accept("mouseRay-again-Tile.obj", self.mouseRayIn)
        self.accept("mouseRay-out-Tile.obj", self.mouseRayOut)

        self.taskMgr.add(self.hoverMouse)

    def hoverMouse(self, task):
        if self.mouseWatcherNode.hasMouse():
            mpos = self.mouseWatcherNode.getMouse()
            self.pickerRay.setFromLens(self.camNode, mpos.getX(), mpos.getY())
            self.myTraverser.traverse(self.render)
        return Task.cont

    def mouseRayIn(self, entry):
        print("In!")
        
    def mouseRayOut(self, entry):
        print("Out!")


app = MyApp()
app.run()

Edit: I have figured out logically that the ray is just going through the tile and so it has an intermediate detection for when it’s inside and then as it leaves the program says it is out. So I have solved my own problem by using CollisionHandlerQueue and only grabbing the closest element. Then using a variable to track the last touched tile and when that tile stops being touched (either I collide with another tile, or no tile at all. I detect the latter by clearing the queue once I use the first element) I reset the colour.

I think there’s no need to complicate logic so much.

from panda3d.core import CollisionTraverser, NodePath, CollisionNode, CollisionRay, CollisionHandlerQueue, BitMask32
from direct.showbase.ShowBase import ShowBase

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        self.col_traverser = CollisionTraverser()
        self.col_handler_queue = CollisionHandlerQueue()

        picker_node = CollisionNode('mouse_ray')
        picker_node.set_from_collide_mask(BitMask32.bit(1))
        picker_node.set_into_collide_mask(BitMask32.all_off())

        picker_cam = NodePath(picker_node)
        picker_cam.reparent_to(camera)

        self.col_ray = CollisionRay()
        picker_node.add_solid(self.col_ray)

        self.col_traverser.add_collider(picker_cam, self.col_handler_queue)

        for x in range(2):
            for y in range(2):
                tile = loader.load_model("plane.bam")
                tile.set_pos(3 + 3*x, -6 + 3*y, 0)
                tile.set_tag('tile', f"{x}{y}")
                tile.set_collide_mask(BitMask32.bit(1))
                tile.reparent_to(render)

        self.taskMgr.add(self.hoverMouse)

        self.save_entry_obj = None

    def hoverMouse(self, task):
        if base.mouseWatcherNode.has_mouse():
            mpos = base.mouseWatcherNode.get_mouse()
            self.col_ray.set_from_lens(base.camNode, mpos.get_x(), mpos.get_y())
            self.col_traverser.traverse(render)
            self.col_handler_queue.sort_entries()

            if (self.col_handler_queue.get_num_entries() > 0):
                entry = self.col_handler_queue.get_entry(0)
                entry.into_node_path.set_color(0.6, 1.0, 0.2 ,1)
                print(entry.into_node_path.get_tag('tile'))
                self.save_entry_obj = entry.into_node_path

            else:
                if self.save_entry_obj != None:
                    self.save_entry_obj.clear_color()
                    self.save_entry_obj = None

        return task.cont

app = MyApp()
app.run()

plane.bam (1.1 KB)

Your problem with tags is not clear, please elaborate.

This is tangential, but let me note that it’s superfluous to have a traverser be both manually instructed to traverse and assigned to “cTrav”.

(That is, to have both
self.cTrav = self.myTraverser
and
self.myTraverser.traverse(render)
)

Indeed, doing so will, I would imagine, result in traversal happening twice per frame–which might be why you were seeing “in” printed twice.

Yes I was doing some testing and forgot to remove the line from the code I posted here. I believe if that were the case I would also see two “out” messages no? In either case I have switched back to the CollisionHandlerQueue and it is working well!

Thanks for your input!

1 Like

I think there’s no need to complicate logic so much.

In my edit when I switched over to CollisionHandlerQueue I simplified the collision logic a bit but as for the creation of the tiles I was just following another post I had found and I definitely prefer just changing the parent to render like you did, so I’ve made that change.

Your logic for the collision handling is correct except it does not handle the case where the current tile entry is different from the last (where it would need to reset the color). I don’t know if this was just a misunderstanding of what I was trying to do but for completeness in the thread I just added these lines

if (self.col_handler_queue.get_num_entries() > 0):
                entry = self.col_handler_queue.get_entry(0)
                
                #Lines I added
                if self.save_entry_obj != entry.into_node_path and self.save_entry_obj != None:
                            self.save_entry_obj.clear_color()
                #End of lines I added

                entry.into_node_path.set_color(0.6, 1.0, 0.2 ,1)
                print(entry.into_node_path.get_tag('tile'))
                self.save_entry_obj = entry.into_node_path

Your logic is very concise and I appreciate the assistance!

Your problem with tags is not clear, please elaborate.

For this line:

print(entry.into_node_path.get_tag('tile'))

My understanding is this should print out the {x}{y} from when I set the tag, but all it does is print out blank lines. Additionally when I print just “entry.into_node_path” I get “render/Tile.obj/Tile.obj/Body1” which to me looks incorrect?

I do not know much about how the tags function so it could be working properly but hopefully that helps you understand my issue! Thanks again!

However, I fixed it later, otherwise.

from panda3d.core import CollisionTraverser, NodePath, CollisionNode, CollisionRay, CollisionHandlerQueue, BitMask32
from direct.showbase.ShowBase import ShowBase

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        self.col_traverser = CollisionTraverser()
        self.col_handler_queue = CollisionHandlerQueue()

        picker_node = CollisionNode('mouse_ray')
        picker_node.set_from_collide_mask(BitMask32.bit(1))
        picker_node.set_into_collide_mask(BitMask32.all_off())

        picker_cam = NodePath(picker_node)
        picker_cam.reparent_to(camera)

        self.col_ray = CollisionRay()
        picker_node.add_solid(self.col_ray)

        self.col_traverser.add_collider(picker_cam, self.col_handler_queue)

        for x in range(2):
            for y in range(2):
                tile = loader.load_model("plane.bam")
                tile.set_pos(3*x, 3*y, 0)
                tile.set_tag('tile', f"{x} {y}")
                tile.set_collide_mask(BitMask32.bit(1))
                tile.reparent_to(render)

        self.taskMgr.add(self.hoverMouse)

        self.save_entry_obj = []

    def hoverMouse(self, task):
        if base.mouseWatcherNode.has_mouse():
            mpos = base.mouseWatcherNode.get_mouse()
            self.col_ray.set_from_lens(base.camNode, mpos.get_x(), mpos.get_y())
            self.col_traverser.traverse(render)
            self.col_handler_queue.sort_entries()

            if (self.col_handler_queue.get_num_entries() > 0):
                entry = self.col_handler_queue.get_entry(0)
                entry.into_node_path.set_color(0.6,1.0,0.2,1)
                print(entry.into_node_path.get_tag('tile'))
                self.save_entry_obj.append(entry.into_node_path)

            else:
                if self.save_entry_obj != []:
                    for node in self.save_entry_obj:
                        node.clear_color()
                    self.save_entry_obj = []

        return task.cont

app = MyApp()
app.run()

Hmm, but I get the coordinates from the tags that I set.

I think that what’s happening here is due to the fact that models loaded from file–even simple ones–tend to be not single nodes, but at the least one or more GeomNodes all beneath a ModelNode (I think that it is).

(Of course, the hierarchy can be more complex; this is just a simple case.)

As such, it’s this ModelNode on which your tag is currently being set.

Conversely, however, when you collide with the visible geometry, it’s the actual GeomNode below the ModelNode that’s being “hit”.

But, as per the above, that GeomNode does not have a tag set, and so nothing useful is being returned by a call to “get_tag”.

Hence what you’re seeing: Empty output from “get_tag”, and a deeper hierarchy when printing the object being collided with.

1 Like

Is there a way to give this tag to the GeomNode or get it to inherit the tag from the ModelNode? Or does that kind of defeat the purpose of the tag in the first place?

I think you can get rid of rudimentary nodes with this function.
tile.clear_model_nodes()
or you can do this, to get the children, the level will have to be selected depending on the nesting of the hierarchy in order to get the GeomNode node.
tile = tile.get_child(0)

1 Like

You can also search upwards for the tag, I believe, via the “get_net_tag” method.

[edit]
Oh, and when setting your tags you could search for the model’s GeomNode, and apply the tag there. (See this manual page for information on searching.)

That said, I do generally advise against colliding with visible geometry. If you were to instead create collision geometry in code, then you could apply your tags to that.

I will have to try this! Thanks.

Yessir you do! I have seen your replies on various threads and that is advice I have seen repeated. Is the way that I’m doing it defaulting to visible geometry? Is there a manual page on collision geometry, I feel like in my search for all of this I would have seen it but I will have to look again.

Will give this a shot next time I’m working on it, thanks!

Hahah, my apologies if I’ve been tiresome! XD;

Yes–that’s what the line “pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())” does, to be specific: it sets the ray’s mask to match that used by default by visible geometry.

There is! See this page:
https://docs.panda3d.org/1.10/python/programming/collision-detection/collision-solids

Now, if you want a custom shape–something generated in a 3D modelling program–then that’s a little more complicated, and the exact process may depend on the 3D modelling program and the exporter.

Still, it can usually be done, and I’d suggest looking for and checking whatever project-page (e.g. GitHub repository) is home to the exporter in question.