parent
03f8df05b6
commit
51215a352e
@ -0,0 +1,134 @@ |
||||
package com.jme3.scene.plugins.blender.animations; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
|
||||
import com.jme3.animation.BoneTrack; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.animation.SpatialTrack; |
||||
import com.jme3.math.Quaternion; |
||||
import com.jme3.math.Vector3f; |
||||
import com.jme3.scene.Node; |
||||
|
||||
/** |
||||
* An abstract representation of animation. The data stored here is mainly a |
||||
* raw action data loaded from blender. It can later be transformed into |
||||
* bone or spatial animation and applied to the specified node. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
public class BlenderAction implements Cloneable { |
||||
/** The action name. */ |
||||
/* package */final String name; |
||||
/** Animation speed - frames per second. */ |
||||
/* package */int fps; |
||||
/** |
||||
* The last frame of the animation (the last ipo curve node position is |
||||
* used as a last frame). |
||||
*/ |
||||
/* package */int stopFrame; |
||||
/** |
||||
* Tracks of the features. In case of bone animation the keys are the |
||||
* names of the bones. In case of spatial animation - the node's name is |
||||
* used. A single ipo contains all tracks for location, rotation and |
||||
* scales. |
||||
*/ |
||||
/* package */Map<String, Ipo> featuresTracks = new HashMap<String, Ipo>(); |
||||
|
||||
public BlenderAction(String name, int fps) { |
||||
this.name = name; |
||||
this.fps = fps; |
||||
} |
||||
|
||||
public void removeTracksThatAreNotInTheCollection(Collection<String> trackNames) { |
||||
Map<String, Ipo> newTracks = new HashMap<String, Ipo>(); |
||||
for (String trackName : trackNames) { |
||||
if (featuresTracks.containsKey(trackName)) { |
||||
newTracks.put(trackName, featuresTracks.get(trackName)); |
||||
} |
||||
} |
||||
featuresTracks = newTracks; |
||||
} |
||||
|
||||
@Override |
||||
public BlenderAction clone() { |
||||
BlenderAction result = new BlenderAction(name, fps); |
||||
result.stopFrame = stopFrame; |
||||
result.featuresTracks = new HashMap<String, Ipo>(featuresTracks); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Converts the action into JME spatial animation tracks. |
||||
* |
||||
* @param node |
||||
* the node that will be animated |
||||
* @return the spatial tracks for the node |
||||
*/ |
||||
public SpatialTrack[] toTracks(Node node) { |
||||
List<SpatialTrack> tracks = new ArrayList<SpatialTrack>(featuresTracks.size()); |
||||
for (Entry<String, Ipo> entry : featuresTracks.entrySet()) { |
||||
tracks.add((SpatialTrack) entry.getValue().calculateTrack(0, node.getLocalTranslation(), node.getLocalRotation(), node.getLocalScale(), 1, stopFrame, fps, true)); |
||||
} |
||||
return tracks.toArray(new SpatialTrack[tracks.size()]); |
||||
} |
||||
|
||||
/** |
||||
* Converts the action into JME bone animation tracks. |
||||
* |
||||
* @param skeleton |
||||
* the skeleton that will be animated |
||||
* @return the bone tracks for the node |
||||
*/ |
||||
public BoneTrack[] toTracks(Skeleton skeleton) { |
||||
List<BoneTrack> tracks = new ArrayList<BoneTrack>(featuresTracks.size()); |
||||
for (Entry<String, Ipo> entry : featuresTracks.entrySet()) { |
||||
int boneIndex = skeleton.getBoneIndex(entry.getKey()); |
||||
tracks.add((BoneTrack) entry.getValue().calculateTrack(boneIndex, Vector3f.ZERO, Quaternion.IDENTITY, Vector3f.UNIT_XYZ, 1, stopFrame, fps, false)); |
||||
} |
||||
return tracks.toArray(new BoneTrack[tracks.size()]); |
||||
} |
||||
|
||||
/** |
||||
* @return the name of the action |
||||
*/ |
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
/** |
||||
* @return the time of animations (in seconds) |
||||
*/ |
||||
public float getAnimationTime() { |
||||
return (stopFrame - 1) / (float) fps; |
||||
} |
||||
|
||||
/** |
||||
* Determines if the current action has a track of a given name. |
||||
* CAUTION! The names are case sensitive. |
||||
* |
||||
* @param name |
||||
* the name of the track |
||||
* @return <B>true</b> if the track of a given name exists for the |
||||
* action and <b>false</b> otherwise |
||||
*/ |
||||
public boolean hasTrackName(String name) { |
||||
return featuresTracks.containsKey(name); |
||||
} |
||||
|
||||
/** |
||||
* @return the amount of tracks in current action |
||||
*/ |
||||
public int getTracksCount() { |
||||
return featuresTracks.size(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "BlenderTrack [name = " + name + "; tracks = [" + featuresTracks.keySet() + "]]"; |
||||
} |
||||
} |
@ -1,396 +1,396 @@ |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.nio.Buffer; |
||||
import java.nio.ByteBuffer; |
||||
import java.nio.FloatBuffer; |
||||
import java.nio.IntBuffer; |
||||
import java.nio.ShortBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.math.Matrix4f; |
||||
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.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.AnimationHelper; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.animations.BoneEnvelope; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
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.meshes.MeshContext.VertexGroup; |
||||
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; // JME
|
||||
|
||||
private static final int FLAG_VERTEX_GROUPS = 0x01; |
||||
private static final int FLAG_BONE_ENVELOPES = 0x02; |
||||
|
||||
private Structure armatureObject; |
||||
private Skeleton skeleton; |
||||
private Structure meshStructure; |
||||
/** The wold transform matrix of the armature object. */ |
||||
private Matrix4f objectWorldMatrix; |
||||
/** Old memory address of the mesh that will have the skeleton applied. */ |
||||
private Long meshOMA; |
||||
/** The variable tells if the vertex groups of the mesh should be used to assign verts to bones. */ |
||||
private boolean useVertexGroups; |
||||
/** The variable tells if the bones' envelopes should be used to assign verts to bones. */ |
||||
private boolean useBoneEnvelopes; |
||||
|
||||
/** |
||||
* 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().get(0); |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
Pointer pArmatureObject = (Pointer) modifierStructure.getFieldValue("object"); |
||||
if (pArmatureObject.isNotNull()) { |
||||
int deformflag = ((Number) modifierStructure.getFieldValue("deformflag")).intValue(); |
||||
useVertexGroups = (deformflag & FLAG_VERTEX_GROUPS) != 0; |
||||
useBoneEnvelopes = (deformflag & FLAG_BONE_ENVELOPES) != 0; |
||||
modifying = useBoneEnvelopes || useVertexGroups; |
||||
if (modifying) {// if neither option is used the modifier will not modify anything anyway
|
||||
armatureObject = pArmatureObject.fetchData().get(0); |
||||
|
||||
// load skeleton
|
||||
Structure armatureStructure = ((Pointer) armatureObject.getFieldValue("data")).fetchData().get(0); |
||||
List<Structure> bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(); |
||||
List<Bone> bonesList = new ArrayList<Bone>(); |
||||
for (int i = 0; i < bonebase.size(); ++i) { |
||||
this.buildBones(armatureObject.getOldMemoryAddress(), bonebase.get(i), null, bonesList, objectStructure.getOldMemoryAddress(), blenderContext); |
||||
} |
||||
bonesList.add(0, new Bone("")); |
||||
Bone[] bones = bonesList.toArray(new Bone[bonesList.size()]); |
||||
skeleton = new Skeleton(bones); |
||||
blenderContext.setSkeleton(armatureObject.getOldMemoryAddress(), skeleton); |
||||
this.meshStructure = meshStructure; |
||||
|
||||
// read mesh indexes
|
||||
meshOMA = meshStructure.getOldMemoryAddress(); |
||||
|
||||
if (useBoneEnvelopes) { |
||||
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
||||
Spatial object = (Spatial) blenderContext.getLoadedFeature(objectStructure.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE); |
||||
objectWorldMatrix = constraintHelper.toMatrix(object.getWorldTransform(), new Matrix4f()); |
||||
} |
||||
} |
||||
} else { |
||||
modifying = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method builds the object's bones structure. |
||||
* |
||||
* @param armatureObjectOMA |
||||
* the OMa of the armature node |
||||
* @param boneStructure |
||||
* the structure containing the bones' data |
||||
* @param parent |
||||
* the parent bone |
||||
* @param result |
||||
* the list where the newly created bone will be added |
||||
* @param spatialOMA |
||||
* the OMA of the spatial that will own the skeleton |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when there is problem with the blender |
||||
* file |
||||
*/ |
||||
private void buildBones(Long armatureObjectOMA, Structure boneStructure, Bone parent, List<Bone> result, Long spatialOMA, BlenderContext blenderContext) throws BlenderFileException { |
||||
BoneContext bc = new BoneContext(armatureObjectOMA, boneStructure, blenderContext); |
||||
bc.buildBone(result, spatialOMA, blenderContext); |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public void 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 (skeleton != null) { |
||||
// setting weights for bones
|
||||
List<Geometry> geomList = (List<Geometry>) blenderContext.getLoadedFeature(meshOMA, LoadedFeatureDataType.LOADED_FEATURE); |
||||
MeshContext meshContext = blenderContext.getMeshContext(meshOMA); |
||||
for (Geometry geom : geomList) { |
||||
int materialIndex = meshContext.getMaterialIndex(geom); |
||||
Mesh mesh = geom.getMesh(); |
||||
|
||||
MeshWeightsData buffers = this.readVerticesWeightsData(meshContext, skeleton, materialIndex, mesh, blenderContext); |
||||
if (buffers != null) { |
||||
mesh.setMaxNumWeights(buffers.maximumWeightsPerVertex); |
||||
mesh.setBuffer(buffers.verticesWeights); |
||||
mesh.setBuffer(buffers.verticesWeightsIndices); |
||||
|
||||
LOGGER.fine("Generating bind pose and normal buffers."); |
||||
mesh.generateBindPose(true); |
||||
|
||||
// change the usage type of vertex and normal buffers from
|
||||
// Static to Stream
|
||||
mesh.getBuffer(Type.Position).setUsage(Usage.Stream); |
||||
mesh.getBuffer(Type.Normal).setUsage(Usage.Stream); |
||||
|
||||
// creating empty buffers for HW skinning
|
||||
// the buffers will be setup if ever used.
|
||||
VertexBuffer verticesWeightsHW = new VertexBuffer(Type.HWBoneWeight); |
||||
VertexBuffer verticesWeightsIndicesHW = new VertexBuffer(Type.HWBoneIndex); |
||||
mesh.setBuffer(verticesWeightsHW); |
||||
mesh.setBuffer(verticesWeightsIndicesHW); |
||||
} |
||||
} |
||||
|
||||
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); |
||||
animationHelper.applyAnimations(node, skeleton, blenderContext.getBlenderKey().getSkeletonAnimationNames(node.getName())); |
||||
node.updateModelBound(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Reads the vertices data and prepares appropriate buffers to be added to the mesh. There is a bone index buffer and weitghts buffer. |
||||
* |
||||
* @param meshContext |
||||
* the mesh context |
||||
* @param skeleton |
||||
* the current skeleton |
||||
* @param materialIndex |
||||
* the material index |
||||
* @param mesh |
||||
* the mesh we create the buffers for |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return an instance that aggregates all needed data for the mesh |
||||
*/ |
||||
private MeshWeightsData readVerticesWeightsData(MeshContext meshContext, Skeleton skeleton, int materialIndex, Mesh mesh, BlenderContext blenderContext) { |
||||
int vertexListSize = meshContext.getVertexCount(materialIndex); |
||||
Map<Integer, List<Integer>> vertexReferenceMap = meshContext.getVertexReferenceMap(materialIndex); |
||||
|
||||
Map<String, VertexGroup> vertexGroups = new HashMap<String, VertexGroup>(); |
||||
Buffer indexes = mesh.getBuffer(Type.Index).getData(); |
||||
FloatBuffer positions = mesh.getFloatBuffer(Type.Position); |
||||
|
||||
int maximumWeightsPerVertex = 0; |
||||
if (useVertexGroups) { |
||||
LOGGER.fine("Attaching verts to bones using vertex groups."); |
||||
for (int boneIndex = 1; boneIndex < skeleton.getBoneCount(); ++boneIndex) {// bone with index 0 is a root bone
|
||||
Bone bone = skeleton.getBone(boneIndex); |
||||
VertexGroup vertexGroup = meshContext.getGroup(bone.getName()); |
||||
if (vertexGroup != null) { |
||||
vertexGroup.setBoneIndex(boneIndex); |
||||
vertexGroups.put(bone.getName(), vertexGroup); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (useBoneEnvelopes) { |
||||
LOGGER.fine("Attaching verts to bones using bone envelopes."); |
||||
Vector3f pos = new Vector3f(); |
||||
|
||||
for (int boneIndex = 1; boneIndex < skeleton.getBoneCount(); ++boneIndex) {// bone with index 0 is a root bone
|
||||
Bone bone = skeleton.getBone(boneIndex); |
||||
BoneContext boneContext = blenderContext.getBoneContext(bone); |
||||
BoneEnvelope boneEnvelope = boneContext.getBoneEnvelope(); |
||||
if (boneEnvelope != null) { |
||||
VertexGroup vertexGroup = vertexGroups.get(bone.getName()); |
||||
if (vertexGroup == null) { |
||||
vertexGroup = new VertexGroup(); |
||||
vertexGroups.put(bone.getName(), vertexGroup); |
||||
} |
||||
vertexGroup.setBoneIndex(boneIndex); |
||||
|
||||
for (Entry<Integer, List<Integer>> entry : vertexReferenceMap.entrySet()) { |
||||
List<Integer> vertexIndices = entry.getValue(); |
||||
for (int j = 0; j < indexes.limit(); ++j) { |
||||
int index = indexes instanceof ShortBuffer ? ((ShortBuffer) indexes).get(j) : ((IntBuffer) indexes).get(j); |
||||
if (vertexIndices.contains(index)) {// current geometry has the index assigned to the current mesh
|
||||
int ii = index * 3; |
||||
pos.set(positions.get(ii), positions.get(ii + 1), positions.get(ii + 2)); |
||||
// move the vertex to the global space position
|
||||
objectWorldMatrix.mult(pos, pos);// TODO: optimize: check every vertex once and apply its references
|
||||
if (boneEnvelope.isInEnvelope(pos)) { |
||||
vertexGroup.addVertex(index, boneEnvelope.getWeight()); |
||||
} else if (boneIndex == 5) { |
||||
System.out.println("Si nie zaapa: " + pos); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Map<Integer, WeightsAndBoneIndexes> weights = new HashMap<Integer, WeightsAndBoneIndexes>();// [vertex_index; [bone_index; weight]]
|
||||
if (vertexGroups.size() > 0) { |
||||
LOGGER.fine("Gathering vertex groups information to prepare the buffers for the mesh."); |
||||
for (VertexGroup vertexGroup : vertexGroups.values()) { |
||||
for (Entry<Integer, Float> entry : vertexGroup.entrySet()) { |
||||
WeightsAndBoneIndexes vertexWeights = weights.get(entry.getKey()); |
||||
if (vertexWeights == null) { |
||||
vertexWeights = new WeightsAndBoneIndexes(); |
||||
weights.put(entry.getKey(), vertexWeights); |
||||
} |
||||
vertexWeights.put(vertexGroup.getBoneIndex(), entry.getValue()); |
||||
} |
||||
} |
||||
|
||||
LOGGER.log(Level.FINE, "Equalizing the amount of weights per vertex to {0} if any of them has more or less.", MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
for (Entry<Integer, WeightsAndBoneIndexes> entry : weights.entrySet()) { |
||||
maximumWeightsPerVertex = Math.max(maximumWeightsPerVertex, entry.getValue().size()); |
||||
entry.getValue().normalize(MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
} |
||||
|
||||
if (maximumWeightsPerVertex > MAXIMUM_WEIGHTS_PER_VERTEX) { |
||||
LOGGER.log(Level.WARNING, "{0} has vertices with more than 4 weights assigned. The model may not behave as it should.", meshStructure.getName()); |
||||
maximumWeightsPerVertex = MAXIMUM_WEIGHTS_PER_VERTEX;// normalization already made at most 'MAXIMUM_WEIGHTS_PER_VERTEX' weights per vertex
|
||||
} |
||||
} |
||||
|
||||
if(maximumWeightsPerVertex == 0) { |
||||
LOGGER.fine("No vertex group data nor bone envelopes found to attach vertices to bones!"); |
||||
return null; |
||||
} |
||||
|
||||
LOGGER.fine("Preparing buffers for the mesh."); |
||||
FloatBuffer weightsFloatData = BufferUtils.createFloatBuffer(vertexListSize * MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
ByteBuffer indicesData = BufferUtils.createByteBuffer(vertexListSize * MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
for (int i = 0; i < indexes.limit(); ++i) { |
||||
int index = indexes instanceof ShortBuffer ? ((ShortBuffer) indexes).get(i) : ((IntBuffer) indexes).get(i); |
||||
WeightsAndBoneIndexes weightsAndBoneIndexes = weights.get(index); |
||||
if (weightsAndBoneIndexes != null) { |
||||
int count = 0; |
||||
for (Entry<Integer, Float> entry : weightsAndBoneIndexes.entrySet()) { |
||||
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getValue()); |
||||
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getKey().byteValue()); |
||||
++count; |
||||
} |
||||
} else { |
||||
// if no bone is assigned to this vertex then attach it to the 0-indexed root bone
|
||||
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 1.0f); |
||||
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0); |
||||
} |
||||
} |
||||
VertexBuffer verticesWeights = new VertexBuffer(Type.BoneWeight); |
||||
verticesWeights.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.Float, weightsFloatData); |
||||
|
||||
VertexBuffer verticesWeightsIndices = new VertexBuffer(Type.BoneIndex); |
||||
verticesWeightsIndices.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.UnsignedByte, indicesData); |
||||
|
||||
return new MeshWeightsData(maximumWeightsPerVertex, verticesWeights, verticesWeightsIndices); |
||||
} |
||||
|
||||
/** |
||||
* A class that gathers the data for mesh bone buffers. |
||||
* Added to increase code readability. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class MeshWeightsData { |
||||
public final int maximumWeightsPerVertex; |
||||
public final VertexBuffer verticesWeights; |
||||
public final VertexBuffer verticesWeightsIndices; |
||||
|
||||
public MeshWeightsData(int maximumWeightsPerVertex, VertexBuffer verticesWeights, VertexBuffer verticesWeightsIndices) { |
||||
this.maximumWeightsPerVertex = maximumWeightsPerVertex; |
||||
this.verticesWeights = verticesWeights; |
||||
this.verticesWeightsIndices = verticesWeightsIndices; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A map between the bone index and the bone's weight. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class WeightsAndBoneIndexes extends HashMap<Integer, Float> { |
||||
private static final long serialVersionUID = 2754299007299077459L; |
||||
|
||||
/** |
||||
* The method normalizes the weights and bone indexes data. |
||||
* First it truncates the amount to MAXIMUM_WEIGHTS_PER_VERTEX because this is how many weights JME can handle. |
||||
* Next it normalizes the weights so that the sum of all verts is 1. |
||||
* @param maximumSize |
||||
* the maximum size that the data will be truncated to (usually: MAXIMUM_WEIGHTS_PER_VERTEX) |
||||
*/ |
||||
public void normalize(int maximumSize) { |
||||
if (this.size() > maximumSize) {// select only the most significant weights
|
||||
float lowestWeight = Float.MAX_VALUE; |
||||
int lowestWeightIndex = -1; |
||||
HashMap<Integer, Float> msw = new HashMap<Integer, Float>(maximumSize);// msw = Most Significant Weight
|
||||
for (Entry<Integer, Float> entry : this.entrySet()) { |
||||
if (msw.size() < maximumSize) { |
||||
msw.put(entry.getKey(), entry.getValue()); |
||||
if (entry.getValue() < lowestWeight) { |
||||
lowestWeight = entry.getValue(); |
||||
lowestWeightIndex = entry.getKey(); |
||||
} |
||||
} else if (entry.getValue() > lowestWeight) { |
||||
msw.remove(lowestWeightIndex); |
||||
msw.put(lowestWeightIndex, lowestWeight); |
||||
|
||||
// search again for the lowest weight
|
||||
lowestWeight = Float.MAX_VALUE; |
||||
for (Entry<Integer, Float> e : msw.entrySet()) { |
||||
if (e.getValue() < lowestWeight) { |
||||
lowestWeight = e.getValue(); |
||||
lowestWeightIndex = e.getKey(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// replace current weights with the given ones
|
||||
this.clear(); |
||||
this.putAll(msw); |
||||
} |
||||
|
||||
// normalizing the weights so that the sum of the values is equal to '1'
|
||||
float sum = 0; |
||||
for (Entry<Integer, Float> entry : this.entrySet()) { |
||||
sum += entry.getValue(); |
||||
} |
||||
|
||||
if (sum != 0 && sum != 1) { |
||||
for (Entry<Integer, Float> entry : this.entrySet()) { |
||||
entry.setValue(entry.getValue() / sum); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
package com.jme3.scene.plugins.blender.modifiers; |
||||
|
||||
import java.nio.Buffer; |
||||
import java.nio.ByteBuffer; |
||||
import java.nio.FloatBuffer; |
||||
import java.nio.IntBuffer; |
||||
import java.nio.ShortBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Map.Entry; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.animation.Bone; |
||||
import com.jme3.animation.Skeleton; |
||||
import com.jme3.math.Matrix4f; |
||||
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.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.AnimationHelper; |
||||
import com.jme3.scene.plugins.blender.animations.BoneContext; |
||||
import com.jme3.scene.plugins.blender.animations.BoneEnvelope; |
||||
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper; |
||||
import com.jme3.scene.plugins.blender.file.BlenderFileException; |
||||
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.meshes.MeshContext.VertexGroup; |
||||
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; // JME
|
||||
|
||||
private static final int FLAG_VERTEX_GROUPS = 0x01; |
||||
private static final int FLAG_BONE_ENVELOPES = 0x02; |
||||
|
||||
private Structure armatureObject; |
||||
private Skeleton skeleton; |
||||
private Structure meshStructure; |
||||
/** The wold transform matrix of the armature object. */ |
||||
private Matrix4f objectWorldMatrix; |
||||
/** Old memory address of the mesh that will have the skeleton applied. */ |
||||
private Long meshOMA; |
||||
/** The variable tells if the vertex groups of the mesh should be used to assign verts to bones. */ |
||||
private boolean useVertexGroups; |
||||
/** The variable tells if the bones' envelopes should be used to assign verts to bones. */ |
||||
private boolean useBoneEnvelopes; |
||||
|
||||
/** |
||||
* 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().get(0); |
||||
if (this.validate(modifierStructure, blenderContext)) { |
||||
Pointer pArmatureObject = (Pointer) modifierStructure.getFieldValue("object"); |
||||
if (pArmatureObject.isNotNull()) { |
||||
int deformflag = ((Number) modifierStructure.getFieldValue("deformflag")).intValue(); |
||||
useVertexGroups = (deformflag & FLAG_VERTEX_GROUPS) != 0; |
||||
useBoneEnvelopes = (deformflag & FLAG_BONE_ENVELOPES) != 0; |
||||
modifying = useBoneEnvelopes || useVertexGroups; |
||||
if (modifying) {// if neither option is used the modifier will not modify anything anyway
|
||||
armatureObject = pArmatureObject.fetchData().get(0); |
||||
|
||||
// load skeleton
|
||||
Structure armatureStructure = ((Pointer) armatureObject.getFieldValue("data")).fetchData().get(0); |
||||
List<Structure> bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(); |
||||
List<Bone> bonesList = new ArrayList<Bone>(); |
||||
for (int i = 0; i < bonebase.size(); ++i) { |
||||
this.buildBones(armatureObject.getOldMemoryAddress(), bonebase.get(i), null, bonesList, objectStructure.getOldMemoryAddress(), blenderContext); |
||||
} |
||||
bonesList.add(0, new Bone("")); |
||||
Bone[] bones = bonesList.toArray(new Bone[bonesList.size()]); |
||||
skeleton = new Skeleton(bones); |
||||
blenderContext.setSkeleton(armatureObject.getOldMemoryAddress(), skeleton); |
||||
this.meshStructure = meshStructure; |
||||
|
||||
// read mesh indexes
|
||||
meshOMA = meshStructure.getOldMemoryAddress(); |
||||
|
||||
if (useBoneEnvelopes) { |
||||
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); |
||||
Spatial object = (Spatial) blenderContext.getLoadedFeature(objectStructure.getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE); |
||||
objectWorldMatrix = constraintHelper.toMatrix(object.getWorldTransform(), new Matrix4f()); |
||||
} |
||||
} |
||||
} else { |
||||
modifying = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This method builds the object's bones structure. |
||||
* |
||||
* @param armatureObjectOMA |
||||
* the OMa of the armature node |
||||
* @param boneStructure |
||||
* the structure containing the bones' data |
||||
* @param parent |
||||
* the parent bone |
||||
* @param result |
||||
* the list where the newly created bone will be added |
||||
* @param spatialOMA |
||||
* the OMA of the spatial that will own the skeleton |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @throws BlenderFileException |
||||
* an exception is thrown when there is problem with the blender |
||||
* file |
||||
*/ |
||||
private void buildBones(Long armatureObjectOMA, Structure boneStructure, Bone parent, List<Bone> result, Long spatialOMA, BlenderContext blenderContext) throws BlenderFileException { |
||||
BoneContext bc = new BoneContext(armatureObjectOMA, boneStructure, blenderContext); |
||||
bc.buildBone(result, spatialOMA, blenderContext); |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public void 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 (skeleton != null) { |
||||
// setting weights for bones
|
||||
List<Geometry> geomList = (List<Geometry>) blenderContext.getLoadedFeature(meshOMA, LoadedFeatureDataType.LOADED_FEATURE); |
||||
MeshContext meshContext = blenderContext.getMeshContext(meshOMA); |
||||
for (Geometry geom : geomList) { |
||||
int materialIndex = meshContext.getMaterialIndex(geom); |
||||
Mesh mesh = geom.getMesh(); |
||||
|
||||
MeshWeightsData buffers = this.readVerticesWeightsData(meshContext, skeleton, materialIndex, mesh, blenderContext); |
||||
if (buffers != null) { |
||||
mesh.setMaxNumWeights(buffers.maximumWeightsPerVertex); |
||||
mesh.setBuffer(buffers.verticesWeights); |
||||
mesh.setBuffer(buffers.verticesWeightsIndices); |
||||
|
||||
LOGGER.fine("Generating bind pose and normal buffers."); |
||||
mesh.generateBindPose(true); |
||||
|
||||
// change the usage type of vertex and normal buffers from
|
||||
// Static to Stream
|
||||
mesh.getBuffer(Type.Position).setUsage(Usage.Stream); |
||||
mesh.getBuffer(Type.Normal).setUsage(Usage.Stream); |
||||
|
||||
// creating empty buffers for HW skinning
|
||||
// the buffers will be setup if ever used.
|
||||
VertexBuffer verticesWeightsHW = new VertexBuffer(Type.HWBoneWeight); |
||||
VertexBuffer verticesWeightsIndicesHW = new VertexBuffer(Type.HWBoneIndex); |
||||
mesh.setBuffer(verticesWeightsHW); |
||||
mesh.setBuffer(verticesWeightsIndicesHW); |
||||
} |
||||
} |
||||
|
||||
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); |
||||
animationHelper.applyAnimations(node, skeleton, blenderContext.getBlenderKey().getAnimationMatchMethod()); |
||||
node.updateModelBound(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Reads the vertices data and prepares appropriate buffers to be added to the mesh. There is a bone index buffer and weitghts buffer. |
||||
* |
||||
* @param meshContext |
||||
* the mesh context |
||||
* @param skeleton |
||||
* the current skeleton |
||||
* @param materialIndex |
||||
* the material index |
||||
* @param mesh |
||||
* the mesh we create the buffers for |
||||
* @param blenderContext |
||||
* the blender context |
||||
* @return an instance that aggregates all needed data for the mesh |
||||
*/ |
||||
private MeshWeightsData readVerticesWeightsData(MeshContext meshContext, Skeleton skeleton, int materialIndex, Mesh mesh, BlenderContext blenderContext) { |
||||
int vertexListSize = meshContext.getVertexCount(materialIndex); |
||||
Map<Integer, List<Integer>> vertexReferenceMap = meshContext.getVertexReferenceMap(materialIndex); |
||||
|
||||
Map<String, VertexGroup> vertexGroups = new HashMap<String, VertexGroup>(); |
||||
Buffer indexes = mesh.getBuffer(Type.Index).getData(); |
||||
FloatBuffer positions = mesh.getFloatBuffer(Type.Position); |
||||
|
||||
int maximumWeightsPerVertex = 0; |
||||
if (useVertexGroups) { |
||||
LOGGER.fine("Attaching verts to bones using vertex groups."); |
||||
for (int boneIndex = 1; boneIndex < skeleton.getBoneCount(); ++boneIndex) {// bone with index 0 is a root bone
|
||||
Bone bone = skeleton.getBone(boneIndex); |
||||
VertexGroup vertexGroup = meshContext.getGroup(bone.getName()); |
||||
if (vertexGroup != null) { |
||||
vertexGroup.setBoneIndex(boneIndex); |
||||
vertexGroups.put(bone.getName(), vertexGroup); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (useBoneEnvelopes) { |
||||
LOGGER.fine("Attaching verts to bones using bone envelopes."); |
||||
Vector3f pos = new Vector3f(); |
||||
|
||||
for (int boneIndex = 1; boneIndex < skeleton.getBoneCount(); ++boneIndex) {// bone with index 0 is a root bone
|
||||
Bone bone = skeleton.getBone(boneIndex); |
||||
BoneContext boneContext = blenderContext.getBoneContext(bone); |
||||
BoneEnvelope boneEnvelope = boneContext.getBoneEnvelope(); |
||||
if (boneEnvelope != null) { |
||||
VertexGroup vertexGroup = vertexGroups.get(bone.getName()); |
||||
if (vertexGroup == null) { |
||||
vertexGroup = new VertexGroup(); |
||||
vertexGroups.put(bone.getName(), vertexGroup); |
||||
} |
||||
vertexGroup.setBoneIndex(boneIndex); |
||||
|
||||
for (Entry<Integer, List<Integer>> entry : vertexReferenceMap.entrySet()) { |
||||
List<Integer> vertexIndices = entry.getValue(); |
||||
for (int j = 0; j < indexes.limit(); ++j) { |
||||
int index = indexes instanceof ShortBuffer ? ((ShortBuffer) indexes).get(j) : ((IntBuffer) indexes).get(j); |
||||
if (vertexIndices.contains(index)) {// current geometry has the index assigned to the current mesh
|
||||
int ii = index * 3; |
||||
pos.set(positions.get(ii), positions.get(ii + 1), positions.get(ii + 2)); |
||||
// move the vertex to the global space position
|
||||
objectWorldMatrix.mult(pos, pos);// TODO: optimize: check every vertex once and apply its references
|
||||
if (boneEnvelope.isInEnvelope(pos)) { |
||||
vertexGroup.addVertex(index, boneEnvelope.getWeight()); |
||||
} else if (boneIndex == 5) { |
||||
System.out.println("Si nie zaapa: " + pos); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Map<Integer, WeightsAndBoneIndexes> weights = new HashMap<Integer, WeightsAndBoneIndexes>();// [vertex_index; [bone_index; weight]]
|
||||
if (vertexGroups.size() > 0) { |
||||
LOGGER.fine("Gathering vertex groups information to prepare the buffers for the mesh."); |
||||
for (VertexGroup vertexGroup : vertexGroups.values()) { |
||||
for (Entry<Integer, Float> entry : vertexGroup.entrySet()) { |
||||
WeightsAndBoneIndexes vertexWeights = weights.get(entry.getKey()); |
||||
if (vertexWeights == null) { |
||||
vertexWeights = new WeightsAndBoneIndexes(); |
||||
weights.put(entry.getKey(), vertexWeights); |
||||
} |
||||
vertexWeights.put(vertexGroup.getBoneIndex(), entry.getValue()); |
||||
} |
||||
} |
||||
|
||||
LOGGER.log(Level.FINE, "Equalizing the amount of weights per vertex to {0} if any of them has more or less.", MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
for (Entry<Integer, WeightsAndBoneIndexes> entry : weights.entrySet()) { |
||||
maximumWeightsPerVertex = Math.max(maximumWeightsPerVertex, entry.getValue().size()); |
||||
entry.getValue().normalize(MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
} |
||||
|
||||
if (maximumWeightsPerVertex > MAXIMUM_WEIGHTS_PER_VERTEX) { |
||||
LOGGER.log(Level.WARNING, "{0} has vertices with more than 4 weights assigned. The model may not behave as it should.", meshStructure.getName()); |
||||
maximumWeightsPerVertex = MAXIMUM_WEIGHTS_PER_VERTEX;// normalization already made at most 'MAXIMUM_WEIGHTS_PER_VERTEX' weights per vertex
|
||||
} |
||||
} |
||||
|
||||
if(maximumWeightsPerVertex == 0) { |
||||
LOGGER.fine("No vertex group data nor bone envelopes found to attach vertices to bones!"); |
||||
return null; |
||||
} |
||||
|
||||
LOGGER.fine("Preparing buffers for the mesh."); |
||||
FloatBuffer weightsFloatData = BufferUtils.createFloatBuffer(vertexListSize * MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
ByteBuffer indicesData = BufferUtils.createByteBuffer(vertexListSize * MAXIMUM_WEIGHTS_PER_VERTEX); |
||||
for (int i = 0; i < indexes.limit(); ++i) { |
||||
int index = indexes instanceof ShortBuffer ? ((ShortBuffer) indexes).get(i) : ((IntBuffer) indexes).get(i); |
||||
WeightsAndBoneIndexes weightsAndBoneIndexes = weights.get(index); |
||||
if (weightsAndBoneIndexes != null) { |
||||
int count = 0; |
||||
for (Entry<Integer, Float> entry : weightsAndBoneIndexes.entrySet()) { |
||||
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getValue()); |
||||
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX + count, entry.getKey().byteValue()); |
||||
++count; |
||||
} |
||||
} else { |
||||
// if no bone is assigned to this vertex then attach it to the 0-indexed root bone
|
||||
weightsFloatData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, 1.0f); |
||||
indicesData.put(index * MAXIMUM_WEIGHTS_PER_VERTEX, (byte) 0); |
||||
} |
||||
} |
||||
VertexBuffer verticesWeights = new VertexBuffer(Type.BoneWeight); |
||||
verticesWeights.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.Float, weightsFloatData); |
||||
|
||||
VertexBuffer verticesWeightsIndices = new VertexBuffer(Type.BoneIndex); |
||||
verticesWeightsIndices.setupData(Usage.CpuOnly, maximumWeightsPerVertex, Format.UnsignedByte, indicesData); |
||||
|
||||
return new MeshWeightsData(maximumWeightsPerVertex, verticesWeights, verticesWeightsIndices); |
||||
} |
||||
|
||||
/** |
||||
* A class that gathers the data for mesh bone buffers. |
||||
* Added to increase code readability. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class MeshWeightsData { |
||||
public final int maximumWeightsPerVertex; |
||||
public final VertexBuffer verticesWeights; |
||||
public final VertexBuffer verticesWeightsIndices; |
||||
|
||||
public MeshWeightsData(int maximumWeightsPerVertex, VertexBuffer verticesWeights, VertexBuffer verticesWeightsIndices) { |
||||
this.maximumWeightsPerVertex = maximumWeightsPerVertex; |
||||
this.verticesWeights = verticesWeights; |
||||
this.verticesWeightsIndices = verticesWeightsIndices; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A map between the bone index and the bone's weight. |
||||
* |
||||
* @author Marcin Roguski (Kaelthas) |
||||
*/ |
||||
private static class WeightsAndBoneIndexes extends HashMap<Integer, Float> { |
||||
private static final long serialVersionUID = 2754299007299077459L; |
||||
|
||||
/** |
||||
* The method normalizes the weights and bone indexes data. |
||||
* First it truncates the amount to MAXIMUM_WEIGHTS_PER_VERTEX because this is how many weights JME can handle. |
||||
* Next it normalizes the weights so that the sum of all verts is 1. |
||||
* @param maximumSize |
||||
* the maximum size that the data will be truncated to (usually: MAXIMUM_WEIGHTS_PER_VERTEX) |
||||
*/ |
||||
public void normalize(int maximumSize) { |
||||
if (this.size() > maximumSize) {// select only the most significant weights
|
||||
float lowestWeight = Float.MAX_VALUE; |
||||
int lowestWeightIndex = -1; |
||||
HashMap<Integer, Float> msw = new HashMap<Integer, Float>(maximumSize);// msw = Most Significant Weight
|
||||
for (Entry<Integer, Float> entry : this.entrySet()) { |
||||
if (msw.size() < maximumSize) { |
||||
msw.put(entry.getKey(), entry.getValue()); |
||||
if (entry.getValue() < lowestWeight) { |
||||
lowestWeight = entry.getValue(); |
||||
lowestWeightIndex = entry.getKey(); |
||||
} |
||||
} else if (entry.getValue() > lowestWeight) { |
||||
msw.remove(lowestWeightIndex); |
||||
msw.put(lowestWeightIndex, lowestWeight); |
||||
|
||||
// search again for the lowest weight
|
||||
lowestWeight = Float.MAX_VALUE; |
||||
for (Entry<Integer, Float> e : msw.entrySet()) { |
||||
if (e.getValue() < lowestWeight) { |
||||
lowestWeight = e.getValue(); |
||||
lowestWeightIndex = e.getKey(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// replace current weights with the given ones
|
||||
this.clear(); |
||||
this.putAll(msw); |
||||
} |
||||
|
||||
// normalizing the weights so that the sum of the values is equal to '1'
|
||||
float sum = 0; |
||||
for (Entry<Integer, Float> entry : this.entrySet()) { |
||||
sum += entry.getValue(); |
||||
} |
||||
|
||||
if (sum != 0 && sum != 1) { |
||||
for (Entry<Integer, Float> entry : this.entrySet()) { |
||||
entry.setValue(entry.getValue() / sum); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue