From 51215a352e00d82013bf9957ec2be8ef31b5dc1e Mon Sep 17 00:00:00 2001 From: jmekaelthas Date: Sat, 7 Jun 2014 13:33:02 +0200 Subject: [PATCH] Feature: added automatic action mapping instead of explicit mapping in BlenderKey. --- .../main/java/com/jme3/asset/BlenderKey.java | 211 ++--- .../scene/plugins/blender/BlenderContext.java | 32 +- .../blender/animations/AnimationHelper.java | 217 ++--- .../blender/animations/BlenderAction.java | 134 +++ .../scene/plugins/blender/animations/Ipo.java | 69 +- .../blender/modifiers/ArmatureModifier.java | 792 +++++++++--------- .../plugins/blender/objects/ObjectHelper.java | 8 +- 7 files changed, 772 insertions(+), 691 deletions(-) create mode 100644 jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BlenderAction.java diff --git a/jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java b/jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java index 64683526a..555150ea2 100644 --- a/jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java +++ b/jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java @@ -33,11 +33,7 @@ package com.jme3.asset; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.Queue; import com.jme3.animation.Animation; @@ -66,70 +62,68 @@ import com.jme3.texture.Texture; */ public class BlenderKey extends ModelKey { - protected static final int DEFAULT_FPS = 25; + protected static final int DEFAULT_FPS = 25; /** * FramesPerSecond parameter describe how many frames there are in each second. It allows to calculate the time * between the frames. */ - protected int fps = DEFAULT_FPS; + protected int fps = DEFAULT_FPS; /** * This variable is a bitwise flag of FeatureToLoad interface values; By default everything is being loaded. */ - protected int featuresToLoad = FeaturesToLoad.ALL; + protected int featuresToLoad = FeaturesToLoad.ALL; /** This variable determines if assets that are not linked to the objects should be loaded. */ - protected boolean loadUnlinkedAssets; + protected boolean loadUnlinkedAssets; /** The root path for all the assets. */ - protected String assetRootPath; + protected String assetRootPath; /** This variable indicate if Y axis is UP axis. If not then Z is up. By default set to true. */ - protected boolean fixUpAxis = true; + protected boolean fixUpAxis = true; /** Generated textures resolution (PPU - Pixels Per Unit). */ - protected int generatedTexturePPU = 128; + protected int generatedTexturePPU = 128; /** * The name of world settings that the importer will use. If not set or specified name does not occur in the file * then the first world settings in the file will be used. */ - protected String usedWorld; + protected String usedWorld; /** * User's default material that is set fo objects that have no material definition in blender. The default value is * null. If the value is null the importer will use its own default material (gray color - like in blender). */ - protected Material defaultMaterial; + protected Material defaultMaterial; /** Face cull mode. By default it is disabled. */ - protected FaceCullMode faceCullMode = FaceCullMode.Back; + protected FaceCullMode faceCullMode = FaceCullMode.Back; /** * Variable describes which layers will be loaded. N-th bit set means N-th layer will be loaded. * If set to -1 then the current layer will be loaded. */ - protected int layersToLoad = -1; + protected int layersToLoad = -1; /** A variable that toggles the object custom properties loading. */ - protected boolean loadObjectProperties = true; + protected boolean loadObjectProperties = true; /** * Maximum texture size. Might be dependant on the graphic card. * This value is taken from org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE. */ - protected int maxTextureSize = 8192; + protected int maxTextureSize = 8192; /** Allows to toggle generated textures loading. Disabled by default because it very often takes too much memory and needs to be used wisely. */ - protected boolean loadGeneratedTextures; + protected boolean loadGeneratedTextures; /** Tells if the mipmaps will be generated by jme or not. By default generation is dependant on the blender settings. */ - protected MipmapGenerationMethod mipmapGenerationMethod = MipmapGenerationMethod.GENERATE_WHEN_NEEDED; + protected MipmapGenerationMethod mipmapGenerationMethod = MipmapGenerationMethod.GENERATE_WHEN_NEEDED; /** * If the sky has only generated textures applied then they will have the following size (both width and height). If 2d textures are used then the generated * textures will get their proper size. */ - protected int skyGeneratedTextureSize = 1000; + protected int skyGeneratedTextureSize = 1000; /** The radius of a shape that will be used while creating the generated texture for the sky. The higher it is the larger part of the texture will be seen. */ - protected float skyGeneratedTextureRadius = 1; + protected float skyGeneratedTextureRadius = 1; /** The shape against which the generated texture for the sky will be created. */ - protected SkyGeneratedTextureShape skyGeneratedTextureShape = SkyGeneratedTextureShape.SPHERE; + protected SkyGeneratedTextureShape skyGeneratedTextureShape = SkyGeneratedTextureShape.SPHERE; /** * This field tells if the importer should optimise the use of textures or not. If set to true, then textures of the same mapping type will be merged together * and textures that in the final result will never be visible - will be discarded. */ - protected boolean optimiseTextures; - /** A map between node name and its animation names. */ - protected Map> nodeAnimationMap = new HashMap>(); - /** A map between node name and its skeleton animation names. */ - protected Map> skeletonAnimationMap = new HashMap>(); + protected boolean optimiseTextures; + /** The method of matching animations to skeletons. The default value is: AT_LEAST_ONE_NAME_MATCH. */ + protected AnimationMatchMethod animationMatchMethod = AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH; /** * Constructor used by serialization mechanisms. @@ -446,6 +440,23 @@ public class BlenderKey extends ModelKey { return optimiseTextures; } + /** + * Sets the way the animations will be matched with skeletons. + * + * @param animationMatchMethod + * the way the animations will be matched with skeletons + */ + public void setAnimationMatchMethod(AnimationMatchMethod animationMatchMethod) { + this.animationMatchMethod = animationMatchMethod; + } + + /** + * @return the way the animations will be matched with skeletons + */ + public AnimationMatchMethod getAnimationMatchMethod() { + return animationMatchMethod; + } + /** * This mehtod sets the name of the WORLD data block taht should be used during file loading. By default the name is * not set. If no name is set or the given name does not occur in the file - the first WORLD data block will be used @@ -482,58 +493,6 @@ public class BlenderKey extends ModelKey { return defaultMaterial; } - /** - * Adds spatial animation name for specified node. - * @param nodeName - * the name of the node - * @param animationName - * the spatial animation name - */ - public void addNodeAnimation(String nodeName, String animationName) { - List animations = nodeAnimationMap.get(nodeName); - if (animations == null) { - animations = new ArrayList(); - nodeAnimationMap.put(nodeName, animations); - } - animations.add(animationName); - } - - /** - * Returns all spatial animation names for the given node. - * @param nodeName - * the name of the node - * @return all spatial animations names or null if none are defined - */ - public List getNodeAnimationNames(String nodeName) { - return nodeAnimationMap.get(nodeName); - } - - /** - * Adds bone animation name for specified node. - * @param nodeName - * the name of the node - * @param animationName - * the bone animation name - */ - public void addSkeletonAnimation(String nodeName, String animationName) { - List animations = skeletonAnimationMap.get(nodeName); - if (animations == null) { - animations = new ArrayList(); - skeletonAnimationMap.put(nodeName, animations); - } - animations.add(animationName); - } - - /** - * Returns all bone animation names for the given node. - * @param nodeName - * the name of the node - * @return all bone animations names or null if none are defined - */ - public List getSkeletonAnimationNames(String nodeName) { - return skeletonAnimationMap.get(nodeName); - } - @Override public void write(JmeExporter e) throws IOException { super.write(e); @@ -553,30 +512,7 @@ public class BlenderKey extends ModelKey { oc.write(skyGeneratedTextureRadius, "sky-generated-texture-radius", 1f); oc.write(skyGeneratedTextureShape, "sky-generated-texture-shape", SkyGeneratedTextureShape.SPHERE); oc.write(optimiseTextures, "optimise-textures", false); - - if (nodeAnimationMap == null) { - oc.write(0, "node-anims-map-size", 0); - } else { - oc.write(nodeAnimationMap.size(), "node-anims-map-size", 0); - int counter = 0; - for (Entry> entry : nodeAnimationMap.entrySet()) { - oc.write(entry.getKey(), "node-anim-" + counter, null); - oc.write(entry.getValue().toArray(new String[entry.getValue().size()]), "node-anims-" + counter, null); - ++counter; - } - } - - if (skeletonAnimationMap == null) { - oc.write(0, "skeleton-anims-map-size", 0); - } else { - oc.write(skeletonAnimationMap.size(), "skeleton-anims-map-size", 0); - int counter = 0; - for (Entry> entry : skeletonAnimationMap.entrySet()) { - oc.write(entry.getKey(), "skeleton-anim-" + counter, null); - oc.write(entry.getValue().toArray(new String[entry.getValue().size()]), "skeleton-anims-" + counter, null); - ++counter; - } - } + oc.write(animationMatchMethod, "animation-match-method", AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH); } @Override @@ -598,32 +534,14 @@ public class BlenderKey extends ModelKey { skyGeneratedTextureRadius = ic.readFloat("sky-generated-texture-radius", 1f); skyGeneratedTextureShape = ic.readEnum("sky-generated-texture-shape", SkyGeneratedTextureShape.class, SkyGeneratedTextureShape.SPHERE); optimiseTextures = ic.readBoolean("optimise-textures", false); - - int animsSize = ic.readInt("node-anims-map-size", 0); - nodeAnimationMap = new HashMap>(animsSize); - if (animsSize > 0) { - for (int i = 0; i < animsSize; ++i) { - String nodeName = ic.readString("node-anim-" + i, null); - String[] anims = ic.readStringArray("node-anims-" + i, null); - nodeAnimationMap.put(nodeName, new ArrayList(Arrays.asList(anims)));// must create ArrayList because 'asList' method returns unmodifiable list - } - } - - animsSize = ic.readInt("skeleton-anims-map-size", 0); - skeletonAnimationMap = new HashMap>(animsSize); - if (animsSize > 0) { - for (int i = 0; i < animsSize; ++i) { - String nodeName = ic.readString("skeleton-anim-" + i, null); - String[] anims = ic.readStringArray("skeleton-anims-" + i, null); - skeletonAnimationMap.put(nodeName, new ArrayList(Arrays.asList(anims)));// must create ArrayList because 'asList' method returns unmodifiable list - } - } + animationMatchMethod = ic.readEnum("animation-match-method", AnimationMatchMethod.class, AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH); } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); + result = prime * result + (animationMatchMethod == null ? 0 : animationMatchMethod.hashCode()); result = prime * result + (assetRootPath == null ? 0 : assetRootPath.hashCode()); result = prime * result + (defaultMaterial == null ? 0 : defaultMaterial.hashCode()); result = prime * result + (faceCullMode == null ? 0 : faceCullMode.hashCode()); @@ -637,9 +555,7 @@ public class BlenderKey extends ModelKey { result = prime * result + (loadUnlinkedAssets ? 1231 : 1237); result = prime * result + maxTextureSize; result = prime * result + (mipmapGenerationMethod == null ? 0 : mipmapGenerationMethod.hashCode()); - result = prime * result + (nodeAnimationMap == null ? 0 : nodeAnimationMap.hashCode()); result = prime * result + (optimiseTextures ? 1231 : 1237); - result = prime * result + (skeletonAnimationMap == null ? 0 : skeletonAnimationMap.hashCode()); result = prime * result + Float.floatToIntBits(skyGeneratedTextureRadius); result = prime * result + (skyGeneratedTextureShape == null ? 0 : skyGeneratedTextureShape.hashCode()); result = prime * result + skyGeneratedTextureSize; @@ -649,13 +565,13 @@ public class BlenderKey extends ModelKey { @Override public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof BlenderKey)) { + if (obj instanceof BlenderKey) { return false; } BlenderKey other = (BlenderKey) obj; + if (animationMatchMethod != other.animationMatchMethod) { + return false; + } if (assetRootPath == null) { if (other.assetRootPath != null) { return false; @@ -703,23 +619,9 @@ public class BlenderKey extends ModelKey { if (mipmapGenerationMethod != other.mipmapGenerationMethod) { return false; } - if (nodeAnimationMap == null) { - if (other.nodeAnimationMap != null) { - return false; - } - } else if (!nodeAnimationMap.equals(other.nodeAnimationMap)) { - return false; - } if (optimiseTextures != other.optimiseTextures) { return false; } - if (skeletonAnimationMap == null) { - if (other.skeletonAnimationMap != null) { - return false; - } - } else if (!skeletonAnimationMap.equals(other.skeletonAnimationMap)) { - return false; - } if (Float.floatToIntBits(skyGeneratedTextureRadius) != Float.floatToIntBits(other.skyGeneratedTextureRadius)) { return false; } @@ -773,6 +675,29 @@ public class BlenderKey extends ModelKey { CUBE, SPHERE; } + /** + * This enum describes which animations should be attached to which armature. + * Blender does not store the mapping between action and armature. That is why the importer + * will try to match those by comparing bone name of the armature with the channel names + * int the actions. + * + * @author Marcin Roguski (Kaelthas) + */ + public static enum AnimationMatchMethod { + /** + * Animation is matched with skeleton when at leas one bone name matches the name of the action channel. + * All the bones that do not have their corresponding channel in the animation will not get the proper tracks for + * this particulat animation. + * Also the channel will not be used for the animation if it does not find the proper bone name. + */ + AT_LEAST_ONE_NAME_MATCH, + /** + * Animation is matched when all action names are covered by the target names (bone names or the name of the + * animated spatial. + */ + ALL_NAMES_MATCH; + } + /** * This class holds the loading results according to the given loading flag. * @author Marcin Roguski (Kaelthas) diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java index 637a5a174..67241e54e 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java @@ -47,6 +47,7 @@ import com.jme3.asset.BlenderKey; import com.jme3.material.Material; import com.jme3.math.ColorRGBA; import com.jme3.scene.Node; +import com.jme3.scene.plugins.blender.animations.BlenderAction; import com.jme3.scene.plugins.blender.animations.BoneContext; import com.jme3.scene.plugins.blender.constraints.Constraint; import com.jme3.scene.plugins.blender.file.BlenderInputStream; @@ -76,7 +77,7 @@ public class BlenderContext { /** The asset manager. */ private AssetManager assetManager; /** The blocks read from the file. */ - protected List blocks; + protected List blocks; /** * A map containing the file block headers. The key is the old memory address. */ @@ -114,6 +115,8 @@ public class BlenderContext { private Map helpers = new HashMap(); /** Markers used by loading classes to store some custom data. This is made to avoid putting this data into user properties. */ private Map> markers = new HashMap>(); + /** A map of blender actions. The key is the action name and the value is the action itself. */ + private Map actions = new HashMap(); /** * This method sets the blender file version. @@ -405,7 +408,7 @@ public class BlenderContext { } return result; } - + /** * This method adds the animation for the specified OMA of its owner. * @@ -416,13 +419,13 @@ public class BlenderContext { */ public void addAnimation(Long ownerOMA, Animation animation) { List animList = animations.get(ownerOMA); - if(animList == null) { + if (animList == null) { animList = new ArrayList(); animations.put(ownerOMA, animList); } animList.add(animation); } - + /** * This method returns the animation data for the specified owner. * @@ -534,14 +537,15 @@ public class BlenderContext { /** * Returns bone by given name. * - * @param skeletonOMA the OMA of the skeleton where the bone will be searched + * @param skeletonOMA + * the OMA of the skeleton where the bone will be searched * @param name * the name of the bone * @return found bone or null if none bone of a given name exists */ public BoneContext getBoneByName(Long skeletonOMA, String name) { for (Entry entry : boneContexts.entrySet()) { - if(entry.getValue().getArmatureObjectOMA().equals(skeletonOMA)) { + if (entry.getValue().getArmatureObjectOMA().equals(skeletonOMA)) { Bone bone = entry.getValue().getBone(); if (bone != null && name.equals(bone.getName())) { return entry.getValue(); @@ -619,6 +623,22 @@ public class BlenderContext { return markersMap == null ? null : markersMap.get(feature); } + /** + * Adds blender action to the context. + * @param action + * the action loaded from the blend file + */ + public void addAction(BlenderAction action) { + actions.put(action.getName(), action); + } + + /** + * @return a map of blender actions; the key is the action name and the value is action itself + */ + public Map getActions() { + return actions; + } + /** * This enum defines what loaded data type user wants to retreive. It can be * either filled structure or already converted data. diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationHelper.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationHelper.java index 1a123cf59..b058ae665 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationHelper.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationHelper.java @@ -2,9 +2,10 @@ package com.jme3.scene.plugins.blender.animations; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -14,11 +15,11 @@ import com.jme3.animation.BoneTrack; import com.jme3.animation.Skeleton; import com.jme3.animation.SkeletonControl; import com.jme3.animation.SpatialTrack; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; +import com.jme3.asset.BlenderKey.AnimationMatchMethod; import com.jme3.scene.Node; import com.jme3.scene.plugins.blender.AbstractBlenderHelper; import com.jme3.scene.plugins.blender.BlenderContext; +import com.jme3.scene.plugins.blender.animations.Ipo.ConstIpo; import com.jme3.scene.plugins.blender.curves.BezierCurve; import com.jme3.scene.plugins.blender.file.BlenderFileException; import com.jme3.scene.plugins.blender.file.BlenderInputStream; @@ -32,10 +33,7 @@ import com.jme3.scene.plugins.blender.objects.ObjectHelper; * @author Marcin Roguski (Kaelthas) */ public class AnimationHelper extends AbstractBlenderHelper { - private static final Logger LOGGER = Logger.getLogger(AnimationHelper.class.getName()); - - /** A map of blender actions. */ - private Map actions = new HashMap(); + private static final Logger LOGGER = Logger.getLogger(AnimationHelper.class.getName()); public AnimationHelper(String blenderVersion, BlenderContext blenderContext) { super(blenderVersion, blenderContext); @@ -54,7 +52,7 @@ public class AnimationHelper extends AbstractBlenderHelper { for (FileBlockHeader header : actionHeaders) { Structure actionStructure = header.getStructure(blenderContext); LOGGER.log(Level.INFO, "Found animation: {0}.", actionStructure.getName()); - actions.put(actionStructure.getName(), this.getTracks(actionStructure, blenderContext)); + blenderContext.addAction(this.getTracks(actionStructure, blenderContext)); } } } @@ -63,24 +61,20 @@ public class AnimationHelper extends AbstractBlenderHelper { * The method applies animations to the given node. The names of the animations should be the same as actions names in the blender file. * @param node * the node to whom the animations will be applied - * @param animationNames - * the names of the animations to be applied + * @param animationMatchMethod + * the way animation should be matched with node */ - public void applyAnimations(Node node, List animationNames) { - if (animationNames != null && animationNames.size() > 0) { + public void applyAnimations(Node node, AnimationMatchMethod animationMatchMethod) { + List actions = this.getActions(node, animationMatchMethod); + if (actions.size() > 0) { List animations = new ArrayList(); - for (String animationName : animationNames) { - BlenderAction action = actions.get(animationName); - if (action != null) { - SpatialTrack[] tracks = action.toTracks(node); - if (tracks != null && tracks.length > 0) { - Animation spatialAnimation = new Animation(animationName, action.getAnimationTime()); - spatialAnimation.setTracks(tracks); - animations.add(spatialAnimation); - blenderContext.addAnimation((Long) node.getUserData(ObjectHelper.OMA_MARKER), spatialAnimation); - } - } else { - LOGGER.log(Level.WARNING, "Cannot find animation named: {0}.", animationName); + for (BlenderAction action : actions) { + SpatialTrack[] tracks = action.toTracks(node); + if (tracks != null && tracks.length > 0) { + Animation spatialAnimation = new Animation(action.getName(), action.getAnimationTime()); + spatialAnimation.setTracks(tracks); + animations.add(spatialAnimation); + blenderContext.addAnimation((Long) node.getUserData(ObjectHelper.OMA_MARKER), spatialAnimation); } } @@ -103,28 +97,24 @@ public class AnimationHelper extends AbstractBlenderHelper { * the node where the animations will be applied * @param skeleton * the skeleton of the node - * @param animationNames - * the names of the skeleton animations + * @param animationMatchMethod + * the way animation should be matched with skeleton */ - public void applyAnimations(Node node, Skeleton skeleton, List animationNames) { + public void applyAnimations(Node node, Skeleton skeleton, AnimationMatchMethod animationMatchMethod) { node.addControl(new SkeletonControl(skeleton)); blenderContext.setNodeForSkeleton(skeleton, node); + List actions = this.getActions(skeleton, animationMatchMethod); - if (animationNames != null && animationNames.size() > 0) { + if (actions.size() > 0) { List animations = new ArrayList(); - for (String animationName : animationNames) { - BlenderAction action = actions.get(animationName); - if (action != null) { - BoneTrack[] tracks = action.toTracks(skeleton); - if (tracks != null && tracks.length > 0) { - Animation boneAnimation = new Animation(animationName, action.getAnimationTime()); - boneAnimation.setTracks(tracks); - animations.add(boneAnimation); - Long animatedNodeOMA = ((Number)blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, node)).longValue(); - blenderContext.addAnimation(animatedNodeOMA, boneAnimation); - } - } else { - LOGGER.log(Level.WARNING, "Cannot find animation named: {0}.", animationName); + for (BlenderAction action : actions) { + BoneTrack[] tracks = action.toTracks(skeleton); + if (tracks != null && tracks.length > 0) { + Animation boneAnimation = new Animation(action.getName(), action.getAnimationTime()); + boneAnimation.setTracks(tracks); + animations.add(boneAnimation); + Long animatedNodeOMA = ((Number) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, node)).longValue(); + blenderContext.addAnimation(animatedNodeOMA, boneAnimation); } } if (animations.size() > 0) { @@ -136,10 +126,10 @@ public class AnimationHelper extends AbstractBlenderHelper { } control.setAnimations(anims); node.addControl(control); - - //make sure that SkeletonControl is added AFTER the AnimControl + + // make sure that SkeletonControl is added AFTER the AnimControl SkeletonControl skeletonControl = node.getControl(SkeletonControl.class); - if(skeletonControl != null) { + if (skeletonControl != null) { node.removeControl(SkeletonControl.class); node.addControl(skeletonControl); } @@ -230,7 +220,7 @@ public class AnimationHelper extends AbstractBlenderHelper { LOGGER.log(Level.FINE, "Getting tracks!"); Structure groups = (Structure) actionStructure.getFieldValue("groups"); List actionGroups = groups.evaluateListBase();// bActionGroup - BlenderAction blenderAction = new BlenderAction(blenderContext.getBlenderKey().getFps()); + BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps()); int lastFrame = 1; for (Structure actionGroup : actionGroups) { String name = actionGroup.getFieldValue("name").toString(); @@ -269,7 +259,7 @@ public class AnimationHelper extends AbstractBlenderHelper { LOGGER.log(Level.FINE, "Getting tracks!"); Structure chanbase = (Structure) actionStructure.getFieldValue("chanbase"); List actionChannels = chanbase.evaluateListBase();// bActionChannel - BlenderAction blenderAction = new BlenderAction(blenderContext.getBlenderKey().getFps()); + BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps()); int lastFrame = 1; for (Structure bActionChannel : actionChannels) { String animatedFeatureName = bActionChannel.getFieldValue("name").toString(); @@ -277,7 +267,7 @@ public class AnimationHelper extends AbstractBlenderHelper { if (!p.isNull()) { Structure ipoStructure = p.fetchData().get(0); Ipo ipo = this.fromIpoStructure(ipoStructure, blenderContext); - if(ipo != null) {//this can happen when ipo with no curves appear in blender file + if (ipo != null) {// this can happen when ipo with no curves appear in blender file lastFrame = Math.max(lastFrame, ipo.getLastFrame()); blenderAction.featuresTracks.put(animatedFeatureName, ipo); } @@ -321,104 +311,77 @@ public class AnimationHelper extends AbstractBlenderHelper { if (rnaPath.endsWith("rotation") || rnaPath.endsWith("rotation_euler")) { return Ipo.OB_ROT_X + arrayIndex; } - LOGGER.warning("Unknown curve rna path: " + rnaPath); + LOGGER.log(Level.WARNING, "Unknown curve rna path: {0}", rnaPath); return -1; } /** - * 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) + * The method returns the actions for the given skeleton. The actions represent armature animation in blender. + * @param skeleton + * the skeleton we fetch the actions for + * @param animationMatchMethod + * the method of animation matching + * @return a list of animations for the specified skeleton */ - private static class BlenderAction { - /** Animation speed - frames per second. */ - private int fps; - /** The last frame of the animation (the last ipo curve node position is used as a last frame). */ - private 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. - */ - private Map featuresTracks = new HashMap(); + private List getActions(Skeleton skeleton, AnimationMatchMethod animationMatchMethod) { + List result = new ArrayList(); - public BlenderAction(int fps) { - this.fps = fps; + // first get a set of bone names + Set boneNames = new HashSet(); + for (int i = 0; i < skeleton.getBoneCount(); ++i) { + String boneName = skeleton.getBone(i).getName(); + if (boneName != null && boneName.length() > 0) { + boneNames.add(skeleton.getBone(i).getName()); + } } - /** - * 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 tracks = new ArrayList(featuresTracks.size()); - for (Entry entry : featuresTracks.entrySet()) { - tracks.add((SpatialTrack) entry.getValue().calculateTrack(0, node.getLocalTranslation(), node.getLocalRotation(), node.getLocalScale(), 1, stopFrame, fps, true)); + // finding matches + Set matchingNames = new HashSet(); + for (Entry actionEntry : blenderContext.getActions().entrySet()) { + // compute how many action tracks match the skeleton bones' names + for (String boneName : boneNames) { + if (actionEntry.getValue().hasTrackName(boneName)) { + matchingNames.add(boneName); + } + } + + BlenderAction action = null; + if (animationMatchMethod == AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH && matchingNames.size() > 0) { + action = actionEntry.getValue(); + } else if (matchingNames.size() == actionEntry.getValue().getTracksCount()) { + action = actionEntry.getValue(); } - 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 tracks = new ArrayList(featuresTracks.size()); - for (Entry 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)); + if (action != null) { + // remove the tracks that do not match the bone names if the matching method is different from ALL_NAMES_MATCH + if (animationMatchMethod != AnimationMatchMethod.ALL_NAMES_MATCH) { + action = action.clone(); + action.removeTracksThatAreNotInTheCollection(matchingNames); + } + result.add(action); } - return tracks.toArray(new BoneTrack[tracks.size()]); - } - /** - * @return the time of animations (in seconds) - */ - public float getAnimationTime() { - return (stopFrame - 1) / (float) fps; + matchingNames.clear(); } + return result; } /** - * Ipo constant curve. This is a curve with only one value and no specified - * type. This type of ipo cannot be used to calculate tracks. It should only - * be used to calculate single value for a given frame. - * - * @author Marcin Roguski (Kaelthas) + * The method returns the actions for the given node. The actions represent object animation in blender. + * @param node + * the node we fetch the actions for + * @param animationMatchMethod + * the method of animation matching + * @return a list of animations for the specified node */ - private class ConstIpo extends Ipo { - - /** The constant value of this ipo. */ - private float constValue; + private List getActions(Node node, AnimationMatchMethod animationMatchMethod) { + List result = new ArrayList(); - /** - * Constructor. Stores the constant value of this ipo. - * - * @param constValue - * the constant value of this ipo - */ - public ConstIpo(float constValue) { - super(null, false, 0);// the version is not important here - this.constValue = constValue; - } - - @Override - public float calculateValue(int frame) { - return constValue; - } - - @Override - public float calculateValue(int frame, int curveIndex) { - return constValue; - } - - @Override - public BoneTrack calculateTrack(int boneIndex, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean boneTrack) { - throw new IllegalStateException("Constatnt ipo object cannot be used for calculating bone tracks!"); + for (Entry actionEntry : blenderContext.getActions().entrySet()) { + if (actionEntry.getValue().hasTrackName(node.getName())) { + result.add(actionEntry.getValue()); + } } + return result; } } diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BlenderAction.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BlenderAction.java new file mode 100644 index 000000000..aa36c918e --- /dev/null +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BlenderAction.java @@ -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 featuresTracks = new HashMap(); + + public BlenderAction(String name, int fps) { + this.name = name; + this.fps = fps; + } + + public void removeTracksThatAreNotInTheCollection(Collection trackNames) { + Map newTracks = new HashMap(); + 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(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 tracks = new ArrayList(featuresTracks.size()); + for (Entry 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 tracks = new ArrayList(featuresTracks.size()); + for (Entry 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 true if the track of a given name exists for the + * action and false 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() + "]]"; + } +} diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java index 95779a676..f08a14770 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java @@ -170,7 +170,7 @@ public class Ipo { for (int j = 0; j < bezierCurves.length; ++j) { double value = bezierCurves[j].evaluate(frame, BezierCurve.Y_VALUE); switch (bezierCurves[j].getType()) { - // LOCATION + // LOCATION case AC_LOC_X: translation[0] = (float) value; break; @@ -186,18 +186,18 @@ public class Ipo { // EULER ROTATION case OB_ROT_X: - eulerRotationUsed = true; + eulerRotationUsed = true; eulerRotation[0] = (float) value * degreeToRadiansFactor; break; case OB_ROT_Y: - eulerRotationUsed = true; + eulerRotationUsed = true; if (swapAxes && value != 0) { value = -value; } eulerRotation[yIndex] = (float) value * degreeToRadiansFactor; break; case OB_ROT_Z: - eulerRotationUsed = true; + eulerRotationUsed = true; eulerRotation[zIndex] = (float) value * degreeToRadiansFactor; break; @@ -214,15 +214,15 @@ public class Ipo { // QUATERNION ROTATION (used with bone animation) case AC_QUAT_W: - queternionRotationUsed = true; + queternionRotationUsed = true; quaternionRotation[3] = (float) value; break; case AC_QUAT_X: - queternionRotationUsed = true; + queternionRotationUsed = true; quaternionRotation[0] = (float) value; break; case AC_QUAT_Y: - queternionRotationUsed = true; + queternionRotationUsed = true; if (swapAxes && value != 0) { value = -value; } @@ -236,12 +236,12 @@ public class Ipo { } } translations[index] = localRotation.multLocal(new Vector3f(translation[0], translation[1], translation[2])); - if(queternionRotationUsed) { - rotations[index] = new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]); + if (queternionRotationUsed) { + rotations[index] = new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]); } else { - rotations[index] = new Quaternion().fromAngles(eulerRotation); + rotations[index] = new Quaternion().fromAngles(eulerRotation); } - + scales[index] = new Vector3f(scale[0], scale[1], scale[2]); } if (spatialTrack) { @@ -249,12 +249,51 @@ public class Ipo { } else { calculatedTrack = new BoneTrack(targetIndex, times, translations, rotations, scales); } - - if(queternionRotationUsed && eulerRotationUsed) { - LOGGER.warning("Animation uses both euler and quaternion tracks for rotations. Quaternion rotation is applied. Make sure that this is what you wanted!"); + + if (queternionRotationUsed && eulerRotationUsed) { + LOGGER.warning("Animation uses both euler and quaternion tracks for rotations. Quaternion rotation is applied. Make sure that this is what you wanted!"); } } - + return calculatedTrack; } + + /** + * Ipo constant curve. This is a curve with only one value and no specified + * type. This type of ipo cannot be used to calculate tracks. It should only + * be used to calculate single value for a given frame. + * + * @author Marcin Roguski (Kaelthas) + */ + /* package */static class ConstIpo extends Ipo { + + /** The constant value of this ipo. */ + private float constValue; + + /** + * Constructor. Stores the constant value of this ipo. + * + * @param constValue + * the constant value of this ipo + */ + public ConstIpo(float constValue) { + super(null, false, 0);// the version is not important here + this.constValue = constValue; + } + + @Override + public float calculateValue(int frame) { + return constValue; + } + + @Override + public float calculateValue(int frame, int curveIndex) { + return constValue; + } + + @Override + public BoneTrack calculateTrack(int boneIndex, Vector3f localTranslation, Quaternion localRotation, Vector3f localScale, int startFrame, int stopFrame, int fps, boolean boneTrack) { + throw new IllegalStateException("Constatnt ipo object cannot be used for calculating bone tracks!"); + } + } } diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArmatureModifier.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArmatureModifier.java index 9576d94ac..f12a3cb39 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArmatureModifier.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArmatureModifier.java @@ -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 bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(); - List bonesList = new ArrayList(); - 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 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 geomList = (List) 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> vertexReferenceMap = meshContext.getVertexReferenceMap(materialIndex); - - Map vertexGroups = new HashMap(); - 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> entry : vertexReferenceMap.entrySet()) { - List 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 weights = new HashMap();// [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 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 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 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 { - 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 msw = new HashMap(maximumSize);// msw = Most Significant Weight - for (Entry 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 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 entry : this.entrySet()) { - sum += entry.getValue(); - } - - if (sum != 0 && sum != 1) { - for (Entry 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 bonebase = ((Structure) armatureStructure.getFieldValue("bonebase")).evaluateListBase(); + List bonesList = new ArrayList(); + 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 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 geomList = (List) 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> vertexReferenceMap = meshContext.getVertexReferenceMap(materialIndex); + + Map vertexGroups = new HashMap(); + 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> entry : vertexReferenceMap.entrySet()) { + List 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 weights = new HashMap();// [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 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 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 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 { + 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 msw = new HashMap(maximumSize);// msw = Most Significant Weight + for (Entry 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 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 entry : this.entrySet()) { + sum += entry.getValue(); + } + + if (sum != 0 && sum != 1) { + for (Entry entry : this.entrySet()) { + entry.setValue(entry.getValue() / sum); + } + } + } + } +} diff --git a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/ObjectHelper.java b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/ObjectHelper.java index b8d63db02..49fb475ad 100644 --- a/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/ObjectHelper.java +++ b/jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/ObjectHelper.java @@ -190,8 +190,8 @@ public class ObjectHelper extends AbstractBlenderHelper { LightHelper lightHelper = blenderContext.getHelper(LightHelper.class); List lampsArray = pLamp.fetchData(); result = lightHelper.toLight(lampsArray.get(0), blenderContext); - if(result == null) { - //probably some light type is not supported, just create a node so that we can maintain child-parent relationship for nodes + if (result == null) { + // probably some light type is not supported, just create a node so that we can maintain child-parent relationship for nodes result = new Node(name); } } @@ -218,7 +218,7 @@ public class ObjectHelper extends AbstractBlenderHelper { if (objectType == ObjectType.ARMATURE) { blenderContext.addMarker(ARMATURE_NODE_MARKER, result, Boolean.TRUE); } - + result.setLocalTransform(t); result.setCullHint(visible ? CullHint.Always : CullHint.Inherit); if (parent instanceof Node) { @@ -245,7 +245,7 @@ public class ObjectHelper extends AbstractBlenderHelper { LOGGER.fine("Applying animations to the object if such are defined."); AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); - animationHelper.applyAnimations(result, blenderContext.getBlenderKey().getNodeAnimationNames(name)); + animationHelper.applyAnimations(result, blenderContext.getBlenderKey().getAnimationMatchMethod()); LOGGER.fine("Loading constraints connected with this object."); ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);