Question about moving 3D objects

I have a similar question about the moving 3d object. I don’t know if I should start a new post to discuss this issue. If I have done something wrong, please let me know.
My problem is that when I click on an object, it moves the CollisionSphere instead of moving the 3D object.

Here is my code:

import sys

from direct.showbase.ShowBase import ShowBase
from direct.showbase.ShowBaseGlobal import globalClock
from panda3d.core import CollisionNode, CollisionSphere, BitMask32, CollisionTraverser, CollisionHandlerQueue, \
    CollisionRay

keyMap = {
    "up": False,
    "down": False,
    "left": False,
    "right": False,
    "rotate": False,
}


def updateKeyMap(key, state):
    keyMap[key] = state


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

        self.cam.setPos(0, -20, 0)

        # quit when esc is pressed
        self.accept('escape', sys.exit)

        self.disableMouse()

        # pick obj
        self.pickedObj = None

        # self.jack = self.loader.loadModel("models/jack")
        # self.jack.setHpr(0, 180, 180)
        # self.jack.reparentTo(self.render)

        panda = self.loader.loadModel("models/panda")
        panda.reparentTo(self.render)
        panda.setPos(0, 10, 0)
        panda.setScale(0.5, 0.5, 0.5)
        cNodePanda = panda.attachNewNode(CollisionNode('cnode_panda'))
        cNodePanda.node().addSolid(CollisionSphere(0, 0, 5, 5))
        cNodePanda.show()

        # set pointer
        self.picker = CollisionTraverser()
        self.picker.showCollisions(self.render)
        self.pq = CollisionHandlerQueue()

        self.pickerNode = CollisionNode('mouseRay')
        self.pickerNP = self.cam.attachNewNode(self.pickerNode)
        self.pickerNode.setFromCollideMask(BitMask32.bit(1))

        self.pickerRay = CollisionRay()
        self.pickerNode.addSolid(self.pickerRay)
        self.picker.addCollider(self.pickerNP, self.pq)

        self.accept("g", self.createTree)

        self.accept("mouse1", self.mouseClick)

        # obj move
        self.accept("arrow_left", updateKeyMap, ["left", True])
        self.accept("arrow_left-up", updateKeyMap, ["left", False])

        self.accept("arrow_right", updateKeyMap, ["right", True])
        self.accept("arrow_right-up", updateKeyMap, ["right", False])

        self.accept("arrow_up", updateKeyMap, ["up", True])
        self.accept("arrow_up-up", updateKeyMap, ["up", False])

        self.accept("arrow_down", updateKeyMap, ["down", True])
        self.accept("arrow_down-up", updateKeyMap, ["down", False])

        self.accept("space", updateKeyMap, ["rotate", True])
        self.accept("space-up", updateKeyMap, ["rotate", False])

        self.speed = 4
        self.angle = 0
        # print(self.camera.getPos())

        self.taskMgr.add(self.update, "update")

    def createTree(self):
        object = self.loader.loadModel("jack")
        object.reparentTo(self.render)
        object.setPos(self.cam, (0, 10, 0))

        cNodeObj = object.attachNewNode(CollisionNode('jack'))
        cNodeObj.node().addSolid(CollisionSphere(0, 0, 1, 1))
        cNodeObj.show()

        cNodeObj.setCollideMask(BitMask32.bit(1))

    def mouseClick(self):
        print('mouse click')
        # check mouse exist
        if self.mouseWatcherNode.hasMouse():

            # mouse position
            m_pos = self.mouseWatcherNode.getMouse()

            # set the ray position
            self.pickerRay.setFromLens(self.camNode, m_pos.getX(), m_pos.getY())
            self.picker.traverse(self.render)
            # highlight the node if we hit something
            if self.pq.getNumEntries() > 0:
                self.pq.sortEntries()
                self.pickedObj = self.pq.getEntry(0).getIntoNodePath()
                print('click on ' + self.pickedObj.getName())
            else:
                self.pickedObj =None

    def update(self, task):
        dt = globalClock.getDt()

        if self.pickedObj != None:
            print(self.pickedObj.getName() + " exist")

            x = self.pickedObj.getPos().getX()
            y = self.pickedObj.getPos().getY()
            z = self.pickedObj.getPos().getZ()

            h = self.pickedObj.getH()
            p = self.pickedObj.getP()
            r = self.pickedObj.getR()

            pos = self.pickedObj.getPos()

            # pos = self.jack.getPos()

            if keyMap["left"]:
                print("left")
                pos.x -= self.speed * dt
                self.pickedObj.setX(x-0.1)

            if keyMap["right"]:
                pos.x += self.speed * dt

            if keyMap["up"]:
                pos.z += self.speed * dt

            if keyMap["down"]:
                pos.z -= self.speed * dt

            if keyMap["rotate"]:
                self.angle += 1
                self.jack.setH(self.angle)

            # self.jack.setPos(pos)
            self.pickedObj.setPos(pos)


        else:
            print("obj not pick")

        return task.cont


