Control a model's joints in physics simulation - Tutorial

Hi there, I’m pretty new in Panda3D and I would like to create an environment to train Reinforcement Learning agents. So, I ask if someone could list some relevant resources on how to create a physics simulation in which one can control models and their joints. Also, I would like to ask you if someone can answer here with a sample script about it, creating an environment with standard physics law like gravity, and creating a simple model like a bipedal robot and making walk controlling its joints.

Thanks so much.

I presume that by “joints” you mean physical joints, not the sort used in armature-based animation. If so, then you might want to look into Panda’s Bullet integration–the manual covers this, starting here, I believe.

The manual pages in question should at least provide basic sample code, if I recall correctly–albeit that I doubt that they go so far as the construction of a physically-driven bipedal figure!

Thanks for your response! I previously gave a look into the Panda’s bullet manual, but it is very poor at the moment.
I was wondering if anyone is able to show even a very simple example of a model with one “physical” joint and how to control it.

Does the manual page on Bullet’s “constraints”–which seems to be how Bullet handles “joints”–not cover this? It has sample code, I do note.

As to control, you would presumably just update the HPR values of the relevant nodes as appropriate.

In fact, access is possible programmatically to the joints, but you must write logic that will be based on physics.

https://docs.panda3d.org/1.10/python/programming/models-and-actors/controlling-joints

Use the utility egg-optchar to separate the model.

https://docs.panda3d.org/1.10/python/programming/scene-graph/manipulating-a-piece-of-a-model

Use this example for training: looking-and-gripping

The page you posted covers a bit what I’d like to do, but in this https://www.panda3d.org/download/noversion/bullet-samples.zip I found some complete scripts about bullet’s constraints. But still, I don’t get it how to arrange a simple model from them, and for controlling, bullet’s constraints don’t have HPR values to update.

Thanks for the comments. My problem is exactly how I can combine the physics engine while controlling the joints. Could you provide some basic script in order to do it?

This I think depends to some degree on how you want to compose your model: Do you have a predefined model with an armature, or separate models that you want to “glue together”? (Or something else again.)

But in short, I think that you would want your parts–be they separate models or the pieces of an armature–to be parented below your collision nodes–this should allow the collision nodes to control the visual geometry.

If you’re using an armature, you would presumably want to gain control of the parts of that armature; this can be done via Actor’s “controlJoint” method, I believe.

I may be mistaken–I don’t think that I’ve much used Bullet’s constraints before–but my understanding is that you wouldn’t control the model via its constraints. Rather, you would control the NodePaths affected by those constraints, with the constraints limiting the results appropriately.

So for example, let’s say that you had an arm composed of an “upper arm” and a “lower arm”, joined by an appropriate constraint. In order to bend the arm, you would rotate the lower arm; the constraint would prevent the arm from moving outside of the applied limitations.

Yes of course, but this ODE.

RagDoll.zip (181.5 KB)

Example with bones, use the Ralph folder to run, you need a folder with resources.

from direct.directbase import DirectStart
from panda3d.core import *
from panda3d.ode import (OdeWorld, OdeSimpleSpace, OdeJointGroup,
                         OdePlaneGeom, OdeBody, OdeMass, OdeSphereGeom,
                         OdeUniversalJoint)
from direct.actor.Actor import Actor
import random


