/* * 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); } } } }