Explorar o código

publicize Joint.applyBindPose(), add DynamicAnimControl & related stuff

Stephen Gold %!s(int64=6) %!d(string=hai) anos
pai
achega
235b9db2ca

+ 493 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/BoneLink.java

@@ -0,0 +1,493 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Joint;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.joints.SixDofJoint;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Matrix3f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Link an animated bone in a skeleton to a jointed rigid body in a ragdoll.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class BoneLink extends PhysicsLink {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger2
+            = Logger.getLogger(BoneLink.class.getName());
+    /**
+     * local copy of {@link com.jme3.math.Matrix3f#IDENTITY}
+     */
+    final private static Matrix3f matrixIdentity = new Matrix3f();
+    // *************************************************************************
+    // fields
+
+    /**
+     * bones managed by this link, in a pre-order, depth-first traversal of the
+     * skeleton, starting with the linked bone
+     */
+    private Joint[] managedBones = null;
+    /**
+     * submode when kinematic
+     */
+    private KinematicSubmode submode = KinematicSubmode.Animated;
+    /**
+     * local transform of each managed bone from the previous update
+     */
+    private Transform[] prevBoneTransforms = null;
+    /**
+     * local transform of each managed bone at the start of the most recent
+     * blend interval
+     */
+    private Transform[] startBoneTransforms = null;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * No-argument constructor needed by SavableClassUtil. Do not invoke
+     * directly!
+     */
+    public BoneLink() {
+    }
+
+    /**
+     * Instantiate a purely kinematic link between the named skeleton bone and
+     * the specified rigid body.
+     *
+     * @param control the control that will manage this link (not null, alias
+     * created)
+     * @param bone the linked bone (not null, alias created)
+     * @param collisionShape the desired shape (not null, alias created)
+     * @param linkConfig the link configuration (not null)
+     * @param localOffset the location of the body's center (in the bone's local
+     * coordinates, not null, unaffected)
+     */
+    BoneLink(DacLinks control, Joint bone, CollisionShape collisionShape,
+            float mass, Vector3f localOffset) {
+        super(control, bone, collisionShape, mass, localOffset);
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Add a physics joint to this link and configure its range of motion. Also
+     * initialize the link's parent and its array of managed bones.
+     *
+     * @param parentLink (not null, alias created)
+     */
+    void addJoint(PhysicsLink parentLink) {
+        assert parentLink != null;
+        assert getJoint() == null;
+
+        setParent(parentLink);
+
+        Transform parentToWorld = parentLink.physicsTransform(null);
+        parentToWorld.setScale(1f);
+        Transform worldToParent = parentToWorld.invert();
+
+        Transform childToWorld = physicsTransform(null);
+        childToWorld.setScale(1f);
+
+        Transform childToParent = childToWorld.clone();
+        childToParent.combineWithParent(worldToParent);
+
+        Spatial transformer = getControl().getTransformer();
+        Vector3f pivotMesh = getBone().getModelTransform().getTranslation();
+        Vector3f pivotWorld = transformer.localToWorld(pivotMesh, null);
+
+        PhysicsRigidBody parentBody = parentLink.getRigidBody();
+        PhysicsRigidBody childBody = getRigidBody();
+        Vector3f pivotParent
+                = parentToWorld.transformInverseVector(pivotWorld, null);
+        Vector3f pivotChild
+                = childToWorld.transformInverseVector(pivotWorld, null);
+        Matrix3f rotParent = childToParent.getRotation().toRotationMatrix();
+        Matrix3f rotChild = matrixIdentity;
+        // TODO try HingeJoint or ConeJoint
+        SixDofJoint joint = new SixDofJoint(parentBody, childBody, pivotParent,
+                pivotChild, rotParent, rotChild, true);
+        super.setJoint(joint);
+
+        String name = boneName();
+        RangeOfMotion rangeOfMotion = getControl().getJointLimits(name);
+        rangeOfMotion.setupJoint(joint);
+
+        joint.setCollisionBetweenLinkedBodys(false);
+
+        assert managedBones == null;
+        managedBones = getControl().listManagedBones(name);
+
+        int numManagedBones = managedBones.length;
+        startBoneTransforms = new Transform[numManagedBones];
+        for (int i = 0; i < numManagedBones; ++i) {
+            startBoneTransforms[i] = new Transform();
+        }
+    }
+
+    /**
+     * Begin blending this link to a purely kinematic mode.
+     *
+     * @param submode enum value (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    public void blendToKinematicMode(KinematicSubmode submode,
+            float blendInterval) {
+        super.blendToKinematicMode(blendInterval);
+
+        this.submode = submode;
+        /*
+         * Save initial bone transforms for blending.
+         */
+        int numManagedBones = managedBones.length;
+        for (int mbIndex = 0; mbIndex < numManagedBones; ++mbIndex) {
+            Transform transform;
+            if (prevBoneTransforms == null) { // this link not updated yet
+                Joint managedBone = managedBones[mbIndex];
+                transform = managedBone.getLocalTransform().clone();
+            } else {
+                transform = prevBoneTransforms[mbIndex];
+            }
+            startBoneTransforms[mbIndex].set(transform);
+        }
+    }
+    // *************************************************************************
+    // PhysicsLink methods
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned link into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this link (not null)
+     * @param original the instance from which this link was shallow-cloned
+     * (unused)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        managedBones = cloner.clone(managedBones);
+        prevBoneTransforms = cloner.clone(prevBoneTransforms);
+        startBoneTransforms = cloner.clone(startBoneTransforms);
+    }
+
+    /**
+     * Update this link in Dynamic mode, setting the linked bone's transform
+     * based on the transform of the rigid body.
+     */
+    @Override
+    protected void dynamicUpdate() {
+        assert !getRigidBody().isKinematic();
+
+        Transform transform = localBoneTransform(null);
+        getBone().setLocalTransform(transform);
+
+        for (Joint managedBone : managedBones) {
+            managedBone.updateModelTransforms();
+        }
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public BoneLink jmeClone() {
+        try {
+            BoneLink clone = (BoneLink) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * Update this link in blended Kinematic mode.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    protected void kinematicUpdate(float tpf) {
+        assert tpf >= 0f : tpf;
+        assert getRigidBody().isKinematic();
+
+        Transform transform = new Transform();
+        for (int mbIndex = 0; mbIndex < managedBones.length; ++mbIndex) {
+            Joint managedBone = managedBones[mbIndex];
+            switch (submode) {
+                case Animated:
+                    transform.set(managedBone.getLocalTransform());
+                    break;
+                case Frozen:
+                    transform.set(prevBoneTransforms[mbIndex]);
+                    break;
+                default:
+                    throw new IllegalStateException(submode.toString());
+            }
+
+            if (kinematicWeight() < 1f) { // not purely kinematic yet
+                /*
+                 * For a smooth transition, blend the saved bone transform
+                 * (from the start of the blend interval)
+                 * into the goal transform.
+                 */
+                Transform start = startBoneTransforms[mbIndex];
+                Quaternion startQuat = start.getRotation();
+                Quaternion endQuat = transform.getRotation();
+                if (startQuat.dot(endQuat) < 0f) {
+                    endQuat.multLocal(-1f);
+                }
+                transform.interpolateTransforms(
+                        startBoneTransforms[mbIndex].clone(), transform,
+                        kinematicWeight());
+            }
+            /*
+             * Update the managed bone.
+             */
+            managedBone.setLocalTransform(transform);
+            managedBone.updateModelTransforms();
+        }
+
+        super.kinematicUpdate(tpf);
+    }
+
+    /**
+     * Unambiguously identify this link by name, within its DynamicAnimControl.
+     *
+     * @return a brief textual description (not null, not empty)
+     */
+    @Override
+    public String name() {
+        String result = "Bone:" + boneName();
+        return result;
+    }
+
+    /**
+     * Copy animation data from the specified link, which must have the same
+     * name and the same managed bones.
+     *
+     * @param oldLink the link to copy from (not null, unaffected)
+     */
+    void postRebuild(BoneLink oldLink) {
+        int numManagedBones = managedBones.length;
+        assert oldLink.managedBones.length == numManagedBones;
+
+        super.postRebuild(oldLink);
+        if (oldLink.isKinematic()) {
+            submode = oldLink.submode;
+        } else {
+            submode = KinematicSubmode.Frozen;
+        }
+
+        if (prevBoneTransforms == null) {
+            prevBoneTransforms = new Transform[numManagedBones];
+            for (int i = 0; i < numManagedBones; ++i) {
+                prevBoneTransforms[i] = new Transform();
+            }
+        }
+        for (int i = 0; i < numManagedBones; ++i) {
+            prevBoneTransforms[i].set(oldLink.prevBoneTransforms[i]);
+            startBoneTransforms[i].set(oldLink.startBoneTransforms[i]);
+        }
+    }
+
+    /**
+     * De-serialize this link, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        Savable[] tmp = ic.readSavableArray("managedBones", null);
+        if (tmp == null) {
+            managedBones = null;
+        } else {
+            managedBones = new Joint[tmp.length];
+            for (int i = 0; i < tmp.length; ++i) {
+                managedBones[i] = (Joint) tmp[i];
+            }
+        }
+
+        submode = ic.readEnum("submode", KinematicSubmode.class,
+                KinematicSubmode.Animated);
+        prevBoneTransforms = RagUtils.readTransformArray(ic,
+                "prevBoneTransforms");
+        startBoneTransforms = RagUtils.readTransformArray(ic,
+                "startBoneTransforms");
+    }
+
+    /**
+     * Immediately put this link into dynamic mode and update the range of
+     * motion of its joint.
+     *
+     * @param uniformAcceleration the uniform acceleration vector (in
+     * physics-space coordinates, not null, unaffected)
+     */
+    @Override
+    public void setDynamic(Vector3f uniformAcceleration) {
+        getControl().verifyReadyForDynamicMode("put link into dynamic mode");
+
+        super.setDynamic(uniformAcceleration);
+
+        String name = boneName();
+        RangeOfMotion preset = getControl().getJointLimits(name);
+        preset.setupJoint((SixDofJoint) getJoint());
+    }
+
+    /**
+     * Internal callback, invoked once per frame during the logical-state
+     * update, provided the control is added to a scene.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    void update(float tpf) {
+        assert tpf >= 0f : tpf;
+
+        if (prevBoneTransforms == null) {
+            /*
+             * On the first update, allocate and initialize
+             * the array of previous bone transforms, if it wasn't
+             * allocated in blendToKinematicMode().
+             */
+            int numManagedBones = managedBones.length;
+            prevBoneTransforms = new Transform[numManagedBones];
+            for (int mbIndex = 0; mbIndex < numManagedBones; ++mbIndex) {
+                Joint managedBone = managedBones[mbIndex];
+                Transform boneTransform
+                        = managedBone.getLocalTransform().clone();
+                prevBoneTransforms[mbIndex] = boneTransform;
+            }
+        }
+
+        super.update(tpf);
+        /*
+         * Save copies of the latest bone transforms.
+         */
+        for (int mbIndex = 0; mbIndex < managedBones.length; ++mbIndex) {
+            Transform lastTransform = prevBoneTransforms[mbIndex];
+            Joint managedBone = managedBones[mbIndex];
+            lastTransform.set(managedBone.getLocalTransform());
+        }
+    }
+
+    /**
+     * Serialize this link, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.write(managedBones, "managedBones", null);
+        oc.write(submode, "submode", KinematicSubmode.Animated);
+        oc.write(prevBoneTransforms, "prevBoneTransforms", new Transform[0]);
+        oc.write(startBoneTransforms, "startBoneTransforms", new Transform[0]);
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Calculate the local bone transform to match the physics transform of the
+     * rigid body.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the calculated bone transform (in local coordinates, either
+     * storeResult or a new transform, not null)
+     */
+    private Transform localBoneTransform(Transform storeResult) {
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+        Vector3f location = result.getTranslation();
+        Quaternion orientation = result.getRotation();
+        Vector3f scale = result.getScale();
+        /*
+         * Start with the rigid body's transform in physics/world coordinates.
+         */
+        PhysicsRigidBody body = getRigidBody();
+        body.getPhysicsLocation(result.getTranslation());
+        body.getPhysicsRotation(result.getRotation());
+        result.setScale(body.getCollisionShape().getScale());
+        /*
+         * Convert to mesh coordinates.
+         */
+        Transform worldToMesh = getControl().meshTransform(null).invert();
+        result.combineWithParent(worldToMesh);
+        /*
+         * Convert to the bone's local coordinate system by factoring out the
+         * parent bone's transform.
+         */
+        Joint parentBone = getBone().getParent();
+        RagUtils.meshToLocal(parentBone, result);
+        /*
+         * Subtract the body's local offset, rotated and scaled.
+         */
+        Vector3f parentOffset = localOffset(null);
+        parentOffset.multLocal(scale);
+        orientation.mult(parentOffset, parentOffset);
+        location.subtractLocal(parentOffset);
+
+        return result;
+    }
+}

+ 566 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/DacConfiguration.java

