You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1109 lines
36 KiB
1109 lines
36 KiB
/*
|
|
* Copyright (c) 2018-2019 jMonkeyEngine
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
*
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
package com.jme3.bullet.animation;
|
|
|
|
import com.jme3.anim.Armature;
|
|
import com.jme3.anim.Joint;
|
|
import com.jme3.anim.SkinningControl;
|
|
import com.jme3.bullet.PhysicsSpace;
|
|
import com.jme3.bullet.PhysicsTickListener;
|
|
import com.jme3.bullet.collision.shapes.CollisionShape;
|
|
import com.jme3.bullet.collision.shapes.HullCollisionShape;
|
|
import com.jme3.bullet.joints.PhysicsJoint;
|
|
import com.jme3.bullet.joints.SixDofJoint;
|
|
import com.jme3.bullet.objects.PhysicsRigidBody;
|
|
import com.jme3.export.InputCapsule;
|
|
import com.jme3.export.JmeExporter;
|
|
import com.jme3.export.JmeImporter;
|
|
import com.jme3.export.OutputCapsule;
|
|
import com.jme3.export.Savable;
|
|
import com.jme3.math.Quaternion;
|
|
import com.jme3.math.Transform;
|
|
import com.jme3.math.Vector3f;
|
|
import com.jme3.scene.Mesh;
|
|
import com.jme3.scene.Node;
|
|
import com.jme3.scene.Spatial;
|
|
import com.jme3.util.clone.Cloner;
|
|
import java.io.IOException;
|
|
import java.nio.FloatBuffer;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
/**
|
|
* Access a DynamicAnimControl at the PhysicsLink level once it's been added to
|
|
* a Spatial.
|
|
* <p>
|
|
* This class is shared between JBullet and Native Bullet.
|
|
*
|
|
* @author Stephen Gold 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<BoneLink> boneLinkList = null;
|
|
/**
|
|
* map bone names to bone links
|
|
*/
|
|
private Map<String, BoneLink> boneLinks = new HashMap<>(32);
|
|
/**
|
|
* skeleton being controlled
|
|
*/
|
|
private Armature skeleton = null;
|
|
/**
|
|
* spatial that provides the mesh-coordinate transform
|
|
*/
|
|
private Spatial transformer = null;
|
|
/**
|
|
* torso link for this control
|
|
*/
|
|
private TorsoLink torsoLink = null;
|
|
// *************************************************************************
|
|
// constructors
|
|
|
|
/**
|
|
* Instantiate an enabled control without any linked bones or attachments
|
|
* (torso only).
|
|
*/
|
|
DacLinks() {
|
|
}
|
|
// *************************************************************************
|
|
// new methods exposed
|
|
|
|
/**
|
|
* Access the named bone.
|
|
* <p>
|
|
* Allowed only when the control IS added to a spatial.
|
|
*
|
|
* @param boneName the name of the skeleton bone to access
|
|
* @return the pre-existing instance, or null if not found
|
|
*/
|
|
public Joint findBone(String boneName) {
|
|
verifyAddedToSpatial("access a bone");
|
|
Joint result = skeleton.getJoint(boneName);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Access the BoneLink for the named bone. Returns null if bone is not
|
|
* linked, or if the control is not added to a spatial.
|
|
*
|
|
* @param boneName the name of the bone (not null, not empty)
|
|
* @return the pre-existing BoneLink, or null if not found
|
|
*/
|
|
public BoneLink findBoneLink(String boneName) {
|
|
BoneLink boneLink = boneLinks.get(boneName);
|
|
return boneLink;
|
|
}
|
|
|
|
/**
|
|
* Access the named link. Returns null if the name is invalid, or if the
|
|
* control is not added to a spatial.
|
|
*
|
|
* @param linkName the name of the link (not null, not empty)
|
|
* @return the pre-existing link, or null if not found
|
|
*/
|
|
public PhysicsLink findLink(String linkName) {
|
|
PhysicsLink link;
|
|
if (linkName.startsWith("Bone:")) {
|
|
String boneName = linkName.substring(5);
|
|
link = findBoneLink(boneName);
|
|
} else {
|
|
assert linkName.equals("Torso:");
|
|
link = torsoLink;
|
|
}
|
|
|
|
return link;
|
|
}
|
|
|
|
/**
|
|
* Access the skeleton. Returns null if the control is not added to a
|
|
* spatial.
|
|
*
|
|
* @return the pre-existing skeleton, or null
|
|
*/
|
|
public Armature getSkeleton() {
|
|
return skeleton;
|
|
}
|
|
|
|
/**
|
|
* Access the TorsoLink. Returns null if the control is not added to a
|
|
* spatial.
|
|
*
|
|
* @return the pre-existing TorsoLink, or null
|
|
*/
|
|
public TorsoLink getTorsoLink() {
|
|
return torsoLink;
|
|
}
|
|
|
|
/**
|
|
* Access the spatial with the mesh-coordinate transform. Returns null if
|
|
* the control is not added to a spatial.
|
|
*
|
|
* @return the pre-existing spatial, or null
|
|
*/
|
|
Spatial getTransformer() {
|
|
return transformer;
|
|
}
|
|
|
|
/**
|
|
* Test whether this control is ready for dynamic mode.
|
|
*
|
|
* @return true if ready, otherwise false
|
|
*/
|
|
public boolean isReady() {
|
|
return isReady;
|
|
}
|
|
|
|
/**
|
|
* Enumerate all physics links of the specified type managed by this
|
|
* control.
|
|
*
|
|
* @param <T> subclass of PhysicsLink
|
|
* @param linkType the subclass of PhysicsLink to search for (not null)
|
|
* @return a new array of links (not null, not empty)
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
public <T extends PhysicsLink> List<T> listLinks(Class<T> linkType) {
|
|
int numLinks = countLinks();
|
|
List<T> result = new ArrayList<>(numLinks);
|
|
|
|
if (torsoLink != null
|
|
&& linkType.isAssignableFrom(torsoLink.getClass())) {
|
|
result.add((T) torsoLink);
|
|
}
|
|
for (BoneLink link : boneLinkList) {
|
|
if (linkType.isAssignableFrom(link.getClass())) {
|
|
result.add((T) link);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Enumerate all managed bones of the named link, in a pre-order,
|
|
* depth-first traversal of the skeleton, such that child bones never
|
|
* precede their ancestors.
|
|
*
|
|
* @param managerName the name of the managing link (not null)
|
|
* @return a new array of managed bones, including the manager if it is not
|
|
* the torso
|
|
*/
|
|
Joint[] listManagedBones(String managerName) {
|
|
List<Joint> list = new ArrayList<>(8);
|
|
|
|
if (torsoName.equals(managerName)) {
|
|
Joint[] roots = skeleton.getRoots();
|
|
for (Joint rootBone : roots) {
|
|
list.add(rootBone);
|
|
addUnlinkedDescendants(rootBone, list);
|
|
}
|
|
|
|
} else {
|
|
BoneLink manager = findBoneLink(managerName);
|
|
if (manager == null) {
|
|
String msg = "No link named " + managerName;
|
|
throw new IllegalArgumentException(msg);
|
|
}
|
|
Joint managerBone = manager.getBone();
|
|
list.add(managerBone);
|
|
addUnlinkedDescendants(managerBone, list);
|
|
}
|
|
/*
|
|
* Convert the list to an array.
|
|
*/
|
|
int numManagedBones = list.size();
|
|
Joint[] array = new Joint[numManagedBones];
|
|
list.toArray(array);
|
|
|
|
return array;
|
|
}
|
|
|
|
/**
|
|
* Enumerate all rigid bodies managed by this control.
|
|
* <p>
|
|
* Allowed only when the control IS added to a spatial.
|
|
*
|
|
* @return a new array of pre-existing rigid bodies (not null, not empty)
|
|
*/
|
|
public PhysicsRigidBody[] listRigidBodies() {
|
|
verifyAddedToSpatial("enumerate rigid bodies");
|
|
|
|
int numLinks = countLinks();
|
|
PhysicsRigidBody[] result = new PhysicsRigidBody[numLinks];
|
|
|
|
int linkIndex = 0;
|
|
if (torsoLink != null) {
|
|
result[0] = torsoLink.getRigidBody();
|
|
++linkIndex;
|
|
}
|
|
for (BoneLink boneLink : boneLinkList) {
|
|
result[linkIndex] = boneLink.getRigidBody();
|
|
++linkIndex;
|
|
}
|
|
assert linkIndex == numLinks;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Copy the model's mesh-to-world transform.
|
|
*
|
|
* @param storeResult storage for the result (modified if not null)
|
|
* @return the model's mesh transform (in world coordinates, either
|
|
* storeResult or a new transform, not null)
|
|
*/
|
|
Transform meshTransform(Transform storeResult) {
|
|
Transform result = transformer.getWorldTransform().clone();
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculate the physics transform to match the specified skeleton bone.
|
|
*
|
|
* @param bone the skeleton bone to match (not null, unaffected)
|
|
* @param localOffset the location of the body's center (in the bone's local
|
|
* coordinates, not null, unaffected)
|
|
* @param storeResult storage for the result (modified if not null)
|
|
* @return the calculated physics transform (either storeResult or a new
|
|
* transform, not null)
|
|
*/
|
|
Transform physicsTransform(Joint bone, Vector3f localOffset,
|
|
Transform storeResult) {
|
|
Transform result
|
|
= (storeResult == null) ? new Transform() : storeResult;
|
|
/*
|
|
* Start with the body's transform in the bone's local coordinates.
|
|
*/
|
|
result.setTranslation(localOffset);
|
|
result.setRotation(rotateIdentity);
|
|
result.setScale(1f);
|
|
/*
|
|
* Convert to mesh coordinates.
|
|
*/
|
|
Transform localToMesh = bone.getModelTransform();
|
|
result.combineWithParent(localToMesh);
|
|
/*
|
|
* Convert to world (physics-space) coordinates.
|
|
*/
|
|
Transform meshToWorld = meshTransform(null);
|
|
result.combineWithParent(meshToWorld);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Rebuild the ragdoll. This is useful if you applied scale to the model
|
|
* after it was initialized.
|
|
* <p>
|
|
* Allowed only when the control IS added to a spatial.
|
|
*/
|
|
public void rebuild() {
|
|
verifyAddedToSpatial("rebuild the ragdoll");
|
|
|
|
Map<String, BoneLink> saveBones = new HashMap<>(boneLinks);
|
|
TorsoLink saveTorso = torsoLink;
|
|
|
|
Spatial controlledSpatial = getSpatial();
|
|
removeSpatialData(controlledSpatial);
|
|
createSpatialData(controlledSpatial);
|
|
|
|
for (Map.Entry<String, BoneLink> entry : boneLinks.entrySet()) {
|
|
String name = entry.getKey();
|
|
BoneLink newLink = entry.getValue();
|
|
BoneLink oldLink = saveBones.get(name);
|
|
newLink.postRebuild(oldLink);
|
|
}
|
|
if (torsoLink != null) {
|
|
torsoLink.postRebuild(saveTorso);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Alter the mass of the specified link.
|
|
*
|
|
* @param link the link to modify (not null)
|
|
* @param mass the desired mass (>0)
|
|
*/
|
|
public void setMass(PhysicsLink link, float mass) {
|
|
if (link instanceof BoneLink) {
|
|
String boneName = link.boneName();
|
|
setMass(boneName, mass);
|
|
} else {
|
|
assert link instanceof TorsoLink;
|
|
setMass(torsoName, mass);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify that this control is ready for dynamic mode, which implies that it
|
|
* is added to a Spatial.
|
|
*
|
|
* @param desiredAction (not null, not empty)
|
|
*/
|
|
public void verifyReadyForDynamicMode(String desiredAction) {
|
|
assert desiredAction != null;
|
|
|
|
verifyAddedToSpatial(desiredAction);
|
|
|
|
if (!isReady) {
|
|
String message = "Cannot " + desiredAction
|
|
+ " until the physics has been stepped.";
|
|
throw new IllegalStateException(message);
|
|
}
|
|
}
|
|
// *************************************************************************
|
|
// new protected methods
|
|
|
|
/**
|
|
* Access the list of bone links in a pre-order, depth-first traversal of
|
|
* the link hierarchy.
|
|
*
|
|
* @return the pre-existing list (not null)
|
|
*/
|
|
protected List<BoneLink> getBoneLinks() {
|
|
assert boneLinkList != null;
|
|
return boneLinkList;
|
|
}
|
|
|
|
/**
|
|
* Verify that this control is added to a Spatial.
|
|
*
|
|
* @param desiredAction (not null, not empty)
|
|
*/
|
|
protected void verifyAddedToSpatial(String desiredAction) {
|
|
assert desiredAction != null;
|
|
|
|
Spatial controlledSpatial = getSpatial();
|
|
if (controlledSpatial == null) {
|
|
String message = "Cannot " + desiredAction
|
|
+ " unless the Control is added to a Spatial.";
|
|
throw new IllegalStateException(message);
|
|
}
|
|
}
|
|
// *************************************************************************
|
|
// DacConfiguration methods
|
|
|
|
/**
|
|
* Add all managed physics objects to the PhysicsSpace.
|
|
*/
|
|
@Override
|
|
protected void addPhysics(PhysicsSpace space) {
|
|
Vector3f gravity = gravity(null);
|
|
|
|
PhysicsRigidBody rigidBody;
|
|
if (torsoLink != null) {
|
|
rigidBody = torsoLink.getRigidBody();
|
|
space.add(rigidBody);
|
|
rigidBody.setGravity(gravity);
|
|
}
|
|
|
|
for (BoneLink boneLink : boneLinkList) {
|
|
rigidBody = boneLink.getRigidBody();
|
|
space.add(rigidBody);
|
|
rigidBody.setGravity(gravity);
|
|
|
|
PhysicsJoint joint = boneLink.getJoint();
|
|
space.add(joint);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback from {@link com.jme3.util.clone.Cloner} to convert this
|
|
* shallow-cloned control into a deep-cloned one, using the specified cloner
|
|
* and original to resolve copied fields.
|
|
*
|
|
* @param cloner the cloner that's cloning this control (not null, modified)
|
|
* @param original the control from which this control was shallow-cloned
|
|
* (not null, unaffected)
|
|
*/
|
|
@Override
|
|
public void cloneFields(Cloner cloner, Object original) {
|
|
super.cloneFields(cloner, original);
|
|
DacLinks originalDac = (DacLinks) original;
|
|
|
|
boneLinkList = cloner.clone(boneLinkList);
|
|
|
|
boneLinks = new HashMap<>(32);
|
|
for (Map.Entry<String, BoneLink> entry
|
|
: originalDac.boneLinks.entrySet()) {
|
|
String boneName = entry.getKey();
|
|
BoneLink link = entry.getValue();
|
|
BoneLink copyLink = cloner.clone(link);
|
|
boneLinks.put(boneName, copyLink);
|
|
}
|
|
|
|
skeleton = cloner.clone(skeleton);
|
|
transformer = cloner.clone(transformer);
|
|
torsoLink = cloner.clone(torsoLink);
|
|
}
|
|
|
|
/**
|
|
* Create spatial-dependent data. Invoked each time the control is added to
|
|
* a spatial. Also invoked by {@link #rebuild()}.
|
|
*
|
|
* @param spatial the controlled spatial (not null)
|
|
*/
|
|
@Override
|
|
protected void createSpatialData(Spatial spatial) {
|
|
RagUtils.validate(spatial);
|
|
|
|
SkinningControl skeletonControl
|
|
= spatial.getControl(SkinningControl.class);
|
|
if (skeletonControl == null) {
|
|
throw new IllegalArgumentException(
|
|
"The controlled spatial must have a SkinningControl. "
|
|
+ "Make sure the control is there and not on a subnode.");
|
|
}
|
|
sortControls(skeletonControl);
|
|
skeletonControl.setHardwareSkinningPreferred(false);
|
|
/*
|
|
* Analyze the model's skeleton.
|
|
*/
|
|
skeleton = skeletonControl.getArmature();
|
|
validateSkeleton();
|
|
String[] tempManagerMap = managerMap(skeleton);
|
|
int numBones = skeleton.getJointCount();
|
|
/*
|
|
* Temporarily set all bones' local translations and rotations to bind.
|
|
*/
|
|
Transform[] savedTransforms = new Transform[numBones];
|
|
for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
|
|
Joint bone = skeleton.getJoint(boneIndex);
|
|
savedTransforms[boneIndex] = bone.getLocalTransform().clone();
|
|
bone.applyBindPose(); // TODO adjust the scale?
|
|
}
|
|
skeleton.update();
|
|
/*
|
|
* Find the target meshes and choose the transform spatial.
|
|
*/
|
|
List<Mesh> targetList = RagUtils.listAnimatedMeshes(spatial, null);
|
|
Mesh[] targets = new Mesh[targetList.size()];
|
|
targetList.toArray(targets);
|
|
transformer = RagUtils.findAnimatedGeometry(spatial);
|
|
if (transformer == null) {
|
|
transformer = spatial;
|
|
}
|
|
/*
|
|
* Enumerate mesh-vertex coordinates and assign them to managers.
|
|
*/
|
|
Map<String, VectorSet> coordsMap
|
|
= RagUtils.coordsMap(targets, tempManagerMap);
|
|
/*
|
|
* Create the torso link.
|
|
*/
|
|
VectorSet vertexLocations = coordsMap.get(torsoName);
|
|
createTorsoLink(vertexLocations, targets);
|
|
/*
|
|
* Create bone links without joints.
|
|
*/
|
|
String[] linkedBoneNames = listLinkedBoneNames();
|
|
for (String boneName : linkedBoneNames) {
|
|
vertexLocations = coordsMap.get(boneName);
|
|
createBoneLink(boneName, vertexLocations);
|
|
}
|
|
int numLinkedBones = countLinkedBones();
|
|
assert boneLinks.size() == numLinkedBones;
|
|
/*
|
|
* Add joints to connect each BoneLink rigid body with its parent in the
|
|
* link hierarchy. Also initialize the boneLinkList.
|
|
*/
|
|
boneLinkList = new ArrayList<>(numLinkedBones);
|
|
addJoints(torsoLink);
|
|
assert boneLinkList.size() == numLinkedBones : boneLinkList.size();
|
|
/*
|
|
* Restore the skeleton's pose.
|
|
*/
|
|
for (int boneIndex = 0; boneIndex < numBones; ++boneIndex) {
|
|
Joint bone = skeleton.getJoint(boneIndex);
|
|
bone.setLocalTransform(savedTransforms[boneIndex]);
|
|
}
|
|
skeleton.update();
|
|
|
|
if (added) {
|
|
addPhysics(space);
|
|
}
|
|
|
|
logger3.log(Level.FINE, "Created ragdoll for skeleton.");
|
|
}
|
|
|
|
/**
|
|
* Create a shallow clone for the JME cloner.
|
|
*
|
|
* @return a new instance
|
|
*/
|
|
@Override
|
|
public DacLinks jmeClone() {
|
|
try {
|
|
DacLinks clone = (DacLinks) super.clone();
|
|
return clone;
|
|
} catch (CloneNotSupportedException exception) {
|
|
throw new RuntimeException(exception);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the mass of the named bone/torso.
|
|
*
|
|
* @param boneName the name of the bone/torso (not null)
|
|
* @return the mass (>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<String> childNames = childNames(parentLink);
|
|
for (String childName : childNames) {
|
|
/*
|
|
* Add the joint and configure its range of motion.
|
|
* Also initialize the BoneLink's parent and its array
|
|
* of managed bones.
|
|
*/
|
|
BoneLink childLink = findBoneLink(childName);
|
|
childLink.addJoint(parentLink);
|
|
/*
|
|
* Add the BoneLink to the pre-order list.
|
|
*/
|
|
boneLinkList.add(childLink);
|
|
|
|
addJoints(childLink);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enumerate all immediate child BoneLinks of the specified bone/torso link.
|
|
*
|
|
* @param link the bone/torso link (not null)
|
|
* @return a new list of bone names
|
|
*/
|
|
private List<String> childNames(PhysicsLink link) {
|
|
assert link != null;
|
|
|
|
String linkName;
|
|
if (link == torsoLink) {
|
|
linkName = torsoName;
|
|
} else {
|
|
linkName = link.boneName();
|
|
}
|
|
|
|
List<String> result = new ArrayList<>(8);
|
|
for (String childName : listLinkedBoneNames()) {
|
|
Joint bone = findBone(childName);
|
|
Joint parent = bone.getParent();
|
|
if (parent != null && findManager(parent).equals(linkName)) {
|
|
result.add(childName);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Create a jointless BoneLink for the named bone, and add it to the
|
|
* boneLinks map.
|
|
*
|
|
* @param boneName the name of the bone to be linked (not null)
|
|
* @param vertexLocations the set of vertex locations (not null, not empty)
|
|
*/
|
|
private void createBoneLink(String boneName, VectorSet vertexLocations) {
|
|
Joint bone = findBone(boneName);
|
|
Transform boneToMesh = bone.getModelTransform();
|
|
Transform meshToBone = boneToMesh.invert();
|
|
//logger3.log(Level.INFO, "meshToBone = {0}", meshToBone);
|
|
/*
|
|
* Create the CollisionShape and locate the center of mass.
|
|
*/
|
|
CollisionShape shape;
|
|
Vector3f center;
|
|
if (vertexLocations == null || vertexLocations.numVectors() == 0) {
|
|
throw new IllegalStateException("no vertex for " + boneName);
|
|
} else {
|
|
center = vertexLocations.mean(null);
|
|
center.subtractLocal(bone.getModelTransform().getTranslation());
|
|
shape = createShape(meshToBone, center, vertexLocations);
|
|
}
|
|
|
|
meshToBone.getTranslation().zero();
|
|
float mass = super.mass(boneName);
|
|
Vector3f offset = meshToBone.transformVector(center, null);
|
|
BoneLink link = new BoneLink(this, bone, shape, mass, offset);
|
|
boneLinks.put(boneName, link);
|
|
}
|
|
|
|
/**
|
|
* Create a CollisionShape for the specified transform, center, and vertex
|
|
* locations.
|
|
*
|
|
* @param vertexToShape the transform from vertex coordinates to de-scaled
|
|
* shape coordinates (not null, unaffected)
|
|
* @param center the location of the shape's center, in vertex coordinates
|
|
* (not null, unaffected)
|
|
* @param vertexLocations the set of vertex locations (not null, not empty,
|
|
* TRASHED)
|
|
* @return a new CollisionShape
|
|
*/
|
|
private CollisionShape createShape(Transform vertexToShape, Vector3f center,
|
|
VectorSet vertexLocations) {
|
|
int numVectors = vertexLocations.numVectors();
|
|
assert numVectors > 0 : numVectors;
|
|
|
|
Vector3f tempLocation = new Vector3f();
|
|
int numPoints = vertexLocations.numVectors();
|
|
float points[] = new float[3 * numPoints];
|
|
FloatBuffer buffer = vertexLocations.toBuffer();
|
|
buffer.rewind();
|
|
int floatIndex = 0;
|
|
while (buffer.hasRemaining()) {
|
|
tempLocation.x = buffer.get();
|
|
tempLocation.y = buffer.get();
|
|
tempLocation.z = buffer.get();
|
|
/*
|
|
* Translate so that vertex coordinates are relative to
|
|
* the shape's center.
|
|
*/
|
|
tempLocation.subtractLocal(center);
|
|
/*
|
|
* Transform vertex coordinates to de-scaled shape coordinates.
|
|
*/
|
|
vertexToShape.transformVector(tempLocation, tempLocation);
|
|
points[floatIndex] = tempLocation.x;
|
|
points[floatIndex + 1] = tempLocation.y;
|
|
points[floatIndex + 2] = tempLocation.z;
|
|
floatIndex += 3;
|
|
}
|
|
|
|
CollisionShape result = new HullCollisionShape(points);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Create the TorsoLink.
|
|
*
|
|
* @param vertexLocations the set of vertex locations (not null, not empty)
|
|
* @param meshes array of animated meshes to use (not null, unaffected)
|
|
*/
|
|
private void createTorsoLink(VectorSet vertexLocations, Mesh[] meshes) {
|
|
if (vertexLocations == null || vertexLocations.numVectors() == 0) {
|
|
throw new IllegalArgumentException(
|
|
"No mesh vertices for the torso."
|
|
+ " Make sure the root bone is not linked.");
|
|
}
|
|
/*
|
|
* Create the CollisionShape.
|
|
*/
|
|
Joint bone = RagUtils.findMainBone(skeleton, meshes);
|
|
assert bone.getParent() == null;
|
|
Transform boneToMesh = bone.getModelTransform();
|
|
Transform meshToBone = boneToMesh.invert();
|
|
Vector3f center = vertexLocations.mean(null);
|
|
center.subtractLocal(boneToMesh.getTranslation());
|
|
CollisionShape shape = createShape(meshToBone, center, vertexLocations);
|
|
|
|
meshToBone.getTranslation().zero();
|
|
Vector3f offset = meshToBone.transformVector(center, null);
|
|
|
|
Transform meshToModel;
|
|
Spatial cgm = getSpatial();
|
|
if (cgm instanceof Node) {
|
|
Transform modelToMesh
|
|
= RagUtils.relativeTransform(transformer, (Node) cgm, null);
|
|
meshToModel = modelToMesh.invert();
|
|
} else {
|
|
meshToModel = transformIdentity;
|
|
}
|
|
|
|
float mass = super.mass(torsoName);
|
|
torsoLink = new TorsoLink(this, bone, shape, mass, meshToModel, offset);
|
|
}
|
|
|
|
/**
|
|
* Sort the controls of the controlled spatial, such that this control will
|
|
* come BEFORE the specified SkinningControl.
|
|
*
|
|
* @param skinningControl (not null)
|
|
*/
|
|
private void sortControls(SkinningControl skinningControl) {
|
|
assert skinningControl != null;
|
|
|
|
int dacIndex = RagUtils.findIndex(spatial, this);
|
|
assert dacIndex != -1;
|
|
int scIndex = RagUtils.findIndex(spatial, skinningControl);
|
|
assert scIndex != -1;
|
|
assert dacIndex != scIndex;
|
|
|
|
if (dacIndex > scIndex) {
|
|
/*
|
|
* Remove the SkinningControl and re-add it to make sure it will get
|
|
* updated *after* this control.
|
|
*/
|
|
spatial.removeControl(skinningControl);
|
|
spatial.addControl(skinningControl);
|
|
|
|
dacIndex = RagUtils.findIndex(spatial, this);
|
|
assert dacIndex != -1;
|
|
scIndex = RagUtils.findIndex(spatial, skinningControl);
|
|
assert scIndex != -1;
|
|
assert dacIndex < scIndex;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate the model's skeleton.
|
|
*/
|
|
private void validateSkeleton() {
|
|
RagUtils.validate(skeleton);
|
|
|
|
for (String boneName : listLinkedBoneNames()) {
|
|
Joint bone = findBone(boneName);
|
|
if (bone == null) {
|
|
String msg = String.format(
|
|
"Linked bone %s not found in skeleton.", boneName);
|
|
throw new IllegalArgumentException(msg);
|
|
}
|
|
if (bone.getParent() == null) {
|
|
logger3.log(Level.WARNING, "Linked bone {0} is a root bone.",
|
|
boneName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|