Can't get RotationalLimitMotors to work

Hi there,



I’m trying to make an articulated character, on @normen 's advice and much like @cmeyer I’m looking to modulate RotationalLimitMotors (I’m using a KinematicRagdoll). I can’t get any motors to produce movement. I think I must be missing something basic, but I can’t tell what it is. Here is my setup code which should make a single shoulder joint move. What it actually does is produce a ragdoll that falls to the ground and collapses as if no motors are active.



[java]

private void setupWalkChar(boolean showDebugInfo) {

walkChar = (Node)assetManager.loadModel(“Models/WalkCharComplexBone/WalkCharComplexBone.j3o”);



Spatial wcSpatial = ((Node)walkChar.getChild(“Armature”)).getChild(“Cylinder”);



AnimControl animControl = wcSpatial.getControl(AnimControl.class);



SkeletonDebugger skeletonDebug = null;

if (showDebugInfo) {

skeletonDebug = new SkeletonDebugger(“skeleton”, animControl.getSkeleton());

Material mat2 = new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);

mat2.setColor(“Color”, ColorRGBA.Green);

mat2.getAdditionalRenderState().setDepthTest(false);

skeletonDebug.setMaterial(mat2);

skeletonDebug.setLocalTranslation(walkChar.getLocalTranslation());

}



walkChar_phy = new KinematicRagdollControl(0.4f);

walkChar_phy.addBoneName(“Thigh.L”);

walkChar_phy.addBoneName(“Thigh.R”);

walkChar_phy.addBoneName(“Shin.L”);

walkChar_phy.addBoneName(“Shin.R”);

walkChar_phy.addBoneName(“Foot.L”);

walkChar_phy.addBoneName(“Foot.R”);

walkChar_phy.addBoneName(“Shoulder.L”);

walkChar_phy.addBoneName(“Shoulder.R”);

walkChar_phy.addBoneName(“Head”);

walkChar_phy.addBoneName(“Tail”);

walkChar_phy.addBoneName(“Root”);





wcSpatial.addControl(walkChar_phy);

if (showDebugInfo) ((Node)wcSpatial).attachChild(skeletonDebug);



rootNode.attachChild(walkChar);

walkChar_phy.setEnabled(true);

walkChar_phy.setRagdollMode();



bulletAppState.getPhysicsSpace().add(walkChar_phy);





walkChar_phy.setJointLimit(“Shoulder.L”, 0,0,0,0,0,0);

walkChar_phy.setJointLimit(“Shoulder.R”, 0,0,0,0,0,0);

walkChar_phy.setJointLimit(“Head”, 0,0,0,0,0,0);

walkChar_phy.setJointLimit(“Tail”, 0,0,0,0,0,0);





chaseCam = new ChaseCamera(cam, wcSpatial, inputManager);



if (showDebugInfo) bulletAppState.getPhysicsSpace().enableDebug(assetManager);



PhysicsRigidBody head = walkChar_phy.getBoneRigidBody(“Head”);

PhysicsRigidBody tail = walkChar_phy.getBoneRigidBody(“Tail”);

PhysicsRigidBody shoulderL = walkChar_phy.getBoneRigidBody(“Shoulder.L”);

PhysicsRigidBody shoulderR = walkChar_phy.getBoneRigidBody(“Shoulder.R”);

thighL = walkChar_phy.getJoint(“Thigh.L”);

thighR = walkChar_phy.getJoint(“Thigh.R”);



thighR.getRotationalLimitMotor(0).setEnableMotor(true);

thighR.getRotationalLimitMotor(1).setEnableMotor(true);

thighR.getRotationalLimitMotor(2).setEnableMotor(true);

thighR.getRotationalLimitMotor(0).setMaxMotorForce(1.0f);

thighR.getRotationalLimitMotor(1).setMaxMotorForce(1.0f);

thighR.getRotationalLimitMotor(2).setMaxMotorForce(1.0f);



}





@Override

public void simpleUpdate(float tpf) {

walkChar.updateModelBound();





thighR.getRotationalLimitMotor(0).setTargetVelocity(10.0f);

thighR.getRotationalLimitMotor(1).setTargetVelocity(10.0f);

thighR.getRotationalLimitMotor(2).setTargetVelocity(10.0f);

}

[/java]





Thanks for any help.

