
com.jme3.bullet.animation.RagUtils Maven / Gradle / Ivy
/*
* 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 [email protected]
*
* Based on KinematicRagdollControl by Normen Hansen and Rémy Bouquet (Nehon).
*/
public class RagUtils {
// *************************************************************************
// constants and loggers
/**
* message logger for this class
*/
final private static Logger logger
= Logger.getLogger(RagUtils.class.getName());
// *************************************************************************
// constructors
/**
* A private constructor to inhibit instantiation of this class.
*/
private RagUtils() {
}
// *************************************************************************
// new methods exposed
/**
* Assign each mesh vertex to a bone/torso link and add its location (mesh
* coordinates in bind pose) to that link's list.
*
* @param meshes array of animated meshes to use (not null, unaffected)
* @param managerMap a map from bone indices to managing link names (not
* null, unaffected)
* @return a new map from bone/torso names to sets of vertex coordinates
*/
static Map 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;
}
}