1 package com.jme3.scene.plugins.blender.modifiers;
\r
3 import java.nio.ByteBuffer;
\r
4 import java.nio.FloatBuffer;
\r
5 import java.util.ArrayList;
\r
6 import java.util.HashMap;
\r
7 import java.util.List;
\r
8 import java.util.Map;
\r
9 import java.util.Map.Entry;
\r
10 import java.util.TreeMap;
\r
11 import java.util.logging.Level;
\r
12 import java.util.logging.Logger;
\r
14 import com.jme3.animation.AnimControl;
\r
15 import com.jme3.animation.Animation;
\r
16 import com.jme3.animation.Bone;
\r
17 import com.jme3.animation.BoneTrack;
\r
18 import com.jme3.animation.Skeleton;
\r
19 import com.jme3.animation.SkeletonControl;
\r
20 import com.jme3.math.Matrix4f;
\r
21 import com.jme3.scene.Geometry;
\r
22 import com.jme3.scene.Mesh;
\r
23 import com.jme3.scene.Node;
\r
24 import com.jme3.scene.VertexBuffer;
\r
25 import com.jme3.scene.VertexBuffer.Format;
\r
26 import com.jme3.scene.VertexBuffer.Type;
\r
27 import com.jme3.scene.VertexBuffer.Usage;
\r
28 import com.jme3.scene.plugins.blender.BlenderContext;
\r
29 import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
\r
30 import com.jme3.scene.plugins.blender.animations.AnimationData;
\r
31 import com.jme3.scene.plugins.blender.animations.ArmatureHelper;
\r
32 import com.jme3.scene.plugins.blender.animations.BoneContext;
\r
33 import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
\r
34 import com.jme3.scene.plugins.blender.file.FileBlockHeader;
\r
35 import com.jme3.scene.plugins.blender.file.Pointer;
\r
36 import com.jme3.scene.plugins.blender.file.Structure;
\r
37 import com.jme3.scene.plugins.blender.meshes.MeshContext;
\r
38 import com.jme3.scene.plugins.blender.objects.ObjectHelper;
\r
39 import com.jme3.util.BufferUtils;
\r
42 * This modifier allows to add bone animation to the object.
\r
44 * @author Marcin Roguski (Kaelthas)
\r
46 /* package */class ArmatureModifier extends Modifier {
\r
47 private static final Logger LOGGER = Logger.getLogger(ArmatureModifier.class.getName());
\r
48 private static final int MAXIMUM_WEIGHTS_PER_VERTEX = 4; // JME
\r
51 private Skeleton skeleton;
\r
52 private Structure objectStructure;
\r
53 private Structure meshStructure;
\r
55 /** Loaded animation data. */
\r
56 private AnimationData animationData;
\r
57 /** Old memory address of the mesh that will have the skeleton applied. */
\r
58 private Long meshOMA;
\r
61 * This constructor reads animation data from the object structore. The
\r
62 * stored data is the AnimData and additional data is armature's OMA.
\r
64 * @param objectStructure
\r
65 * the structure of the object
\r
66 * @param modifierStructure
\r
67 * the structure of the modifier
\r
68 * @param blenderContext
\r
69 * the blender context
\r
70 * @throws BlenderFileException
\r
71 * this exception is thrown when the blender file is somehow
\r
74 public ArmatureModifier(Structure objectStructure, Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException {
\r
75 Structure meshStructure = ((Pointer) objectStructure.getFieldValue("data")).fetchData(blenderContext.getInputStream()).get(0);
\r
76 Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert = DeformVERTices
\r
78 // if pDvert==null then there are not vertex groups and no need to load
\r
79 // skeleton (untill bone envelopes are supported)
\r
80 if (this.validate(modifierStructure, blenderContext) && pDvert.isNotNull()) {
\r
81 Pointer pArmatureObject = (Pointer) modifierStructure.getFieldValue("object");
\r
82 if (pArmatureObject.isNotNull()) {
\r
83 ArmatureHelper armatureHelper = blenderContext.getHelper(ArmatureHelper.class);
\r
85 Structure armatureObject = pArmatureObject.fetchData(blenderContext.getInputStream()).get(0);
\r
88 Structure armatureStructure = ((Pointer) armatureObject.getFieldValue("data")).fetchData(blenderContext.getInputStream()).get(0);
\r
90 ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
\r
91 boolean fixUpAxis = blenderContext.getBlenderKey().isFixUpAxis();
\r
92 Matrix4f armatureObjectMatrix = objectHelper.getMatrix(armatureObject, "obmat", fixUpAxis);
\r
93 Matrix4f inverseMeshObjectMatrix = objectHelper.getMatrix(objectStructure, "imat", fixUpAxis);
\r
94 Matrix4f objectToArmatureTransformation = armatureObjectMatrix.multLocal(inverseMeshObjectMatrix);
\r
96 List<Structure> bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(blenderContext);
\r
97 List<Bone> bonesList = new ArrayList<Bone>();
\r
98 for (int i = 0; i < bonebase.size(); ++i) {
\r
99 armatureHelper.buildBones(armatureObject.getOldMemoryAddress(), bonebase.get(i), null, bonesList, objectToArmatureTransformation, blenderContext);
\r
101 bonesList.add(0, new Bone(""));
\r
102 Bone[] bones = bonesList.toArray(new Bone[bonesList.size()]);
\r
103 skeleton = new Skeleton(bones);
\r
104 blenderContext.setSkeleton(armatureObject.getOldMemoryAddress(), skeleton);
\r
105 this.objectStructure = objectStructure;
\r
106 this.meshStructure = meshStructure;
\r
108 // read mesh indexes
\r
109 this.meshOMA = meshStructure.getOldMemoryAddress();
\r
112 ArrayList<Animation> animations = new ArrayList<Animation>();
\r
113 List<FileBlockHeader> actionHeaders = blenderContext.getFileBlocks(Integer.valueOf(FileBlockHeader.BLOCK_AC00));
\r
114 if (actionHeaders != null) {// it may happen that the model has
\r
115 // armature with no actions
\r
116 for (FileBlockHeader header : actionHeaders) {
\r
117 Structure actionStructure = header.getStructure(blenderContext);
\r
118 String actionName = actionStructure.getName();
\r
120 BoneTrack[] tracks = armatureHelper.getTracks(actionStructure, skeleton, blenderContext);
\r
121 if (tracks != null && tracks.length > 0) {
\r
122 // determining the animation time
\r
123 float maximumTrackLength = 0;
\r
124 for (BoneTrack track : tracks) {
\r
125 float length = track.getLength();
\r
126 if (length > maximumTrackLength) {
\r
127 maximumTrackLength = length;
\r
131 Animation boneAnimation = new Animation(actionName, maximumTrackLength);
\r
132 boneAnimation.setTracks(tracks);
\r
133 animations.add(boneAnimation);
\r
137 // fetching action defined in object
\r
138 Pointer pAction = (Pointer) objectStructure.getFieldValue("action");
\r
139 if (pAction.isNotNull()) {
\r
140 Structure actionStructure = pAction.fetchData(blenderContext.getInputStream()).get(0);
\r
141 String actionName = actionStructure.getName();
\r
143 BoneTrack[] tracks = armatureHelper.getTracks(actionStructure, skeleton, blenderContext);
\r
144 if (tracks != null && tracks.length > 0) {
\r
145 // determining the animation time
\r
146 float maximumTrackLength = 0;
\r
147 for (BoneTrack track : tracks) {
\r
148 float length = track.getLength();
\r
149 if (length > maximumTrackLength) {
\r
150 maximumTrackLength = length;
\r
154 Animation boneAnimation = new Animation(actionName, maximumTrackLength);
\r
155 boneAnimation.setTracks(tracks);
\r
156 animations.add(boneAnimation);
\r
160 animationData = new AnimationData(skeleton, animations);
\r
162 // store the animation data for each bone
\r
163 for (Bone bone : bones) {
\r
164 if(bone.getName().length() > 0) {
\r
165 BoneContext boneContext = blenderContext.getBoneContext(bone);
\r
166 Long boneOma = boneContext.getBoneOma();
\r
167 if (boneOma != null) {
\r
168 blenderContext.setAnimData(boneOma, animationData);
\r
179 @SuppressWarnings("unchecked")
\r
180 public Node apply(Node node, BlenderContext blenderContext) {
\r
182 LOGGER.log(Level.WARNING, "Armature modifier is invalid! Cannot be applied to: {0}", node.getName());
\r
183 }// if invalid, animData will be null
\r
184 if (animationData == null || skeleton == null) {
\r
188 // setting weights for bones
\r
189 List<Geometry> geomList = (List<Geometry>) blenderContext.getLoadedFeature(meshOMA, LoadedFeatureDataType.LOADED_FEATURE);
\r
190 MeshContext meshContext = blenderContext.getMeshContext(meshOMA);
\r
191 int[] bonesGroups = new int[] { 0 };
\r
192 for (Geometry geom : geomList) {
\r
193 int materialIndex = meshContext.getMaterialIndex(geom);
\r
194 Mesh mesh = geom.getMesh();
\r
197 VertexBuffer[] buffers = this.readVerticesWeightsData(objectStructure, meshStructure, skeleton, materialIndex, bonesGroups, blenderContext);
\r
198 if (buffers != null) {
\r
199 mesh.setMaxNumWeights(bonesGroups[0]);
\r
200 mesh.setBuffer(buffers[0]);
\r
201 mesh.setBuffer(buffers[1]);
\r
203 //FIXME @Kaelthas this should be replaced by a call to
\r
204 //mesh.generateBindPos(true)
\r
205 VertexBuffer bindNormalBuffer = meshContext.getBindNormalBuffer(materialIndex);
\r
206 if (bindNormalBuffer != null) {
\r
207 mesh.setBuffer(bindNormalBuffer);
\r
209 VertexBuffer bindPoseBuffer = meshContext.getBindPoseBuffer(materialIndex);
\r
210 if (bindPoseBuffer != null) {
\r
211 mesh.setBuffer(bindPoseBuffer);
\r
213 // change the usage type of vertex and normal buffers from
\r
214 // Static to Stream
\r
215 mesh.getBuffer(Type.Position).setUsage(Usage.Stream);
\r
216 mesh.getBuffer(Type.Normal).setUsage(Usage.Stream);
\r
219 //creating empty buffers for HW skinning
\r
220 //the buffers will be setup if ever used.
\r
221 VertexBuffer verticesWeightsHW = new VertexBuffer(Type.HWBoneWeight);
\r
222 VertexBuffer verticesWeightsIndicesHW = new VertexBuffer(Type.HWBoneIndex);
\r
223 mesh.setBuffer(verticesWeightsHW);
\r
224 mesh.setBuffer(verticesWeightsIndicesHW);
\r
226 } catch (BlenderFileException e) {
\r
227 LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
\r
228 this.invalid = true;
\r
233 // applying animations
\r
234 AnimControl control = new AnimControl(animationData.skeleton);
\r
235 List<Animation> animList = animationData.anims;
\r
236 if (animList != null && animList.size() > 0) {
\r
237 HashMap<String, Animation> anims = new HashMap<String, Animation>(animList.size());
\r
238 for (int i = 0; i < animList.size(); ++i) {
\r
239 Animation animation = animList.get(i);
\r
240 anims.put(animation.getName(), animation);
\r
242 control.setAnimations(anims);
\r
244 node.addControl(control);
\r
245 node.addControl(new SkeletonControl(animationData.skeleton));
\r
247 blenderContext.setNodeForSkeleton(skeleton, node);
\r
253 * This method reads mesh indexes
\r
255 * @param objectStructure
\r
256 * structure of the object that has the armature modifier applied
\r
257 * @param meshStructure
\r
258 * the structure of the object's mesh
\r
259 * @param blenderContext
\r
260 * the blender context
\r
261 * @throws BlenderFileException
\r
262 * this exception is thrown when the blend file structure is
\r
263 * somehow invalid or corrupted
\r
265 private VertexBuffer[] readVerticesWeightsData(Structure objectStructure, Structure meshStructure, Skeleton skeleton, int materialIndex, int[] bonesGroups, BlenderContext blenderContext) throws BlenderFileException {
\r
266 ArmatureHelper armatureHelper = blenderContext.getHelper(ArmatureHelper.class);
\r
267 Structure defBase = (Structure) objectStructure.getFieldValue("defbase");
\r
268 Map<Integer, Integer> groupToBoneIndexMap = armatureHelper.getGroupToBoneIndexMap(defBase, skeleton, blenderContext);
\r
270 MeshContext meshContext = blenderContext.getMeshContext(meshStructure.getOldMemoryAddress());
\r
272 return this.getBoneWeightAndIndexBuffer(meshStructure, meshContext.getVertexCount(materialIndex), bonesGroups, meshContext.getVertexReferenceMap(materialIndex), groupToBoneIndexMap, blenderContext);
\r
276 * This method returns an array of size 2. The first element is a vertex
\r
277 * buffer holding bone weights for every vertex in the model. The second
\r
278 * element is a vertex buffer holding bone indices for vertices (the indices
\r
279 * of bones the vertices are assigned to).
\r
281 * @param meshStructure
\r
282 * the mesh structure object
\r
283 * @param vertexListSize
\r
284 * a number of vertices in the model
\r
285 * @param bonesGroups
\r
286 * this is an output parameter, it should be a one-sized array;
\r
287 * the maximum amount of weights per vertex (up to
\r
288 * MAXIMUM_WEIGHTS_PER_VERTEX) is stored there
\r
289 * @param vertexReferenceMap
\r
290 * this reference map allows to map the original vertices read
\r
291 * from blender to vertices that are really in the model; one
\r
292 * vertex may appear several times in the result model
\r
293 * @param groupToBoneIndexMap
\r
294 * this object maps the group index (to which a vertices in
\r
295 * blender belong) to bone index of the model
\r
296 * @param blenderContext
\r
297 * the blender context
\r
298 * @return arrays of vertices weights and their bone indices and (as an
\r
299 * output parameter) the maximum amount of weights for a vertex
\r
300 * @throws BlenderFileException
\r
301 * this exception is thrown when the blend file structure is
\r
302 * somehow invalid or corrupted
\r
304 private VertexBuffer[] getBoneWeightAndIndexBuffer(Structure meshStructure, int vertexListSize, int[] bonesGroups, Map<Integer, List<Integer>> vertexReferenceMap, Map<Integer, Integer> groupToBoneIndexMap, BlenderContext blenderContext)
\r
305 throws BlenderFileException {
\r
306 bonesGroups[0] = 0;
\r
307 Pointer pDvert = (Pointer) meshStructure.getFieldValue("dvert");// dvert
\r
310 FloatBuffer weightsFloatData = BufferUtils.createFloatBuffer(vertexListSize * MAXIMUM_WEIGHTS_PER_VERTEX);
\r
311 ByteBuffer indicesData = BufferUtils.createByteBuffer(vertexListSize * MAXIMUM_WEIGHTS_PER_VERTEX);
\r
313 if (pDvert.isNotNull()) {// assigning weights and bone indices
\r
314 boolean warnAboutTooManyVertexWeights = false;
\r
315 // dverts.size() = verticesAmount (one dvert per vertex in blender)
\r
316 List<Structure> dverts = pDvert.fetchData(blenderContext.getInputStream());
\r
317 int vertexIndex = 0;
\r
318 // use tree map to sort weights from the lowest to the highest ones
\r
319 TreeMap<Float, Integer> weightToIndexMap = new TreeMap<Float, Integer>();
\r
321 for (Structure dvert : dverts) {
\r
322 //we fetch the referenced vertices here
\r
323 List<Integer> vertexIndices = vertexReferenceMap.get(Integer.valueOf(vertexIndex));
\r
324 if (vertexIndices != null) {
\r
325 // total amount of wights assigned to the vertex (max. 4 in JME)
\r
326 int totweight = ((Number) dvert.getFieldValue("totweight")).intValue();
\r
327 Pointer pDW = (Pointer) dvert.getFieldValue("dw");
\r
328 if (totweight > 0 && groupToBoneIndexMap != null) {
\r
329 weightToIndexMap.clear();
\r
330 int weightIndex = 0;
\r
331 List<Structure> dw = pDW.fetchData(blenderContext.getInputStream());
\r
332 for (Structure deformWeight : dw) {
\r
333 Integer boneIndex = groupToBoneIndexMap.get(((Number) deformWeight.getFieldValue("def_nr")).intValue());
\r
334 float weight = ((Number) deformWeight.getFieldValue("weight")).floatValue();
\r
335 // boneIndex == null: it here means that we came
\r
336 // accross group that has no bone attached to, so
\r
337 // simply ignore it
\r
338 // if weight == 0 and weightIndex == 0 then ignore
\r
339 // the weight (do not set weight = 0 as a first
\r
341 if (boneIndex != null && (weight > 0.0f || weightIndex > 0)) {
\r
342 if (weightIndex < MAXIMUM_WEIGHTS_PER_VERTEX) {
\r
343 if (weight == 0.0f) {
\r
344 boneIndex = Integer.valueOf(0);
\r
346 // we apply the weight to all referenced
\r
348 for (Integer index : vertexIndices) {
\r
349 weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + weightIndex, weight);
\r
350 indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + weightIndex, boneIndex.byteValue());
\r
352 weightToIndexMap.put(weight, weightIndex);
\r
353 bonesGroups[0] = Math.max(bonesGroups[0], weightIndex + 1);
\r
354 } else if (weight > 0) {// if weight is zero the
\r
355 // simply ignore it
\r
356 warnAboutTooManyVertexWeights = true;
\r
357 Entry<Float, Integer> lowestWeightAndIndex = weightToIndexMap.firstEntry();
\r
358 if (lowestWeightAndIndex != null && lowestWeightAndIndex.getKey() < weight) {
\r
359 // we apply the weight to all referenced
\r
361 for (Integer index : vertexIndices) {
\r
362 weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + lowestWeightAndIndex.getValue(), weight);
\r
363 indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + lowestWeightAndIndex.getValue(), boneIndex.byteValue());
\r
365 weightToIndexMap.remove(lowestWeightAndIndex.getKey());
\r
366 weightToIndexMap.put(weight, lowestWeightAndIndex.getValue());
\r
373 // 0.0 weight indicates, do not transform this vertex,
\r
374 // but keep it in bind pose.
\r
375 for (Integer index : vertexIndices) {
\r
376 weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 0.0f);
\r
377 indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0);
\r
384 if (warnAboutTooManyVertexWeights) {
\r
385 LOGGER.log(Level.WARNING, "{0} has vertices with more than 4 weights assigned. The model may not behave as it should.", meshStructure.getName());
\r
388 // always bind all vertices to 0-indexed bone
\r
389 // this bone makes the model look normally if vertices have no bone
\r
390 // assigned and it is used in object animation, so if we come
\r
392 // animation we can use the 0-indexed bone for this
\r
393 for (List<Integer> vertexIndexList : vertexReferenceMap.values()) {
\r
394 // we apply the weight to all referenced vertices
\r
395 for (Integer index : vertexIndexList) {
\r
396 weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 1.0f);
\r
397 indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0);
\r
402 bonesGroups[0] = Math.max(bonesGroups[0], 1);
\r
404 this.endBoneAssigns(vertexListSize, weightsFloatData);
\r
405 VertexBuffer verticesWeights = new VertexBuffer(Type.BoneWeight);
\r
406 verticesWeights.setupData(Usage.CpuOnly, bonesGroups[0], Format.Float, weightsFloatData);
\r
408 VertexBuffer verticesWeightsIndices = new VertexBuffer(Type.BoneIndex);
\r
409 verticesWeightsIndices.setupData(Usage.CpuOnly, bonesGroups[0], Format.UnsignedByte, indicesData);
\r
410 return new VertexBuffer[] { verticesWeights, verticesWeightsIndices };
\r
414 * Normalizes weights if needed and finds largest amount of weights used for
\r
415 * all vertices in the buffer.
\r
418 * amount of vertices
\r
419 * @param weightsFloatData
\r
420 * weights for vertices
\r
422 private void endBoneAssigns(int vertCount, FloatBuffer weightsFloatData) {
\r
423 weightsFloatData.rewind();
\r
424 float[] weights = new float[MAXIMUM_WEIGHTS_PER_VERTEX];
\r
425 for (int v = 0; v < vertCount; ++v) {
\r
427 for (int i = 0; i < MAXIMUM_WEIGHTS_PER_VERTEX; ++i) {
\r
428 weights[i] = weightsFloatData.get();
\r
431 if (sum != 1f && sum != 0.0f) {
\r
432 weightsFloatData.position(weightsFloatData.position() - MAXIMUM_WEIGHTS_PER_VERTEX);
\r
433 // compute new vals based on sum
\r
434 float sumToB = 1f / sum;
\r
435 for (int i = 0; i < MAXIMUM_WEIGHTS_PER_VERTEX; ++i) {
\r
436 weightsFloatData.put(weights[i] * sumToB);
\r
440 weightsFloatData.rewind();
\r