@nehon: does the RagDollControl somehow reset these? @eubarch: you can also look at what it does internally, just right-click the RagDollControl somewhere in the source and select “View Source”.

Yes, there is a RagdollPreset that does all of this. Default is a HumanoidRagdollPreset

@eubarch if you want to do this you have to create your own RagdollPreset and set it to the ragdollControl.

Just extend the RagdollPreset class (it’s an abstract class) and override the initBoneMap method.

Look at how RagdollPreset and HumanoidRagdollPreset are done.

I cannot verify if your code is working or not because I do not have the model you are loading. I think your code should work. Maybe the motors are quickly reaching a limit and you do not see that it is working. Better implement button control to change the target velocity during runtime. I wrote something similar with the Oto model. You can use UIOJKL to control the three rotational motors and the right leg is moving.



While trying to control a character via motors I ran into a problem which I haven’t been able to solve yet. The SixDofJoint or btGeneric6DofConstraint in Bullet limits the movement of the yaw motor. I guess to prevent gimbal lock because it uses euler angles.



There is another joint in bullet which uses quaternions for its motor: btConeTwistConstraint. But it is not yet supported in jME and you have to extend the bridge to native bullet to support it. Unfortunately I was unable to control the maximum force of that motor, so the motor changes immediately to the desired position. setMaxMotorImpulse does not seem to have any effect. At the moment I am trying to figure out how to change this.



If you find a solution I would be happy to hear from it. Already spent a lot of time digging around but no real success yet :frowning:



[java]

package com.jme3test.ragdoll;



import java.util.logging.Logger;



import jme3test.bullet.PhysicsTestHelper;



import com.jme3.animation.AnimControl;

import com.jme3.app.SimpleApplication;

import com.jme3.bullet.BulletAppState;

import com.jme3.bullet.control.KinematicRagdollControl;

import com.jme3.bullet.joints.SixDofJoint;

import com.jme3.bullet.joints.motors.RotationalLimitMotor;

import com.jme3.bullet.objects.PhysicsRigidBody;

import com.jme3.input.KeyInput;

import com.jme3.input.controls.ActionListener;

import com.jme3.input.controls.KeyTrigger;

import com.jme3.light.DirectionalLight;

import com.jme3.material.Material;

import com.jme3.math.ColorRGBA;

import com.jme3.math.Vector3f;

import com.jme3.scene.Node;

import com.jme3.scene.debug.SkeletonDebugger;



public class TestMotorRagdoll extends SimpleApplication implements

ActionListener {

private static final Logger logger =

Logger.getLogger(TestMotorRagdoll.class.getName());



private Node otoModel;

private KinematicRagdollControl ragdollControl;

private RotationalLimitMotor pitchMotor;

private RotationalLimitMotor yawMotor;

private RotationalLimitMotor rollMotor;



@Override

public void simpleInitApp() {

BulletAppState bulletAppState = new BulletAppState();

bulletAppState.setEnabled(true);

stateManager.attach(bulletAppState);

// bulletAppState.getPhysicsSpace().setGravity(Vector3f.ZERO);



PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager,

bulletAppState.getPhysicsSpace());

setupLight();

setupKeys();



otoModel = (Node) assetManager.loadModel(“Models/Oto/Oto.mesh.xml”);

otoModel.setLocalScale(0.25f);



AnimControl animControl = otoModel.getControl(AnimControl.class);



SkeletonDebugger skeletonDebug = null;

skeletonDebug =

new SkeletonDebugger(“skeleton”, animControl.getSkeleton());

Material mat2 =

new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);

mat2.setColor(“Color”, ColorRGBA.Green);

mat2.getAdditionalRenderState().setDepthTest(false);

skeletonDebug.setMaterial(mat2);

skeletonDebug.setLocalTranslation(otoModel.getLocalTranslation());



ragdollControl = new KinematicRagdollControl(0.4f);



otoModel.addControl(ragdollControl);

otoModel.attachChild(skeletonDebug);



rootNode.attachChild(otoModel);

ragdollControl.setEnabled(true);

ragdollControl.setRagdollMode();



bulletAppState.getPhysicsSpace().add(ragdollControl);

bulletAppState.getPhysicsSpace().enableDebug(assetManager);



String boneName = “hip.right”;

PhysicsRigidBody body = ragdollControl.getBoneRigidBody(boneName);

body.setAngularSleepingThreshold(0);



SixDofJoint joint = ragdollControl.getJoint(boneName);

pitchMotor = joint.getRotationalLimitMotor(0);

yawMotor = joint.getRotationalLimitMotor(1);

rollMotor = joint.getRotationalLimitMotor(2);

pitchMotor.setEnableMotor(true);

yawMotor.setEnableMotor(true);

rollMotor.setEnableMotor(true);

pitchMotor.setMaxMotorForce(10.0f);

yawMotor.setMaxMotorForce(10.0f);

rollMotor.setMaxMotorForce(10.0f);

}



private void setupLight() {

DirectionalLight dl = new DirectionalLight();

dl.setDirection(new Vector3f(-0.1f, -0.7f, -1).normalizeLocal());

dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));

rootNode.addLight(dl);

}



private void setupKeys() {

inputManager.addMapping(“PitchPlus”, new KeyTrigger(KeyInput.KEY_I));

inputManager.addMapping(“PitchMinus”, new KeyTrigger(KeyInput.KEY_K));

inputManager.addMapping(“YawPlus”, new KeyTrigger(KeyInput.KEY_O));

inputManager.addMapping(“YawMinus”, new KeyTrigger(KeyInput.KEY_U));

inputManager.addMapping(“RollPlus”, new KeyTrigger(KeyInput.KEY_L));

inputManager.addMapping(“RollMinus”, new KeyTrigger(KeyInput.KEY_J));

inputManager.addMapping(“motorReset”,

new KeyTrigger(KeyInput.KEY_SPACE));

inputManager.addListener(this, “RollMinus”, “RollPlus”, “PitchMinus”, “PitchPlus”, “YawMinus”, “YawPlus”, “motorReset”);

}



@Override

public void onAction(String name, boolean isPressed, float tpf) {

if (name.equals(“PitchPlus”) && isPressed) {

pitchMotor.setTargetVelocity(1);

}

if (name.equals(“PitchMinus”) && isPressed) {

pitchMotor.setTargetVelocity(-1);

}

if (name.equals(“YawPlus”) && isPressed) {

yawMotor.setTargetVelocity(1);

}

if (name.equals(“YawMinus”) && isPressed) {

yawMotor.setTargetVelocity(-1);

}

if (name.equals(“RollPlus”) && isPressed) {

rollMotor.setTargetVelocity(1);

}

if (name.equals(“RollMinus”) && isPressed) {

rollMotor.setTargetVelocity(-1);

}

if (name.equals(“motorReset”) && isPressed) {

pitchMotor.setTargetVelocity(0);

yawMotor.setTargetVelocity(0);

rollMotor.setTargetVelocity(0);

}



logger.info("npitch motor: " + pitchMotor.getTargetVelocity()

  • "nyaw motor: " + yawMotor.getTargetVelocity()
  • "nroll motor: " + rollMotor.getTargetVelocity());

    }



    public static void main(String[] args) {

    TestMotorRagdoll app = new TestMotorRagdoll();

    app.start();

    }

    }

    [/java]

FYI @nehon – com.jme3.bullet.control.ragdoll.RagdollPreset.LexiconEntry does not have a public constructor, so it can’t be used while overriding initLexicon().



Also: Took a look at the HumanoidRagdollPreset class. All of the calls to boneMap.put() in initBoneMap use bone names that don’t distinguish between left or right, so “upperleg” is there, but no “upperleg.L” or “upperleg.R”. The only string matching code that I can find comes from key lookups in HashMaps. How is this working? Is the expectation to have identically named bones, or should my override of initBoneMap() contain specific names of bones such as “Shoulder.R”?

@eubarch don’t bother with the lexicon thing it’s just here to make the ragdoll “magically” find bones on different types of skeletons.

this method shouldn’t be abstract I should have made a default implementation



You can use the exact bone names as you know them



For matching the bone names, for the humanoidPreset, the skeleton is assumed to be symmetrical. So basically the joint setting for the Shoulder.R is the same than the Shoulder.L one. “Shoulder” matches “Shoulder.R” and “Shoulder.L”

Of course you can make different setups by naming the bones exactly.

@nehon At first glance, it doesn’t look like that…



The only way you can put a bone name value into member resultName is by successfully retrieving something from member lexicon. If lexicon is empty, you get a null member preset, which means that your bone is set up via new JointPreset().setupJoint(joint).



