CollisionRay not working Properly? (Picking 3D objects)

Greetings,

I am getting really desperate to solve this issue and could really use some help. I have read several past solutions but I simply cannot get object selection to work.

Goal:

My goal is to be able to select or pick a 3D object using the mouse. As simple as it sounds, I have had no success implementing the recommended CollisionRay technique shown in:
https://docs.panda3d.org/1.10/python/programming/collision-detection/clicking-on-3d-objects .
Every time I click on any of the pickable objects no collision is registered.

I have also looked at similar problems like: Adding pickable objects during runtime , but I haven’t found a solution.

Here is the relevant code

        def cursorWatch(mouseWasDown,objcmlist,task):
            if base.mouseWatcherNode.hasMouse():
                if base.mouseWatcherNode.isButtonDown(MouseButton.one()):
                    if not mouseWasDown[0]:
                        mouseWasDown[0] = True

                        myTraverser = CollisionTraverser()
                        myHandler = CollisionHandlerQueue()

                        pickerNode = CollisionNode('mouseRay')
                        pickerNP = base.camera.attachNewNode(pickerNode)
                        pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
                        pickerRay = CollisionRay()
                        pickerNode.addSolid(pickerRay)
                        myTraverser.addCollider(pickerNP, myHandler)

                        mpos = base.mouseWatcherNode.getMouse()
                        pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
                        oocnp = NodePath("collision nodepath")

                        for obstaclecm in objcmlist:
                            obstaclecm.copycdnpTo(oocnp)

                        myTraverser.traverse(oocnp)
                        # Assume for simplicity's sake that myHandler is a CollisionHandlerQueue.
                        if myHandler.getNumEntries() > 0:
                            print("Collision Registered")
                            # Do stuff...

                else:
                    mouseWasDown[0] = False
            return task.again

        taskMgr.doMethodLater(0.05, cursorWatch, "cursorWatch",
                              extraArgs=[self.mouseWasDown,self.objcmList],
                              appendTask=True)

In my code, I implemented a Task and task manager to monitor the state of the mouse so, whenever the user clicks we check if the mouse is over one of the objects on a list of pickable objects.

objcmlist is a python list filled with one or more CollisionModel objects. CollisionModel is a custom made class that lets me handle the collision nodes, nodepath and other necessary elements for loading a .stl file as a 3D object and handle possible collisions.

The method “copycdnpTo(oocnp)” basically lets me attach the associated object’s collision Node (self.__cdcn) to the oocnp NodePath as shown in the code:

    def copycdnpTo(self, nodepath):
        """
        return a nodepath including the cdcn,
        the returned nodepath is attached to the given one

        :param nodepath: parent np
        :return:
        """
        returnnp = nodepath.attachNewNode(copy.deepcopy(self.__cdcn))
        returnnp.setMat(self.__objnp.getMat())
        return returnnp

The object is surrounded by a collisionBox generated by the code:

    #self.__objnp is a nodePath associated to the loaded object
    self.__cdcn = self.__gencdboxcn(self.__objnp, radius = expand_radius)
    def __gencdboxcn(self, pandanp, name='boxcd', radius=15.0):
        """

        :param obstacle:
        :return:

        """

        cnp = CollisionNode(name)
        bottomLeft, topRight = pandanp.getTightBounds()
        center = (bottomLeft + topRight) / 2.0
        # enlarge the bounding box
        bottomLeft -= (bottomLeft - center).normalize() * radius
        topRight += (topRight - center).normalize() * radius
        cbn = CollisionBox(bottomLeft, topRight)
        cnp.addSolid(cbn)
        return cnp

Now, the collision box is correctly surrounding the objects as you can see in the following picture:

scc

But every time I tried to click on the object boxes no collision is detected whatsoever. I started suspecting that the CollisionRay might be guilty so I decided to print out the Ray Data. So I added a print statement so every time I click on the screen the code does a print(pickerRay) and I got the following rays:

ray, o (-0.283897 1 0.119706), d (-14217.6 50080.2 5994.89)

ray, o (0.0566176 1 -0.125368), d (2835.42 50080.2 -6278.44)

ray, o (-0.317059 1 0.139118), d (-15878.4 50080.2 6967.04)

ray, o (0.129412 1 -0.118088), d (6480.97 50080.2 -5913.88)

Now, I am not exactly sure how the CollisionRay is supposed to work, but I find it really odd that the origin coordinates ALWAYS include a “1” in the, I assume Y-axis, If I understand correctly the CollisionRay is an infinite straight line that connects points “o” and “d” right? however, I tried showing that line in my worldbase and it just doesnt make sense:

qscc

Do you have any idea what could be the problem? Did I explain myself correctly?

Thanks for your time and reading this,

Hope you can help me.

Hmm… I’m not sure.

This is a long shot, but what happens if you change the following:
pickerNP = base.camera.attachNewNode(pickerNode)
to this:
pickerNP = base.cam.attachNewNode(pickerNode)

Hmm… I don’t know whether setting the matrix like this will take into account any parent transformations. If the original object is a child of a NodePath that has a transformation, perhaps your copied collision-objects aren’t ending up where expected.

What happens if, instead of copying your colliders, you have your collision-traverser just traverse render? (Or whatever root-node your scene uses.)

Something like this:

                        mpos = base.mouseWatcherNode.getMouse()
                        pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())

                        myTraverser.traverse(render)
                        # Assume for simplicity's sake that myHandler is a CollisionHandlerQueue.
                        if myHandler.getNumEntries() > 0:
                            print("Collision Registered")

I think that “d” is a direction, not a point through which the ray passes. That is, the ray starts at point “o”, and extends in direction “d”. I may be mistaken, however!

As to that origin-point, where is your camera located? I would expect the origin to correspond to position of the camera, since you’re attaching it to the camera and setting it to extend along the camera’s view.

By the way, I notice that you’re creating your ray, traverser, and queue every time that your “cursorWatch” task reaches the relevant piece of code–potentially once per frame under certain conditions. It might be more efficient to create those objects once, and then simply use them in the task.

1 Like

Hey, thanks for the quick reply.

A combination of your recommendations actually solved the problem! Thank you very much! It was driving mad.

I changed the line to:

pickerNP = base.cam.attachNewNode(pickerNode)

And I changed the traverser to traverse render

myTraverser.traverse(base.render)

I had tried before to traverse render without success, I also tried to first change the line to pickerNP = base.cam.attachNewNode(pickerNode) without success. But doing both things seems to make the trick. May I ask what is the difference between “cam” and “camera”? I am also not sure why traversing oocnp does not register a collision; if I understood correctly, the traverser should just check for collisions with oocnp children? With the current solution it registers collisions with all elements on the world and not just my list of elements ( Though this is not a big problem, and I will make use of Tags to differentiate objects that I want to pick from other static objects, hope it works.)

Also, just for reference if somebody else has this problem I will answer the question:

I am pretty sure the collision objects are placed in the correct position, the nodes only parent is render and they dont get modified by other things, but thanks for pointing it out.

I see, thanks for the info. Do you know if it is possible to get a line function off of the Ray?

I actually re-checked my code and I think the problem had to do with me using cam and not camera when setting up the “base” class. I figured pickerNP = base.camera.attachNewNode(pickerNode) was the ONLY option.

Thank you, you are very kind, I had actually moved the code inside of the Task to ease my life trying to find the cause of my problem. I will move it out, as suggested.

Again, thanks for your help!

It’s my pleasure, and I’m glad that you found a solution to the issue! :slight_smile:

I wasn’t sure myself, to be honest, but a quick forum-search turned up an answer, starting in the third paragraph of the below-linked post:

I still suspect that there’s some problem with the way that you were copying those elements, something that was resulting in the collision objects either not being at their intended locations, or not having their intended shapes. Perhaps it was the matrix-assignment that I pointed out earlier. Perhaps collision-objects don’t respond well to “deepcopy”. Perhaps it’s something else. I’m not sure, I’m afraid.

Ah, wait! I think that I may see the problem: Your ray wasn’t a child (directly or indirectly) of “oocnp”, and nor did it have a common parent with “oocnp”. In short, the ray and the collision-objects were on separate scene-graphs, and thus didn’t interact.

Part of this, I think, is that you’re calling “pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())”. That line causes the ray to collide with anything that has GeomNode’s default collide-mask–which is to say, any (unmodified) GeomNode. It thus causes the ray to collide with things like visible geometry.

If you remove that line, your ray should collide only with collision objects, which may fit better with what you seem to be doing.

And if you want to collide with only a sub-set of your collision-objects, perhaps more reliable than "deepcopy"ing would be to temporarily reparent your objects to a separate NodePath–one that’s nevertheless attached to render, directly or indirectly–using “wrtReparentTo” in order to preserve the relative transformations of the objects. Once the traversal has been done, you could then use wrtReparentTo in order to transfer the objects back to their proper parentage.

Hm… I don’t know offhand, I’m afraid. You could dig through the API, however!

1 Like

Awesome!

Thanks for the explanation and suggestions! I will try to implement them between today and tomorrow. Would it be ok to keep this thread alive a little longer if I want to ask questions regarding the “wrtReparentTo” part or should I start a new thread if I have other problems? (I will obviously check for possible solutions on this forum and google beforehand)

I will also check the API.

Again, thank you very much!

1 Like

I think that for a new problem, a new thread might be better.

It means that another user looking for an answer to a similar problem is more likely to find any answers given, as they’re not hidden in a thread that seems unrelated.

And again, the help is my pleasure. :slight_smile:

You are right.

Thank you, I wish you a nice day!

1 Like

I hope for a pleasant day for you, too! :slight_smile:

Due to the call to set_from_lens, the origin point o is placed on the near plane of the lens. Therefore, those coordinates are given in camera space, with the y-coordinate always equal to the near distance :slight_smile: .

1 Like

I see, I assumed it would be world coordinates. That explains a lot.

Thanks for the explanation! :grinning:

I guess you can use the bulletRayHit method in envrionment.bulletcdhelper.

def bulletRayHit(pfrom, pto, geom):
    """
    find the nearest collision point between vec(pto-pfrom) and the mesh of nodepath
    :param pfrom: starting point of the ray, Point3
    :param pto: ending point of the ray, Point3
    :param geom: meshmodel, a panda3d datatype
    :return: None or Point3
    """
    bulletworld = BulletWorld()
    facetmesh = BulletTriangleMesh()
    facetmesh.addGeom(geom)
    facetmeshnode = BulletRigidBodyNode('facet')
    bullettmshape = BulletTriangleMeshShape(facetmesh, dynamic=True)
    bullettmshape.setMargin(1e-6)
    facetmeshnode.addShape(bullettmshape)
    bulletworld.attach(facetmeshnode)
    result = bulletworld.rayTestClosest(pfrom, pto)
    if result.hasHit():
        return result.getHitPos()
    else:
        return None
1 Like