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 chanbase = ((Structure) pose.getFieldValue("chanbase")).evaluateListBase(blenderContext); Map bonesPoseChannels = new HashMap(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 bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(blenderContext); List bonesList = new ArrayList(); 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 animations = new ArrayList(); List 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 geomList = (List) 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 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 animList = animData.anims; if (animList != null && animList.size() > 0) { HashMap anims = new HashMap(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 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> vertexReferenceMap, Map 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 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 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 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 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; } }