[java]

public void setupJointForBone(String boneName, SixDofJoint joint) {



if (boneMap.isEmpty()) {

initBoneMap();

}

if (lexicon.isEmpty()) {

initLexicon();

}

String resultName = “”;

int resultScore = 0;



for (String key : lexicon.keySet()) {



int score = lexicon.get(key).getScore(boneName);

if (score > resultScore) {

resultScore = score;

resultName = key;

}



}



JointPreset preset = boneMap.get(resultName);



if (preset != null && resultScore >= 50) {

logger.log(Level.INFO, “Found matching joint for bone {0} : {1} with score {2}”, new Object[]{boneName, resultName, resultScore});

preset.setupJoint(joint);

} else {

logger.log(Level.INFO, “No joint match found for bone {0}”, boneName);

if (resultScore > 0) {

logger.log(Level.INFO, “Best match found is {0} with score {1}”, new Object[]{resultName, resultScore});

}

new JointPreset().setupJoint(joint);

}

[/java]

ok, I’ll look into it tonight, i did this a long time ago and maybe it needs a bit of refactoring.

@nehon The fix is just a blank public constructor in LexiconEntry. I added it in for you:





[java]

/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.

    */

    package com.jme3.bullet.control.ragdoll;



    import com.jme3.bullet.joints.SixDofJoint;

    import java.util.HashMap;

    import java.util.Map;

    import java.util.logging.Level;

    import java.util.logging.Logger;



    /**

    *
  • @author Nehon

    */

    public abstract class RagdollPreset {



    protected static final Logger logger = Logger.getLogger(RagdollPreset.class.getName());

    protected Map<String, JointPreset> boneMap = new HashMap<String, JointPreset>();

    protected Map<String, LexiconEntry> lexicon = new HashMap<String, LexiconEntry>();



    protected abstract void initBoneMap();



    protected abstract void initLexicon();



    public void setupJointForBone(String boneName, SixDofJoint joint) {



    if (boneMap.isEmpty()) {

    initBoneMap();

    }

    if (lexicon.isEmpty()) {

    initLexicon();

    }

    String resultName = "";

    int resultScore = 0;



    for (String key : lexicon.keySet()) {



    int score = lexicon.get(key).getScore(boneName);

    if (score > resultScore) {

    resultScore = score;

    resultName = key;

    }



    }



    JointPreset preset = boneMap.get(resultName);



    if (preset != null && resultScore >= 50) {

    logger.log(Level.INFO, "Found matching joint for bone {0} : {1} with score {2}", new Object[]{boneName, resultName, resultScore});

    preset.setupJoint(joint);

    } else {

    logger.log(Level.INFO, "No joint match found for bone {0}", boneName);

    if (resultScore > 0) {

    logger.log(Level.INFO, "Best match found is {0} with score {1}", new Object[]{resultName, resultScore});

    }

    new JointPreset().setupJoint(joint);

    }



    }



    protected class JointPreset {



    private float maxX, minX, maxY, minY, maxZ, minZ;



    public JointPreset() {

    }



    public JointPreset(float maxX, float minX, float maxY, float minY, float maxZ, float minZ) {

    this.maxX = maxX;

    this.minX = minX;

    this.maxY = maxY;

    this.minY = minY;

    this.maxZ = maxZ;

    this.minZ = minZ;

    }



    public void setupJoint(SixDofJoint joint) {

    joint.getRotationalLimitMotor(0).setHiLimit(maxX);

    joint.getRotationalLimitMotor(0).setLoLimit(minX);

    joint.getRotationalLimitMotor(1).setHiLimit(maxY);

    joint.getRotationalLimitMotor(1).setLoLimit(minY);

    joint.getRotationalLimitMotor(2).setHiLimit(maxZ);

    joint.getRotationalLimitMotor(2).setLoLimit(minZ);

    }

    }



    protected class LexiconEntry extends HashMap<String, Integer> {



    public LexiconEntry() {

    }



    public void addSynonym(String word, int score) {

    put(word.toLowerCase(), score);

    }



    public int getScore(String word) {

    int score = 0;

    String searchWord = word.toLowerCase();

    for (String key : this.keySet()) {

    if (searchWord.indexOf(key) >= 0) {

    score += get(key);

    }

    }

    return score;

    }

    }

    }

    [/java]
1 Like