app = MyApp()
app.run()

Have you solved this problem? Can you provide a coding example?

Thanks for reading this comment.

Based on an admittedly-quick look at your code, I would suggest parenting the 3D object below the collision-object. That way, when the collision-object is moved, the 3D object should move with it due to being a child of it.

1 Like

Thank you for the reply.
I’m not familiar with panda3D, can you provide some code for reference?

I try some code, but it doesn’t work:

        cs=CollisionNode('jack')
        cs.addSolid(CollisionSphere(0,0,1,1))
        cs.addChild(object)
        cs.show()

In short, I’m suggesting that you use “reparentTo” much as you already are, but the other way around.

That is, instead of the following:

object = self.loader.loadModel("jack")
object.reparentTo(self.render)
cNodeObj = object.attachNewNode(CollisionNode('jack'))

I suggest that you do this:

cNodeObj = render.attachNewNode(CollisionNode('jack'))
object = self.loader.loadModel("jack")
object.reparentTo(cNodeObj)

That said, I forget: Have I pointed you to my “beginner’s tutorial” before? If not, then let me do so now, as it covers some of this stuff, I believe, and may prove useful:

1 Like

Thank you for the reply.

I’m trying the idea of using CollideMask instead of CollisionNode, but it doesn’t work. (Just to test and learn Panda3D.)
Can I use CollideMask like this?

Here is my code(doesn’t work):

        cNodeObj = self.render.attachNewNode(CollisionNode('jack'))
        cNodeObj.setCollideMask(GeomNode.getDefaultCollideMask())
        cNodeObj.setPos(self.cam, (0, 10, 0))

        object = self.loader.loadModel("jack")
        object.reparentTo(cNodeObj)

To clarify, a collide-mask isn’t something that one uses instead of a CollisionNode–it’s something that one uses with a CollisionNode.

Specifically, a collision-mask determines which CollisionNodes collide with which other CollisionNodes–it’s a means of filtering collisions.

That said, looking at your code… that seems to be what you’re doing?

So I suppose that my question is this: what is it that the code isn’t doing that you’re expecting to see?

One thing that occurs to me is that NodePath’s “setCollideMask” method sets the into collide-mask, not the from collide-mask. And if the collider that’s made in your code-snippet above is the only one, then there isn’t anything available to collide into it. (Visible geometry will generally not collide into things, if I’m not much mistaken.)

If you want to have your collider interact with all visible objects (including your “jack” model, let me note), then I’d suggest that you instead set the CollisionNode’s “from” collide-mask. That way your collider will collide into visible geometry.

However, note that NodePath doesn’t provide direct access to the setting of this mask–instead, it’s set via the “setFromCollideMask” method of CollisionNode, I believe.

1 Like

Thanks for the reply.

I try your code to use attach 3D model in collision node. But the collision node will show up with the 3D model at the same time. Therefore, when I hide the collision node, it will hide 3D model at the same time.

