blob: c15e91f3b0dcf467a12279481c451b1b32cb9b00 [file] [log] [blame]
package com.jme3.scene.plugins.blender.modifiers;
import java.nio.ByteBuffer;
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;
import com.jme3.animation.AnimControl;
import com.jme3.animation.Animation;
import com.jme3.animation.Bone;
import com.jme3.animation.BoneTrack;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SkeletonControl;
import com.jme3.math.Matrix4f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Format;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.VertexBuffer.Usage;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
import com.jme3.scene.plugins.blender.constraints.Constraint;
import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.meshes.MeshContext;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.scene.plugins.ogre.AnimData;
import com.jme3.util.BufferUtils;
/**
* This modifier allows to add bone animation to the object.
*
* @author Marcin Roguski (Kaelthas)
*/
/* package */class ArmatureModifier extends Modifier {
private static final Logger LOGGER = Logger.getLogger(ArmatureModifier.class.getName());
private static final int MAXIMUM_WEIGHTS_PER_VERTEX = 4;
// @Marcin it was an Ogre limitation, but as long as we use a MaxNumWeight
// variable in mesh,
// i guess this limitation has no sense for the blender loader...so i guess
// it's up to you. You'll have to deternine the max weight according to the
// provided blend file
// I added a check to avoid crash when loading a model that has more than 4
// weight per vertex on line 258
// If you decide to remove this limitation, remove this code.
// Rémy
/** Loaded animation data. */
private AnimData animData;
/** Old memory address of the mesh that will have the skeleton applied. */
private Long meshOMA;
/**
* The maxiumum amount of bone groups applied to a single vertex (max =
* MAXIMUM_WEIGHTS_PER_VERTEX).
*/
private int boneGroups;
/** The weights of vertices. */
private VertexBuffer verticesWeights;
/** The indexes of bones applied to vertices. */
private VertexBuffer verticesWeightsIndices;
/**
* This constructor reads animation data from the object structore. The
* stored data is the AnimData and additional data is armature's OMA.
*
* @param objectStructure
* the structure of the object
* @param modifierStructure
* the structure of the modifier
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blender file is somehow
* corrupted
*/
public ArmatureModifier(Structure objectStructure, Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException {
Structure meshStructure = ((Pointer) objectStructure.getFieldValue("data")).fetchData(blenderContext.getInputStream()).get(0);
Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert
// =
// DeformVERTices
// if pDvert==null then there are not vertex groups and no need to load
// skeleton (untill bone envelopes are supported)
if (this.validate(modifierStructure, blenderContext) && pDvert.isNotNull()) {
Pointer pArmatureObject = (Pointer) modifierStructure.getFieldValue("object");
if (pArmatureObject.isNotNull()) {
ArmatureHelper armatureHelper = blenderContext.getHelper(ArmatureHelper.class);
Structure armatureObject = pArmatureObject.fetchData(blenderContext.getInputStream()).get(0);
// load skeleton
Structure armatureStructure = ((Pointer) armatureObject.getFieldValue("data")).fetchData(blenderContext.getInputStream()).get(0);
Structure pose = ((Pointer) armatureObject.getFieldValue("pose")).fetchData(blenderContext.getInputStream()).get(0);
List<Structure> chanbase = ((Structure) pose.getFieldValue("chanbase")).evaluateListBase(blenderContext);
Map<Long, Structure> bonesPoseChannels = new HashMap<Long, Structure>(chanbase.size());
for (Structure poseChannel : chanbase) {
Pointer pBone = (Pointer) poseChannel.getFieldValue("bone");
bonesPoseChannels.put(pBone.getOldMemoryAddress(), poseChannel);
}
ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
Matrix4f armatureObjectMatrix = objectHelper.getMatrix(armatureObject, "obmat", true);
Matrix4f inverseMeshObjectMatrix = objectHelper.getMatrix(objectStructure, "obmat", true).invertLocal();
Matrix4f objectToArmatureTransformation = armatureObjectMatrix.multLocal(inverseMeshObjectMatrix);
List<Structure> bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(blenderContext);
List<Bone> bonesList = new ArrayList<Bone>();
for (int i = 0; i < bonebase.size(); ++i) {
armatureHelper.buildBones(bonebase.get(i), null, bonesList, objectToArmatureTransformation, bonesPoseChannels, blenderContext);
}
bonesList.add(0, new Bone(""));
Bone[] bones = bonesList.toArray(new Bone[bonesList.size()]);
Skeleton skeleton = new Skeleton(bones);
// read mesh indexes
this.meshOMA = meshStructure.getOldMemoryAddress();
this.readVerticesWeightsData(objectStructure, meshStructure, skeleton, blenderContext);
// read animations
ArrayList<Animation> animations = new ArrayList<Animation>();
List<FileBlockHeader> actionHeaders = blenderContext.getFileBlocks(Integer.valueOf(FileBlockHeader.BLOCK_AC00));
if (actionHeaders != null) {// it may happen that the model has
// armature with no actions
for (FileBlockHeader header : actionHeaders) {
Structure actionStructure = header.getStructure(blenderContext);
String actionName = actionStructure.getName();
BoneTrack[] tracks = armatureHelper.getTracks(actionStructure, skeleton, blenderContext);
if(tracks != null && tracks.length > 0) {
// determining the animation time
float maximumTrackLength = 0;
for (BoneTrack track : tracks) {
float length = track.getLength();
if (length > maximumTrackLength) {
maximumTrackLength = length;
}
}
Animation boneAnimation = new Animation(actionName, maximumTrackLength);
boneAnimation.setTracks(tracks);
animations.add(boneAnimation);
}
}
}
animData = new AnimData(skeleton, animations);
// store the animation data for each bone
for (Bone bone : bones) {
Long boneOma = armatureHelper.getBoneOMA(bone);
if (boneOma != null) {
blenderContext.setAnimData(boneOma, animData);
}
}
}
}
}
@Override
@SuppressWarnings("unchecked")
public Node apply(Node node, BlenderContext blenderContext) {
if (invalid) {
LOGGER.log(Level.WARNING, "Armature modifier is invalid! Cannot be applied to: {0}", node.getName());
}// if invalid, animData will be null
if (animData == null) {
return node;
}
// setting weights for bones
List<Geometry> geomList = (List<Geometry>) blenderContext.getLoadedFeature(this.meshOMA, LoadedFeatureDataType.LOADED_FEATURE);
for (Geometry geom : geomList) {
Mesh mesh = geom.getMesh();
if (this.verticesWeights != null) {
mesh.setMaxNumWeights(this.boneGroups);
mesh.setBuffer(this.verticesWeights);
mesh.setBuffer(this.verticesWeightsIndices);
}
}
// applying constraints to Bones
ArmatureHelper armatureHelper = blenderContext.getHelper(ArmatureHelper.class);
for (int i = 0; i < animData.skeleton.getBoneCount(); ++i) {
Long boneOMA = armatureHelper.getBoneOMA(animData.skeleton.getBone(i));
List<Constraint> constraints = blenderContext.getConstraints(boneOMA);
if (constraints != null && constraints.size() > 0) {
for (Constraint constraint : constraints) {
constraint.bake();
}
}
}
// applying animations
AnimControl control = new AnimControl(animData.skeleton);
ArrayList<Animation> animList = animData.anims;
if (animList != null && animList.size() > 0) {
HashMap<String, Animation> anims = new HashMap<String, Animation>(animList.size());
for (int i = 0; i < animList.size(); ++i) {
Animation animation = animList.get(i);
anims.put(animation.getName(), animation);
}
control.setAnimations(anims);
}
node.addControl(control);
node.addControl(new SkeletonControl(animData.skeleton));
return node;
}
/**
* This method reads mesh indexes
*
* @param objectStructure
* structure of the object that has the armature modifier applied
* @param meshStructure
* the structure of the object's mesh
* @param blenderContext
* the blender context
* @throws BlenderFileException
* this exception is thrown when the blend file structure is
* somehow invalid or corrupted
*/
private void readVerticesWeightsData(Structure objectStructure, Structure meshStructure, Skeleton skeleton, BlenderContext blenderContext) throws BlenderFileException {
ArmatureHelper armatureHelper = blenderContext.getHelper(ArmatureHelper.class);
Structure defBase = (Structure) objectStructure.getFieldValue("defbase");
Map<Integer, Integer> groupToBoneIndexMap = armatureHelper.getGroupToBoneIndexMap(defBase, skeleton, blenderContext);
int[] bonesGroups = new int[] { 0 };
MeshContext meshContext = blenderContext.getMeshContext(meshStructure.getOldMemoryAddress());
VertexBuffer[] boneWeightsAndIndex = this.getBoneWeightAndIndexBuffer(meshStructure, meshContext.getVertexList().size(), bonesGroups, meshContext.getVertexReferenceMap(), groupToBoneIndexMap, blenderContext);
this.verticesWeights = boneWeightsAndIndex[0];
this.verticesWeightsIndices = boneWeightsAndIndex[1];
this.boneGroups = bonesGroups[0];
}
/**
* This method returns an array of size 2. The first element is a vertex
* buffer holding bone weights for every vertex in the model. The second
* element is a vertex buffer holding bone indices for vertices (the indices
* of bones the vertices are assigned to).
*
* @param meshStructure
* the mesh structure object
* @param vertexListSize
* a number of vertices in the model
* @param bonesGroups
* this is an output parameter, it should be a one-sized array;
* the maximum amount of weights per vertex (up to
* MAXIMUM_WEIGHTS_PER_VERTEX) is stored there
* @param vertexReferenceMap
* this reference map allows to map the original vertices read
* from blender to vertices that are really in the model; one
* vertex may appear several times in the result model
* @param groupToBoneIndexMap
* this object maps the group index (to which a vertices in
* blender belong) to bone index of the model
* @param blenderContext
* the blender context
* @return arrays of vertices weights and their bone indices and (as an
* output parameter) the maximum amount of weights for a vertex
* @throws BlenderFileException
* this exception is thrown when the blend file structure is
* somehow invalid or corrupted
*/
private VertexBuffer[] getBoneWeightAndIndexBuffer(Structure meshStructure, int vertexListSize, int[] bonesGroups, Map<Integer, List<Integer>> vertexReferenceMap, Map<Integer, Integer> groupToBoneIndexMap, BlenderContext blenderContext)
throws BlenderFileException {
Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
FloatBuffer weightsFloatData = BufferUtils.createFloatBuffer(vertexListSize * MAXIMUM_WEIGHTS_PER_VERTEX);
ByteBuffer indicesData = BufferUtils.createByteBuffer(vertexListSize * MAXIMUM_WEIGHTS_PER_VERTEX);
if (pDvert.isNotNull()) {// assigning weights and bone indices
List<Structure> dverts = pDvert.fetchData(blenderContext.getInputStream());// dverts.size() == verticesAmount (one dvert per
// vertex in blender)
int vertexIndex = 0;
for (Structure dvert : dverts) {
int totweight = ((Number) dvert.getFieldValue("totweight")).intValue();// total amount of weights assignet to the vertex
// (max. 4 in JME)
Pointer pDW = (Pointer) dvert.getFieldValue("dw");
List<Integer> vertexIndices = vertexReferenceMap.get(Integer.valueOf(vertexIndex));// we fetch the referenced vertices here
if (totweight > 0 && pDW.isNotNull() && groupToBoneIndexMap!=null) {// pDW should never be null here, but I check it just in case :)
int weightIndex = 0;
List<Structure> dw = pDW.fetchData(blenderContext.getInputStream());
for (Structure deformWeight : dw) {
Integer boneIndex = groupToBoneIndexMap.get(((Number) deformWeight.getFieldValue("def_nr")).intValue());
// Remove this code if 4 weights limitation is removed
if (weightIndex == 4) {
LOGGER.log(Level.WARNING, "{0} has more than 4 weight on bone index {1}", new Object[] { meshStructure.getName(), boneIndex });
break;
}
// null here means that we came accross group that has no bone attached to
if (boneIndex != null) {
float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue();
if (weight == 0.0f) {
weight = 1;
boneIndex = Integer.valueOf(0);
}
// we apply the weight to all referenced vertices
for (Integer index : vertexIndices) {
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + weightIndex, weight);
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + weightIndex, boneIndex.byteValue());
}
}
++weightIndex;
}
} else {
for (Integer index : vertexIndices) {
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 1.0f);
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0);
}
}
++vertexIndex;
}
} else {
// always bind all vertices to 0-indexed bone
// this bone makes the model look normally if vertices have no bone
// assigned
// and it is used in object animation, so if we come accross object
// animation
// we can use the 0-indexed bone for this
for (List<Integer> vertexIndexList : vertexReferenceMap.values()) {
// we apply the weight to all referenced vertices
for (Integer index : vertexIndexList) {
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 1.0f);
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0);
}
}
}
bonesGroups[0] = this.endBoneAssigns(vertexListSize, weightsFloatData);
VertexBuffer verticesWeights = new VertexBuffer(Type.BoneWeight);
verticesWeights.setupData(Usage.CpuOnly, bonesGroups[0], Format.Float, weightsFloatData);
VertexBuffer verticesWeightsIndices = new VertexBuffer(Type.BoneIndex);
verticesWeightsIndices.setupData(Usage.CpuOnly, bonesGroups[0], Format.UnsignedByte, indicesData);
return new VertexBuffer[] { verticesWeights, verticesWeightsIndices };
}
/**
* Normalizes weights if needed and finds largest amount of weights used for
* all vertices in the buffer.
*
* @param vertCount
* amount of vertices
* @param weightsFloatData
* weights for vertices
*/
private int endBoneAssigns(int vertCount, FloatBuffer weightsFloatData) {
int maxWeightsPerVert = 0;
weightsFloatData.rewind();
for (int v = 0; v < vertCount; ++v) {
float w0 = weightsFloatData.get(), w1 = weightsFloatData.get(), w2 = weightsFloatData.get(), w3 = weightsFloatData.get();
if (w3 != 0) {
maxWeightsPerVert = Math.max(maxWeightsPerVert, 4);
} else if (w2 != 0) {
maxWeightsPerVert = Math.max(maxWeightsPerVert, 3);
} else if (w1 != 0) {
maxWeightsPerVert = Math.max(maxWeightsPerVert, 2);
} else if (w0 != 0) {
maxWeightsPerVert = Math.max(maxWeightsPerVert, 1);
}
float sum = w0 + w1 + w2 + w3;
if (sum != 1f && sum != 0.0f) {
weightsFloatData.position(weightsFloatData.position() - 4);
// compute new vals based on sum
float sumToB = 1f / sum;
weightsFloatData.put(w0 * sumToB);
weightsFloatData.put(w1 * sumToB);
weightsFloatData.put(w2 * sumToB);
weightsFloatData.put(w3 * sumToB);
}
}
weightsFloatData.rewind();
return maxWeightsPerVert;
}
@Override
public String getType() {
return Modifier.ARMATURE_MODIFIER_DATA;
}
}