@@ -0,0 +1,566 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.bullet.control.AbstractPhysicsControl;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Configure a DynamicAnimControl and access its configuration.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+abstract public class DacConfiguration extends AbstractPhysicsControl {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger2
+            = Logger.getLogger(DacConfiguration.class.getName());
+    /**
+     * name for the ragdoll's torso, must not be used for any bone
+     */
+    final public static String torsoName = "";
+    // *************************************************************************
+    // fields
+
+    /**
+     * viscous damping ratio for new rigid bodies (0&rarr;no damping,
+     * 1&rarr;critically damped, default=0.6)
+     */
+    private float damping = 0.6f;
+    /**
+     * minimum applied impulse for a collision event to be dispatched to
+     * listeners (default=0)
+     */
+    private float eventDispatchImpulseThreshold = 0f;
+    /**
+     * mass for the torso
+     */
+    private float torsoMass = 1f;
+    /**
+     * map linked bone names to masses
+     */
+    private Map<String, Float> blConfigMap = new HashMap<>(50);
+    /**
+     * map linked bone names to ranges of motion for createSpatialData()
+     */
+    private Map<String, RangeOfMotion> jointMap = new HashMap<>(50);
+    /**
+     * gravitational acceleration vector for ragdolls (default is 9.8 in the -Y
+     * direction, approximating Earth-normal in MKS units)
+     */
+    private Vector3f gravityVector = new Vector3f(0f, -9.8f, 0f);
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate an enabled control without any attachments or linked bones
+     * (torso only).
+     */
+    DacConfiguration() {
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Count the linked bones.
+     *
+     * @return count (&ge;0)
+     */
+    public int countLinkedBones() {
+        int count = blConfigMap.size();
+
+        assert count == jointMap.size();
+        assert count >= 0 : count;
+        return count;
+    }
+
+    /**
+     * Count the links.
+     *
+     * @return count (&ge;0)
+     */
+    public int countLinks() {
+        int result = countLinkedBones() + 1;
+        return result;
+    }
+
+    /**
+     * Read the damping ratio for new rigid bodies.
+     *
+     * @return the viscous damping ratio (0&rarr;no damping, 1&rarr;critically
+     * damped)
+     */
+    public float damping() {
+        assert damping >= 0f : damping;
+        return damping;
+    }
+
+    /**
+     * Read the event-dispatch impulse threshold of this control.
+     *
+     * @return the threshold value (&ge;0)
+     */
+    public float eventDispatchImpulseThreshold() {
+        assert eventDispatchImpulseThreshold >= 0f;
+        return eventDispatchImpulseThreshold;
+    }
+
+    /**
+     * Access the nominal range of motion for the joint connecting the named
+     * linked bone to its parent in the hierarchy.
+     *
+     * @param boneName the name of the linked bone (not null, not empty)
+     * @return the pre-existing instance (not null)
+     */
+    public RangeOfMotion getJointLimits(String boneName) {
+        if (!hasBoneLink(boneName)) {
+            String msg = "No linked bone named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+        RangeOfMotion result = jointMap.get(boneName);
+
+        assert result != null;
+        return result;
+    }
+
+    /**
+     * Copy this control's gravitational acceleration for Ragdoll mode.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return an acceleration vector (in physics-space coordinates, either
+     * storeResult or a new vector, not null)
+     */
+    public Vector3f gravity(Vector3f storeResult) {
+        Vector3f result = (storeResult == null) ? new Vector3f() : storeResult;
+        result.set(gravityVector);
+        return result;
+    }
+
+    /**
+     * Test whether a BoneLink exists for the named bone.
+     *
+     * @param boneName the name of the bone (may be null)
+     * @return true if found, otherwise false
+     */
+    public boolean hasBoneLink(String boneName) {
+        boolean result;
+        if (boneName == null) {
+            result = false;
+        } else {
+            result = blConfigMap.containsKey(boneName);
+        }
+
+        return result;
+    }
+
+    /**
+     * Link the named bone using the specified mass and range of motion.
+     * <p>
+     * Allowed only when the control is NOT added to a spatial.
+     *
+     * @param boneName the name of the bone to link (not null, not empty)
+     * @param mass the desired mass of the bone (&gt;0)
+     * @param rom the desired range of motion (not null)
+     * @see #setJointLimits(java.lang.String,
+     * com.jme3.bullet.animation.RangeOfMotion)
+     */
+    public void link(String boneName, float mass, RangeOfMotion rom) {
+        verifyNotAddedToSpatial("link a bone");
+        if (hasBoneLink(boneName)) {
+            logger2.log(Level.WARNING, "Bone {0} is already linked.", boneName);
+        }
+
+        jointMap.put(boneName, rom);
+        blConfigMap.put(boneName, mass);
+    }
+
+    /**
+     * Enumerate all bones with bone links.
+     *
+     * @return a new array of bone names (not null, may be empty)
+     */
+    public String[] listLinkedBoneNames() {
+        int size = countLinkedBones();
+        String[] result = new String[size];
+        Collection<String> names = blConfigMap.keySet();
+        names.toArray(result);
+
+        return result;
+    }
+
+    /**
+     * Read the mass of the named bone/torso.
+     *
+     * @param boneName the name of the bone/torso (not null)
+     * @return the mass (in physics units, &gt;0)
+     */
+    public float mass(String boneName) {
+        float mass;
+        if (torsoName.equals(boneName)) {
+            mass = torsoMass;
+        } else {
+            mass = blConfigMap.get(boneName);
+        }
+        return mass;
+    }
+
+    /**
+     * Alter the viscous damping ratio for new rigid bodies.
+     *
+     * @param dampingRatio the desired damping ratio (non-negative, 0&rarr;no
+     * damping, 1&rarr;critically damped, default=0.6)
+     */
+    public void setDamping(float dampingRatio) {
+        damping = dampingRatio;
+    }
+
+    /**
+     * Alter the event-dispatch impulse threshold of this control.
+     *
+     * @param threshold the desired threshold (&ge;0)
+     */
+    public void setEventDispatchImpulseThreshold(float threshold) {
+        eventDispatchImpulseThreshold = threshold;
+    }
+
+    /**
+     * Alter this control's gravitational acceleration for Ragdoll mode.
+     *
+     * @param gravity the desired acceleration vector (in physics-space
+     * coordinates, not null, unaffected, default=0,-9.8,0)
+     */
+    public void setGravity(Vector3f gravity) {
+        gravityVector.set(gravity);
+    }
+
+    /**
+     * Alter the range of motion of the joint connecting the named BoneLink to
+     * its parent in the link hierarchy.
+     *
+     * @param boneName the name of the BoneLink (not null, not empty)
+     * @param rom the desired range of motion (not null)
+     */
+    public void setJointLimits(String boneName, RangeOfMotion rom) {
+        if (!hasBoneLink(boneName)) {
+            String msg = "No linked bone named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+
+        jointMap.put(boneName, rom);
+    }
+
+    /**
+     * Alter the mass of the named bone/torso.
+     *
+     * @param boneName the name of the bone, or torsoName (not null)
+     * @param mass the desired mass (&gt;0)
+     */
+    public void setMass(String boneName, float mass) {
+        if (torsoName.equals(boneName)) {
+            torsoMass = mass;
+        } else if (hasBoneLink(boneName)) {
+            blConfigMap.put(boneName, mass);
+        } else {
+            String msg = "No bone/torso named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+    /**
+     * Calculate the ragdoll's total mass.
+     *
+     * @return the total mass (&gt;0)
+     */
+    public float totalMass() {
+        float totalMass = torsoMass;
+        for (float mass : blConfigMap.values()) {
+            totalMass += mass;
+        }
+
+        return totalMass;
+    }
+
+    /**
+     * Unlink the BoneLink of the named bone.
+     * <p>
+     * Allowed only when the control is NOT added to a spatial.
+     *
+     * @param boneName the name of the bone to unlink (not null, not empty)
+     */
+    public void unlinkBone(String boneName) {
+        if (!hasBoneLink(boneName)) {
+            String msg = "No linked bone named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+        verifyNotAddedToSpatial("unlink a bone");
+
+        jointMap.remove(boneName);
+        blConfigMap.remove(boneName);
+    }
+    // *************************************************************************
+    // new protected methods
+
+    /**
+     * Add unlinked descendants of the specified bone to the specified
+     * collection. Note: recursive.
+     *
+     * @param startBone the starting bone (not null, unaffected)
+     * @param addResult the collection of bone names to append to (not null,
+     * modified)
+     */
+    protected void addUnlinkedDescendants(Joint startBone,
+            Collection<Joint> addResult) {
+        for (Joint childBone : startBone.getChildren()) {
+            String childName = childBone.getName();
+            if (!hasBoneLink(childName)) {
+                addResult.add(childBone);
+                addUnlinkedDescendants(childBone, addResult);
+            }
+        }
+    }
+
+    /**
+     * Find the manager of the specified bone.
+     *
+     * @param startBone the bone (not null, unaffected)
+     * @return a bone/torso name (not null)
+     */
+    protected String findManager(Joint startBone) {
+        String managerName;
+        Joint bone = startBone;
+        while (true) {
+            String boneName = bone.getName();
+            if (hasBoneLink(boneName)) {
+                managerName = boneName;
+                break;
+            }
+            bone = bone.getParent();
+            if (bone == null) {
+                managerName = torsoName;
+                break;
+            }
+        }
+
+        assert managerName != null;
+        return managerName;
+    }
+
+    /**
+     * Create a map from bone indices to the names of the bones that manage
+     * them.
+     *
+     * @param skeleton (not null, unaffected)
+     * @return a new array of bone/torso names (not null)
+     */
+    protected String[] managerMap(Armature skeleton) {
+        int numBones = skeleton.getJointCount();
+        String[] managerMap = new String[numBones];
+        for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
+            Joint bone = skeleton.getJoint(boneIndex);
+            managerMap[boneIndex] = findManager(bone);
+        }
+
+        return managerMap;
+    }
+    // *************************************************************************
+    // AbstractPhysicsControl methods
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned control into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this control (not null, modified)
+     * @param original the control from which this control was shallow-cloned
+     * (not null, unaffected)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        blConfigMap = cloner.clone(blConfigMap);
+        jointMap = cloner.clone(jointMap);
+        gravityVector = cloner.clone(gravityVector);
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public DacConfiguration jmeClone() {
+        try {
+            DacConfiguration clone
+                    = (DacConfiguration) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * De-serialize this control, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        damping = ic.readFloat("damping", 0.6f);
+        eventDispatchImpulseThreshold
+                = ic.readFloat("eventDispatchImpulseThreshold", 0f);
+
+        jointMap.clear();
+        blConfigMap.clear();
+        String[] linkedBoneNames = ic.readStringArray("linkedBoneNames", null);
+        Savable[] linkedBoneJoints
+                = ic.readSavableArray("linkedBoneJoints", null);
+        float[] blConfigs = ic.readFloatArray("blConfigs", null);
+        for (int i = 0; i < linkedBoneNames.length; ++i) {
+            String boneName = linkedBoneNames[i];
+            RangeOfMotion rom = (RangeOfMotion) linkedBoneJoints[i];
+            jointMap.put(boneName, rom);
+            blConfigMap.put(boneName, blConfigs[i]);
+        }
+
+        torsoMass = ic.readFloat("torsoMass", 1f);
+        gravityVector = (Vector3f) ic.readSavable("gravity", null);
+    }
+
+    /**
+     * Render this control. Invoked once per view port per frame, provided the
+     * control is added to a scene. Should be invoked only by a subclass or by
+     * the RenderManager.
+     *
+     * @param rm the render manager (not null)
+     * @param vp the view port to render (not null)
+     */
+    @Override
+    public void render(RenderManager rm, ViewPort vp) {
+    }
+
+    /**
+     * Alter whether physics-space coordinates should match the spatial's local
+     * coordinates.
+     *
+     * @param applyPhysicsLocal true&rarr;match local coordinates,
+     * false&rarr;match world coordinates (default=false)
+     */
+    @Override
+    public void setApplyPhysicsLocal(boolean applyPhysicsLocal) {
+        if (applyPhysicsLocal) {
+            throw new UnsupportedOperationException(
+                    "DynamicAnimControl does not support local physics.");
+        }
+    }
+
+    /**
+     * Serialize this control, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.write(damping, "damping", 0.6f);
+        oc.write(eventDispatchImpulseThreshold, "eventDispatchImpulseThreshold",
+                0f);
+
+        int count = countLinkedBones();
+        String[] linkedBoneNames = new String[count];
+        RangeOfMotion[] roms = new RangeOfMotion[count];
+        float[] blConfigs = new float[count];
+        int i = 0;
+        for (Map.Entry<String, Float> entry : blConfigMap.entrySet()) {
+            linkedBoneNames[i] = entry.getKey();
+            roms[i] = jointMap.get(entry.getKey());
+            blConfigs[i] = entry.getValue();
+            ++i;
+        }
+        oc.write(linkedBoneNames, "linkedBoneNames", null);
+        oc.write(roms, "linkedBoneJoints", null);
+        oc.write(blConfigs, "blConfigs", null);
+
+        oc.write(torsoMass, "torsoMass", 1f);
+        oc.write(gravityVector, "gravity", null);
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Verify that this control is NOT added to a Spatial.
+     *
+     * @param desiredAction (not null, not empty)
+     */
+    private void verifyNotAddedToSpatial(String desiredAction) {
+        assert desiredAction != null;
+
+        Spatial controlledSpatial = getSpatial();
+        if (controlledSpatial != null) {
+            String message = "Cannot " + desiredAction
+                    + " while the Control is added to a Spatial.";
+            throw new IllegalStateException(message);
+        }
+    }
+}

+ 1109 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/DacLinks.java

@@ -0,0 +1,1109 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.anim.SkinningControl;
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.PhysicsTickListener;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.collision.shapes.HullCollisionShape;
+import com.jme3.bullet.joints.PhysicsJoint;
+import com.jme3.bullet.joints.SixDofJoint;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Access a DynamicAnimControl at the PhysicsLink level once it's been added to
+ * a Spatial.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class DacLinks
+        extends DacConfiguration
+        implements PhysicsTickListener {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger3
+            = Logger.getLogger(DacLinks.class.getName());
+    /**
+     * local copy of {@link com.jme3.math.Quaternion#IDENTITY}
+     */
+    final private static Quaternion rotateIdentity = new Quaternion();
+    /**
+     * local copy of {@link com.jme3.math.Transform#IDENTITY}
+     */
+    final private static Transform transformIdentity = new Transform();
+    /**
+     * local copy of {@link com.jme3.math.Vector3f#ZERO}
+     */
+    final private static Vector3f translateIdentity = new Vector3f(0f, 0f, 0f);
+    // *************************************************************************
+    // fields
+
+    /**
+     * false until the 1st physics tick, true thereafter, indicating that all
+     * links are ready for dynamic mode
+     */
+    private boolean isReady = false;
+    /**
+     * bone links in a pre-order, depth-first traversal of the link hierarchy
+     */
+    private List<BoneLink> boneLinkList = null;
+    /**
+     * map bone names to bone links
+     */
+    private Map<String, BoneLink> boneLinks = new HashMap<>(32);
+    /**
+     * skeleton being controlled
+     */
+    private Armature skeleton = null;
+    /**
+     * spatial that provides the mesh-coordinate transform
+     */
+    private Spatial transformer = null;
+    /**
+     * torso link for this control
+     */
+    private TorsoLink torsoLink = null;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate an enabled control without any linked bones or attachments
+     * (torso only).
+     */
+    DacLinks() {
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Access the named bone.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param boneName the name of the skeleton bone to access
+     * @return the pre-existing instance, or null if not found
+     */
+    public Joint findBone(String boneName) {
+        verifyAddedToSpatial("access a bone");
+        Joint result = skeleton.getJoint(boneName);
+        return result;
+    }
+
+    /**
+     * Access the BoneLink for the named bone. Returns null if bone is not
+     * linked, or if the control is not added to a spatial.
+     *
+     * @param boneName the name of the bone (not null, not empty)
+     * @return the pre-existing BoneLink, or null if not found
+     */
+    public BoneLink findBoneLink(String boneName) {
+        BoneLink boneLink = boneLinks.get(boneName);
+        return boneLink;
+    }
+
+    /**
+     * Access the named link. Returns null if the name is invalid, or if the
+     * control is not added to a spatial.
+     *
+     * @param linkName the name of the link (not null, not empty)
+     * @return the pre-existing link, or null if not found
+     */
+    public PhysicsLink findLink(String linkName) {
+        PhysicsLink link;
+        if (linkName.startsWith("Bone:")) {
+            String boneName = linkName.substring(5);
+            link = findBoneLink(boneName);
+        } else {
+            assert linkName.equals("Torso:");
+            link = torsoLink;
+        }
+
+        return link;
+    }
+
+    /**
+     * Access the skeleton. Returns null if the control is not added to a
+     * spatial.
+     *
+     * @return the pre-existing skeleton, or null
+     */
+    public Armature getSkeleton() {
+        return skeleton;
+    }
+
+    /**
+     * Access the TorsoLink. Returns null if the control is not added to a
+     * spatial.
+     *
+     * @return the pre-existing TorsoLink, or null
+     */
+    public TorsoLink getTorsoLink() {
+        return torsoLink;
+    }
+
+    /**
+     * Access the spatial with the mesh-coordinate transform. Returns null if
+     * the control is not added to a spatial.
+     *
+     * @return the pre-existing spatial, or null
+     */
+    Spatial getTransformer() {
+        return transformer;
+    }
+
+    /**
+     * Test whether this control is ready for dynamic mode.
+     *
+     * @return true if ready, otherwise false
+     */
+    public boolean isReady() {
+        return isReady;
+    }
+
+    /**
+     * Enumerate all physics links of the specified type managed by this
+     * control.
+     *
+     * @param <T> subclass of PhysicsLink
+     * @param linkType the subclass of PhysicsLink to search for (not null)
+     * @return a new array of links (not null, not empty)
+     */
+    @SuppressWarnings("unchecked")
+    public <T extends PhysicsLink> List<T> listLinks(Class<T> linkType) {
+        int numLinks = countLinks();
+        List<T> result = new ArrayList<>(numLinks);
+
+        if (torsoLink != null
+                && linkType.isAssignableFrom(torsoLink.getClass())) {
+            result.add((T) torsoLink);
+        }
+        for (BoneLink link : boneLinkList) {
+            if (linkType.isAssignableFrom(link.getClass())) {
+                result.add((T) link);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Enumerate all managed bones of the named link, in a pre-order,
+     * depth-first traversal of the skeleton, such that child bones never
+     * precede their ancestors.
+     *
+     * @param managerName the name of the managing link (not null)
+     * @return a new array of managed bones, including the manager if it is not
+     * the torso
+     */
+    Joint[] listManagedBones(String managerName) {
+        List<Joint> list = new ArrayList<>(8);
+
+        if (torsoName.equals(managerName)) {
+            Joint[] roots = skeleton.getRoots();
+            for (Joint rootBone : roots) {
+                list.add(rootBone);
+                addUnlinkedDescendants(rootBone, list);
+            }
+
+        } else {
+            BoneLink manager = findBoneLink(managerName);
+            if (manager == null) {
+                String msg = "No link named " + managerName;
+                throw new IllegalArgumentException(msg);
+            }
+            Joint managerBone = manager.getBone();
+            list.add(managerBone);
+            addUnlinkedDescendants(managerBone, list);
+        }
+        /*
+         * Convert the list to an array.
+         */
+        int numManagedBones = list.size();
+        Joint[] array = new Joint[numManagedBones];
+        list.toArray(array);
+
+        return array;
+    }
+
+    /**
+     * Enumerate all rigid bodies managed by this control.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @return a new array of pre-existing rigid bodies (not null, not empty)
+     */
+    public PhysicsRigidBody[] listRigidBodies() {
+        verifyAddedToSpatial("enumerate rigid bodies");
+
+        int numLinks = countLinks();
+        PhysicsRigidBody[] result = new PhysicsRigidBody[numLinks];
+
+        int linkIndex = 0;
+        if (torsoLink != null) {
+            result[0] = torsoLink.getRigidBody();
+            ++linkIndex;
+        }
+        for (BoneLink boneLink : boneLinkList) {
+            result[linkIndex] = boneLink.getRigidBody();
+            ++linkIndex;
+        }
+        assert linkIndex == numLinks;
+
+        return result;
+    }
+
+    /**
+     * Copy the model's mesh-to-world transform.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the model's mesh transform (in world coordinates, either
+     * storeResult or a new transform, not null)
+     */
+    Transform meshTransform(Transform storeResult) {
+        Transform result = transformer.getWorldTransform().clone();
+        return result;
+    }
+
+    /**
+     * Calculate the physics transform to match the specified skeleton bone.
+     *
+     * @param bone the skeleton bone to match (not null, unaffected)
+     * @param localOffset the location of the body's center (in the bone's local
+     * coordinates, not null, unaffected)
+     * @param storeResult storage for the result (modified if not null)
+     * @return the calculated physics transform (either storeResult or a new
+     * transform, not null)
+     */
+    Transform physicsTransform(Joint bone, Vector3f localOffset,
+            Transform storeResult) {
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+        /*
+         * Start with the body's transform in the bone's local coordinates.
+         */
+        result.setTranslation(localOffset);
+        result.setRotation(rotateIdentity);
+        result.setScale(1f);
+        /*
+         * Convert to mesh coordinates.
+         */
+        Transform localToMesh = bone.getModelTransform();
+        result.combineWithParent(localToMesh);
+        /*
+         * Convert to world (physics-space) coordinates.
+         */
+        Transform meshToWorld = meshTransform(null);
+        result.combineWithParent(meshToWorld);
+
+        return result;
+    }
+
+    /**
+     * Rebuild the ragdoll. This is useful if you applied scale to the model
+     * after it was initialized.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     */
+    public void rebuild() {
+        verifyAddedToSpatial("rebuild the ragdoll");
+
+        Map<String, BoneLink> saveBones = new HashMap<>(boneLinks);
+        TorsoLink saveTorso = torsoLink;
+
+        Spatial controlledSpatial = getSpatial();
+        removeSpatialData(controlledSpatial);
+        createSpatialData(controlledSpatial);
+
+        for (Map.Entry<String, BoneLink> entry : boneLinks.entrySet()) {
+            String name = entry.getKey();
+            BoneLink newLink = entry.getValue();
+            BoneLink oldLink = saveBones.get(name);
+            newLink.postRebuild(oldLink);
+        }
+        if (torsoLink != null) {
+            torsoLink.postRebuild(saveTorso);
+        }
+    }
+
+    /**
+     * Alter the mass of the specified link.
+     *
+     * @param link the link to modify (not null)
+     * @param mass the desired mass (&gt;0)
+     */
+    public void setMass(PhysicsLink link, float mass) {
+        if (link instanceof BoneLink) {
+            String boneName = link.boneName();
+            setMass(boneName, mass);
+        } else {
+            assert link instanceof TorsoLink;
+            setMass(torsoName, mass);
+        }
+    }
+
+    /**
+     * Verify that this control is ready for dynamic mode, which implies that it
+     * is added to a Spatial.
+     *
+     * @param desiredAction (not null, not empty)
+     */
+    public void verifyReadyForDynamicMode(String desiredAction) {
+        assert desiredAction != null;
+
+        verifyAddedToSpatial(desiredAction);
+
+        if (!isReady) {
+            String message = "Cannot " + desiredAction
+                    + " until the physics has been stepped.";
+            throw new IllegalStateException(message);
+        }
+    }
+    // *************************************************************************
+    // new protected methods
+
+    /**
+     * Access the list of bone links in a pre-order, depth-first traversal of
+     * the link hierarchy.
+     *
+     * @return the pre-existing list (not null)
+     */
+    protected List<BoneLink> getBoneLinks() {
+        assert boneLinkList != null;
+        return boneLinkList;
+    }
+
+    /**
+     * Verify that this control is added to a Spatial.
+     *
+     * @param desiredAction (not null, not empty)
+     */
+    protected void verifyAddedToSpatial(String desiredAction) {
+        assert desiredAction != null;
+
+        Spatial controlledSpatial = getSpatial();
+        if (controlledSpatial == null) {
+            String message = "Cannot " + desiredAction
+                    + " unless the Control is added to a Spatial.";
+            throw new IllegalStateException(message);
+        }
+    }
+    // *************************************************************************
+    // DacConfiguration methods
+
+    /**
+     * Add all managed physics objects to the PhysicsSpace.
+     */
+    @Override
+    protected void addPhysics(PhysicsSpace space) {
+        Vector3f gravity = gravity(null);
+
+        PhysicsRigidBody rigidBody;
+        if (torsoLink != null) {
+            rigidBody = torsoLink.getRigidBody();
+            space.add(rigidBody);
+            rigidBody.setGravity(gravity);
+        }
+
+        for (BoneLink boneLink : boneLinkList) {
+            rigidBody = boneLink.getRigidBody();
+            space.add(rigidBody);
+            rigidBody.setGravity(gravity);
+
+            PhysicsJoint joint = boneLink.getJoint();
+            space.add(joint);
+        }
+    }
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned control into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this control (not null, modified)
+     * @param original the control from which this control was shallow-cloned
+     * (not null, unaffected)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+        DacLinks originalDac = (DacLinks) original;
+
+        boneLinkList = cloner.clone(boneLinkList);
+
+        boneLinks = new HashMap<>(32);
+        for (Map.Entry<String, BoneLink> entry
+                : originalDac.boneLinks.entrySet()) {
+            String boneName = entry.getKey();
+            BoneLink link = entry.getValue();
+            BoneLink copyLink = cloner.clone(link);
+            boneLinks.put(boneName, copyLink);
+        }
+
+        skeleton = cloner.clone(skeleton);
+        transformer = cloner.clone(transformer);
+        torsoLink = cloner.clone(torsoLink);
+    }
+
+    /**
+     * Create spatial-dependent data. Invoked each time the control is added to
+     * a spatial. Also invoked by {@link #rebuild()}.
+     *
+     * @param spatial the controlled spatial (not null)
+     */
+    @Override
+    protected void createSpatialData(Spatial spatial) {
+        RagUtils.validate(spatial);
+
+        SkinningControl skeletonControl
+                = spatial.getControl(SkinningControl.class);
+        if (skeletonControl == null) {
+            throw new IllegalArgumentException(
+                    "The controlled spatial must have a SkinningControl. "
+                    + "Make sure the control is there and not on a subnode.");
+        }
+        sortControls(skeletonControl);
+        skeletonControl.setHardwareSkinningPreferred(false);
+        /*
+         * Analyze the model's skeleton.
+         */
+        skeleton = skeletonControl.getArmature();
+        validateSkeleton();
+        String[] tempManagerMap = managerMap(skeleton);
+        int numBones = skeleton.getJointCount();
+        /*
+         * Temporarily set all bones' local translations and rotations to bind.
+         */
+        Transform[] savedTransforms = new Transform[numBones];
+        for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
+            Joint bone = skeleton.getJoint(boneIndex);
+            savedTransforms[boneIndex] = bone.getLocalTransform().clone();
+            bone.applyBindPose(); // TODO adjust the scale?
+        }
+        skeleton.update();
+        /*
+         * Find the target meshes and choose the transform spatial.
+         */
+        List<Mesh> targetList = RagUtils.listAnimatedMeshes(spatial, null);
+        Mesh[] targets = new Mesh[targetList.size()];
+        targetList.toArray(targets);
+        transformer = RagUtils.findAnimatedGeometry(spatial);
+        if (transformer == null) {
+            transformer = spatial;
+        }
+        /*
+         * Enumerate mesh-vertex coordinates and assign them to managers.
+         */
+        Map<String, VectorSet> coordsMap
+                = RagUtils.coordsMap(targets, tempManagerMap);
+        /*
+         * Create the torso link.
+         */
+        VectorSet vertexLocations = coordsMap.get(torsoName);
+        createTorsoLink(vertexLocations, targets);
+        /*
+         * Create bone links without joints.
+         */
+        String[] linkedBoneNames = listLinkedBoneNames();
+        for (String boneName : linkedBoneNames) {
+            vertexLocations = coordsMap.get(boneName);
+            createBoneLink(boneName, vertexLocations);
+        }
+        int numLinkedBones = countLinkedBones();
+        assert boneLinks.size() == numLinkedBones;
+        /*
+         * Add joints to connect each BoneLink rigid body with its parent in the
+         * link hierarchy.  Also initialize the boneLinkList.
+         */
+        boneLinkList = new ArrayList<>(numLinkedBones);
+        addJoints(torsoLink);
+        assert boneLinkList.size() == numLinkedBones : boneLinkList.size();
+        /*
+         * Restore the skeleton's pose.
+         */
+        for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
+            Joint bone = skeleton.getJoint(boneIndex);
+            bone.setLocalTransform(savedTransforms[boneIndex]);
+        }
+        skeleton.update();
+
+        if (added) {
+            addPhysics(space);
+        }
+
+        logger3.log(Level.FINE, "Created ragdoll for skeleton.");
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public DacLinks jmeClone() {
+        try {
+            DacLinks clone = (DacLinks) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * Read the mass of the named bone/torso.
+     *
+     * @param boneName the name of the bone/torso (not null)
+     * @return the mass (&gt;0) or NaN if undetermined
+     */
+    @Override
+    public float mass(String boneName) {
+        float mass;
+        if (getSpatial() == null) {
+            mass = super.mass(boneName);
+        } else if (torsoName.equals(boneName)) {
+            PhysicsRigidBody rigidBody = torsoLink.getRigidBody();
+            mass = rigidBody.getMass();
+        } else if (boneLinks.containsKey(boneName)) {
+            BoneLink link = boneLinks.get(boneName);
+            PhysicsRigidBody rigidBody = link.getRigidBody();
+            mass = rigidBody.getMass();
+        } else {
+            String msg = "No bone/torso named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+
+        return mass;
+    }
+
+    /**
+     * De-serialize this control, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        boneLinkList
+                = ic.readSavableArrayList("boneLinkList", null);
+        for (BoneLink link : boneLinkList) {
+            String name = link.boneName();
+            boneLinks.put(name, link);
+        }
+
+        skeleton = (Armature) ic.readSavable("skeleton", null);
+        transformer = (Spatial) ic.readSavable("transformer", null);
+        torsoLink = (TorsoLink) ic.readSavable("torsoLink", null);
+    }
+
+    /**
+     * Remove all managed physics objects from the PhysicsSpace.
+     */
+    @Override
+    protected void removePhysics(PhysicsSpace space) {
+        assert added;
+
+        PhysicsRigidBody rigidBody;
+        if (torsoLink != null) {
+            rigidBody = torsoLink.getRigidBody();
+            space.remove(rigidBody);
+        }
+
+        for (BoneLink boneLink : boneLinks.values()) {
+            rigidBody = boneLink.getRigidBody();
+            space.remove(rigidBody);
+
+            PhysicsJoint joint = boneLink.getJoint();
+            space.remove(joint);
+        }
+    }
+
+    /**
+     * Remove spatial-dependent data. Invoked each time this control is rebuilt
+     * or removed from a spatial.
+     *
+     * @param spat the previously controlled spatial (unused)
+     */
+    @Override
+    protected void removeSpatialData(Spatial spat) {
+        if (added) {
+            removePhysics(space);
+        }
+
+        skeleton = null;
+
+        boneLinks.clear();
+        boneLinkList = null;
+        torsoLink = null;
+        transformer = null;
+    }
+
+    /**
+     * Alter the viscous damping ratio for all rigid bodies, including new ones.
+     *
+     * @param dampingRatio the desired damping ratio (non-negative, 0&rarr;no
+     * damping, 1&rarr;critically damped, default=0.6)
+     */
+    @Override
+    public void setDamping(float dampingRatio) {
+        super.setDamping(dampingRatio);
+
+        if (getSpatial() != null) {
+            PhysicsRigidBody[] bodies = listRigidBodies();
+            for (PhysicsRigidBody rigidBody : bodies) {
+                rigidBody.setDamping(dampingRatio, dampingRatio);
+            }
+        }
+    }
+
+    /**
+     * Alter this control's gravitational acceleration for Ragdoll mode.
+     *
+     * @param gravity the desired acceleration vector (in physics-space
+     * coordinates, not null, unaffected, default=0,-9.8,0)
+     */
+    @Override
+    public void setGravity(Vector3f gravity) {
+        super.setGravity(gravity);
+
+        if (getSpatial() != null) { // TODO make sure it's in ragdoll mode
+            PhysicsRigidBody[] bodies = listRigidBodies();
+            for (PhysicsRigidBody rigidBody : bodies) {
+                rigidBody.setGravity(gravity);
+            }
+        }
+    }
+
+    /**
+     * Alter the range of motion of the joint connecting the named BoneLink to
+     * its parent in the link hierarchy.
+     *
+     * @param boneName the name of the BoneLink (not null, not empty)
+     * @param rom the desired range of motion (not null)
+     */
+    @Override
+    public void setJointLimits(String boneName, RangeOfMotion rom) {
+        if (!hasBoneLink(boneName)) {
+            String msg = "No linked bone named " + boneName;
+            throw new IllegalArgumentException(msg);
+        }
+
+        super.setJointLimits(boneName, rom);
+
+        if (getSpatial() != null) {
+            BoneLink boneLink = findBoneLink(boneName);
+            SixDofJoint joint = (SixDofJoint) boneLink.getJoint();
+            rom.setupJoint(joint);
+        }
+    }
+
+    /**
+     * Alter the mass of the named bone/torso.
+     *
+     * @param boneName the name of the bone, or torsoName (not null)
+     * @param mass the desired mass (&gt;0)
+     */
+    @Override
+    public void setMass(String boneName, float mass) {
+        super.setMass(boneName, mass);
+
+        if (getSpatial() != null) {
+            PhysicsRigidBody rigidBody;
+            if (torsoName.equals(boneName)) {
+                rigidBody = torsoLink.getRigidBody();
+            } else {
+                BoneLink link = findBoneLink(boneName);
+                rigidBody = link.getRigidBody();
+            }
+            rigidBody.setMass(mass);
+        }
+    }
+
+    /**
+     * Translate the torso to the specified location.
+     *
+     * @param vec desired location (not null, unaffected)
+     */
+    @Override
+    protected void setPhysicsLocation(Vector3f vec) {
+        torsoLink.getRigidBody().setPhysicsLocation(vec);
+    }
+
+    /**
+     * Rotate the torso to the specified orientation.
+     *
+     * @param quat desired orientation (not null, unaffected)
+     */
+    @Override
+    protected void setPhysicsRotation(Quaternion quat) {
+        torsoLink.getRigidBody().setPhysicsRotation(quat);
+    }
+
+    /**
+     * Update this control. Invoked once per frame during the logical-state
+     * update, provided the control is added to a scene. Do not invoke directly
+     * from user code.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    public void update(float tpf) {
+        verifyAddedToSpatial("update the control");
+        if (!isEnabled()) {
+            return;
+        }
+
+        if (torsoLink != null) {
+            torsoLink.update(tpf);
+        }
+        for (BoneLink boneLink : boneLinkList) {
+            boneLink.update(tpf);
+        }
+    }
+
+    /**
+     * Serialize this control, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        int count = countLinkedBones();
+        Savable[] savableArray = new Savable[count];
+        boneLinkList.toArray(savableArray);
+        oc.write(savableArray, "boneLinkList", null);
+
+        oc.write(skeleton, "skeleton", null);
+        oc.write(transformer, "transformer", null);
+        oc.write(torsoLink, "torsoLink", null);
+    }
+    // *************************************************************************
+    // PhysicsTickListener methods
+
+    /**
+     * Callback from Bullet, invoked just after the physics has been stepped.
+     * Used to re-activate any deactivated rigid bodies.
+     *
+     * @param space the space that was just stepped (not null)
+     * @param timeStep the time per physics step (in seconds, &ge;0)
+     */
+    @Override
+    public void physicsTick(PhysicsSpace space, float timeStep) {
+        assert space == getPhysicsSpace();
+
+        torsoLink.postTick();
+        for (BoneLink boneLink : boneLinkList) {
+            boneLink.postTick();
+        }
+
+        isReady = true;
+    }
+
+    /**
+     * Callback from Bullet, invoked just before the physics is stepped. A good
+     * time to clear/apply forces.
+     *
+     * @param space the space that is about to be stepped (not null)
+     * @param timeStep the time per physics step (in seconds, &ge;0)
+     */
+    @Override
+    public void prePhysicsTick(PhysicsSpace space, float timeStep) {
+        assert space == getPhysicsSpace();
+
+        torsoLink.preTick(timeStep);
+        for (BoneLink boneLink : boneLinkList) {
+            boneLink.preTick(timeStep);
+        }
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Add joints to connect the named bone/torso link with each of its
+     * children. Also fill in the boneLinkList. Note: recursive!
+     *
+     * @param parentName the parent bone/torso link (not null)
+     */
+    private void addJoints(PhysicsLink parentLink) {
+        List<String> childNames = childNames(parentLink);
+        for (String childName : childNames) {
+            /*
+             * Add the joint and configure its range of motion.
+             * Also initialize the BoneLink's parent and its array
+             * of managed bones.
+             */
+            BoneLink childLink = findBoneLink(childName);
+            childLink.addJoint(parentLink);
+            /*
+             * Add the BoneLink to the pre-order list.
+             */
+            boneLinkList.add(childLink);
+
+            addJoints(childLink);
+        }
+    }
+
+    /**
+     * Enumerate all immediate child BoneLinks of the specified bone/torso link.
+     *
+     * @param link the bone/torso link (not null)
+     * @return a new list of bone names
+     */
+    private List<String> childNames(PhysicsLink link) {
+        assert link != null;
+
+        String linkName;
+        if (link == torsoLink) {
+            linkName = torsoName;
+        } else {
+            linkName = link.boneName();
+        }
+
+        List<String> result = new ArrayList<>(8);
+        for (String childName : listLinkedBoneNames()) {
+            Joint bone = findBone(childName);
+            Joint parent = bone.getParent();
+            if (parent != null && findManager(parent).equals(linkName)) {
+                result.add(childName);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Create a jointless BoneLink for the named bone, and add it to the
+     * boneLinks map.
+     *
+     * @param boneName the name of the bone to be linked (not null)
+     * @param vertexLocations the set of vertex locations (not null, not empty)
+     */
+    private void createBoneLink(String boneName, VectorSet vertexLocations) {
+        Joint bone = findBone(boneName);
+        Transform boneToMesh = bone.getModelTransform();
+        Transform meshToBone = boneToMesh.invert();
+        //logger3.log(Level.INFO, "meshToBone = {0}", meshToBone);
+        /*
+         * Create the CollisionShape and locate the center of mass.
+         */
+        CollisionShape shape;
+        Vector3f center;
+        if (vertexLocations == null || vertexLocations.numVectors() == 0) {
+            throw new IllegalStateException("no vertex for " + boneName);
+        } else {
+            center = vertexLocations.mean(null);
+            center.subtractLocal(bone.getModelTransform().getTranslation());
+            shape = createShape(meshToBone, center, vertexLocations);
+        }
+
+        meshToBone.getTranslation().zero();
+        float mass = super.mass(boneName);
+        Vector3f offset = meshToBone.transformVector(center, null);
+        BoneLink link = new BoneLink(this, bone, shape, mass, offset);
+        boneLinks.put(boneName, link);
+    }
+
+    /**
+     * Create a CollisionShape for the specified transform, center, and vertex
+     * locations.
+     *
+     * @param vertexToShape the transform from vertex coordinates to de-scaled
+     * shape coordinates (not null, unaffected)
+     * @param center the location of the shape's center, in vertex coordinates
+     * (not null, unaffected)
+     * @param vertexLocations the set of vertex locations (not null, not empty,
+     * TRASHED)
+     * @return a new CollisionShape
+     */
+    private CollisionShape createShape(Transform vertexToShape, Vector3f center,
+            VectorSet vertexLocations) {
+        int numVectors = vertexLocations.numVectors();
+        assert numVectors > 0 : numVectors;
+
+        Vector3f tempLocation = new Vector3f();
+        int numPoints = vertexLocations.numVectors();
+        float points[] = new float[3 * numPoints];
+        FloatBuffer buffer = vertexLocations.toBuffer();
+        buffer.rewind();
+        int floatIndex = 0;
+        while (buffer.hasRemaining()) {
+            tempLocation.x = buffer.get();
+            tempLocation.y = buffer.get();
+            tempLocation.z = buffer.get();
+            /*
+             * Translate so that vertex coordinates are relative to
+             * the shape's center.
+             */
+            tempLocation.subtractLocal(center);
+            /*
+             * Transform vertex coordinates to de-scaled shape coordinates.
+             */
+            vertexToShape.transformVector(tempLocation, tempLocation);
+            points[floatIndex] = tempLocation.x;
+            points[floatIndex + 1] = tempLocation.y;
+            points[floatIndex + 2] = tempLocation.z;
+            floatIndex += 3;
+        }
+
+        CollisionShape result = new HullCollisionShape(points);
+
+        return result;
+    }
+
+    /**
+     * Create the TorsoLink.
+     *
+     * @param vertexLocations the set of vertex locations (not null, not empty)
+     * @param meshes array of animated meshes to use (not null, unaffected)
+     */
+    private void createTorsoLink(VectorSet vertexLocations, Mesh[] meshes) {
+        if (vertexLocations == null || vertexLocations.numVectors() == 0) {
+            throw new IllegalArgumentException(
+                    "No mesh vertices for the torso."
+                    + " Make sure the root bone is not linked.");
+        }
+        /*
+         * Create the CollisionShape.
+         */
+        Joint bone = RagUtils.findMainBone(skeleton, meshes);
+        assert bone.getParent() == null;
+        Transform boneToMesh = bone.getModelTransform();
+        Transform meshToBone = boneToMesh.invert();
+        Vector3f center = vertexLocations.mean(null);
+        center.subtractLocal(boneToMesh.getTranslation());
+        CollisionShape shape = createShape(meshToBone, center, vertexLocations);
+
+        meshToBone.getTranslation().zero();
+        Vector3f offset = meshToBone.transformVector(center, null);
+
+        Transform meshToModel;
+        Spatial cgm = getSpatial();
+        if (cgm instanceof Node) {
+            Transform modelToMesh
+                    = RagUtils.relativeTransform(transformer, (Node) cgm, null);
+            meshToModel = modelToMesh.invert();
+        } else {
+            meshToModel = transformIdentity;
+        }
+
+        float mass = super.mass(torsoName);
+        torsoLink = new TorsoLink(this, bone, shape, mass, meshToModel, offset);
+    }
+
+    /**
+     * Sort the controls of the controlled spatial, such that this control will
+     * come BEFORE the specified SkinningControl.
+     *
+     * @param skinningControl (not null)
+     */
+    private void sortControls(SkinningControl skinningControl) {
+        assert skinningControl != null;
+
+        int dacIndex = RagUtils.findIndex(spatial, this);
+        assert dacIndex != -1;
+        int scIndex = RagUtils.findIndex(spatial, skinningControl);
+        assert scIndex != -1;
+        assert dacIndex != scIndex;
+
+        if (dacIndex > scIndex) {
+            /*
+             * Remove the SkinningControl and re-add it to make sure it will get
+             * updated *after* this control.
+             */
+            spatial.removeControl(skinningControl);
+            spatial.addControl(skinningControl);
+
+            dacIndex = RagUtils.findIndex(spatial, this);
+            assert dacIndex != -1;
+            scIndex = RagUtils.findIndex(spatial, skinningControl);
+            assert scIndex != -1;
+            assert dacIndex < scIndex;
+        }
+    }
+
+    /**
+     * Validate the model's skeleton.
+     */
+    private void validateSkeleton() {
+        RagUtils.validate(skeleton);
+
+        for (String boneName : listLinkedBoneNames()) {
+            Joint bone = findBone(boneName);
+            if (bone == null) {
+                String msg = String.format(
+                        "Linked bone %s not found in skeleton.", boneName);
+                throw new IllegalArgumentException(msg);
+            }
+            if (bone.getParent() == null) {
+                logger3.log(Level.WARNING, "Linked bone {0} is a root bone.",
+                        boneName);
+            }
+        }
+    }
+}

+ 537 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/DynamicAnimControl.java

@@ -0,0 +1,537 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionListener;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.util.SafeArrayList;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Before adding this control to a spatial, configure it by invoking
+ * {@link #link(java.lang.String, float, com.jme3.bullet.animation.RangeOfMotion)}
+ * for each bone that should have its own rigid body. Leave unlinked bones near
+ * the root of the skeleton to form the torso of the ragdoll.
+ * <p>
+ * When you add the control to a spatial, it generates a ragdoll consisting of a
+ * rigid body for the torso and another for each linked bone. It also creates a
+ * SixDofJoint connecting each rigid body to its parent in the link hierarchy.
+ * The mass of each rigid body and the range-of-motion of each joint can be
+ * reconfigured on the fly.
+ * <p>
+ * Each link is either dynamic (driven by forces and torques) or kinematic
+ * (unperturbed by forces and torques). Transitions from dynamic to kinematic
+ * can be immediate or gradual.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class DynamicAnimControl
+        extends DacLinks
+        implements PhysicsCollisionListener {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger35
+            = Logger.getLogger(DynamicAnimControl.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * calculated total mass, not including released attachments
+     */
+    private float ragdollMass = 0f;
+    /**
+     * list of registered collision listeners
+     */
+    private List<RagdollCollisionListener> collisionListeners
+            = new SafeArrayList<>(RagdollCollisionListener.class);
+    /*
+     * center-of-mass actual location (in physics-space coordinates)
+     */
+    private Vector3f centerLocation = new Vector3f();
+    /*
+     * center-of-mass estimated velocity (psu/second in physics-space coordinates)
+     */
+    private Vector3f centerVelocity = new Vector3f();
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate an enabled control without any linked bones or attachments
+     * (torso only).
+     */
+    public DynamicAnimControl() {
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Add a collision listener to this control.
+     *
+     * @param listener (not null, alias created)
+     */
+    public void addCollisionListener(RagdollCollisionListener listener) {
+        collisionListeners.add(listener);
+    }
+
+    /**
+     * Begin blending the specified link and all its descendants to kinematic
+     * animation.
+     *
+     * @param rootLink the root of the subtree to bind (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    public void animateSubtree(PhysicsLink rootLink, float blendInterval) {
+        verifyAddedToSpatial("change modes");
+        blendSubtree(rootLink, KinematicSubmode.Animated, blendInterval);
+    }
+
+    /**
+     * Begin blending all links to purely kinematic mode, driven by animation.
+     * TODO callback when the transition completes
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     * @param endModelTransform the desired local transform for the controlled
+     * spatial when the transition completes or null for no change to local
+     * transform (unaffected)
+     */
+    public void blendToKinematicMode(float blendInterval,
+            Transform endModelTransform) {
+        verifyAddedToSpatial("change modes");
+
+        getTorsoLink().blendToKinematicMode(KinematicSubmode.Animated, blendInterval,
+                endModelTransform);
+        for (BoneLink boneLink : getBoneLinks()) {
+            boneLink.blendToKinematicMode(KinematicSubmode.Animated,
+                    blendInterval);
+        }
+    }
+
+    /**
+     * Calculate the ragdoll's total mass and center of mass, excluding released
+     * attachments.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param storeLocation storage for the location of the center (in
+     * physics-space coordinates, modified if not null)
+     * @param storeVelocity storage for the velocity of the center (psu/second
+     * in physics-space coordinates, modified if not null)
+     * @return the total mass (&gt;0)
+     */
+    public float centerOfMass(Vector3f storeLocation, Vector3f storeVelocity) {
+        verifyReadyForDynamicMode("calculate the center of mass");
+
+        recalculateCenter();
+        if (storeLocation != null) {
+            storeLocation.set(centerLocation);
+        }
+        if (storeVelocity != null) {
+            storeVelocity.set(centerVelocity);
+        }
+
+        return ragdollMass;
+    }
+
+    /**
+     * Alter the contact-response setting of the specified link and all its
+     * descendants (excluding released attachments). Note: recursive!
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param rootLink the root of the subtree to modify (not null)
+     * @param desiredResponse true for the usual rigid-body response, false for
+     * ghost-like response
+     */
+    public void setContactResponseSubtree(PhysicsLink rootLink,
+            boolean desiredResponse) {
+        verifyAddedToSpatial("change modes");
+
+        if (!rootLink.isReleased()) {
+            PhysicsRigidBody rigidBody = rootLink.getRigidBody();
+            rigidBody.setContactResponse(desiredResponse);
+
+            PhysicsLink[] children = rootLink.listChildren();
+            for (PhysicsLink child : children) {
+                setContactResponseSubtree(child, desiredResponse);
+            }
+        }
+    }
+
+    /**
+     * Immediately put the specified link and all its ancestors (excluding the
+     * torso) into dynamic mode. Note: recursive!
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param startLink the start of the chain to modify (not null)
+     * @param chainLength the maximum number of links to modify (&ge;0)
+     * @param uniformAcceleration the uniform acceleration vector (in
+     * physics-space coordinates, not null, unaffected)
+     */
+    public void setDynamicChain(PhysicsLink startLink, int chainLength,
+            Vector3f uniformAcceleration) {
+        if (chainLength == 0) {
+            return;
+        }
+        verifyAddedToSpatial("change modes");
+
+        if (startLink instanceof BoneLink) {
+            BoneLink boneLink = (BoneLink) startLink;
+            boneLink.setDynamic(uniformAcceleration);
+        }
+
+        PhysicsLink parent = startLink.getParent();
+        if (parent != null && chainLength > 1) {
+            setDynamicChain(parent, chainLength - 1, uniformAcceleration);
+        }
+    }
+
+    /**
+     * Immediately put the specified link and all its descendants (excluding
+     * released attachments) into dynamic mode. Note: recursive!
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     *
+     * @param rootLink the root of the subtree to modify (not null)
+     * @param uniformAcceleration the uniform acceleration vector (in
+     * physics-space coordinates, not null, unaffected)
+     */
+    public void setDynamicSubtree(PhysicsLink rootLink,
+            Vector3f uniformAcceleration) {
+        verifyAddedToSpatial("change modes");
+
+        if (rootLink == getTorsoLink()) {
+            getTorsoLink().setDynamic(uniformAcceleration);
+        } else if (rootLink instanceof BoneLink) {
+            BoneLink boneLink = (BoneLink) rootLink;
+            boneLink.setDynamic(uniformAcceleration);
+        }
+
+        PhysicsLink[] children = rootLink.listChildren();
+        for (PhysicsLink child : children) {
+            setDynamicSubtree(child, uniformAcceleration);
+        }
+    }
+
+    /**
+     * Immediately put all links into purely kinematic mode.
+     * <p>
+     * Allowed only when the control IS added to a spatial.
+     */
+    public void setKinematicMode() {
+        verifyAddedToSpatial("set kinematic mode");
+
+        Transform localTransform = getSpatial().getLocalTransform();
+        blendToKinematicMode(0f, localTransform);
+    }
+
+    /**
+     * Immediately put this control into ragdoll mode.
+     * <p>
+     * Allowed only when the control IS added to a spatial and all links are
+     * "ready".
+     */
+    public void setRagdollMode() {
+        verifyReadyForDynamicMode("set ragdoll mode");
+
+        TorsoLink torsoLink = getTorsoLink();
+        Vector3f acceleration = gravity(null);
+        torsoLink.setDynamic(acceleration);
+        for (BoneLink boneLink : getBoneLinks()) {
+            boneLink.setDynamic(acceleration);
+        }
+    }
+    // *************************************************************************
+    // DacPhysicsLinks methods
+
+    /**
+     * Add all managed physics objects to the PhysicsSpace.
+     */
+    @Override
+    protected void addPhysics(PhysicsSpace space) {
+        super.addPhysics(space);
+
+        space.addCollisionListener(this);
+        space.addTickListener(this);
+    }
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned control into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this control (not null, modified)
+     * @param original the control from which this control was shallow-cloned
+     * (not null, unaffected)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        collisionListeners = cloner.clone(collisionListeners);
+        centerLocation = cloner.clone(centerLocation);
+        centerVelocity = cloner.clone(centerVelocity);
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public DynamicAnimControl jmeClone() {
+        try {
+            DynamicAnimControl clone = (DynamicAnimControl) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * De-serialize this control, for example when loading from a J3O file.
+     *
+     * @param im the importer (not null)
+     * @throws IOException from the importer
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        // isReady and collisionListeners not read
+        ragdollMass = ic.readFloat("ragdollMass", 1f);
+        centerLocation
+                = (Vector3f) ic.readSavable("centerLocation", new Vector3f());
+        centerVelocity
+                = (Vector3f) ic.readSavable("centerVelocity", new Vector3f());
+    }
+
+    /**
+     * Remove all managed physics objects from the PhysicsSpace.
+     */
+    @Override
+    protected void removePhysics(PhysicsSpace space) {
+        super.removePhysics(space);
+
+        space.removeCollisionListener(this);
+        space.removeTickListener(this);
+    }
+
+    /**
+     * Serialize this control, for example when saving to a J3O file.
+     *
+     * @param ex the exporter (not null)
+     * @throws IOException from the exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        // isReady and collisionListeners not written
+        oc.write(ragdollMass, "ragdollMass", 1f);
+        oc.write(centerLocation, "centerLocation", null);
+        oc.write(centerVelocity, "centerVelocity", null);
+    }
+    // *************************************************************************
+    // PhysicsCollisionListener methods
+
+    /**
+     * For internal use only: callback for collision events.
+     *
+     * @param event (not null)
+     */
+    @Override
+    public void collision(PhysicsCollisionEvent event) {
+        if (event.getNodeA() == null && event.getNodeB() == null) {
+            return;
+        }
+        /*
+         * Determine which bone was involved (if any) and also the
+         * other collision object involved.
+         */
+        boolean isThisControlInvolved = false;
+        PhysicsLink physicsLink = null;
+        PhysicsCollisionObject otherPco = null;
+        PhysicsCollisionObject pcoA = event.getObjectA();
+        PhysicsCollisionObject pcoB = event.getObjectB();
+
+        Object userA = pcoA.getUserObject();
+        Object userB = pcoB.getUserObject();
+        if (userA instanceof PhysicsLink) {
+            physicsLink = (PhysicsLink) userA;
+            DacLinks control = physicsLink.getControl();
+            if (control == this) {
+                isThisControlInvolved = true;
+            }
+            otherPco = pcoB;
+        }
+        if (userB instanceof PhysicsLink) {
+            physicsLink = (PhysicsLink) userB;
+            DacLinks control = physicsLink.getControl();
+            if (control == this) {
+                isThisControlInvolved = true;
+            }
+            otherPco = pcoA;
+        }
+        /*
+         * Discard collisions that don't involve this control.
+         */
+        if (!isThisControlInvolved) {
+            return;
+        }
+        /*
+         * Discard low-impulse collisions.
+         */
+        float impulseThreshold = eventDispatchImpulseThreshold();
+        if (event.getAppliedImpulse() < impulseThreshold) {
+            return;
+        }
+        /*
+         * Dispatch an event.
+         */
+        for (RagdollCollisionListener listener : collisionListeners) {
+            listener.collide(physicsLink, otherPco, event);
+        }
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Begin blending the descendents of the specified link to the specified
+     * kinematic submode. Note: recursive!
+     *
+     * @param rootLink the root of the subtree to blend (not null)
+     * @param submode an enum value (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    private void blendDescendants(PhysicsLink rootLink,
+            KinematicSubmode submode, float blendInterval) {
+        assert rootLink != null;
+        assert submode != null;
+        assert blendInterval >= 0f : blendInterval;
+
+        PhysicsLink[] children = rootLink.listChildren();
+        for (PhysicsLink child : children) {
+            if (child instanceof BoneLink) {
+                BoneLink boneLink = (BoneLink) child;
+                boneLink.blendToKinematicMode(submode, blendInterval);
+            }
+            blendDescendants(child, submode, blendInterval);
+        }
+    }
+
+    /**
+     * Begin blending the specified link and all its descendants to the
+     * specified kinematic submode.
+     *
+     * @param rootLink the root of the subtree to blend (not null)
+     * @param submode the desired submode (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    private void blendSubtree(PhysicsLink rootLink, KinematicSubmode submode,
+            float blendInterval) {
+        assert rootLink != null;
+        assert submode != null;
+        assert blendInterval >= 0f : blendInterval;
+
+        blendDescendants(rootLink, submode, blendInterval);
+
+        if (rootLink == getTorsoLink()) {
+            getTorsoLink().blendToKinematicMode(submode, blendInterval, null);
+        } else if (rootLink instanceof BoneLink) {
+            BoneLink boneLink = (BoneLink) rootLink;
+            boneLink.blendToKinematicMode(submode, blendInterval);
+        }
+    }
+
+    /**
+     * Recalculate the total mass of the ragdoll, not including released
+     * attachments. Also updates the location and estimated velocity of the
+     * center of mass.
+     */
+    private void recalculateCenter() {
+        double massSum = 0.0;
+        Vector3f locationSum = new Vector3f();
+        Vector3f velocitySum = new Vector3f();
+        Vector3f tmpVector = new Vector3f();
+        List<PhysicsLink> links = listLinks(PhysicsLink.class);
+        for (PhysicsLink link : links) {
+            if (!link.isReleased()) {
+                PhysicsRigidBody rigidBody = link.getRigidBody();
+                float mass = rigidBody.getMass();
+                massSum += mass;
+
+                rigidBody.getPhysicsLocation(tmpVector);
+                tmpVector.multLocal(mass);
+                locationSum.addLocal(tmpVector);
+
+                link.velocity(tmpVector);
+                tmpVector.multLocal(mass);
+                velocitySum.addLocal(tmpVector);
+            }
+        }
+
+        float invMass = (float) (1.0 / massSum);
+        locationSum.mult(invMass, centerLocation);
+        velocitySum.mult(invMass, centerVelocity);
+        ragdollMass = (float) massSum;
+    }
+}

+ 50 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/KinematicSubmode.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+/**
+ * Enumerate submodes for a link in kinematic mode.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ */
+public enum KinematicSubmode {
+    /**
+     * driven by animation (if any)
+     */
+    Animated,
+    /**
+     * frozen in the transform it had when blending started
+     */
+    Frozen;
+}

+ 643 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/PhysicsLink.java

@@ -0,0 +1,643 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Joint;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.joints.PhysicsJoint;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The abstract base class used by DynamicAnimControl to link pieces of a JME
+ * model to their corresponding collision objects in a ragdoll. Subclasses
+ * include: AttachmentLink, BoneLink, and TorsoLink. The links in each
+ * DynamicAnimControl form a hierarchy with the TorsoLink at its root.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+abstract public class PhysicsLink
+        implements JmeCloneable, Savable {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger
+            = Logger.getLogger(PhysicsLink.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * immediate children in the link hierarchy (not null)
+     */
+    private ArrayList<PhysicsLink> children = new ArrayList<>(8);
+    /**
+     * corresponding bone in the skeleton (not null)
+     */
+    private Joint bone;
+    /**
+     * scene-graph control that manages this link (not null)
+     */
+    private DacLinks control;
+    /**
+     * duration of the most recent blend interval (in seconds, &ge;0)
+     */
+    private float blendInterval = 1f;
+    /**
+     * weighting of kinematic movement (&ge;0, &le;1, 0=purely dynamic, 1=purely
+     * kinematic, default=1, progresses from 0 to 1 during the blend interval)
+     */
+    private float kinematicWeight = 1f;
+    /**
+     * joint between the rigid body and the parent's rigid body, or null if not
+     * yet created
+     */
+    private PhysicsJoint joint = null;
+    /**
+     * parent/manager in the link hierarchy, or null if none
+     */
+    private PhysicsLink parent = null;
+    /**
+     * linked rigid body in the ragdoll (not null)
+     */
+    private PhysicsRigidBody rigidBody;
+    /**
+     * transform of the rigid body as of the most recent update (in
+     * physics-space coordinates, updated in kinematic mode only)
+     */
+    private Transform kpTransform = new Transform();
+    /**
+     * estimate of the body's linear velocity as of the most recent update
+     * (psu/sec in physics-space coordinates, kinematic mode only)
+     */
+    private Vector3f kpVelocity = new Vector3f();
+    /**
+     * location of the rigid body's center (in the skeleton bone's local
+     * coordinates)
+     */
+    private Vector3f localOffset;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * No-argument constructor needed by SavableClassUtil. Do not invoke
+     * directly!
+     */
+    public PhysicsLink() {
+    }
+
+    /**
+     * Instantiate a purely kinematic link between the specified skeleton bone
+     * and the specified rigid body.
+     *
+     * @param control the control that will manage this link (not null, alias
+     * created)
+     * @param bone the corresponding bone (not null, alias created)
+     * @param collisionShape the desired shape (not null, alias created)
+     * @param mass the mass (in physics-mass units)
+     * @param localOffset the location of the body's center (in the bone's local
+     * coordinates, not null, unaffected)
+     */
+    PhysicsLink(DacLinks control, Joint bone, CollisionShape collisionShape,
+            float mass, Vector3f localOffset) {
+        assert control != null;
+        assert bone != null;
+        assert collisionShape != null;
+        assert localOffset != null;
+
+        this.control = control;
+        this.bone = bone;
+        rigidBody = createRigidBody(mass, collisionShape);
+
+        logger.log(Level.FINE, "Creating link for bone {0} with mass={1}",
+                new Object[]{bone.getName(), rigidBody.getMass()});
+
+        this.localOffset = localOffset.clone();
+        updateKPTransform();
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Read the name of the corresponding bone.
+     *
+     * @return the bone name (not null)
+     */
+    public String boneName() {
+        String boneName = bone.getName();
+
+        assert boneName != null;
+        return boneName;
+    }
+
+    /**
+     * Count this link's immediate children in the link hierarchy.
+     *
+     * @return the count (&ge;0)
+     */
+    public int countChildren() {
+        int numChildren = children.size();
+        return numChildren;
+    }
+
+    /**
+     * Access the corresponding bone.
+     *
+     * @return the pre-existing instance (not null)
+     */
+    final public Joint getBone() {
+        assert bone != null;
+        return bone;
+    }
+
+    /**
+     * Access the control that manages this link.
+     *
+     * @return the pre-existing instance (not null)
+     */
+    public DacLinks getControl() {
+        assert control != null;
+        return control;
+    }
+
+    /**
+     * Access the joint between this link's rigid body and that of its parent.
+     *
+     * @return the pre-existing instance, or null for the torso
+     */
+    public PhysicsJoint getJoint() {
+        return joint;
+    }
+
+    /**
+     * Access this link's parent/manager in the link hierarchy.
+     *
+     * @return the link, or null if none
+     */
+    public PhysicsLink getParent() {
+        return parent;
+    }
+
+    /**
+     * Access the linked rigid body.
+     *
+     * @return the pre-existing instance (not null)
+     */
+    public PhysicsRigidBody getRigidBody() {
+        assert rigidBody != null;
+        return rigidBody;
+    }
+
+    /**
+     * Test whether the link is in kinematic mode.
+     *
+     * @return true if kinematic, or false if purely dynamic
+     */
+    public boolean isKinematic() {
+        if (kinematicWeight > 0f) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Test whether the attached model (if any) has been released.
+     *
+     * @return false unless this is an AttachmentLink
+     */
+    public boolean isReleased() {
+        return false;
+    }
+
+    /**
+     * Read the kinematic weight of this link.
+     *
+     * @return 0 if purely dynamic, 1 if purely kinematic
+     */
+    public float kinematicWeight() {
+        assert kinematicWeight >= 0f : kinematicWeight;
+        assert kinematicWeight <= 1f : kinematicWeight;
+        return kinematicWeight;
+    }
+
+    /**
+     * Enumerate this link's immediate children in the link hierarchy.
+     *
+     * @return a new array (not null)
+     */
+    public PhysicsLink[] listChildren() {
+        int numChildren = children.size();
+        PhysicsLink[] result = new PhysicsLink[numChildren];
+        children.toArray(result);
+
+        return result;
+    }
+
+    /**
+     * Unambiguously identify this link by name, within its DynamicAnimControl.
+     *
+     * @return a text string (not null, not empty)
+     */
+    abstract public String name();
+
+    /**
+     * Calculate a physics transform for the rigid body (to match the skeleton
+     * bone).
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the calculated transform (in physics-space coordinates, either
+     * storeResult or a new transform, not null)
+     */
+    public Transform physicsTransform(Transform storeResult) {
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+        if (isKinematic()) {
+            result.set(kpTransform);
+        } else {
+            rigidBody.getPhysicsLocation(result.getTranslation());
+            rigidBody.getPhysicsRotation(result.getRotation());
+            result.setScale(rigidBody.getCollisionShape().getScale());
+        }
+
+        return result;
+    }
+
+    /**
+     * Copy animation data from the specified link, which must correspond to the
+     * same bone.
+     *
+     * @param oldLink the link to copy from (not null, unaffected)
+     */
+    void postRebuild(PhysicsLink oldLink) {
+        assert oldLink != null;
+        assert oldLink.bone == bone;
+
+        if (oldLink.isKinematic()) {
+            blendInterval = oldLink.blendInterval;
+            kinematicWeight = oldLink.kinematicWeight;
+        } else {
+            blendInterval = 0f;
+            kinematicWeight = 1f;
+        }
+    }
+
+    /**
+     * Internal callback, invoked just AFTER the physics is stepped.
+     */
+    void postTick() {
+        if (!isKinematic()) {
+            rigidBody.activate();
+        }
+    }
+
+    /**
+     * Internal callback, invoked just BEFORE the physics is stepped.
+     *
+     * @param timeStep the physics time step (in seconds, &ge;0)
+     */
+    void preTick(float timeStep) {
+        if (isKinematic()) {
+            rigidBody.setPhysicsLocation(kpTransform.getTranslation());
+            rigidBody.setPhysicsRotation(kpTransform.getRotation());
+            rigidBody.getCollisionShape().setScale(kpTransform.getScale());
+        }
+    }
+
+    /**
+     * Immediately put this link into dynamic mode. The control must be "ready".
+     *
+     * @param uniformAcceleration the uniform acceleration vector to apply (in
+     * physics-space coordinates, not null, unaffected)
+     */
+    public void setDynamic(Vector3f uniformAcceleration) {
+        control.verifyReadyForDynamicMode("put link into dynamic mode");
+
+        setKinematicWeight(0f);
+        rigidBody.setGravity(uniformAcceleration);
+    }
+
+    /**
+     * Internal callback, invoked once per frame during the logical-state
+     * update, provided the control is added to a scene.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    void update(float tpf) {
+        assert tpf >= 0f : tpf;
+
+        if (kinematicWeight > 0f) {
+            kinematicUpdate(tpf);
+        } else {
+            dynamicUpdate();
+        }
+    }
+
+    /**
+     * Copy the body's linear velocity, or an estimate thereof.
+     *
+     * @param storeResult (modified if not null)
+     * @return a new velocity vector (psu/sec in physics-space coordinates)
+     */
+    Vector3f velocity(Vector3f storeResult) {
+        Vector3f result = (storeResult == null) ? new Vector3f() : storeResult;
+        if (isKinematic()) {
+            result.set(kpVelocity);
+        } else {
+            assert !rigidBody.isKinematic();
+            rigidBody.getLinearVelocity(result);
+        }
+
+        return result;
+    }
+    // *************************************************************************
+    // new protected methods
+
+    /**
+     * Begin blending this link to a purely kinematic mode.
+     *
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     */
+    protected void blendToKinematicMode(float blendInterval) {
+        assert blendInterval >= 0f : blendInterval;
+
+        this.blendInterval = blendInterval;
+        setKinematicWeight(Float.MIN_VALUE); // non-zero to trigger blending
+    }
+
+    /**
+     * Update this link in Dynamic mode, setting the linked bone's transform
+     * based on the transform of the rigid body.
+     */
+    abstract protected void dynamicUpdate();
+
+    /**
+     * Update this link in blended Kinematic mode.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    protected void kinematicUpdate(float tpf) {
+        assert tpf >= 0f : tpf;
+        assert rigidBody.isKinematic();
+        /*
+         * If blending, increase the kinematic weight.
+         */
+        if (blendInterval == 0f) {
+            setKinematicWeight(1f); // done blending
+        } else {
+            setKinematicWeight(kinematicWeight + tpf / blendInterval);
+        }
+        /*
+         * If we didn't need kpVelocity, we could defer this
+         * calculation until the preTick().
+         */
+        Vector3f previousLocation = kpTransform.getTranslation(null);
+        updateKPTransform();
+        if (tpf > 0f) {
+            kpTransform.getTranslation().subtract(previousLocation, kpVelocity);
+            kpVelocity.divideLocal(tpf);
+        }
+    }
+
+    /**
+     * Copy the local offset of this link.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the offset (in bone local coordinates, either storeResult or a
+     * new vector, not null)
+     */
+    protected Vector3f localOffset(Vector3f storeResult) {
+        Vector3f result = (storeResult == null) ? new Vector3f() : storeResult;
+        result.set(localOffset);
+        return result;
+    }
+
+    /**
+     * Assign a physics joint to this link, or cancel the assigned joint.
+     *
+     * @param joint (may be null, alias created)
+     */
+    final protected void setJoint(PhysicsJoint joint) {
+        this.joint = joint;
+    }
+
+    /**
+     * Assign a parent/manager for this link.
+     *
+     * @param parent (not null, alias created)
+     */
+    final protected void setParent(PhysicsLink parent) {
+        assert parent != null;
+        assert this.parent == null;
+        this.parent = parent;
+        parent.children.add(this);
+    }
+
+    /**
+     * Alter the rigid body for this link.
+     *
+     * @param body the desired rigid body (not null, alias created)
+     */
+    protected void setRigidBody(PhysicsRigidBody body) {
+        assert body != null;
+        assert rigidBody != null;
+        rigidBody = body;
+    }
+    // *************************************************************************
+    // JmeCloneable methods
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned link into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this link (not null)
+     * @param original the instance from which this link was shallow-cloned
+     * (unused)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        bone = cloner.clone(bone);
+        control = cloner.clone(control);
+        children = cloner.clone(children);
+        joint = cloner.clone(joint);
+        parent = cloner.clone(parent);
+        rigidBody = cloner.clone(rigidBody);
+        kpTransform = cloner.clone(kpTransform);
+        kpVelocity = cloner.clone(kpVelocity);
+        localOffset = cloner.clone(localOffset);
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public PhysicsLink jmeClone() {
+        try {
+            PhysicsLink clone = (PhysicsLink) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+    // *************************************************************************
+    // Savable methods
+
+    /**
+     * De-serialize this link, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+
+        children = ic.readSavableArrayList("children", new ArrayList(1));
+        bone = (Joint) ic.readSavable("bone", null);
+        control = (DacLinks) ic.readSavable("control", null);
+        blendInterval = ic.readFloat("blendInterval", 1f);
+        kinematicWeight = ic.readFloat("kinematicWeight", 1f);
+        joint = (PhysicsJoint) ic.readSavable("joint", null);
+        parent = (PhysicsLink) ic.readSavable("parent", null);
+        rigidBody = (PhysicsRigidBody) ic.readSavable("rigidBody", null);
+        kpTransform
+                = (Transform) ic.readSavable("kpTransform", new Transform());
+        kpVelocity = (Vector3f) ic.readSavable("kpVelocity", new Vector3f());
+        localOffset = (Vector3f) ic.readSavable("offset", new Vector3f());
+    }
+
+    /**
+     * Serialize this link, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.writeSavableArrayList(children, "children", null);
+        oc.write(bone, "bone", null);
+        oc.write(control, "control", null);
+        oc.write(blendInterval, "blendInterval", 1f);
+        oc.write(kinematicWeight, "kinematicWeight", 1f);
+        oc.write(joint, "joint", null);
+        oc.write(parent, "parent", null);
+        oc.write(rigidBody, "rigidBody", null);
+        oc.write(kpTransform, "kpTransform", null);
+        oc.write(kpVelocity, "kpVelocity", null);
+        oc.write(localOffset, "offset", null);
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Create and configure a rigid body for this link.
+     *
+     * @param linkConfig the link configuration (not null)
+     * @param collisionShape the desired shape (not null, alias created)
+     * @return a new instance, not in any PhysicsSpace
+     */
+    private PhysicsRigidBody createRigidBody(float mass,
+            CollisionShape collisionShape) {
+        assert collisionShape != null;
+
+        PhysicsRigidBody body = new PhysicsRigidBody(collisionShape, mass);
+
+        float viscousDamping = control.damping();
+        body.setDamping(viscousDamping, viscousDamping);
+
+        body.setKinematic(true);
+        body.setUserObject(this);
+
+        return body;
+    }
+
+    /**
+     * Alter the kinematic weight and copy the physics transform and velocity
+     * info as needed.
+     *
+     * @param weight (&ge;0)
+     */
+    private void setKinematicWeight(float weight) {
+        assert weight >= 0f : weight;
+
+        boolean wasKinematic = (kinematicWeight > 0f);
+        kinematicWeight = (weight > 1f) ? 1f : weight;
+        boolean isKinematic = (kinematicWeight > 0f);
+
+        if (wasKinematic && !isKinematic) {
+            rigidBody.setKinematic(false);
+            rigidBody.setPhysicsLocation(kpTransform.getTranslation());
+            rigidBody.setPhysicsRotation(kpTransform.getRotation());
+            rigidBody.getCollisionShape().setScale(kpTransform.getScale());
+            rigidBody.setLinearVelocity(kpVelocity);
+        } else if (isKinematic && !wasKinematic) {
+            rigidBody.getPhysicsLocation(kpTransform.getTranslation());
+            rigidBody.getPhysicsRotation(kpTransform.getRotation());
+            Vector3f scale = rigidBody.getCollisionShape().getScale();
+            kpTransform.setScale(scale);
+            rigidBody.getLinearVelocity(kpVelocity);
+            rigidBody.setKinematic(true);
+        }
+    }
+
+    /**
+     * Update the kinematic physics transform.
+     */
+    private void updateKPTransform() {
+        control.physicsTransform(bone, localOffset, kpTransform);
+    }
+}

+ 707 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/RagUtils.java

@@ -0,0 +1,707 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Armature;
+import com.jme3.anim.Joint;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.control.Control;
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Logger;
+
+/**
+ * Utility methods used by DynamicAnimControl and associated classes.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class RagUtils {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final private static Logger logger
+            = Logger.getLogger(RagUtils.class.getName());
+    // *************************************************************************
+    // constructors
+
+    /**
+     * A private constructor to inhibit instantiation of this class.
+     */
+    private RagUtils() {
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Assign each mesh vertex to a bone/torso link and add its location (mesh
+     * coordinates in bind pose) to that link's list.
+     *
+     * @param meshes array of animated meshes to use (not null, unaffected)
+     * @param managerMap a map from bone indices to managing link names (not
+     * null, unaffected)
+     * @return a new map from bone/torso names to sets of vertex coordinates
+     */
+    static Map<String, VectorSet> coordsMap(Mesh[] meshes,
+            String[] managerMap) {
+        float[] wArray = new float[4];
+        int[] iArray = new int[4];
+        Vector3f bindPosition = new Vector3f();
+        Map<String, VectorSet> coordsMap = new HashMap<>(32);
+        for (Mesh mesh : meshes) {
+            int numVertices = mesh.getVertexCount();
+            for (int vertexI = 0; vertexI < numVertices; ++vertexI) {
+                String managerName = findManager(mesh, vertexI, iArray, wArray,
+                        managerMap);
+                VectorSet set = coordsMap.get(managerName);
+                if (set == null) {
+                    set = new VectorSet(1);
+                    coordsMap.put(managerName, set);
+                }
+                vertexVector3f(mesh, VertexBuffer.Type.BindPosePosition,
+                        vertexI, bindPosition);
+                set.add(bindPosition);
+            }
+        }
+
+        return coordsMap;
+    }
+
+    /**
+     * Find an animated geometry in the specified subtree of the scene graph.
+     * Note: recursive!
+     *
+     * @param subtree where to search (not null, unaffected)
+     * @return a pre-existing instance, or null if none
+     */
+    static Geometry findAnimatedGeometry(Spatial subtree) {
+        Geometry result = null;
+        if (subtree instanceof Geometry) {
+            Geometry geometry = (Geometry) subtree;
+            Mesh mesh = geometry.getMesh();
+            VertexBuffer indices = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+            boolean hasIndices = indices != null;
+            VertexBuffer weights = mesh.getBuffer(VertexBuffer.Type.BoneWeight);
+            boolean hasWeights = weights != null;
+            if (hasIndices && hasWeights) {
+                result = geometry;
+            }
+
+        } else if (subtree instanceof Node) {
+            Node node = (Node) subtree;
+            List<Spatial> children = node.getChildren();
+            for (Spatial child : children) {
+                result = findAnimatedGeometry(child);
+                if (result != null) {
+                    break;
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Find the index of the specified scene-graph control in the specified
+     * spatial.
+     *
+     * @param spatial the spatial to search (not null, unaffected)
+     * @param sgc the control to search for (not null, unaffected)
+     * @return the index (&ge;0) or -1 if not found
+     */
+    static int findIndex(Spatial spatial, Control sgc) {
+        int numControls = spatial.getNumControls();
+        int result = -1;
+        for (int controlIndex = 0; controlIndex < numControls; ++controlIndex) {
+            Control control = spatial.getControl(controlIndex);
+            if (control == sgc) {
+                result = controlIndex;
+                break;
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Find the main root bone of a skeleton, based on its total bone weight.
+     *
+     * @param skeleton the skeleton (not null, unaffected)
+     * @param targetMeshes an array of animated meshes to provide bone weights
+     * (not null)
+     * @return a root bone, or null if none found
+     */
+    static Joint findMainBone(Armature skeleton, Mesh[] targetMeshes) {
+        Joint[] rootBones = skeleton.getRoots();
+
+        Joint result;
+        if (rootBones.length == 1) {
+            result = rootBones[0];
+        } else {
+            result = null;
+            float[] totalWeights = totalWeights(targetMeshes, skeleton);
+            float greatestTotalWeight = Float.NEGATIVE_INFINITY;
+            for (Joint rootBone : rootBones) {
+                int boneIndex = skeleton.getJointIndex(rootBone);
+                float weight = totalWeights[boneIndex];
+                if (weight > greatestTotalWeight) {
+                    result = rootBone;
+                    greatestTotalWeight = weight;
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Enumerate all animated meshes in the specified subtree of a scene graph.
+     * Note: recursive!
+     *
+     * @param subtree which subtree (aliases created)
+     * @param storeResult (added to if not null)
+     * @return an expanded list (either storeResult or a new instance)
+     */
+    static List<Mesh> listAnimatedMeshes(Spatial subtree,
+            List<Mesh> storeResult) {
+        if (storeResult == null) {
+            storeResult = new ArrayList<>(10);
+        }
+
+        if (subtree instanceof Geometry) {
+            Geometry geometry = (Geometry) subtree;
+            Mesh mesh = geometry.getMesh();
+            VertexBuffer indices = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+            boolean hasIndices = indices != null;
+            VertexBuffer weights = mesh.getBuffer(VertexBuffer.Type.BoneWeight);
+            boolean hasWeights = weights != null;
+            if (hasIndices && hasWeights && !storeResult.contains(mesh)) {
+                storeResult.add(mesh);
+            }
+
+        } else if (subtree instanceof Node) {
+            Node node = (Node) subtree;
+            List<Spatial> children = node.getChildren();
+            for (Spatial child : children) {
+                listAnimatedMeshes(child, storeResult);
+            }
+        }
+
+        return storeResult;
+    }
+
+    /**
+     * Convert a transform from the mesh coordinate system to the local
+     * coordinate system of the specified bone.
+     *
+     * @param parentBone (not null)
+     * @param transform the transform to convert (not null, modified)
+     */
+    static void meshToLocal(Joint parentBone, Transform transform) {
+        Vector3f location = transform.getTranslation();
+        Quaternion orientation = transform.getRotation();
+        Vector3f scale = transform.getScale();
+
+        Transform pmx = parentBone.getModelTransform();
+        Vector3f pmTranslate = pmx.getTranslation();
+        Quaternion pmRotInv = pmx.getRotation().inverse();
+        Vector3f pmScale = pmx.getScale();
+
+        location.subtractLocal(pmTranslate);
+        location.divideLocal(pmScale);
+        pmRotInv.mult(location, location);
+        scale.divideLocal(pmScale);
+        pmRotInv.mult(orientation, orientation);
+    }
+
+    /**
+     * Read an array of transforms from an input capsule.
+     *
+     * @param capsule the input capsule (not null)
+     * @param fieldName the name of the field to read (not null)
+     * @return a new array or null
+     * @throws IOException from capsule
+     */
+    static Transform[] readTransformArray(InputCapsule capsule,
+            String fieldName) throws IOException {
+        Savable[] tmp = capsule.readSavableArray(fieldName, null);
+        Transform[] result;
+        if (tmp == null) {
+            result = null;
+        } else {
+            result = new Transform[tmp.length];
+            for (int i = 0; i < tmp.length; ++i) {
+                result[i] = (Transform) tmp[i];
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Calculate a coordinate transform for the specified spatial relative to a
+     * specified ancestor node. The result incorporates the transform of the
+     * starting spatial, but not that of the ancestor.
+     *
+     * @param startSpatial the starting spatial (not null, unaffected)
+     * @param ancestorNode the ancestor node (not null, unaffected)
+     * @param storeResult storage for the result (modified if not null)
+     * @return a coordinate transform (either storeResult or a new vector, not
+     * null)
+     */
+    static Transform relativeTransform(Spatial startSpatial,
+            Node ancestorNode, Transform storeResult) {
+        assert startSpatial.hasAncestor(ancestorNode);
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+
+        result.loadIdentity();
+        Spatial loopSpatial = startSpatial;
+        while (loopSpatial != ancestorNode) {
+            Transform localTransform = loopSpatial.getLocalTransform();
+            result.combineWithParent(localTransform);
+            loopSpatial = loopSpatial.getParent();
+        }
+
+        return result;
+    }
+
+    /**
+     * Validate a skeleton for use with DynamicAnimControl.
+     *
+     * @param skeleton the skeleton to validate (not null, unaffected)
+     */
+    static void validate(Armature skeleton) {
+        int numBones = skeleton.getJointCount();
+        if (numBones < 0) {
+            throw new IllegalArgumentException("Bone count is negative!");
+        }
+
+        Set<String> nameSet = new TreeSet<>();
+        for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
+            Joint bone = skeleton.getJoint(boneIndex);
+            if (bone == null) {
+                String msg = String.format("Bone %d in skeleton is null!",
+                        boneIndex);
+                throw new IllegalArgumentException(msg);
+            }
+            String boneName = bone.getName();
+            if (boneName == null) {
+                String msg = String.format("Bone %d in skeleton has null name!",
+                        boneIndex);
+                throw new IllegalArgumentException(msg);
+            } else if (boneName.equals(DynamicAnimControl.torsoName)) {
+                String msg = String.format(
+                        "Bone %d in skeleton has a reserved name!",
+                        boneIndex);
+                throw new IllegalArgumentException(msg);
+            } else if (nameSet.contains(boneName)) {
+                String msg = "Duplicate bone name in skeleton: " + boneName;
+                throw new IllegalArgumentException(msg);
+            }
+            nameSet.add(boneName);
+        }
+    }
+
+    /**
+     * Validate a model for use with DynamicAnimControl.
+     *
+     * @param model the model to validate (not null, unaffected)
+     */
+    static void validate(Spatial model) {
+        List<Geometry> geometries = listGeometries(model, null);
+        if (geometries.isEmpty()) {
+            throw new IllegalArgumentException("No meshes in the model.");
+        }
+        for (Geometry geometry : geometries) {
+            if (geometry.isIgnoreTransform()) {
+                throw new IllegalArgumentException(
+                        "A model geometry ignores transforms.");
+            }
+        }
+    }
+    // *************************************************************************
+    // private methods
+
+    private static void addPreOrderJoints(Joint bone, List<Joint> addResult) {
+        assert bone != null;
+        addResult.add(bone);
+        List<Joint> children = bone.getChildren();
+        for (Joint child : children) {
+            addPreOrderJoints(child, addResult);
+        }
+    }
+
+    /**
+     * Add the vertex weights of each bone in the specified mesh to an array of
+     * total weights.
+     *
+     * @param mesh animated mesh to analyze (not null, unaffected)
+     * @param totalWeights (not null, modified)
+     */
+    private static void addWeights(Mesh mesh, float[] totalWeights) {
+        assert totalWeights != null;
+
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+        if (maxWeightsPerVert <= 0) {
+            maxWeightsPerVert = 1;
+        }
+        assert maxWeightsPerVert > 0 : maxWeightsPerVert;
+        assert maxWeightsPerVert <= 4 : maxWeightsPerVert;
+
+        VertexBuffer biBuf = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+        Buffer boneIndexBuffer = biBuf.getDataReadOnly();
+        boneIndexBuffer.rewind();
+        int numBoneIndices = boneIndexBuffer.remaining();
+        assert numBoneIndices % 4 == 0 : numBoneIndices;
+        int numVertices = boneIndexBuffer.remaining() / 4;
+
+        VertexBuffer wBuf = mesh.getBuffer(VertexBuffer.Type.BoneWeight);
+        FloatBuffer weightBuffer = (FloatBuffer) wBuf.getDataReadOnly();
+        weightBuffer.rewind();
+        int numWeights = weightBuffer.remaining();
+        assert numWeights == numVertices * 4 : numWeights;
+
+        for (int vIndex = 0; vIndex < numVertices; ++vIndex) {
+            for (int wIndex = 0; wIndex < 4; ++wIndex) {
+                float weight = weightBuffer.get();
+                int boneIndex = readIndex(boneIndexBuffer);
+                if (wIndex < maxWeightsPerVert) {
+                    totalWeights[boneIndex] += FastMath.abs(weight);
+                }
+            }
+        }
+    }
+
+    /**
+     * Determine which physics link should manage the specified mesh vertex.
+     *
+     * @param mesh the mesh containing the vertex (not null, unaffected)
+     * @param vertexIndex the vertex index in the mesh (&ge;0)
+     * @param iArray temporary storage for bone indices (not null, modified)
+     * @param wArray temporary storage for bone weights (not null, modified)
+     * @param managerMap a map from bone indices to bone/torso names (not null,
+     * unaffected)
+     * @return a bone/torso name
+     */
+    private static String findManager(Mesh mesh, int vertexIndex, int[] iArray,
+            float[] wArray, String[] managerMap) {
+        vertexBoneIndices(mesh, vertexIndex, iArray);
+        vertexBoneWeights(mesh, vertexIndex, wArray);
+        Map<String, Float> weightMap = weightMap(iArray, wArray, managerMap);
+
+        float bestTotalWeight = Float.NEGATIVE_INFINITY;
+        String bestName = null;
+        for (Map.Entry<String, Float> entry : weightMap.entrySet()) {
+            float totalWeight = entry.getValue();
+            if (totalWeight >= bestTotalWeight) {
+                bestTotalWeight = totalWeight;
+                bestName = entry.getKey();
+            }
+        }
+
+        return bestName;
+    }
+
+    /**
+     * Enumerate all geometries in the specified subtree of a scene graph. Note:
+     * recursive!
+     *
+     * @param subtree (not null, aliases created)
+     * @param addResult (added to if not null)
+     * @return an expanded list (either storeResult or a new instance)
+     */
+    private static List<Geometry> listGeometries(Spatial subtree,
+            List<Geometry> addResult) {
+        List<Geometry> result = (addResult == null) ? new ArrayList<Geometry>(50) : addResult;
+
+        if (subtree instanceof Geometry) {
+            Geometry geometry = (Geometry) subtree;
+            if (!result.contains(geometry)) {
+                result.add(geometry);
+            }
+        }
+
+        if (subtree instanceof Node) {
+            Node node = (Node) subtree;
+            List<Spatial> children = node.getChildren();
+            for (Spatial child : children) {
+                listGeometries(child, result);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Enumerate all bones in a pre-order, depth-first traversal of the
+     * skeleton, such that child bones never precede their ancestors.
+     *
+     * @param skeleton the skeleton to traverse (not null, unaffected)
+     * @return a new list of bones
+     */
+    private static List<Joint> preOrderJoints(Armature skeleton) {
+        int numBones = skeleton.getJointCount();
+        List<Joint> result = new ArrayList<>(numBones);
+        Joint[] rootBones = skeleton.getRoots();
+        for (Joint rootBone : rootBones) {
+            addPreOrderJoints(rootBone, result);
+        }
+
+        assert result.size() == numBones : result.size();
+        return result;
+    }
+
+    /**
+     * Read an index from a buffer.
+     *
+     * @param buffer a buffer of bytes or shorts (not null)
+     * @return index (&ge;0)
+     */
+    private static int readIndex(Buffer buffer) {
+        int result;
+        if (buffer instanceof ByteBuffer) {
+            ByteBuffer byteBuffer = (ByteBuffer) buffer;
+            byte b = byteBuffer.get();
+            result = 0xff & b;
+        } else if (buffer instanceof ShortBuffer) {
+            ShortBuffer shortBuffer = (ShortBuffer) buffer;
+            short s = shortBuffer.get();
+            result = 0xffff & s;
+        } else {
+            throw new IllegalArgumentException();
+        }
+
+        assert result >= 0 : result;
+        return result;
+    }
+
+    /**
+     * Calculate the total bone weight animated by each bone in the specified
+     * meshes.
+     *
+     * @param meshes the animated meshes to analyze (not null, unaffected)
+     * @param skeleton (not null, unaffected)
+     * @return a map from bone indices to total bone weight
+     */
+    private static float[] totalWeights(Mesh[] meshes, Armature skeleton) {
+        int numBones = skeleton.getJointCount();
+        float[] result = new float[numBones];
+        for (Mesh mesh : meshes) {
+            RagUtils.addWeights(mesh, result);
+        }
+
+        List<Joint> bones = preOrderJoints(skeleton);
+        Collections.reverse(bones);
+        for (Joint childBone : bones) {
+            int childIndex = skeleton.getJointIndex(childBone);
+            Joint parent = childBone.getParent();
+            if (parent != null) {
+                int parentIndex = skeleton.getJointIndex(parent);
+                result[parentIndex] += result[childIndex];
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Copy the bone indices for the indexed vertex.
+     *
+     * @param mesh subject mesh (not null)
+     * @param vertexIndex index into the mesh's vertices (&ge;0)
+     * @param storeResult (modified if not null)
+     * @return the data vector (either storeResult or a new instance)
+     */
+    private static int[] vertexBoneIndices(Mesh mesh,
+            int vertexIndex, int[] storeResult) {
+        if (storeResult == null) {
+            storeResult = new int[4];
+        } else {
+            assert storeResult.length >= 4 : storeResult.length;
+        }
+
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+        if (maxWeightsPerVert <= 0) {
+            maxWeightsPerVert = 1;
+        }
+
+        VertexBuffer biBuf = mesh.getBuffer(VertexBuffer.Type.BoneIndex);
+        Buffer boneIndexBuffer = biBuf.getDataReadOnly();
+        boneIndexBuffer.position(4 * vertexIndex);
+        for (int wIndex = 0; wIndex < maxWeightsPerVert; ++wIndex) {
+            int boneIndex = readIndex(boneIndexBuffer);
+            storeResult[wIndex] = boneIndex;
+        }
+        /*
+         * Fill with -1s.
+         */
+        int length = storeResult.length;
+        for (int wIndex = maxWeightsPerVert; wIndex < length; ++wIndex) {
+            storeResult[wIndex] = -1;
+        }
+
+        return storeResult;
+    }
+
+    /**
+     * Copy the bone weights for the indexed vertex.
+     *
+     * @param mesh subject mesh (not null)
+     * @param vertexIndex index into the mesh's vertices (&ge;0)
+     * @param storeResult (modified if not null)
+     * @return the data vector (either storeResult or a new instance)
+     */
+    private static float[] vertexBoneWeights(Mesh mesh,
+            int vertexIndex, float[] storeResult) {
+        if (storeResult == null) {
+            storeResult = new float[4];
+        } else {
+            assert storeResult.length >= 4 : storeResult.length;
+        }
+
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+        if (maxWeightsPerVert <= 0) {
+            maxWeightsPerVert = 1;
+        }
+
+        VertexBuffer wBuf = mesh.getBuffer(VertexBuffer.Type.BoneWeight);
+        FloatBuffer weightBuffer = (FloatBuffer) wBuf.getDataReadOnly();
+        weightBuffer.position(4 * vertexIndex);
+        for (int wIndex = 0; wIndex < maxWeightsPerVert; ++wIndex) {
+            storeResult[wIndex] = weightBuffer.get();
+        }
+        /*
+         * Fill with 0s.
+         */
+        int length = storeResult.length;
+        for (int wIndex = maxWeightsPerVert; wIndex < length; ++wIndex) {
+            storeResult[wIndex] = 0f;
+        }
+
+        return storeResult;
+    }
+
+    /**
+     * Copy Vector3f data for the indexed vertex from the specified vertex
+     * buffer.
+     * <p>
+     * A software skin update is required BEFORE reading vertex
+     * positions/normals/tangents from an animated mesh
+     *
+     * @param mesh subject mesh (not null)
+     * @param bufferType which buffer to read (5 legal values)
+     * @param vertexIndex index into the mesh's vertices (&ge;0)
+     * @param storeResult (modified if not null)
+     * @return the data vector (either storeResult or a new instance)
+     */
+    private static Vector3f vertexVector3f(Mesh mesh,
+            VertexBuffer.Type bufferType, int vertexIndex,
+            Vector3f storeResult) {
+        assert bufferType == VertexBuffer.Type.BindPoseNormal
+                || bufferType == VertexBuffer.Type.BindPosePosition
+                || bufferType == VertexBuffer.Type.Binormal
+                || bufferType == VertexBuffer.Type.Normal
+                || bufferType == VertexBuffer.Type.Position : bufferType;
+        if (storeResult == null) {
+            storeResult = new Vector3f();
+        }
+
+        VertexBuffer vertexBuffer = mesh.getBuffer(bufferType);
+        FloatBuffer floatBuffer = (FloatBuffer) vertexBuffer.getDataReadOnly();
+        floatBuffer.position(3 * vertexIndex);
+        storeResult.x = floatBuffer.get();
+        storeResult.y = floatBuffer.get();
+        storeResult.z = floatBuffer.get();
+
+        return storeResult;
+    }
+
+    /**
+     * Tabulate the total bone weight associated with each bone/torso link in a
+     * ragdoll.
+     *
+     * @param biArray the array of bone indices (not null, unaffected)
+     * @param bwArray the array of bone weights (not null, unaffected)
+     * @param managerMap a map from bone indices to managing link names (not
+     * null, unaffected)
+     * @return a new map from link names to total weight
+     */
+    private static Map<String, Float> weightMap(int[] biArray,
+            float[] bwArray, String[] managerMap) {
+        assert biArray.length == 4;
+        assert bwArray.length == 4;
+
+        Map<String, Float> weightMap = new HashMap<>(4);
+        for (int j = 0; j < 4; ++j) {
+            int boneIndex = biArray[j];
+            if (boneIndex != -1) {
+                String managerName = managerMap[boneIndex];
+                if (weightMap.containsKey(managerName)) {
+                    float oldWeight = weightMap.get(managerName);
+                    float newWeight = oldWeight + bwArray[j];
+                    weightMap.put(managerName, newWeight);
+                } else {
+                    weightMap.put(managerName, bwArray[j]);
+                }
+            }
+        }
+
+        return weightMap;
+    }
+}

+ 56 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/RagdollCollisionListener.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2009-2018 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 com.jme3.bullet.animation;
+
+import com.jme3.bullet.collision.PhysicsCollisionEvent;
+import com.jme3.bullet.collision.PhysicsCollisionObject;
+
+/**
+ * Interface to receive notifications whenever a linked rigid body in a
+ * DynamicAnimControl collides with another physics object.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Nehon
+ */
+public interface RagdollCollisionListener {
+
+    /**
+     * Invoked when a collision involving a linked rigid body occurs.
+     *
+     * @param physicsLink the physics link that collided (not null)
+     * @param object the collision object that collided with the bone (not null)
+     * @param event other event details (not null)
+     */
+    void collide(PhysicsLink physicsLink, PhysicsCollisionObject object,
+            PhysicsCollisionEvent event);
+}

+ 313 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/RangeOfMotion.java

@@ -0,0 +1,313 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+import com.jme3.bullet.PhysicsSpace;
+import com.jme3.bullet.joints.SixDofJoint;
+import com.jme3.bullet.joints.motors.RotationalLimitMotor;
+import com.jme3.bullet.joints.motors.TranslationalLimitMotor;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Vector3f;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Range of motion for a ragdoll joint. Immutable except for
+ * {@link #read(com.jme3.export.JmeImporter)}.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on RagdollPreset by Rémy Bouquet (Nehon).
+ */
+public class RangeOfMotion implements Savable {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger
+            = Logger.getLogger(RangeOfMotion.class.getName());
+    /**
+     * local copy of {@link com.jme3.math.Vector3f#ZERO}
+     */
+    final private static Vector3f translateIdentity = new Vector3f(0f, 0f, 0f);
+    // *************************************************************************
+    // fields
+
+    /**
+     * maximum rotation angle around the X axis (in radians)
+     */
+    private float maxX = 0f;
+    /**
+     * minimum rotation angle around the X axis (in radians)
+     */
+    private float minX = 0f;
+    /**
+     * maximum rotation angle around the Y axis (in radians)
+     */
+    private float maxY = 0f;
+    /**
+     * minimum rotation angle around the Y axis (in radians)
+     */
+    private float minY = 0f;
+    /**
+     * maximum rotation angle around the Z axis (in radians)
+     */
+    private float maxZ = 0f;
+    /**
+     * minimum rotation angle around the Z axis (in radians)
+     */
+    private float minZ = 0f;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate a preset with no motion allowed.
+     */
+    public RangeOfMotion() {
+    }
+
+    /**
+     * Instantiate a preset with the specified range of motion.
+     *
+     * @param maxX the maximum rotation around the X axis (in radians)
+     * @param minX the minimum rotation around the X axis (in radians)
+     * @param maxY the maximum rotation around the Y axis (in radians)
+     * @param minY the minimum rotation around the Y axis (in radians)
+     * @param maxZ the maximum rotation around the Z axis (in radians)
+     * @param minZ the minimum rotation around the Z axis (in radians)
+     */
+    public RangeOfMotion(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;
+    }
+
+    /**
+     * Instantiate a preset with the specified symmetric range of motion.
+     *
+     * @param maxX the maximum rotation around the X axis (in radians, &ge;0)
+     * @param maxY the maximum rotation around the Y axis (in radians, &ge;0)
+     * @param maxZ the maximum rotation around the Z axis (in radians, &ge;0)
+     */
+    public RangeOfMotion(float maxX, float maxY, float maxZ) {
+        this.maxX = maxX;
+        this.minX = -maxX;
+        this.maxY = maxY;
+        this.minY = -maxY;
+        this.maxZ = maxZ;
+        this.minZ = -maxZ;
+    }
+
+    /**
+     * Instantiate a preset with the specified symmetric range of motion.
+     *
+     * @param maxAngle the maximum rotation around each axis (in radians, &ge;0)
+     */
+    public RangeOfMotion(float maxAngle) {
+        maxX = maxAngle;
+        minX = -maxAngle;
+        maxY = maxAngle;
+        minY = -maxAngle;
+        maxZ = maxAngle;
+        minZ = -maxAngle;
+    }
+
+    /**
+     * Instantiate a preset for rotation on a single axis.
+     *
+     * @param axisIndex which axis: 0&rarr;X, 1&rarr;Y, 2&rarr;Z
+     */
+    public RangeOfMotion(int axisIndex) {
+        switch (axisIndex) {
+            case PhysicsSpace.AXIS_X:
+                maxX = 1f;
+                minX = -1f;
+                break;
+            case PhysicsSpace.AXIS_Y:
+                maxY = 1f;
+                minY = -1f;
+                break;
+            case PhysicsSpace.AXIS_Z:
+                maxZ = 1f;
+                minZ = -1f;
+                break;
+            default:
+                String msg = String.format("axisIndex=%d", axisIndex);
+                throw new IllegalArgumentException(msg);
+        }
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Read the maximum rotation around the indexed axis.
+     *
+     * @param axisIndex which axis: 0&rarr;X, 1&rarr;Y, 2&rarr;Z
+     *
+     * @return the rotation angle (in radians)
+     */
+    public float getMaxRotation(int axisIndex) {
+        float result;
+        switch (axisIndex) {
+            case PhysicsSpace.AXIS_X:
+                result = maxX;
+                break;
+            case PhysicsSpace.AXIS_Y:
+                result = maxY;
+                break;
+            case PhysicsSpace.AXIS_Z:
+                result = maxZ;
+                break;
+            default:
+                String msg = String.format("axisIndex=%d", axisIndex);
+                throw new IllegalArgumentException(msg);
+        }
+
+        return result;
+    }
+
+    /**
+     * Read the minimum rotation around the indexed axis.
+     *
+     * @param axisIndex which axis: 0&rarr;X, 1&rarr;Y, 2&rarr;Z
+     *
+     * @return the rotation angle (in radians)
+     */
+    public float getMinRotation(int axisIndex) {
+        float result;
+        switch (axisIndex) {
+            case PhysicsSpace.AXIS_X:
+                result = minX;
+                break;
+            case PhysicsSpace.AXIS_Y:
+                result = minY;
+                break;
+            case PhysicsSpace.AXIS_Z:
+                result = minZ;
+                break;
+            default:
+                String msg = String.format("axisIndex=%d", axisIndex);
+                throw new IllegalArgumentException(msg);
+        }
+
+        return result;
+    }
+
+    /**
+     * Apply this preset to the specified joint.
+     *
+     * @param joint where to apply this preset (not null, modified)
+     */
+    public void setupJoint(SixDofJoint joint) {
+        Vector3f lower = new Vector3f(minX, minY, minZ);
+        Vector3f upper = new Vector3f(maxX, maxY, maxZ);
+
+        RotationalLimitMotor rotX
+                = joint.getRotationalLimitMotor(PhysicsSpace.AXIS_X);
+        rotX.setLoLimit(lower.x);
+        rotX.setHiLimit(upper.x);
+
+        RotationalLimitMotor rotY
+                = joint.getRotationalLimitMotor(PhysicsSpace.AXIS_Y);
+        rotY.setLoLimit(lower.y);
+        rotY.setHiLimit(upper.y);
+
+        RotationalLimitMotor rotZ
+                = joint.getRotationalLimitMotor(PhysicsSpace.AXIS_Z);
+        rotZ.setLoLimit(lower.z);
+        rotZ.setHiLimit(upper.z);
+
+        joint.setAngularLowerLimit(lower);
+        joint.setAngularUpperLimit(upper);
+
+        for (int i = 0; i < 3; ++i) {
+            RotationalLimitMotor rot = joint.getRotationalLimitMotor(i);
+            rot.setMaxMotorForce(1e8f);
+            rot.setMaxLimitForce(1e9f);
+        }
+
+        joint.setLinearLowerLimit(translateIdentity);
+        joint.setLinearUpperLimit(translateIdentity);
+
+        TranslationalLimitMotor tra = joint.getTranslationalLimitMotor();
+        tra.setLowerLimit(translateIdentity);
+        tra.setUpperLimit(translateIdentity);
+    }
+    // *************************************************************************
+    // Savable methods
+
+    /**
+     * De-serialize this preset, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule capsule = im.getCapsule(this);
+        maxX = capsule.readFloat("maxX", 0f);
+        minX = capsule.readFloat("minX", 0f);
+        maxY = capsule.readFloat("maxY", 0f);
+        minY = capsule.readFloat("minY", 0f);
+        maxZ = capsule.readFloat("maxZ", 0f);
+        minZ = capsule.readFloat("minZ", 0f);
+    }
+
+    /**
+     * Serialize this preset, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule capsule = ex.getCapsule(this);
+        capsule.write(maxX, "maxX", 0f);
+        capsule.write(minX, "minX", 0f);
+        capsule.write(maxY, "maxY", 0f);
+        capsule.write(minY, "minY", 0f);
+        capsule.write(maxZ, "maxZ", 0f);
+        capsule.write(minZ, "minZ", 0f);
+    }
+}

+ 486 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/TorsoLink.java

@@ -0,0 +1,486 @@
+/*
+ * Copyright (c) 2018-2019 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 com.jme3.bullet.animation;
+
+import com.jme3.anim.Joint;
+import com.jme3.bullet.collision.shapes.CollisionShape;
+import com.jme3.bullet.objects.PhysicsRigidBody;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Transform;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Node;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Link the torso of an animated model to a rigid body in a ragdoll.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ *
+ * Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
+ */
+public class TorsoLink extends PhysicsLink {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final public static Logger logger2
+            = Logger.getLogger(TorsoLink.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * bones managed by this link, in a pre-order, depth-first traversal of the
+     * skeleton
+     */
+    private Joint[] managedBones = null;
+    /**
+     * submode when kinematic
+     */
+    private KinematicSubmode submode = KinematicSubmode.Animated;
+    /**
+     * local transform for the controlled spatial at the end of this link's most
+     * recent blend interval, or null for no spatial blending
+     */
+    private Transform endModelTransform = null;
+    /**
+     * transform from mesh coordinates to model coordinates
+     */
+    private Transform meshToModel = null;
+    /**
+     * local transform of the controlled spatial at the start of this link's
+     * most recent blend interval
+     */
+    private Transform startModelTransform = new Transform();
+    /**
+     * local transform of each managed bone from the previous update
+     */
+    private Transform[] prevBoneTransforms = null;
+    /**
+     * local transform of each managed bone at the start of the most recent
+     * blend interval
+     */
+    private Transform[] startBoneTransforms = null;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * No-argument constructor needed by SavableClassUtil. Do not invoke
+     * directly!
+     */
+    public TorsoLink() {
+    }
+
+    /**
+     * Instantiate a purely kinematic link between the torso of the specified
+     * control and the specified rigid body.
+     *
+     * @param control the control that will manage this link (not null, alias
+     * created)
+     * @param mainRootBone the root bone with the most animation weight (not
+     * null, alias created)
+     * @param collisionShape the desired shape (not null, alias created)
+     * @param mass the mass (in physics-mass units)
+     * @param meshToModel the transform from mesh coordinates to model
+     * coordinates (not null, unaffected)
+     * @param localOffset the location of the body's center (in the bone's local
+     * coordinates, not null, unaffected)
+     */
+    TorsoLink(DacLinks control, Joint mainRootBone,
+            CollisionShape collisionShape, float mass,
+            Transform meshToModel, Vector3f localOffset) {
+        super(control, mainRootBone, collisionShape, mass, localOffset);
+        this.meshToModel = meshToModel.clone();
+        managedBones = control.listManagedBones(DynamicAnimControl.torsoName);
+
+        int numManagedBones = managedBones.length;
+        startBoneTransforms = new Transform[numManagedBones];
+        for (int i = 0; i < numManagedBones; ++i) {
+            startBoneTransforms[i] = new Transform();
+        }
+    }
+    // *************************************************************************
+    // new methods exposed
+
+    /**
+     * Begin blending this link to a purely kinematic mode.
+     *
+     * @param submode enum value (not null)
+     * @param blendInterval the duration of the blend interval (in seconds,
+     * &ge;0)
+     * @param endModelTransform the desired local transform for the controlled
+     * spatial when the blend completes or null for no change to local transform
+     * (unaffected)
+     */
+    public void blendToKinematicMode(KinematicSubmode submode,
+            float blendInterval, Transform endModelTransform) {
+        super.blendToKinematicMode(blendInterval);
+
+        this.submode = submode;
+        this.endModelTransform = endModelTransform;
+        /*
+         * Save initial transforms for blending.
+         */
+        if (endModelTransform != null) {
+            Transform current = getControl().getSpatial().getLocalTransform();
+            startModelTransform.set(current);
+        }
+        int numManagedBones = managedBones.length;
+        for (int mbIndex = 0; mbIndex < numManagedBones; ++mbIndex) {
+            Transform transform;
+            if (prevBoneTransforms == null) { // this link not updated yet
+                Joint managedBone = managedBones[mbIndex];
+                transform = managedBone.getLocalTransform().clone();
+            } else {
+                transform = prevBoneTransforms[mbIndex];
+            }
+            startBoneTransforms[mbIndex].set(transform);
+        }
+    }
+    // *************************************************************************
+    // PhysicsLink methods
+
+    /**
+     * Callback from {@link com.jme3.util.clone.Cloner} to convert this
+     * shallow-cloned link into a deep-cloned one, using the specified cloner
+     * and original to resolve copied fields.
+     *
+     * @param cloner the cloner that's cloning this link (not null)
+     * @param original the instance from which this link was shallow-cloned
+     * (unused)
+     */
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        managedBones = cloner.clone(managedBones);
+        endModelTransform = cloner.clone(endModelTransform);
+        meshToModel = cloner.clone(meshToModel);
+        prevBoneTransforms = cloner.clone(prevBoneTransforms);
+        startBoneTransforms = cloner.clone(startBoneTransforms);
+        startModelTransform = cloner.clone(startModelTransform);
+    }
+
+    /**
+     * Update this link in Dynamic mode, setting the local transform of the
+     * model's root spatial based on the transform of the linked rigid body.
+     */
+    @Override
+    protected void dynamicUpdate() {
+        /*
+         * Calculate the inverse world transform of the model's parent node.
+         */
+        Transform worldToParent;
+        Node parent = getControl().getSpatial().getParent();
+        if (parent == null) {
+            worldToParent = new Transform();
+        } else {
+            Transform parentToWorld = parent.getWorldTransform();
+            worldToParent = parentToWorld.invert();
+        }
+
+        Transform transform = meshToModel.clone();
+        Transform shapeToWorld = new Transform();
+        PhysicsRigidBody body = getRigidBody();
+        body.getPhysicsLocation(shapeToWorld.getTranslation());
+        body.getPhysicsRotation(shapeToWorld.getRotation());
+        shapeToWorld.setScale(body.getCollisionShape().getScale());
+
+        transform.combineWithParent(shapeToWorld);
+        transform.combineWithParent(worldToParent);
+        getControl().getSpatial().setLocalTransform(transform);
+
+        localBoneTransform(transform);
+        Joint[] rootBones = getControl().getSkeleton().getRoots();
+        for (Joint rootBone : rootBones) {
+            rootBone.setLocalTransform(transform);
+        }
+
+        for (Joint managedBone : managedBones) {
+            managedBone.updateModelTransforms();
+        }
+    }
+
+    /**
+     * Create a shallow clone for the JME cloner.
+     *
+     * @return a new instance
+     */
+    @Override
+    public TorsoLink jmeClone() {
+        try {
+            TorsoLink clone = (TorsoLink) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    /**
+     * Update this link in blended Kinematic mode.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    protected void kinematicUpdate(float tpf) {
+        assert tpf >= 0f : tpf;
+        assert getRigidBody().isKinematic();
+
+        Transform transform = new Transform();
+
+        if (endModelTransform != null) {
+            /*
+             * For a smooth transition, blend the saved model transform
+             * (from the start of the blend interval) into the goal transform.
+             */
+            transform.interpolateTransforms(startModelTransform.clone(),
+                    endModelTransform, kinematicWeight());
+            getControl().getSpatial().setLocalTransform(transform);
+        }
+
+        for (int mbIndex = 0; mbIndex < managedBones.length; ++mbIndex) {
+            Joint managedBone = managedBones[mbIndex];
+            switch (submode) {
+                case Animated:
+                    transform.set(managedBone.getLocalTransform());
+                    break;
+                case Frozen:
+                    transform.set(prevBoneTransforms[mbIndex]);
+                    break;
+                default:
+                    throw new IllegalStateException(submode.toString());
+            }
+
+            if (kinematicWeight() < 1f) { // not purely kinematic yet
+                /*
+                 * For a smooth transition, blend the saved bone transform
+                 * (from the start of the blend interval)
+                 * into the goal transform.
+                 */
+                transform.interpolateTransforms(
+                        startBoneTransforms[mbIndex].clone(), transform,
+                        kinematicWeight());
+            }
+            /*
+             * Update the managed bone.
+             */
+            managedBone.setLocalTransform(transform);
+            managedBone.updateModelTransforms();
+        }
+
+        super.kinematicUpdate(tpf);
+    }
+
+    /**
+     * Unambiguously identify this link by name, within its DynamicAnimControl.
+     *
+     * @return a brief textual description (not null, not empty)
+     */
+    @Override
+    public String name() {
+        return "Torso:";
+    }
+
+    /**
+     * Copy animation data from the specified link, which must have the same
+     * main bone.
+     *
+     * @param oldLink the link to copy from (not null, unaffected)
+     */
+    void postRebuild(TorsoLink oldLink) {
+        int numManagedBones = managedBones.length;
+        assert oldLink.managedBones.length == numManagedBones;
+
+        super.postRebuild(oldLink);
+        if (oldLink.isKinematic()) {
+            submode = oldLink.submode;
+        } else {
+            submode = KinematicSubmode.Frozen;
+        }
+
+        Transform emt = oldLink.endModelTransform;
+        endModelTransform = (emt == null) ? null : emt.clone();
+        startModelTransform.set(oldLink.startModelTransform);
+
+        if (prevBoneTransforms == null) {
+            prevBoneTransforms = new Transform[numManagedBones];
+            for (int i = 0; i < numManagedBones; ++i) {
+                prevBoneTransforms[i] = new Transform();
+            }
+        }
+        for (int i = 0; i < numManagedBones; ++i) {
+            prevBoneTransforms[i].set(oldLink.prevBoneTransforms[i]);
+            startBoneTransforms[i].set(oldLink.startBoneTransforms[i]);
+        }
+    }
+
+    /**
+     * De-serialize this link, for example when loading from a J3O file.
+     *
+     * @param im importer (not null)
+     * @throws IOException from importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        Savable[] tmp = ic.readSavableArray("managedBones", null);
+        if (tmp == null) {
+            managedBones = null;
+        } else {
+            managedBones = new Joint[tmp.length];
+            for (int i = 0; i < tmp.length; ++i) {
+                managedBones[i] = (Joint) tmp[i];
+            }
+        }
+
+        submode = ic.readEnum("submode", KinematicSubmode.class,
+                KinematicSubmode.Animated);
+        endModelTransform = (Transform) ic.readSavable("endModelTransform",
+                new Transform());
+        meshToModel
+                = (Transform) ic.readSavable("meshToModel", new Transform());
+        startModelTransform = (Transform) ic.readSavable("startModelTransform",
+                new Transform());
+        prevBoneTransforms = RagUtils.readTransformArray(ic,
+                "prevBoneTransforms");
+        startBoneTransforms = RagUtils.readTransformArray(ic,
+                "startBoneTransforms");
+    }
+
+    /**
+     * Internal callback, invoked once per frame during the logical-state
+     * update, provided the control is added to a scene.
+     *
+     * @param tpf the time interval between frames (in seconds, &ge;0)
+     */
+    @Override
+    void update(float tpf) {
+        assert tpf >= 0f : tpf;
+
+        if (prevBoneTransforms == null) {
+            /*
+             * On the first update, allocate and initialize
+             * the array of previous bone transforms, if it wasn't
+             * allocated in blendToKinematicMode().
+             */
+            int numManagedBones = managedBones.length;
+            prevBoneTransforms = new Transform[numManagedBones];
+            for (int mbIndex = 0; mbIndex < numManagedBones; ++mbIndex) {
+                Joint managedBone = managedBones[mbIndex];
+                Transform boneTransform
+                        = managedBone.getLocalTransform().clone();
+                prevBoneTransforms[mbIndex] = boneTransform;
+            }
+        }
+
+        super.update(tpf);
+        /*
+         * Save copies of the latest bone transforms.
+         */
+        for (int mbIndex = 0; mbIndex < managedBones.length; ++mbIndex) {
+            Transform lastTransform = prevBoneTransforms[mbIndex];
+            Joint managedBone = managedBones[mbIndex];
+            lastTransform.set(managedBone.getLocalTransform());
+        }
+    }
+
+    /**
+     * Serialize this link, for example when saving to a J3O file.
+     *
+     * @param ex exporter (not null)
+     * @throws IOException from exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.write(managedBones, "managedBones", null);
+        oc.write(submode, "submode", KinematicSubmode.Animated);
+        oc.write(endModelTransform, "endModelTransforms", new Transform());
+        oc.write(meshToModel, "meshToModel", new Transform());
+        oc.write(startModelTransform, "startModelTransforms", new Transform());
+        oc.write(prevBoneTransforms, "prevBoneTransforms", new Transform[0]);
+        oc.write(startBoneTransforms, "startBoneTransforms", new Transform[0]);
+    }
+    // *************************************************************************
+    // private methods
+
+    /**
+     * Calculate the local bone transform to match the physics transform of the
+     * rigid body.
+     *
+     * @param storeResult storage for the result (modified if not null)
+     * @return the calculated bone transform (in local coordinates, either
+     * storeResult or a new transform, not null)
+     */
+    private Transform localBoneTransform(Transform storeResult) {
+        Transform result
+                = (storeResult == null) ? new Transform() : storeResult;
+        Vector3f location = result.getTranslation();
+        Quaternion orientation = result.getRotation();
+        Vector3f scale = result.getScale();
+        /*
+         * Start with the rigid body's transform in physics/world coordinates.
+         */
+        PhysicsRigidBody body = getRigidBody();
+        body.getPhysicsLocation(result.getTranslation());
+        body.getPhysicsRotation(result.getRotation());
+        result.setScale(body.getCollisionShape().getScale());
+        /*
+         * Convert to mesh coordinates.
+         */
+        Transform worldToMesh = getControl().meshTransform(null).invert();
+        result.combineWithParent(worldToMesh);
+        /*
+         * Subtract the body's local offset, rotated and scaled.
+         */
+        Vector3f meshOffset = localOffset(null);
+        meshOffset.multLocal(scale);
+        orientation.mult(meshOffset, meshOffset);
+        location.subtractLocal(meshOffset);
+
+        return result;
+    }
+}

+ 151 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/VectorSet.java

@@ -0,0 +1,151 @@
+/*
+ Copyright (c) 2019 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 com.jme3.bullet.animation;
+
+import com.jme3.math.Vector3f;
+import com.jme3.util.BufferUtils;
+import java.nio.FloatBuffer;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+
+/**
+ * A simplified collection of Vector3f values without duplicates, implemented
+ * using a Collection.
+ * <p>
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold [email protected]
+ */
+public class VectorSet {
+    // *************************************************************************
+    // constants and loggers
+
+    /**
+     * message logger for this class
+     */
+    final private static Logger logger
+            = Logger.getLogger(VectorSet.class.getName());
+    // *************************************************************************
+    // fields
+
+    /**
+     * collection of values
+     */
+    final private Set<Vector3f> set;
+    // *************************************************************************
+    // constructors
+
+    /**
+     * Instantiate an empty set with the specified initial capacity and default
+     * load factor.
+     *
+     * @param numVectors the initial capacity of the hash table (&gt;0)
+     */
+    public VectorSet(int numVectors) {
+        set = new HashSet<>(numVectors);
+    }
+    // *************************************************************************
+    // VectorSet methods
+
+    /**
+     * Add the value of the specified Vector3f to this set.
+     *
+     * @param vector the value to add (not null, unaffected)
+     */
+    public void add(Vector3f vector) {
+        set.add(vector.clone());
+    }
+
+    /**
+     * Test whether this set contains the value of the specified Vector3f.
+     *
+     * @param vector the value to find (not null, unaffected)
+     * @return true if found, otherwise false
+     */
+    public boolean contains(Vector3f vector) {
+        boolean result = set.contains(vector);
+        return result;
+    }
+
+    /**
+     * Calculate the sample mean for each axis over the Vector3f values in this
+     * set.
+     *
+     * @param storeResult (modified if not null)
+     * @return the sample mean for each axis (either storeResult or a new
+     * Vector3f)
+     */
+    public Vector3f mean(Vector3f storeResult) {
+        int numVectors = numVectors();
+        assert numVectors > 0 : numVectors;
+        Vector3f result = (storeResult == null) ? new Vector3f() : storeResult;
+
+        result.zero();
+        for (Vector3f tempVector : set) {
+            result.addLocal(tempVector);
+        }
+        result.divideLocal(numVectors);
+
+        return result;
+    }
+
+    /**
+     * Calculate the number of Vector3f values in this set.
+     *
+     * @return the count (&ge;0)
+     */
+    public int numVectors() {
+        int numVectors = set.size();
+        assert numVectors >= 0 : numVectors;
+        return numVectors;
+    }
+
+    /**
+     * Access the buffer containing all the Vector3f values in this set. No
+     * further add() is allowed.
+     *
+     * @return a new buffer, flipped
+     */
+    public FloatBuffer toBuffer() {
+        int numFloats = 3 * set.size();
+        FloatBuffer buffer = BufferUtils.createFloatBuffer(numFloats);
+        for (Vector3f tempVector : set) {
+            buffer.put(tempVector.x);
+            buffer.put(tempVector.y);
+            buffer.put(tempVector.z);
+        }
+        buffer.flip();
+
+        return buffer;
+    }
+}

+ 35 - 0
jme3-bullet/src/common/java/com/jme3/bullet/animation/package-info.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2018 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.
+ */
+/**
+ * A dynamic animation control and some associated classes.
+ */
+package com.jme3.bullet.animation;

+ 1 - 1
jme3-core/src/main/java/com/jme3/anim/Joint.java

@@ -154,7 +154,7 @@ public class Joint implements Savable, JmeCloneable, HasLocalTransform {
     /**
      * Sets the local transform with the bind transforms
      */
-    protected void applyBindPose() {
+    public void applyBindPose() {
         jointModelTransform.applyBindPose(localTransform, inverseModelBindMatrix, parent);
         updateModelTransforms();
 

+ 0 - 93
jme3-examples/src/main/java/jme3test/bullet/TestIK.java

@@ -1,93 +0,0 @@
-/*
- * Copyright (c) 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 jme3test.bullet;
-
-import com.jme3.animation.AnimEventListener;
-import com.jme3.animation.Bone;
-import com.jme3.bullet.collision.RagdollCollisionListener;
-import com.jme3.bullet.control.KinematicRagdollControl;
-import com.jme3.input.KeyInput;
-import com.jme3.input.controls.ActionListener;
-import com.jme3.input.controls.KeyTrigger;
-import com.jme3.math.Vector3f;
-import com.jme3.scene.Node;
-
-/**
- * @author reden
- */
-public class TestIK extends TestBoneRagdoll implements RagdollCollisionListener, AnimEventListener {
-
-    Node targetNode = new Node("");
-    Vector3f targetPoint;
-    Bone mouseBone;
-    Vector3f oldMousePos;
- 
-    public static void main(String[] args) {
-        TestIK app = new TestIK();
-        app.start();
-    }
-
-    @Override
-    public void simpleInitApp() {
-        super.simpleInitApp();
-        final KinematicRagdollControl ikControl = model.getControl(KinematicRagdollControl.class);
-        inputManager.addListener(new ActionListener() {
-
-            public void onAction(String name, boolean isPressed, float tpf) {
-
-                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, 2);
-                    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, 3);
-                    ikControl.setIKMode();
-                }
-            }
-        }, "one", "two");
-        inputManager.addMapping("one", new KeyTrigger(KeyInput.KEY_1));
-        inputManager.addMapping("two", new KeyTrigger(KeyInput.KEY_2));
-        inputManager.addMapping("stop", new KeyTrigger(KeyInput.KEY_H));
-    }
-    
-}