Feature: added automatic action mapping instead of explicit mapping in

BlenderKey.
experimental
jmekaelthas 11 years ago
parent 03f8df05b6
commit 51215a352e
  1. 211
      jme3-blender/src/main/java/com/jme3/asset/BlenderKey.java
  2. 28
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/BlenderContext.java
  3. 215
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/AnimationHelper.java
  4. 134
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/BlenderAction.java
  5. 63
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/animations/Ipo.java
  6. 2
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/modifiers/ArmatureModifier.java
  7. 6
      jme3-blender/src/main/java/com/jme3/scene/plugins/blender/objects/ObjectHelper.java

@ -33,11 +33,7 @@ package com.jme3.asset;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue; import java.util.Queue;
import com.jme3.animation.Animation; import com.jme3.animation.Animation;
@ -66,70 +62,68 @@ import com.jme3.texture.Texture;
*/ */
public class BlenderKey extends ModelKey { 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 * FramesPerSecond parameter describe how many frames there are in each second. It allows to calculate the time
* between the frames. * 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. * 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. */ /** 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. */ /** 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. */ /** 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). */ /** 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 * 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. * 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 * 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). * 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. */ /** 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. * 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. * 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. */ /** 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. * Maximum texture size. Might be dependant on the graphic card.
* This value is taken from <b>org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE</b>. * This value is taken from <b>org.lwjgl.opengl.GL11.GL_MAX_TEXTURE_SIZE</b>.
*/ */
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. */ /** 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. */ /** 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 * 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. * 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. */ /** 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. */ /** 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 * 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. * and textures that in the final result will never be visible - will be discarded.
*/ */
protected boolean optimiseTextures; protected boolean optimiseTextures;
/** A map between node name and its animation names. */ /** The method of matching animations to skeletons. The default value is: AT_LEAST_ONE_NAME_MATCH. */
protected Map<String, List<String>> nodeAnimationMap = new HashMap<String, List<String>>(); protected AnimationMatchMethod animationMatchMethod = AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH;
/** A map between node name and its skeleton animation names. */
protected Map<String, List<String>> skeletonAnimationMap = new HashMap<String, List<String>>();
/** /**
* Constructor used by serialization mechanisms. * Constructor used by serialization mechanisms.
@ -446,6 +440,23 @@ public class BlenderKey extends ModelKey {
return optimiseTextures; 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 * 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 * 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; 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<String> animations = nodeAnimationMap.get(nodeName);
if (animations == null) {
animations = new ArrayList<String>();
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<String> 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<String> animations = skeletonAnimationMap.get(nodeName);
if (animations == null) {
animations = new ArrayList<String>();
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<String> getSkeletonAnimationNames(String nodeName) {
return skeletonAnimationMap.get(nodeName);
}
@Override @Override
public void write(JmeExporter e) throws IOException { public void write(JmeExporter e) throws IOException {
super.write(e); super.write(e);
@ -553,30 +512,7 @@ public class BlenderKey extends ModelKey {
oc.write(skyGeneratedTextureRadius, "sky-generated-texture-radius", 1f); oc.write(skyGeneratedTextureRadius, "sky-generated-texture-radius", 1f);
oc.write(skyGeneratedTextureShape, "sky-generated-texture-shape", SkyGeneratedTextureShape.SPHERE); oc.write(skyGeneratedTextureShape, "sky-generated-texture-shape", SkyGeneratedTextureShape.SPHERE);
oc.write(optimiseTextures, "optimise-textures", false); oc.write(optimiseTextures, "optimise-textures", false);
oc.write(animationMatchMethod, "animation-match-method", AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH);
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<String, List<String>> 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<String, List<String>> 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;
}
}
} }
@Override @Override
@ -598,32 +534,14 @@ public class BlenderKey extends ModelKey {
skyGeneratedTextureRadius = ic.readFloat("sky-generated-texture-radius", 1f); skyGeneratedTextureRadius = ic.readFloat("sky-generated-texture-radius", 1f);
skyGeneratedTextureShape = ic.readEnum("sky-generated-texture-shape", SkyGeneratedTextureShape.class, SkyGeneratedTextureShape.SPHERE); skyGeneratedTextureShape = ic.readEnum("sky-generated-texture-shape", SkyGeneratedTextureShape.class, SkyGeneratedTextureShape.SPHERE);
optimiseTextures = ic.readBoolean("optimise-textures", false); optimiseTextures = ic.readBoolean("optimise-textures", false);
animationMatchMethod = ic.readEnum("animation-match-method", AnimationMatchMethod.class, AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH);
int animsSize = ic.readInt("node-anims-map-size", 0);
nodeAnimationMap = new HashMap<String, List<String>>(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<String>(Arrays.asList(anims)));// must create ArrayList because 'asList' method returns unmodifiable list
}
}
animsSize = ic.readInt("skeleton-anims-map-size", 0);
skeletonAnimationMap = new HashMap<String, List<String>>(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<String>(Arrays.asList(anims)));// must create ArrayList because 'asList' method returns unmodifiable list
}
}
} }
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = super.hashCode(); int result = super.hashCode();
result = prime * result + (animationMatchMethod == null ? 0 : animationMatchMethod.hashCode());
result = prime * result + (assetRootPath == null ? 0 : assetRootPath.hashCode()); result = prime * result + (assetRootPath == null ? 0 : assetRootPath.hashCode());
result = prime * result + (defaultMaterial == null ? 0 : defaultMaterial.hashCode()); result = prime * result + (defaultMaterial == null ? 0 : defaultMaterial.hashCode());
result = prime * result + (faceCullMode == null ? 0 : faceCullMode.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 + (loadUnlinkedAssets ? 1231 : 1237);
result = prime * result + maxTextureSize; result = prime * result + maxTextureSize;
result = prime * result + (mipmapGenerationMethod == null ? 0 : mipmapGenerationMethod.hashCode()); 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 + (optimiseTextures ? 1231 : 1237);
result = prime * result + (skeletonAnimationMap == null ? 0 : skeletonAnimationMap.hashCode());
result = prime * result + Float.floatToIntBits(skyGeneratedTextureRadius); result = prime * result + Float.floatToIntBits(skyGeneratedTextureRadius);
result = prime * result + (skyGeneratedTextureShape == null ? 0 : skyGeneratedTextureShape.hashCode()); result = prime * result + (skyGeneratedTextureShape == null ? 0 : skyGeneratedTextureShape.hashCode());
result = prime * result + skyGeneratedTextureSize; result = prime * result + skyGeneratedTextureSize;
@ -649,13 +565,13 @@ public class BlenderKey extends ModelKey {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) { if (obj instanceof BlenderKey) {
return true;
}
if (!(obj instanceof BlenderKey)) {
return false; return false;
} }
BlenderKey other = (BlenderKey) obj; BlenderKey other = (BlenderKey) obj;
if (animationMatchMethod != other.animationMatchMethod) {
return false;
}
if (assetRootPath == null) { if (assetRootPath == null) {
if (other.assetRootPath != null) { if (other.assetRootPath != null) {
return false; return false;
@ -703,23 +619,9 @@ public class BlenderKey extends ModelKey {
if (mipmapGenerationMethod != other.mipmapGenerationMethod) { if (mipmapGenerationMethod != other.mipmapGenerationMethod) {
return false; return false;
} }
if (nodeAnimationMap == null) {
if (other.nodeAnimationMap != null) {
return false;
}
} else if (!nodeAnimationMap.equals(other.nodeAnimationMap)) {
return false;
}
if (optimiseTextures != other.optimiseTextures) { if (optimiseTextures != other.optimiseTextures) {
return false; 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)) { if (Float.floatToIntBits(skyGeneratedTextureRadius) != Float.floatToIntBits(other.skyGeneratedTextureRadius)) {
return false; return false;
} }
@ -773,6 +675,29 @@ public class BlenderKey extends ModelKey {
CUBE, SPHERE; 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. * This class holds the loading results according to the given loading flag.
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)

@ -47,6 +47,7 @@ import com.jme3.asset.BlenderKey;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.scene.Node; 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.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.Constraint; import com.jme3.scene.plugins.blender.constraints.Constraint;
import com.jme3.scene.plugins.blender.file.BlenderInputStream; import com.jme3.scene.plugins.blender.file.BlenderInputStream;
@ -76,7 +77,7 @@ public class BlenderContext {
/** The asset manager. */ /** The asset manager. */
private AssetManager assetManager; private AssetManager assetManager;
/** The blocks read from the file. */ /** The blocks read from the file. */
protected List<FileBlockHeader> blocks; protected List<FileBlockHeader> blocks;
/** /**
* A map containing the file block headers. The key is the old memory address. * A map containing the file block headers. The key is the old memory address.
*/ */
@ -114,6 +115,8 @@ public class BlenderContext {
private Map<String, AbstractBlenderHelper> helpers = new HashMap<String, AbstractBlenderHelper>(); private Map<String, AbstractBlenderHelper> helpers = new HashMap<String, AbstractBlenderHelper>();
/** Markers used by loading classes to store some custom data. This is made to avoid putting this data into user properties. */ /** Markers used by loading classes to store some custom data. This is made to avoid putting this data into user properties. */
private Map<String, Map<Object, Object>> markers = new HashMap<String, Map<Object, Object>>(); private Map<String, Map<Object, Object>> markers = new HashMap<String, Map<Object, Object>>();
/** A map of blender actions. The key is the action name and the value is the action itself. */
private Map<String, BlenderAction> actions = new HashMap<String, BlenderAction>();
/** /**
* This method sets the blender file version. * This method sets the blender file version.
@ -416,7 +419,7 @@ public class BlenderContext {
*/ */
public void addAnimation(Long ownerOMA, Animation animation) { public void addAnimation(Long ownerOMA, Animation animation) {
List<Animation> animList = animations.get(ownerOMA); List<Animation> animList = animations.get(ownerOMA);
if(animList == null) { if (animList == null) {
animList = new ArrayList<Animation>(); animList = new ArrayList<Animation>();
animations.put(ownerOMA, animList); animations.put(ownerOMA, animList);
} }
@ -534,14 +537,15 @@ public class BlenderContext {
/** /**
* Returns bone by given name. * 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 * @param name
* the name of the bone * the name of the bone
* @return found bone or null if none bone of a given name exists * @return found bone or null if none bone of a given name exists
*/ */
public BoneContext getBoneByName(Long skeletonOMA, String name) { public BoneContext getBoneByName(Long skeletonOMA, String name) {
for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) { for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) {
if(entry.getValue().getArmatureObjectOMA().equals(skeletonOMA)) { if (entry.getValue().getArmatureObjectOMA().equals(skeletonOMA)) {
Bone bone = entry.getValue().getBone(); Bone bone = entry.getValue().getBone();
if (bone != null && name.equals(bone.getName())) { if (bone != null && name.equals(bone.getName())) {
return entry.getValue(); return entry.getValue();
@ -619,6 +623,22 @@ public class BlenderContext {
return markersMap == null ? null : markersMap.get(feature); 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<String, BlenderAction> getActions() {
return actions;
}
/** /**
* This enum defines what loaded data type user wants to retreive. It can be * This enum defines what loaded data type user wants to retreive. It can be
* either filled structure or already converted data. * either filled structure or already converted data.

@ -2,9 +2,10 @@ package com.jme3.scene.plugins.blender.animations;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -14,11 +15,11 @@ import com.jme3.animation.BoneTrack;
import com.jme3.animation.Skeleton; import com.jme3.animation.Skeleton;
import com.jme3.animation.SkeletonControl; import com.jme3.animation.SkeletonControl;
import com.jme3.animation.SpatialTrack; import com.jme3.animation.SpatialTrack;
import com.jme3.math.Quaternion; import com.jme3.asset.BlenderKey.AnimationMatchMethod;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.AbstractBlenderHelper; import com.jme3.scene.plugins.blender.AbstractBlenderHelper;
import com.jme3.scene.plugins.blender.BlenderContext; 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.curves.BezierCurve;
import com.jme3.scene.plugins.blender.file.BlenderFileException; import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream; import com.jme3.scene.plugins.blender.file.BlenderInputStream;
@ -32,10 +33,7 @@ import com.jme3.scene.plugins.blender.objects.ObjectHelper;
* @author Marcin Roguski (Kaelthas) * @author Marcin Roguski (Kaelthas)
*/ */
public class AnimationHelper extends AbstractBlenderHelper { public class AnimationHelper extends AbstractBlenderHelper {
private static final Logger LOGGER = Logger.getLogger(AnimationHelper.class.getName()); private static final Logger LOGGER = Logger.getLogger(AnimationHelper.class.getName());
/** A map of blender actions. */
private Map<String, BlenderAction> actions = new HashMap<String, BlenderAction>();
public AnimationHelper(String blenderVersion, BlenderContext blenderContext) { public AnimationHelper(String blenderVersion, BlenderContext blenderContext) {
super(blenderVersion, blenderContext); super(blenderVersion, blenderContext);
@ -54,7 +52,7 @@ public class AnimationHelper extends AbstractBlenderHelper {
for (FileBlockHeader header : actionHeaders) { for (FileBlockHeader header : actionHeaders) {
Structure actionStructure = header.getStructure(blenderContext); Structure actionStructure = header.getStructure(blenderContext);
LOGGER.log(Level.INFO, "Found animation: {0}.", actionStructure.getName()); 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. * 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 * @param node
* the node to whom the animations will be applied * the node to whom the animations will be applied
* @param animationNames * @param animationMatchMethod
* the names of the animations to be applied * the way animation should be matched with node
*/ */
public void applyAnimations(Node node, List<String> animationNames) { public void applyAnimations(Node node, AnimationMatchMethod animationMatchMethod) {
if (animationNames != null && animationNames.size() > 0) { List<BlenderAction> actions = this.getActions(node, animationMatchMethod);
if (actions.size() > 0) {
List<Animation> animations = new ArrayList<Animation>(); List<Animation> animations = new ArrayList<Animation>();
for (String animationName : animationNames) { for (BlenderAction action : actions) {
BlenderAction action = actions.get(animationName); SpatialTrack[] tracks = action.toTracks(node);
if (action != null) { if (tracks != null && tracks.length > 0) {
SpatialTrack[] tracks = action.toTracks(node); Animation spatialAnimation = new Animation(action.getName(), action.getAnimationTime());
if (tracks != null && tracks.length > 0) { spatialAnimation.setTracks(tracks);
Animation spatialAnimation = new Animation(animationName, action.getAnimationTime()); animations.add(spatialAnimation);
spatialAnimation.setTracks(tracks); blenderContext.addAnimation((Long) node.getUserData(ObjectHelper.OMA_MARKER), spatialAnimation);
animations.add(spatialAnimation);
blenderContext.addAnimation((Long) node.getUserData(ObjectHelper.OMA_MARKER), spatialAnimation);
}
} else {
LOGGER.log(Level.WARNING, "Cannot find animation named: {0}.", animationName);
} }
} }
@ -103,28 +97,24 @@ public class AnimationHelper extends AbstractBlenderHelper {
* the node where the animations will be applied * the node where the animations will be applied
* @param skeleton * @param skeleton
* the skeleton of the node * the skeleton of the node
* @param animationNames * @param animationMatchMethod
* the names of the skeleton animations * the way animation should be matched with skeleton
*/ */
public void applyAnimations(Node node, Skeleton skeleton, List<String> animationNames) { public void applyAnimations(Node node, Skeleton skeleton, AnimationMatchMethod animationMatchMethod) {
node.addControl(new SkeletonControl(skeleton)); node.addControl(new SkeletonControl(skeleton));
blenderContext.setNodeForSkeleton(skeleton, node); blenderContext.setNodeForSkeleton(skeleton, node);
List<BlenderAction> actions = this.getActions(skeleton, animationMatchMethod);
if (animationNames != null && animationNames.size() > 0) { if (actions.size() > 0) {
List<Animation> animations = new ArrayList<Animation>(); List<Animation> animations = new ArrayList<Animation>();
for (String animationName : animationNames) { for (BlenderAction action : actions) {
BlenderAction action = actions.get(animationName); BoneTrack[] tracks = action.toTracks(skeleton);
if (action != null) { if (tracks != null && tracks.length > 0) {
BoneTrack[] tracks = action.toTracks(skeleton); Animation boneAnimation = new Animation(action.getName(), action.getAnimationTime());
if (tracks != null && tracks.length > 0) { boneAnimation.setTracks(tracks);
Animation boneAnimation = new Animation(animationName, action.getAnimationTime()); animations.add(boneAnimation);
boneAnimation.setTracks(tracks); Long animatedNodeOMA = ((Number) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, node)).longValue();
animations.add(boneAnimation); blenderContext.addAnimation(animatedNodeOMA, 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);
} }
} }
if (animations.size() > 0) { if (animations.size() > 0) {
@ -137,9 +127,9 @@ public class AnimationHelper extends AbstractBlenderHelper {
control.setAnimations(anims); control.setAnimations(anims);
node.addControl(control); 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); SkeletonControl skeletonControl = node.getControl(SkeletonControl.class);
if(skeletonControl != null) { if (skeletonControl != null) {
node.removeControl(SkeletonControl.class); node.removeControl(SkeletonControl.class);
node.addControl(skeletonControl); node.addControl(skeletonControl);
} }
@ -230,7 +220,7 @@ public class AnimationHelper extends AbstractBlenderHelper {
LOGGER.log(Level.FINE, "Getting tracks!"); LOGGER.log(Level.FINE, "Getting tracks!");
Structure groups = (Structure) actionStructure.getFieldValue("groups"); Structure groups = (Structure) actionStructure.getFieldValue("groups");
List<Structure> actionGroups = groups.evaluateListBase();// bActionGroup List<Structure> actionGroups = groups.evaluateListBase();// bActionGroup
BlenderAction blenderAction = new BlenderAction(blenderContext.getBlenderKey().getFps()); BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps());
int lastFrame = 1; int lastFrame = 1;
for (Structure actionGroup : actionGroups) { for (Structure actionGroup : actionGroups) {
String name = actionGroup.getFieldValue("name").toString(); String name = actionGroup.getFieldValue("name").toString();
@ -269,7 +259,7 @@ public class AnimationHelper extends AbstractBlenderHelper {
LOGGER.log(Level.FINE, "Getting tracks!"); LOGGER.log(Level.FINE, "Getting tracks!");
Structure chanbase = (Structure) actionStructure.getFieldValue("chanbase"); Structure chanbase = (Structure) actionStructure.getFieldValue("chanbase");
List<Structure> actionChannels = chanbase.evaluateListBase();// bActionChannel List<Structure> actionChannels = chanbase.evaluateListBase();// bActionChannel
BlenderAction blenderAction = new BlenderAction(blenderContext.getBlenderKey().getFps()); BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps());
int lastFrame = 1; int lastFrame = 1;
for (Structure bActionChannel : actionChannels) { for (Structure bActionChannel : actionChannels) {
String animatedFeatureName = bActionChannel.getFieldValue("name").toString(); String animatedFeatureName = bActionChannel.getFieldValue("name").toString();
@ -277,7 +267,7 @@ public class AnimationHelper extends AbstractBlenderHelper {
if (!p.isNull()) { if (!p.isNull()) {
Structure ipoStructure = p.fetchData().get(0); Structure ipoStructure = p.fetchData().get(0);
Ipo ipo = this.fromIpoStructure(ipoStructure, blenderContext); 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()); lastFrame = Math.max(lastFrame, ipo.getLastFrame());
blenderAction.featuresTracks.put(animatedFeatureName, ipo); blenderAction.featuresTracks.put(animatedFeatureName, ipo);
} }
@ -321,104 +311,77 @@ public class AnimationHelper extends AbstractBlenderHelper {
if (rnaPath.endsWith("rotation") || rnaPath.endsWith("rotation_euler")) { if (rnaPath.endsWith("rotation") || rnaPath.endsWith("rotation_euler")) {
return Ipo.OB_ROT_X + arrayIndex; 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; return -1;
} }
/** /**
* An abstract representation of animation. The data stored here is mainly a raw action data loaded from blender. * The method returns the actions for the given skeleton. The actions represent armature animation in blender.
* It can later be transformed into bone or spatial animation and applied to the specified node. * @param skeleton
* * the skeleton we fetch the actions for
* @author Marcin Roguski (Kaelthas) * @param animationMatchMethod
* the method of animation matching
* @return a list of animations for the specified skeleton
*/ */
private static class BlenderAction { private List<BlenderAction> getActions(Skeleton skeleton, AnimationMatchMethod animationMatchMethod) {
/** Animation speed - frames per second. */ List<BlenderAction> result = new ArrayList<BlenderAction>();
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<String, Ipo> featuresTracks = new HashMap<String, Ipo>();
public BlenderAction(int fps) { // first get a set of bone names
this.fps = fps; Set<String> boneNames = new HashSet<String>();
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());
}
} }
/** // finding matches
* Converts the action into JME spatial animation tracks. Set<String> matchingNames = new HashSet<String>();
* @param node for (Entry<String, BlenderAction> actionEntry : blenderContext.getActions().entrySet()) {
* the node that will be animated // compute how many action tracks match the skeleton bones' names
* @return the spatial tracks for the node for (String boneName : boneNames) {
*/ if (actionEntry.getValue().hasTrackName(boneName)) {
public SpatialTrack[] toTracks(Node node) { matchingNames.add(boneName);
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()]);
}
/** BlenderAction action = null;
* Converts the action into JME bone animation tracks. if (animationMatchMethod == AnimationMatchMethod.AT_LEAST_ONE_NAME_MATCH && matchingNames.size() > 0) {
* @param skeleton action = actionEntry.getValue();
* the skeleton that will be animated } else if (matchingNames.size() == actionEntry.getValue().getTracksCount()) {
* @return the bone tracks for the node action = actionEntry.getValue();
*/
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()]);
}
/** if (action != null) {
* @return the time of animations (in seconds) // 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) {
public float getAnimationTime() { action = action.clone();
return (stopFrame - 1) / (float) fps; action.removeTracksThatAreNotInTheCollection(matchingNames);
}
result.add(action);
}
matchingNames.clear();
} }
return result;
} }
/** /**
* Ipo constant curve. This is a curve with only one value and no specified * The method returns the actions for the given node. The actions represent object animation in blender.
* type. This type of ipo cannot be used to calculate tracks. It should only * @param node
* be used to calculate single value for a given frame. * the node we fetch the actions for
* * @param animationMatchMethod
* @author Marcin Roguski (Kaelthas) * the method of animation matching
* @return a list of animations for the specified node
*/ */
private class ConstIpo extends Ipo { private List<BlenderAction> getActions(Node node, AnimationMatchMethod animationMatchMethod) {
List<BlenderAction> result = new ArrayList<BlenderAction>();
/** The constant value of this ipo. */
private float constValue;
/** for (Entry<String, BlenderAction> actionEntry : blenderContext.getActions().entrySet()) {
* Constructor. Stores the constant value of this ipo. if (actionEntry.getValue().hasTrackName(node.getName())) {
* result.add(actionEntry.getValue());
* @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!");
} }
return result;
} }
} }

@ -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() + "]]";
}
}

@ -170,7 +170,7 @@ public class Ipo {
for (int j = 0; j < bezierCurves.length; ++j) { for (int j = 0; j < bezierCurves.length; ++j) {
double value = bezierCurves[j].evaluate(frame, BezierCurve.Y_VALUE); double value = bezierCurves[j].evaluate(frame, BezierCurve.Y_VALUE);
switch (bezierCurves[j].getType()) { switch (bezierCurves[j].getType()) {
// LOCATION // LOCATION
case AC_LOC_X: case AC_LOC_X:
translation[0] = (float) value; translation[0] = (float) value;
break; break;
@ -186,18 +186,18 @@ public class Ipo {
// EULER ROTATION // EULER ROTATION
case OB_ROT_X: case OB_ROT_X:
eulerRotationUsed = true; eulerRotationUsed = true;
eulerRotation[0] = (float) value * degreeToRadiansFactor; eulerRotation[0] = (float) value * degreeToRadiansFactor;
break; break;
case OB_ROT_Y: case OB_ROT_Y:
eulerRotationUsed = true; eulerRotationUsed = true;
if (swapAxes && value != 0) { if (swapAxes && value != 0) {
value = -value; value = -value;
} }
eulerRotation[yIndex] = (float) value * degreeToRadiansFactor; eulerRotation[yIndex] = (float) value * degreeToRadiansFactor;
break; break;
case OB_ROT_Z: case OB_ROT_Z:
eulerRotationUsed = true; eulerRotationUsed = true;
eulerRotation[zIndex] = (float) value * degreeToRadiansFactor; eulerRotation[zIndex] = (float) value * degreeToRadiansFactor;
break; break;
@ -214,15 +214,15 @@ public class Ipo {
// QUATERNION ROTATION (used with bone animation) // QUATERNION ROTATION (used with bone animation)
case AC_QUAT_W: case AC_QUAT_W:
queternionRotationUsed = true; queternionRotationUsed = true;
quaternionRotation[3] = (float) value; quaternionRotation[3] = (float) value;
break; break;
case AC_QUAT_X: case AC_QUAT_X:
queternionRotationUsed = true; queternionRotationUsed = true;
quaternionRotation[0] = (float) value; quaternionRotation[0] = (float) value;
break; break;
case AC_QUAT_Y: case AC_QUAT_Y:
queternionRotationUsed = true; queternionRotationUsed = true;
if (swapAxes && value != 0) { if (swapAxes && value != 0) {
value = -value; value = -value;
} }
@ -236,10 +236,10 @@ public class Ipo {
} }
} }
translations[index] = localRotation.multLocal(new Vector3f(translation[0], translation[1], translation[2])); translations[index] = localRotation.multLocal(new Vector3f(translation[0], translation[1], translation[2]));
if(queternionRotationUsed) { if (queternionRotationUsed) {
rotations[index] = new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]); rotations[index] = new Quaternion(quaternionRotation[0], quaternionRotation[1], quaternionRotation[2], quaternionRotation[3]);
} else { } else {
rotations[index] = new Quaternion().fromAngles(eulerRotation); rotations[index] = new Quaternion().fromAngles(eulerRotation);
} }
scales[index] = new Vector3f(scale[0], scale[1], scale[2]); scales[index] = new Vector3f(scale[0], scale[1], scale[2]);
@ -250,11 +250,50 @@ public class Ipo {
calculatedTrack = new BoneTrack(targetIndex, times, translations, rotations, scales); calculatedTrack = new BoneTrack(targetIndex, times, translations, rotations, scales);
} }
if(queternionRotationUsed && eulerRotationUsed) { 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!"); 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; 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!");
}
}
} }

@ -178,7 +178,7 @@ import com.jme3.util.BufferUtils;
} }
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class);
animationHelper.applyAnimations(node, skeleton, blenderContext.getBlenderKey().getSkeletonAnimationNames(node.getName())); animationHelper.applyAnimations(node, skeleton, blenderContext.getBlenderKey().getAnimationMatchMethod());
node.updateModelBound(); node.updateModelBound();
} }
} }

@ -190,8 +190,8 @@ public class ObjectHelper extends AbstractBlenderHelper {
LightHelper lightHelper = blenderContext.getHelper(LightHelper.class); LightHelper lightHelper = blenderContext.getHelper(LightHelper.class);
List<Structure> lampsArray = pLamp.fetchData(); List<Structure> lampsArray = pLamp.fetchData();
result = lightHelper.toLight(lampsArray.get(0), blenderContext); result = lightHelper.toLight(lampsArray.get(0), blenderContext);
if(result == null) { if (result == null) {
//probably some light type is not supported, just create a node so that we can maintain child-parent relationship for nodes // 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); result = new Node(name);
} }
} }
@ -245,7 +245,7 @@ public class ObjectHelper extends AbstractBlenderHelper {
LOGGER.fine("Applying animations to the object if such are defined."); LOGGER.fine("Applying animations to the object if such are defined.");
AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class); 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."); LOGGER.fine("Loading constraints connected with this object.");
ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class); ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);

Loading…
Cancel
Save