I found an example of a moving 3D model that might solve this question problem.
But I don’t understand how it deals with 3D model collision node.

Here is the example:
https://docs.panda3d.org/1.10/python/more-resources/samples/chessboard
And the code:

Set up mouse pick in line 77~94.

# Since we are using collision detection to do picking, we set it up like
        # any other collision detection system with a traverser and a handler
        self.picker = CollisionTraverser()  # Make a traverser
        self.pq = CollisionHandlerQueue()  # Make a handler
        # Make a collision node for our picker ray
        self.pickerNode = CollisionNode('mouseRay')
        # Attach that node to the camera since the ray will need to be positioned
        # relative to it
        self.pickerNP = camera.attachNewNode(self.pickerNode)
        # Everything to be picked will use bit 1. This way if we were doing other
        # collision we could separate it
        self.pickerNode.setFromCollideMask(BitMask32.bit(1))
        self.pickerRay = CollisionRay()  # Make our ray
        # Add it to the collision node
        self.pickerNode.addSolid(self.pickerRay)
        # Register the ray as something that can cause collisions
        self.picker.addCollider(self.pickerNP, self.pq)
        # self.picker.showCollisions(render)

Load chess in line 131~141.

        for i in range(8, 16):
            # Load the white pawns
            self.pieces[i] = Pawn(i, WHITE)

And it will run the Pawn() function. In the Pawn() function, it will load the 3D model.

class Piece(object):
    def __init__(self, square, color):
        self.obj = loader.loadModel(self.model)
        self.obj.reparentTo(render)
        self.obj.setColor(color)
        self.obj.setPos(SquarePos(square))

class Pawn(Piece):
    model = "models/pawn"

The mouse click function:


    def mouseTask(self, task):
        # This task deals with the highlighting and dragging based on the mouse

        # First, clear the current highlight
        if self.hiSq is not False:
            self.squares[self.hiSq].setColor(SquareColor(self.hiSq))
            self.hiSq = False

        # Check to see if we can access the mouse. We need it to do anything
        # else
        if self.mouseWatcherNode.hasMouse():
            # get the mouse position
            mpos = self.mouseWatcherNode.getMouse()

            # Set the position of the ray based on the mouse position
            self.pickerRay.setFromLens(self.camNode, mpos.getX(), mpos.getY())

            # If we are dragging something, set the position of the object
            # to be at the appropriate point over the plane of the board
            if self.dragging is not False:
                # Gets the point described by pickerRay.getOrigin(), which is relative to
                # camera, relative instead to render
                nearPoint = render.getRelativePoint(
                    camera, self.pickerRay.getOrigin())
                # Same thing with the direction of the ray
                nearVec = render.getRelativeVector(
                    camera, self.pickerRay.getDirection())
                self.pieces[self.dragging].obj.setPos(
                    PointAtZ(.5, nearPoint, nearVec))

            # Do the actual collision pass (Do it only on the squares for
            # efficiency purposes)
            self.picker.traverse(self.squareRoot)
            if self.pq.getNumEntries() > 0:
                # if we have hit something, sort the hits so that the closest
                # is first, and highlight that node
                self.pq.sortEntries()
                i = int(self.pq.getEntry(0).getIntoNode().getTag('square'))
                # Set the highlight on the picked square
                self.squares[i].setColor(HIGHLIGHT)
                self.hiSq = i

        return Task.cont

However, I can’t find the Piece collision node. So, I don’t know why the Piece can react to the mouse click.

In the mouse click function, I guess self.pickerRay.getOrigin() is to find out the collision node. And this is move the Piece code.

                nearVec = render.getRelativeVector(
                    camera, self.pickerRay.getDirection())
                self.pieces[self.dragging].obj.setPos(
                    PointAtZ(.5, nearPoint, nearVec))

Ah, you’re right, sorry–the hiding of the collision-node does affect non-collision objects below it.

