Hey. I should have done this a long time ago, but i wanted to “do a bit of tweaking” first, which i never came around of doing. So here’s the control in its “as is” form. Hopefully it will come to use for someone, and be improved over time. I’m also attaching a test example. You can select limbs (bones), by clicking on them, and then select where they should move (on a plane) by right clicking. Space resets the scene
Both the example and controller are heavily based on the RagdollController. It’s been quite a while since i did this, and if i have missed to submit something, please tell.
Have fun
Edit: perhaps it should be moved to User Code instead.
IKControl.java
[java]
@Deprecated
[/java]
Patch for KinematicRagdollControl:
[java]
Index: KinematicRagdollControl.java
— KinematicRagdollControl.java (revision 10637)
+++ KinematicRagdollControl.java (working copy)
@@ -52,6 +52,7 @@
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
+import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
@@ -113,11 +114,15 @@
protected float eventDispatchImpulseThreshold = 10;
protected float rootMass = 15;
protected float totalMass = 0;
- private Map<Bone, Vector3f> ikTargets = new HashMap<Bone, Vector3f>();
- private float rotSpeed = 7f;
- private float limbDampening = 0.6f;
public static enum Mode {
Kinematic,
-
Ragdoll
-
Ragdoll,
-
IK
}
public class PhysicsBoneLink implements Savable {
@@ -193,6 +198,8 @@
//if the ragdoll has the control of the skeleton, we update each bone with its position in physic world space.
if (mode == mode.Ragdoll && targetModel.getLocalTranslation().equals(modelPosition)) {
ragDollUpdate(tpf);
-
} else if (mode == mode.IK && targetModel.getLocalTranslation().equals(modelPosition)) {
-
ikUpdate(tpf);
} else {
kinematicUpdate(tpf);
}
@@ -300,7 +307,83 @@
}
vars.release();
}
+
-
private void ikUpdate(float tpf){
-
TempVars vars = TempVars.get();
-
Quaternion tmpRot1 = vars.quat1;
-
Quaternion[] tmpRot2 = new Quaternion[]{vars.quat2, new Quaternion()};
-
Iterator i = ikTargets.keySet().iterator();
-
float distance;
-
Bone bone;
-
while (i.hasNext()) {
-
bone = (Bone) i.next();
-
if (!bone.hasUserControl()) {
-
continue;
-
}
-
distance = bone.getModelSpacePosition().distance(ikTargets.get(bone));
-
if (distance < 0.1f) {
-
continue;
-
}
-
updateBone(boneLinks.get(bone.getParent().getName()), tpf * (float) FastMath.sqrt(distance), vars, tmpRot1, tmpRot2, bone, ikTargets.get(bone));
-
Vector3f position = vars.vect1;
-
for (PhysicsBoneLink link2 : boneLinks.values()) {
-
matchPhysicObjectToBone(link2, position, tmpRot1);
-
}
-
}
-
vars.release();
-
}
-
public void updateBone(PhysicsBoneLink link, float tpf, TempVars vars, Quaternion tmpRot1, Quaternion[] tmpRot2, Bone tipBone, Vector3f target) {
-
if (link.bone.getParent() == null) {
-
return;
-
}
-
Quaternion preQuat = link.bone.getLocalRotation();
-
Vector3f vectorAxis;
-
float[] measureDist = new float[]{Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY};
-
for (int dirIndex = 0; dirIndex < 3; dirIndex++) {
-
if (dirIndex == 0) {
-
vectorAxis = Vector3f.UNIT_Z;
-
} else if (dirIndex == 1) {
-
vectorAxis = Vector3f.UNIT_X;
-
} else {
-
vectorAxis = Vector3f.UNIT_Y;
-
}
-
for (int posOrNeg = 0; posOrNeg < 2; posOrNeg++) {
-
tmpRot1.fromAngleAxis(rotSpeed * tpf / (link.rigidBody.getMass() * 2), vectorAxis);
-
tmpRot2[posOrNeg] = link.bone.getLocalRotation().mult(tmpRot1);
-
tmpRot2[posOrNeg].normalizeLocal();
-
rotSpeed = -rotSpeed;
-
link.bone.setLocalRotation(tmpRot2[posOrNeg]);
-
link.bone.update();
-
measureDist[posOrNeg] = tipBone.getModelSpacePosition().distance(target);
-
link.bone.setLocalRotation(preQuat);
-
}
-
if (measureDist[0] < measureDist[1]) {
-
link.bone.setLocalRotation(tmpRot2[0]);
-
} else if (measureDist[0] > measureDist[1]) {
-
link.bone.setLocalRotation(tmpRot2[1]);
-
}
-
}
-
link.bone.getLocalRotation().normalizeLocal();
-
link.bone.update();
-
if (link.bone.getParent() != null) {
-
updateBone(boneLinks.get(link.bone.getParent().getName()), tpf * limbDampening, vars, tmpRot1, tmpRot2, tipBone, target);
-
}
-
}
/**
- Set the transforms of a rigidBody to match the transforms of a bone. this
- is used to make the ragdoll follow the skeleton motion while in Kinematic
@@ -615,23 +698,25 @@
animControl.setEnabled(mode == Mode.Kinematic);
baseRigidBody.setKinematic(mode == Mode.Kinematic);
-
TempVars vars = TempVars.get();
-
for (PhysicsBoneLink link : boneLinks.values()) {
-
link.rigidBody.setKinematic(mode == Mode.Kinematic);
-
if (mode == Mode.Ragdoll) {
-
Quaternion tmpRot1 = vars.quat1;
-
Vector3f position = vars.vect1;
-
//making sure that the ragdoll is at the correct place.
-
matchPhysicObjectToBone(link, position, tmpRot1);
-
if (mode != Mode.IK) {
-
TempVars vars = TempVars.get();
-
for (PhysicsBoneLink link : boneLinks.values()) {
-
link.rigidBody.setKinematic(mode == Mode.Kinematic);
-
if (mode == Mode.Ragdoll) {
-
Quaternion tmpRot1 = vars.quat1;
-
Vector3f position = vars.vect1;
-
//making sure that the ragdoll is at the correct place.
-
matchPhysicObjectToBone(link, position, tmpRot1);
-
}
}
-
vars.release();
-
for (Bone bone : skeleton.getRoots()) {
-
RagdollUtils.setUserControl(bone, mode == Mode.Ragdoll || mode == Mode.IK);
-
}
}
-
vars.release();
-
for (Bone bone : skeleton.getRoots()) {
-
RagdollUtils.setUserControl(bone, mode == Mode.Ragdoll);
-
}
}
/**
@@ -699,6 +784,16 @@
setMode(Mode.Ragdoll);
}
}
+
- /**
-
* Sets the control into Inverse Kinematics mode. The affected bones are affected by IK.
-
* physics.
-
*/
- public void setIKMode() {
-
if (mode != Mode.IK) {
-
setMode(Mode.IK);
-
}
- }
/**
- retruns the mode of this control
@@ -783,6 +878,9 @@
return null;
}
- public Bone getBone(String boneName) {
-
return boneLinks.get(boneName).bone;
- }
/**
- For internal use only specific render for the ragdoll(if debugging)
@@ -801,7 +899,60 @@
control.setApplyPhysicsLocal(applyLocal);
return control;
}
+
-
public void setIKTarget(Bone bone, Vector3f worldPoint) {
-
ikTargets.put(bone, worldPoint.subtract(targetModel.getWorldTranslation()));
-
while (bone.getParent() != null) {
-
Quaternion tmpRot1 = new Quaternion();
-
Vector3f position = new Vector3f();
-
if (!bone.hasUserControl()) {
-
matchPhysicObjectToBone(boneLinks.get(bone.getName()), position, tmpRot1);
-
bone.setUserControl(true);
-
}
-
bone = bone.getParent();
-
}
-
setIKMode();
-
}
-
public void removeIKTarget(Bone bone) {
-
ikTargets.remove(bone);
-
applyUserControl();
-
}
-
public void removeAllIKTargets(){
-
ikTargets.clear();
-
applyUserControl();
-
}
-
public void applyUserControl() {
-
for (Bone bone : skeleton.getRoots()) {
-
RagdollUtils.setUserControl(bone, false);
-
}
-
if (ikTargets.isEmpty()) {
-
setKinematicMode();
-
} else {
-
Iterator iterator = ikTargets.keySet().iterator();
-
TempVars vars = TempVars.get();
-
while (iterator.hasNext()) {
-
Bone bone = (Bone) iterator.next();
-
while (bone.getParent() != null) {
-
Quaternion tmpRot1 = vars.quat1;
-
Vector3f position = vars.vect1;
-
matchPhysicObjectToBone(boneLinks.get(bone.getName()), position, tmpRot1);
-
bone.setUserControl(true);
-
bone = bone.getParent();
-
}
-
}
-
vars.release();
-
}
-
}
/**
- serialize this control
@@ -828,6 +979,8 @@
oc.write(eventDispatchImpulseThreshold, “eventDispatchImpulseThreshold”, 10);
oc.write(rootMass, “rootMass”, 15);
oc.write(totalMass, “totalMass”, 0);
-
oc.write(rotSpeed, "rotSpeed", 7f);
-
oc.write(limbDampening, "limbDampening", 0.6f);
}
/**
@@ -860,5 +1013,7 @@
eventDispatchImpulseThreshold = ic.readFloat(“eventDispatchImpulseThreshold”, 10);
rootMass = ic.readFloat(“rootMass”, 15);
totalMass = ic.readFloat(“totalMass”, 0);
-
rotSpeed = ic.readFloat("rotSpeed", 7f);
-
limbDampening = ic.readFloat("limbDampening", 0.6f);
}
}
[/java]
TestIK2.java
[java]
/*
- Copyright © 2009-2010 jMonkeyEngine
- All rights reserved.
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are
- met:
-
- Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
-
- Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
-
- Neither the name of ‘jMonkeyEngine’ nor the names of its contributors
- may be used to endorse or promote products derived from this software
- without specific prior written permission.
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package ik;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.Bone;
import com.jme3.animation.LoopMode;
import com.jme3.bullet.BulletAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.TextureKey;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.PhysicsCollisionEvent;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.collision.RagdollCollisionListener;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.control.KinematicRagdollControl;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Plane;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.debug.SkeletonDebugger;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.scene.shape.Sphere.TextureMode;
import com.jme3.texture.Texture;
import mygame.PhysicsTestHelper;
/**
-
PHYSICS RAGDOLLS ARE NOT WORKING PROPERLY YET!
-
@author normenhansen
*/
public class TestIK2 extends SimpleApplication implements RagdollCollisionListener, AnimEventListener {private BulletAppState bulletAppState;
Material matBullet;
Node model;
Node model2;
KinematicRagdollControl ikControl;
float bulletSize = 1f;
Material mat;
Material mat3;
private Sphere bullet;
private SphereCollisionShape bulletCollisionShape;Node targetNode = new Node("");
Vector3f targetPoint;
Bone mouseBone;
Vector3f oldMousePos;Plane plane;
public static void main(String[] args) {
TestIK2 app = new TestIK2();
app.start();
}public void simpleInitApp() {
initCrossHairs();
initMaterial();cam.setLocation(new Vector3f(0.26924422f, 6.646658f, 22.265987f)); cam.setRotation(new Quaternion(-2.302544E-4f, 0.99302495f, -0.117888905f, -0.0019395084f)); bulletAppState = new BulletAppState(); bulletAppState.setEnabled(true); stateManager.attach(bulletAppState); bullet = new Sphere(32, 32, 1.0f, true, false); bullet.setTextureMode(TextureMode.Projected); bulletCollisionShape = new SphereCollisionShape(1.0f);
// bulletAppState.getPhysicsSpace().enableDebug(assetManager);
PhysicsTestHelper.createPhysicsTestWorld(rootNode, assetManager, bulletAppState.getPhysicsSpace());
setupLight();
model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");
plane = new Plane();
AnimControl control = model.getControl(AnimControl.class);
SkeletonDebugger skeletonDebug = new SkeletonDebugger("skeleton", control.getSkeleton());
Material mat2 = new Material(getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat2.setColor("Color", ColorRGBA.Green);
skeletonDebug.setMaterial(mat2);
skeletonDebug.setLocalTranslation(model.getLocalTranslation());
Geometry g = new Geometry("", new Box(0.1f,0.1f,0.1f));
g.setMaterial(mat2);
targetNode.attachChild(g);
rootNode.attachChild(targetNode);
ikControl = new KinematicRagdollControl(0.5f);
setupSinbad(ikControl);
ikControl.addCollisionListener(this);
model.addControl(ikControl);
getPhysicsSpace().add(ikControl);
speed = 1.3f;
rootNode.attachChild(model);
flyCam.setMoveSpeed(50);
animChannel = control.createChannel();
animChannel.setAnim("IdleTop");
control.addListener(this);
inputManager.addListener(new ActionListener() {
public void onAction(String name, boolean isPressed, float tpf) {
if (name.equals("toggle") && isPressed) {
animChannel.setAnim("IdleTop");
ikControl.blendToKinematicMode(0.5f);
}
if (name.equals("stop") && isPressed) {
ikControl.setEnabled(!ikControl.isEnabled());
ikControl.setIKMode();
}
if (name.equals("one") && isPressed) {
//ragdoll.setKinematicMode();
targetPoint = model.getWorldTranslation().add(new Vector3f(0,2,4));
targetNode.setLocalTranslation(targetPoint);
ikControl.setIKTarget(ikControl.getBone("Hand.L"), targetPoint);
ikControl.setIKMode();
}
if (name.equals("two") && isPressed) {
//ragdoll.setKinematicMode();
targetPoint = model.getWorldTranslation().add(new Vector3f(-3,3,0));
targetNode.setLocalTranslation(targetPoint);
ikControl.setIKTarget(ikControl.getBone("Hand.R"), targetPoint);
ikControl.setIKMode();
}
if (name.equals("shoot") && !isPressed) {
ikControl.setRagdollMode();
}
}
}, "toggle", "feet", "rag","shoot", "drag", "stop", "bullet+", "bullet-", "boom", "one", "two", "three", "four");
inputManager.addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_SPACE));
inputManager.addMapping("feet", new KeyTrigger(KeyInput.KEY_F));
inputManager.addMapping("one", new KeyTrigger(KeyInput.KEY_1));
inputManager.addMapping("two", new KeyTrigger(KeyInput.KEY_2));
inputManager.addMapping("stop", new KeyTrigger(KeyInput.KEY_H));
}
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 PhysicsSpace getPhysicsSpace() {
return bulletAppState.getPhysicsSpace();
}
public void initMaterial() {
matBullet = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
TextureKey key2 = new TextureKey("Textures/Terrain/Rock/Rock.PNG");
key2.setGenerateMips(true);
Texture tex2 = assetManager.loadTexture(key2);
matBullet.setTexture("ColorMap", tex2);
}
protected void initCrossHairs() {
guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
BitmapText ch = new BitmapText(guiFont, false);
ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
ch.setText("+"); // crosshairs
ch.setLocalTranslation( // center
settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2,
settings.getHeight() / 2 + ch.getLineHeight() / 2, 0);
guiNode.attachChild(ch);
}
private void setupSinbad(KinematicRagdollControl ragdoll) {
ragdoll.addBoneName("Ulna.L");
ragdoll.addBoneName("Ulna.R");
ragdoll.addBoneName("Chest");
ragdoll.addBoneName("Foot.L");
ragdoll.addBoneName("Foot.R");
ragdoll.addBoneName("Hand.R");
ragdoll.addBoneName("Hand.L");
ragdoll.addBoneName("Neck");
ragdoll.addBoneName("Root");
ragdoll.addBoneName("Stomach");
ragdoll.addBoneName("Waist");
ragdoll.addBoneName("Humerus.L");
ragdoll.addBoneName("Humerus.R");
ragdoll.addBoneName("Thigh.L");
ragdoll.addBoneName("Thigh.R");
ragdoll.addBoneName("Calf.L");
ragdoll.addBoneName("Calf.R");
ragdoll.addBoneName("Clavicle.L");
ragdoll.addBoneName("Clavicle.R");
}
float elTime = 0;
boolean forward = true;
AnimControl animControl;
AnimChannel animChannel;
Vector3f direction = new Vector3f(0, 0, 1);
Quaternion rotate = new Quaternion().fromAngleAxis(FastMath.PI / 8, Vector3f.UNIT_Y);
boolean dance = true;
int collisionCounter = 100;
public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
if (channel.getAnimationName().equals("StandUpBack") || channel.getAnimationName().equals("StandUpFront")) {
channel.setLoopMode(LoopMode.DontLoop);
channel.setAnim("IdleTop", 5);
channel.setLoopMode(LoopMode.Loop);
}
}
public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
}
@Override
public void collide(Bone bone, PhysicsCollisionObject object, PhysicsCollisionEvent event) {
}
}
[/java]
Bone.java (patch):
[java]
Index: Bone.java
— Bone.java (revision 10637)
+++ Bone.java (working copy)
@@ -380,7 +380,7 @@
/**
- Updates world transforms for this bone and it’s children.
*/
- final void update() {
- public final void update() {
this.updateWorldVectors();
for (int i = children.size() - 1; i >= 0; i–) {
@@ -688,4 +688,23 @@
output.write(initialScale, “initialScale”, new Vector3f(1.0f, 1.0f, 1.0f));
output.writeSavableArrayList(children, “children”, null);
}
+
- public Vector3f getInitialPos() {
- return initialPos;
- }
- public Quaternion getInitialRot() {
- return initialRot;
- }
- public void setLocalRotation(Quaternion rot){
- if (!userControl) {
- throw new IllegalStateException(“User control must be on bone to allow user transforms”);
- }
- this.localRot = rot;
- }
- public boolean hasUserControl(){
- return userControl;
- }
}
[/java]
PhysicsTestHelper.java (patch):
[java]
Index: PhysicsTestHelper.java
— PhysicsTestHelper.java (revision 10637)
+++ PhysicsTestHelper.java (working copy)
@@ -42,8 +42,10 @@
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
@@ -106,6 +108,49 @@
}
+ public static void createPhysicsIKTestWorld(Node rootNode, AssetManager assetManager, PhysicsSpace space) {
- AmbientLight light = new AmbientLight();
- light.setColor(ColorRGBA.LightGray);
- rootNode.addLight(light);
- Material material = new Material(assetManager, “Common/MatDefs/Misc/Unshaded.j3md”);
- material.setTexture(“ColorMap”, assetManager.loadTexture(“Interface/Logo/Monkey.jpg”));
- Box floorBox = new Box(140, 0.25f, 140);
- Geometry floorGeometry = new Geometry(“Floor”, floorBox);
- floorGeometry.setMaterial(material);
- floorGeometry.setLocalTranslation(0, -5, 0);
- floorGeometry.rotate(0.0f, 0, 0.2f);
+// Plane plane = new Plane();
+// plane.setOriginNormal(new Vector3f(0, 0.25f, 0), Vector3f.UNIT_Y);
+// floorGeometry.addControl(new RigidBodyControl(new PlaneCollisionShape(plane), 0)); - floorGeometry.addControl(new RigidBodyControl(0));
- rootNode.attachChild(floorGeometry);
- space.add(floorGeometry);
- //movable boxes
- for (int i = 0; i < 3; i++) {
- Box box = new Box(0.25f, 0.25f, 0.25f);
- Geometry boxGeometry = new Geometry(“Box”, box);
- boxGeometry.setMaterial(material);
- boxGeometry.setLocalTranslation(i, 5, -3);
- //RigidBodyControl automatically uses box collision shapes when attached to single geometry with box mesh
- boxGeometry.addControl(new RigidBodyControl(2));
- rootNode.attachChild(boxGeometry);
- space.add(boxGeometry);
- }
- //immovable sphere with mesh collision shape
- Sphere sphere = new Sphere(8, 8, 1);
- Geometry sphereGeometry = new Geometry(“Sphere”, sphere);
- sphereGeometry.setMaterial(material);
- sphereGeometry.setLocalTranslation(4, -4, 2);
- sphereGeometry.addControl(new RigidBodyControl(new MeshCollisionShape(sphere), 0));
- rootNode.attachChild(sphereGeometry);
- space.add(sphereGeometry);
- }
}
[/java]