class RalphWorld:
    def __init__(self):
        # Initialize ODE stuff
        self.world = OdeWorld()
        self.space = OdeSimpleSpace()
        self.contactgroup = OdeJointGroup()

        self.FPS = 90.0
        self.DTAStep = 1.0 / self.FPS

        self.world.setAutoDisableFlag(0)
        self.world.setAutoDisableLinearThreshold(0.15)
        self.world.setAutoDisableAngularThreshold(0.15)
        self.world.setAutoDisableSteps(2)
        self.world.setGravity(0, 0, -15)
        self.world.initSurfaceTable(1)
        self.world.setSurfaceEntry(
            0, 0, 150, 0.0, 9.1, 0.9, 0.00001, 0.0, 0.002)

        self.space.setAutoCollideWorld(self.world)
        self.space.setAutoCollideJointGroup(self.contactgroup)
        self.world.setQuickStepNumIterations(8)
        self.DTA = 0.0

        # Create a plane for Ralph to fall onto.
        self.floor = OdePlaneGeom(self.space, Vec4(0.0, 0.0, 1.0, 0.0))
        self.floor.setCollideBits(BitMask32(0x00000001))
        self.floor.setCategoryBits(BitMask32(0x00000002))

        # Now set up Ralph.
        self.ralph = Actor('models/ralph')
        self.ralph.reparentTo(render)

        # Put him up in the air.  Ralph's due for a fall.
        self.ralph.setPos(0, 0, 20)
        self.ralph.setHpr(random.uniform(0, 360),
                          random.uniform(-90, 90), random.uniform(0, 180))

        # If any part of Ralph is visible, draw all of him.  This is
        # necessary because we might be animating some of his joints
        # outside of his original bounding volume.
        self.ralph.node().setFinal(1)

        # Set up the joints as OdeBody objects.
        self.joints = []
        self.setupRagdoll(self.ralph)

        # Set the camera back a bit.
        base.trackball.node().setPos(0, 50, -10)

        # Start the simulation running, after a brief pause.
        taskMgr.doMethodLater(3, self.__simulationTask, "simulation task")

    def __walkJointHierarchy(self, actor, part, parentNode, parentBody):
        if isinstance(part, CharacterJoint):

            # Create a node that we can control to animate the
            # corresponding joint.  Make sure it is a child of the
            # parent joint's node.
            np = parentNode.attachNewNode(part.getName())
            actor.controlJoint(np, 'modelRoot', part.getName())

            # Ensure the node's original transform is the same as the
            # joint's original transform.
            np.setMat(part.getDefaultValue())

            # Create an OdeBody object to apply physics to that node.
            body = self.__makeJointBody(np)

            # Create an OdeUniversalJoint to connect this node to its parent.
            bj = OdeUniversalJoint(self.world)
            bj.attachBodies(parentBody, body)
            bj.setAnchor(np.getPos(render))

            parentNode = np
            parentBody = body

        for i in range(part.getNumChildren()):
            self.__walkJointHierarchy(
                actor, part.getChild(i), parentNode, parentBody)

    def __makeJointBody(self, np):
        density = 1
        radius = 1

        body = OdeBody(self.world)
        M = OdeMass()
        M.setSphere(density, radius)
        body.setMass(M)
        body.setPosition(np.getPos(render))
        body.setQuaternion(np.getQuat(render))

        geom = OdeSphereGeom(self.space, radius)
        geom.setCollideBits(BitMask32(0x00000002))
        geom.setCategoryBits(BitMask32(0x00000001))
        geom.setBody(body)

        self.joints.append((np, body))

        return body

    def setupRagdoll(self, actor):
        """ Recursively create a hierarchy of NodePaths, parented to  
        the actor, matching the hierarchy of joints.  Each NodePath  
        can be used to control its corresponding joint (as in  
        actor.controlJoint()). """

        bundle = actor.getPartBundle('modelRoot')
        # A body for the overall actor.
        body = self.__makeJointBody(actor)
        self.__walkJointHierarchy(actor, bundle, actor, body)

    def __simulationTask(self, task):
        # Run the appropriate number of ODE steps according to the
        # elapsed time since the last time this task was run.
        self.DTA += globalClock.getDt()
        while self.DTA >= self.DTAStep:
            self.DTA -= self.DTAStep
            self.__simulateStep()

        # Apply ODE transforms to the Panda joints.
        for np, body in self.joints:
            pos = body.getPosition()
            # body.getQuaternion() should return a Quat, but it
            # returns a VBase4 instead.  A nuisance.
            quat = Quat(body.getQuaternion())

            np.setPosQuat(render, pos, quat)

        return task.cont

    def __simulateStep(self):
        self.space.autoCollide()  # Detect collisions and create contact joints
        self.world.quickStep(self.DTAStep)  # Simulate

        self.contactgroup.empty()  # Remove all contact joints


rw = RalphWorld()
base.run()