(I usually use a slightly more complex setup, and so hadn’t tried the parenting described above. Again, my apologies!)

Hmm… It may be that their piece-model includes collision-geometry.

No, that simply gets the location from which the ray begins, I believe.

In this case, they seem to be using as part of their means of to orienting the ray to follow the camera’s direction, if I’m not much mistaken.

The second half is, if I’m not much mistaken. The first half us again part of orienting the ray to follow the camera’s direction, I believe.

Hmm… Okay, let me suggest another approach:

In your code, store a reference to your 3D model in the collision-node via a Python-tag. (Making sure to clean up the Python tag once you’re done with the model and collision-node!) This then would allow you to access the 3D model that’s associated with a given collision-node, and to then move it as well as the collision-node.

In code terms, this might amount to the following changes to your originally-posted code:

In “createTree”, once you have both “object” and “cNodeObj”:

cNodeObj.setPythonTag("owner", object)

In “update”, along with “self.pickedObj.setPos(pos)”:

jackObj = self.pickedObj.getPythonTag("owner")
jackObj.setPos(pos)
1 Like

Thank you for the reply.

I don’t know 3D model can have collision-geometry, thanks for telling me that. It saves me a lot of time to figure out what going on about the collision node.

I use the Python-tag, and the code works. :smiley:

Here is the code:

import sys

from direct.showbase.ShowBase import ShowBase
from direct.showbase.ShowBaseGlobal import globalClock
from panda3d.core import CollisionNode, CollisionSphere, BitMask32, CollisionTraverser, CollisionHandlerQueue, \
    CollisionRay

keyMap = {
    "up": False,
    "down": False,
    "left": False,
    "right": False,
    "rotate": False,
}


