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.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import com.jme3.animation.AnimControl; import com.jme3.animation.Animation; import com.jme3.animation.BoneTrack; import com.jme3.animation.Skeleton; import com.jme3.animation.SkeletonControl; import com.jme3.animation.SpatialTrack; 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.BlenderContext.LoadedDataType; 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; import com.jme3.scene.plugins.blender.file.FileBlockHeader; import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode; import com.jme3.scene.plugins.blender.file.Pointer; import com.jme3.scene.plugins.blender.file.Structure; import com.jme3.scene.plugins.blender.objects.ObjectHelper; /** * The helper class that helps in animations loading. * @author Marcin Roguski (Kaelthas) */ public class AnimationHelper extends AbstractBlenderHelper { private static final Logger LOGGER = Logger.getLogger(AnimationHelper.class.getName()); public AnimationHelper(String blenderVersion, BlenderContext blenderContext) { super(blenderVersion, blenderContext); } /** * Loads all animations that are stored in the blender file. The animations are not yet applied to the scene features. * This should be called before objects are loaded. * @throws BlenderFileException * an exception is thrown when problems with blender file reading occur */ public void loadAnimations() throws BlenderFileException { LOGGER.info("Loading animations that will be later applied to scene features."); List<FileBlockHeader> actionHeaders = blenderContext.getFileBlocks(BlockCode.BLOCK_AC00); if (actionHeaders != null) { for (FileBlockHeader header : actionHeaders) { Structure actionStructure = header.getStructure(blenderContext); LOGGER.log(Level.INFO, "Found animation: {0}.", actionStructure.getName()); blenderContext.addAction(this.getTracks(actionStructure, blenderContext)); } } } /** * 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 animationMatchMethod * the way animation should be matched with node */ public void applyAnimations(Node node, AnimationMatchMethod animationMatchMethod) { List<BlenderAction> actions = this.getActions(node, animationMatchMethod); if (actions.size() > 0) { List<Animation> animations = new ArrayList<Animation>(); for (BlenderAction action : actions) { SpatialTrack[] tracks = action.toTracks(node, blenderContext); 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); } } if (animations.size() > 0) { AnimControl control = new AnimControl(); HashMap<String, Animation> anims = new HashMap<String, Animation>(animations.size()); for (int i = 0; i < animations.size(); ++i) { Animation animation = animations.get(i); anims.put(animation.getName(), animation); } control.setAnimations(anims); node.addControl(control); } } } /** * The method applies skeleton animations to the given node. * @param node * the node where the animations will be applied * @param skeleton * the skeleton of the node * @param animationMatchMethod * the way animation should be matched with skeleton */ public void applyAnimations(Node node, Skeleton skeleton, AnimationMatchMethod animationMatchMethod) { node.addControl(new SkeletonControl(skeleton)); blenderContext.setNodeForSkeleton(skeleton, node); List<BlenderAction> actions = this.getActions(skeleton, animationMatchMethod); if (actions.size() > 0) { List<Animation> animations = new ArrayList<Animation>(); for (BlenderAction action : actions) { BoneTrack[] tracks = action.toTracks(skeleton, blenderContext); 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) { AnimControl control = new AnimControl(skeleton); HashMap<String, Animation> anims = new HashMap<String, Animation>(animations.size()); for (int i = 0; i < animations.size(); ++i) { Animation animation = animations.get(i); anims.put(animation.getName(), animation); } control.setAnimations(anims); node.addControl(control); // make sure that SkeletonControl is added AFTER the AnimControl SkeletonControl skeletonControl = node.getControl(SkeletonControl.class); if (skeletonControl != null) { node.removeControl(SkeletonControl.class); node.addControl(skeletonControl); } } } } /** * This method creates an ipo object used for interpolation calculations. * * @param ipoStructure * the structure with ipo definition * @param blenderContext * the blender context * @return the ipo object * @throws BlenderFileException * this exception is thrown when the blender file is somehow * corrupted */ public Ipo fromIpoStructure(Structure ipoStructure, BlenderContext blenderContext) throws BlenderFileException { Structure curvebase = (Structure) ipoStructure.getFieldValue("curve"); // preparing bezier curves Ipo result = null; List<Structure> curves = curvebase.evaluateListBase();// IpoCurve if (curves.size() > 0) { BezierCurve[] bezierCurves = new BezierCurve[curves.size()]; int frame = 0; for (Structure curve : curves) { Pointer pBezTriple = (Pointer) curve.getFieldValue("bezt"); List<Structure> bezTriples = pBezTriple.fetchData(); int type = ((Number) curve.getFieldValue("adrcode")).intValue(); bezierCurves[frame++] = new BezierCurve(type, bezTriples, 2); } curves.clear(); result = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion()); Long ipoOma = ipoStructure.getOldMemoryAddress(); blenderContext.addLoadedFeatures(ipoOma, LoadedDataType.STRUCTURE, ipoStructure); blenderContext.addLoadedFeatures(ipoOma, LoadedDataType.FEATURE, result); } return result; } /** * This method creates an ipo with only a single value. No track type is * specified so do not use it for calculating tracks. * * @param constValue * the value of this ipo * @return constant ipo */ public Ipo fromValue(float constValue) { return new ConstIpo(constValue); } /** * This method retuns the bone tracks for animation. * * @param actionStructure * the structure containing the tracks * @param blenderContext * the blender context * @return a list of tracks for the specified animation * @throws BlenderFileException * an exception is thrown when there are problems with the blend * file */ private BlenderAction getTracks(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException { if (blenderVersion < 250) { return this.getTracks249(actionStructure, blenderContext); } else { return this.getTracks250(actionStructure, blenderContext); } } /** * This method retuns the bone tracks for animation for blender version 2.50 * and higher. * * @param actionStructure * the structure containing the tracks * @param blenderContext * the blender context * @return a list of tracks for the specified animation * @throws BlenderFileException * an exception is thrown when there are problems with the blend * file */ private BlenderAction getTracks250(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException { LOGGER.log(Level.FINE, "Getting tracks!"); Structure groups = (Structure) actionStructure.getFieldValue("groups"); List<Structure> actionGroups = groups.evaluateListBase();// bActionGroup BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps()); int lastFrame = 1; for (Structure actionGroup : actionGroups) { String name = actionGroup.getFieldValue("name").toString(); List<Structure> channels = ((Structure) actionGroup.getFieldValue("channels")).evaluateListBase(); BezierCurve[] bezierCurves = new BezierCurve[channels.size()]; int channelCounter = 0; for (Structure c : channels) { int type = this.getCurveType(c, blenderContext); Pointer pBezTriple = (Pointer) c.getFieldValue("bezt"); List<Structure> bezTriples = pBezTriple.fetchData(); bezierCurves[channelCounter++] = new BezierCurve(type, bezTriples, 2); } Ipo ipo = new Ipo(bezierCurves, fixUpAxis, blenderContext.getBlenderVersion()); lastFrame = Math.max(lastFrame, ipo.getLastFrame()); blenderAction.featuresTracks.put(name, ipo); } blenderAction.stopFrame = lastFrame; return blenderAction; } /** * This method retuns the bone tracks for animation for blender version 2.49 * (and probably several lower versions too). * * @param actionStructure * the structure containing the tracks * @param blenderContext * the blender context * @return a list of tracks for the specified animation * @throws BlenderFileException * an exception is thrown when there are problems with the blend * file */ private BlenderAction getTracks249(Structure actionStructure, BlenderContext blenderContext) throws BlenderFileException { LOGGER.log(Level.FINE, "Getting tracks!"); Structure chanbase = (Structure) actionStructure.getFieldValue("chanbase"); List<Structure> actionChannels = chanbase.evaluateListBase();// bActionChannel BlenderAction blenderAction = new BlenderAction(actionStructure.getName(), blenderContext.getBlenderKey().getFps()); int lastFrame = 1; for (Structure bActionChannel : actionChannels) { String animatedFeatureName = bActionChannel.getFieldValue("name").toString(); Pointer p = (Pointer) bActionChannel.getFieldValue("ipo"); 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 lastFrame = Math.max(lastFrame, ipo.getLastFrame()); blenderAction.featuresTracks.put(animatedFeatureName, ipo); } } } blenderAction.stopFrame = lastFrame; return blenderAction; } /** * This method returns the type of the ipo curve. * * @param structure * the structure must contain the 'rna_path' field and * 'array_index' field (the type is not important here) * @param blenderContext * the blender context * @return the type of the curve */ public int getCurveType(Structure structure, BlenderContext blenderContext) { // reading rna path first BlenderInputStream bis = blenderContext.getInputStream(); int currentPosition = bis.getPosition(); Pointer pRnaPath = (Pointer) structure.getFieldValue("rna_path"); FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pRnaPath.getOldMemoryAddress()); bis.setPosition(dataFileBlock.getBlockPosition()); String rnaPath = bis.readString(); bis.setPosition(currentPosition); int arrayIndex = ((Number) structure.getFieldValue("array_index")).intValue(); // determining the curve type if (rnaPath.endsWith("location")) { return Ipo.AC_LOC_X + arrayIndex; } if (rnaPath.endsWith("rotation_quaternion")) { return Ipo.AC_QUAT_W + arrayIndex; } if (rnaPath.endsWith("scale")) { return Ipo.AC_SIZE_X + arrayIndex; } if (rnaPath.endsWith("rotation") || rnaPath.endsWith("rotation_euler")) { return Ipo.OB_ROT_X + arrayIndex; } LOGGER.log(Level.WARNING, "Unknown curve rna path: {0}", rnaPath); return -1; } /** * 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 List<BlenderAction> getActions(Skeleton skeleton, AnimationMatchMethod animationMatchMethod) { List<BlenderAction> result = new ArrayList<BlenderAction>(); // first get a set of bone names 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 Set<String> matchingNames = new HashSet<String>(); for (Entry<String, BlenderAction> 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(); } 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); } matchingNames.clear(); } return result; } /** * 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 List<BlenderAction> getActions(Node node, AnimationMatchMethod animationMatchMethod) { List<BlenderAction> result = new ArrayList<BlenderAction>(); for (Entry<String, BlenderAction> actionEntry : blenderContext.getActions().entrySet()) { if (actionEntry.getValue().hasTrackName(node.getName())) { result.add(actionEntry.getValue()); } } return result; } }