From 235b9db2cad9b3c9a1647504e7c48c49aa2b8742 Mon Sep 17 00:00:00 2001 From: Stephen Gold Date: Sat, 30 Mar 2019 14:04:36 -0700 Subject: [PATCH] publicize Joint.applyBindPose(), add DynamicAnimControl & related stuff --- .../com/jme3/bullet/animation/BoneLink.java | 493 ++++++++ .../bullet/animation/DacConfiguration.java | 566 +++++++++ .../com/jme3/bullet/animation/DacLinks.java | 1109 +++++++++++++++++ .../bullet/animation/DynamicAnimControl.java | 537 ++++++++ .../bullet/animation/KinematicSubmode.java | 50 + .../jme3/bullet/animation/PhysicsLink.java | 643 ++++++++++ .../com/jme3/bullet/animation/RagUtils.java | 707 +++++++++++ .../animation/RagdollCollisionListener.java | 56 + .../jme3/bullet/animation/RangeOfMotion.java | 313 +++++ .../com/jme3/bullet/animation/TorsoLink.java | 486 ++++++++ .../com/jme3/bullet/animation/VectorSet.java | 151 +++ .../jme3/bullet/animation/package-info.java | 35 + .../src/main/java/com/jme3/anim/Joint.java | 2 +- .../src/main/java/jme3test/bullet/TestIK.java | 93 -- 14 files changed, 5147 insertions(+), 94 deletions(-) create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/BoneLink.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/DacConfiguration.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/DacLinks.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/DynamicAnimControl.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/KinematicSubmode.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/PhysicsLink.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/RagUtils.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/RagdollCollisionListener.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/RangeOfMotion.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/TorsoLink.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/VectorSet.java create mode 100644 jme3-bullet/src/common/java/com/jme3/bullet/animation/package-info.java delete mode 100644 jme3-examples/src/main/java/jme3test/bullet/TestIK.java 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 blConfigMap = new HashMap<>(50); + /** + * map linked bone names to ranges of motion for createSpatialData() + */ + private Map 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 (≥0) + */ + public int countLinkedBones() { + int count = blConfigMap.size(); + + assert count == jointMap.size(); + assert count >= 0 : count; + return count; + } + + /** + * Count the links. + * + * @return count (≥0) + */ + public int countLinks() { + int result = countLinkedBones() + 1; + return result; + } + + /** + * Read the damping ratio for new rigid bodies. + * + * @return the viscous damping ratio (0→no damping, 1→critically + * damped) + */ + public float damping() { + assert damping >= 0f : damping; + return damping; + } + + /** + * Read the event-dispatch impulse threshold of this control. + * + * @return the threshold value (≥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. + *

+ * 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 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, >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→no + * damping, 1→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 (≥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 (>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 (>0) + */ + public float totalMass() { + float totalMass = torsoMass; + for (float mass : blConfigMap.values()) { + totalMass += mass; + } + + return totalMass; + } + + /** + * Unlink the BoneLink of the named bone. + *

+ * 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 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→match local coordinates, + * false→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 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); + } + } +} diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/DacLinks.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/DacLinks.java new file mode 100644 index 000000000..42dd09775 --- /dev/null +++ b/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. + *

+ * 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 boneLinkList = null; + /** + * map bone names to bone links + */ + private Map 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. + *

+ * 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 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 List listLinks(Class linkType) { + int numLinks = countLinks(); + List 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 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. + *

+ * 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 saveBones = new HashMap<>(boneLinks); + TorsoLink saveTorso = torsoLink; + + Spatial controlledSpatial = getSpatial(); + removeSpatialData(controlledSpatial); + createSpatialData(controlledSpatial); + + for (Map.Entry 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 (>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 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 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 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 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 (>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→no + * damping, 1→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 (>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, ≥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, ≥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, ≥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 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 childNames(PhysicsLink link) { + assert link != null; + + String linkName; + if (link == torsoLink) { + linkName = torsoName; + } else { + linkName = link.boneName(); + } + + List 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); + } + } + } +} diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/DynamicAnimControl.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/DynamicAnimControl.java new file mode 100644 index 000000000..adead3c12 --- /dev/null +++ b/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. + *

+ * 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 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, + * ≥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 + *

+ * 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 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; + } +} diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/KinematicSubmode.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/KinematicSubmode.java new file mode 100644 index 000000000..95ffa186b --- /dev/null +++ b/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. + *

+ * 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 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, ≥0) + */ + private float blendInterval = 1f; + /** + * weighting of kinematic movement (≥0, ≤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 (≥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, ≥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, ≥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, + * ≥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, ≥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 (≥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); + } +} diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/RagUtils.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/RagUtils.java new file mode 100644 index 000000000..89b112283 --- /dev/null +++ b/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. + *

+ * 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 coordsMap(Mesh[] meshes, + String[] managerMap) { + float[] wArray = new float[4]; + int[] iArray = new int[4]; + Vector3f bindPosition = new Vector3f(); + Map 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 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 (≥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 listAnimatedMeshes(Spatial subtree, + List 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 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 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 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 addResult) { + assert bone != null; + addResult.add(bone); + List 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 (≥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 weightMap = weightMap(iArray, wArray, managerMap); + + float bestTotalWeight = Float.NEGATIVE_INFINITY; + String bestName = null; + for (Map.Entry 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 listGeometries(Spatial subtree, + List addResult) { + List result = (addResult == null) ? new ArrayList(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 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 preOrderJoints(Armature skeleton) { + int numBones = skeleton.getJointCount(); + List 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 (≥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 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 (≥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 (≥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. + *

+ * 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 weightMap(int[] biArray, + float[] bwArray, String[] managerMap) { + assert biArray.length == 4; + assert bwArray.length == 4; + + Map 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; + } +} diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/RagdollCollisionListener.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/RagdollCollisionListener.java new file mode 100644 index 000000000..3e0eccdb4 --- /dev/null +++ b/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. + *

+ * 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 set; + // ************************************************************************* + // constructors + + /** + * Instantiate an empty set with the specified initial capacity and default + * load factor. + * + * @param numVectors the initial capacity of the hash table (>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 (≥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; + } +} diff --git a/jme3-bullet/src/common/java/com/jme3/bullet/animation/package-info.java b/jme3-bullet/src/common/java/com/jme3/bullet/animation/package-info.java new file mode 100644 index 000000000..ac9b2f72a --- /dev/null +++ b/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; diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index bf672fa29..0c7153d29 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/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(); diff --git a/jme3-examples/src/main/java/jme3test/bullet/TestIK.java b/jme3-examples/src/main/java/jme3test/bullet/TestIK.java deleted file mode 100644 index 43313e789..000000000 --- a/jme3-examples/src/main/java/jme3test/bullet/TestIK.java +++ /dev/null @@ -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)); - } - -}