def updateKeyMap(key, state):
    keyMap[key] = state


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

        self.cam.setPos(0, -20, 0)

        # quit when esc is pressed
        self.accept('escape', sys.exit)

        self.disableMouse()

        # pick obj
        self.pickedObj = None

        # self.jack = self.loader.loadModel("models/jack")
        # self.jack.setHpr(0, 180, 180)
        # self.jack.reparentTo(self.render)

        # panda = self.loader.loadModel("models/panda")
        # panda.reparentTo(self.render)
        # panda.setPos(0, 10, 0)
        # panda.setScale(0.5, 0.5, 0.5)
        # cNodePanda = panda.attachNewNode(CollisionNode('cnode_panda'))
        # cNodePanda.node().addSolid(CollisionSphere(0, 0, 5, 5))
        # cNodePanda.show()

        # set pointer1
        self.picker = CollisionTraverser()
        # self.picker.showCollisions(self.render)
        self.pq = CollisionHandlerQueue()

        self.pickerNode = CollisionNode('mouseRay')
        self.pickerNP = self.cam.attachNewNode(self.pickerNode)
        self.pickerNode.setFromCollideMask(BitMask32.bit(1))

        self.pickerRay = CollisionRay()
        self.pickerNode.addSolid(self.pickerRay)
        self.picker.addCollider(self.pickerNP, self.pq)

        # press g to create obj
        self.accept("g", self.createTree)

        self.accept("mouse1", self.mouseClick)

        # obj move
        self.accept("arrow_left", updateKeyMap, ["left", True])
        self.accept("arrow_left-up", updateKeyMap, ["left", False])

        self.accept("arrow_right", updateKeyMap, ["right", True])
        self.accept("arrow_right-up", updateKeyMap, ["right", False])

        self.accept("arrow_up", updateKeyMap, ["up", True])
        self.accept("arrow_up-up", updateKeyMap, ["up", False])

        self.accept("arrow_down", updateKeyMap, ["down", True])
        self.accept("arrow_down-up", updateKeyMap, ["down", False])

        self.accept("space", updateKeyMap, ["rotate", True])
        self.accept("space-up", updateKeyMap, ["rotate", False])

        self.speed = 4
        self.angle = 0
        # print(self.camera.getPos())

        self.taskMgr.add(self.update, "update")

    def createTree(self):

        # add collision node to 3d model
        object = self.loader.loadModel("jack")

        object.reparentTo(self.render)
        object.setPos(self.cam, (0, 10, 0))

        cNodeObj = object.attachNewNode(CollisionNode('jack'))
        cNodeObj.node().addSolid(CollisionSphere(0, 0, 1, 1))
        # cNodeObj.show()

        cNodeObj.setCollideMask(BitMask32.bit(1))

        cNodeObj.setPythonTag("jack", object)

    def mouseClick(self):
        print('mouse click')
        # check mouse exist
        if self.mouseWatcherNode.hasMouse():

            # mouse position
            m_pos = self.mouseWatcherNode.getMouse()

            # set the ray position
            self.pickerRay.setFromLens(self.camNode, m_pos.getX(), m_pos.getY())
            self.picker.traverse(self.render)
            # highlight the node if we hit something
            if self.pq.getNumEntries() > 0:
                self.pq.sortEntries()
                self.pickedObj = self.pq.getEntry(0).getIntoNodePath()

                print('click on ' + self.pickedObj.getName())


            else:
                self.pickedObj = None

    def update(self, task):
        dt = globalClock.getDt()

        if self.pickedObj != None:

            objName = self.pickedObj.getName()
            print(f"{objName}")

            objPythonTag = self.pickedObj.getPythonTag(f"{objName}")

            x = objPythonTag.getPos().getX()
            y = objPythonTag.getPos().getY()
            z = objPythonTag.getPos().getZ()

            h = objPythonTag.getH()
            p = objPythonTag.getP()
            r = objPythonTag.getR()

            pos = objPythonTag.getPos()

            print(x, y, z)

            if keyMap["left"]:
                pos.x -= self.speed * dt

            if keyMap["right"]:
                pos.x += self.speed * dt

            if keyMap["up"]:
                pos.z += self.speed * dt

            if keyMap["down"]:
                pos.z -= self.speed * dt

            if keyMap["rotate"]:
                # self.angle += 1
                objPythonTag.setH(h+3)

            objPythonTag.setPos(pos)

        return task.cont


app = MyApp()
app.run()

I use the return collision name to find out it’s python tag name. When I click the object, it will return the collision name self.pickedObj.getName(). Then I put the name into @Thaumaturge suggested code.

            objName = self.pickedObj.getName()
            objPythonTag = self.pickedObj.getPythonTag(f"{objName}")

Is it Python-tag represent a node? And how to clean up the Python-tag?

Can you tell me more about your settings?

1 Like

I’m glad that you have it working! :slight_smile:

It’s broader than that: a Python-tag can store any Python object (as far as I’m aware). Since nodes have a Python representation, they are amongst the things that can be stored in a Python-tag.

Still, you could also store floating-point numbers, or instances of custom classes, or strings, or other things besides, I daresay!

Like so:

myNodePath.clearPythonTag(tagName)

My approach is more or less as I used in my “beginner’s tutorial”–if you like, you can look over the reference code found there.

