diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/BoneLink.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/BoneLink.java new file mode 100644 index 000000000..730b1b170 --- /dev/null +++ b/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. + *
+ * This class is shared between JBullet and Native Bullet. + * + * @author Stephen Gold sgold@sonic.net + * + * 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, + * ≥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, ≥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, ≥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; + } +} diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/DacConfiguration.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/DacConfiguration.java new file mode 100644 index 000000000..d5b46d6cb --- /dev/null +++ b/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. + *
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ *
+ * 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→no damping,
+ * 1→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
+ * 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 (>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
+ * 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
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ *
+ * 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
+ * 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
+ * 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.
+ *
+ * Allowed only when the control IS added to a spatial.
+ */
+ public void rebuild() {
+ verifyAddedToSpatial("rebuild the ragdoll");
+
+ Map
+ * 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.
+ *
+ * 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.
+ *
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ *
+ * 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
+ * Allowed only when the control IS added to a spatial.
+ *
+ * @param blendInterval the duration of the blend interval (in seconds,
+ * ≥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.
+ *
+ * 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 (>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!
+ *
+ * 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!
+ *
+ * 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 (≥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!
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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,
+ * ≥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,
+ * ≥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
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ */
+public enum KinematicSubmode {
+ /**
+ * driven by animation (if any)
+ */
+ Animated,
+ /**
+ * frozen in the transform it had when blending started
+ */
+ Frozen;
+}
diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/PhysicsLink.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/PhysicsLink.java
new file mode 100644
index 000000000..e9633263f
--- /dev/null
+++ b/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.
+ *
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ *
+ * 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
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ *
+ * 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
+ * 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 (≥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
+ * 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);
+}
diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/RangeOfMotion.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/RangeOfMotion.java
new file mode 100644
index 000000000..b79d8519c
--- /dev/null
+++ b/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)}.
+ *
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ *
+ * 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, ≥0)
+ * @param maxY the maximum rotation around the Y axis (in radians, ≥0)
+ * @param maxZ the maximum rotation around the Z axis (in radians, ≥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, ≥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→X, 1→Y, 2→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→X, 1→Y, 2→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→X, 1→Y, 2→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);
+ }
+}
diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/TorsoLink.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/TorsoLink.java
new file mode 100644
index 000000000..c44868116
--- /dev/null
+++ b/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.
+ *
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ *
+ * 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,
+ * ≥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, ≥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, ≥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;
+ }
+}
diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/VectorSet.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/VectorSet.java
new file mode 100644
index 000000000..482f95739
--- /dev/null
+++ b/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.
+ *
+ * This class is shared between JBullet and Native Bullet.
+ *
+ * @author Stephen Gold sgold@sonic.net
+ */
+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