That said, a simple description might be that I create a custom class (often called “GameObject” that holds a NodePath. That NodePath is responsible for the object’s position in the world. The object’s 3D model and collision object are then attached to that NodePath, and thus move with it.

This allows me to implement custom logic and store additional data, as called for.

As in my suggestion above, the collision object stores a reference in a Python tag–however, in this case it’s a reference to the related instance of this custom class (or a descendant thereof). This gives me access to the various functions and data held by the class.

1 Like

Thank you very much.
When you mentioned the beginner’s tutorial earlier, I was already looking at it. But the code inside is more complicated, I need more time to understand the beginner’s tutorial. For example, I don’t quite understand what CollisionSegment is used to do.

Btw, do you know a better way to match CollisionSphere size with the 3D model? For example, when I use the createTree() function to input other 3D models.

1 Like

It’s my pleasure, and I’m glad! :slight_smile:

Ah, that’s fair!

A CollisionSegment is like a CollisionSphere–but instead of a sphere, it’s a line-segment. That is, a line that has a definite start and end-point, and thus that doesn’t run off into infinity in either direction.

(See this manual page for more information on the various solids available in Panda’s built-in collision system.)

Well, the simplest way might just be to measure your model in your 3D modelling tool, if available.

If not, you could load the model in PView and have it print out the size of the model, if I recall correctly. (When you start PView, press “?” for a list of available buttons that it responds to.) This may call for running PView from a command-line/terminal, in order to see its output.

And lastly, you could have your program find the bounds of the model, and then use those.

There are two standard forms of this last approach, if I recall correctly:

First, you can get the general bounds of the model via the “getBounds” method. This is fast, and produces a bounding-object (a sphere, I think) indicating those bounds. However, it produces bounds in broad strokes: while the bounds will (presumably) be appropriate to the model, they may be imprecise. Specifically, I think that they tend to be bigger than the model.

And second, you can get more-precise bounds via the “getTightBounds” method. This should produce a set of bounds that fits the model well. However, it’s also potentially slow, so use with care!

1 Like

Thank you for your reply.
Thank you for the explanation of CollisionSegment, I may not use it. But it has given me a better understanding of the “beginner’s tutorial”.

I use the commend pview /Users/gitdir/cartoon-shader/models/nik-dragon.egg.pz. And I check the Panda3d Pview manual, but I still don’t know how to find the model size.
Here is the pview screenshot :


nik-dragon model file: https://github.com/panda3d/panda3d/tree/master/samples/cartoon-shader/models

I use the getBounds and getTightBounds on the nik-dragon.
Here is the result:

# getBounds result
bsphere, c (-0.42342 4.10625 0.54485), r 15.7569

#getTightBounds result
(LPoint3f(-8.80639, -5.39401, -10.8195), LPoint3f(7.95955, 13.6065, 11.9092))

How can I use those data correctly to make a collision node?

I just try the data on the nik-dragon model, the collision box looks well. But, I don’t know how to extract the data.

        cNodeObj.node().addSolid(CollisionBox(LPoint3f(-8.80639, -5.39401, -10.8195),LPoint3f(7.95955, 13.6065, 11.9092)))

Thank you for reading this post.:smiley:

It’s my pleasure; I’m glad if I’ve helped, and glad if you’re finding your way in the tutorial! :slight_smile:

Note in that list the command “report bounding volume”–that’s the one that I have in mind.

However, as noted above, PView prints the result to the terminal/command-line, so you won’t see the output unless you run it that way.

In the case of “getBounds”, the result should be a “BoundingVolume” object–specifically, a BoundingSphere. (Hence, I daresay, the first part of the output that you’re seeing when you print it: “bsphere”.)

The value that you’re looking for is then the radius of that bounding sphere. You can get this from the sphere via the “getRadius” method.

Something like this:

boundingSphere = myModel.getBounds()
radius = boundingSphere.getRadius()

How you use it, specifically, is up to you–you could use it as the radius of a CollisionSphere, or the half-length of a CollisionBox, or even the length of a CollisionCapsule–or in some other way besides!

In the case of “getTightBounds”, the result should be two points, representing a bounding-box. Specifically, one point represents the “bottom-left” corner of the box, and the other represents the “top-right” of the box. These appear to be returned as a tuple containing the two points.

I see that you’ve found the constructor for CollisionBox that takes two such points–well done on that! :slight_smile:

Thus you should, I think, be able to simply take the two points from the return-value of “getTightBounds”, and pass those two your constructor. Something like this:

bounds = myNodePath.getTightBounds()
bottomLeft = bounds[0]
topRight = bounds[1]
# Or, alternatively: bottomLeft, topRight = bounds
solid = CollisionBox(bottomLeft, topRight)
1 Like