/**
* Copyright (c) 2008-2012 Ardor Labs, Inc.
*
* This file is part of Ardor3D.
*
* Ardor3D is free software: you can redistribute it and/or modify it
* under the terms of its license which may be found in the accompanying
* LICENSE file or at <http://www.ardor3d.com/LICENSE>.
*/
package com.ardor3d.extension.model.collada.jdom;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jdom2.Attribute;
import org.jdom2.DataConversionException;
import org.jdom2.Element;
import com.ardor3d.extension.animation.skeletal.AttachmentPoint;
import com.ardor3d.extension.animation.skeletal.Joint;
import com.ardor3d.extension.animation.skeletal.Skeleton;
import com.ardor3d.extension.animation.skeletal.SkeletonPose;
import com.ardor3d.extension.animation.skeletal.SkinnedMesh;
import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
import com.ardor3d.extension.animation.skeletal.clip.JointChannel;
import com.ardor3d.extension.animation.skeletal.clip.TransformChannel;
import com.ardor3d.extension.model.collada.jdom.ColladaInputPipe.ParamType;
import com.ardor3d.extension.model.collada.jdom.ColladaInputPipe.Type;
import com.ardor3d.extension.model.collada.jdom.data.AnimationItem;
import com.ardor3d.extension.model.collada.jdom.data.ColladaStorage;
import com.ardor3d.extension.model.collada.jdom.data.DataCache;
import com.ardor3d.extension.model.collada.jdom.data.MeshVertPairs;
import com.ardor3d.extension.model.collada.jdom.data.SkinData;
import com.ardor3d.extension.model.collada.jdom.data.TransformElement;
import com.ardor3d.extension.model.collada.jdom.data.TransformElement.TransformElementType;
import com.ardor3d.math.MathUtils;
import com.ardor3d.math.Matrix3;
import com.ardor3d.math.Matrix4;
import com.ardor3d.math.Transform;
import com.ardor3d.math.Vector3;
import com.ardor3d.math.Vector4;
import com.ardor3d.renderer.state.RenderState;
import com.ardor3d.renderer.state.RenderState.StateType;
import com.ardor3d.scenegraph.AbstractBufferData.VBOAccessMode;
import com.ardor3d.scenegraph.Mesh;
import com.ardor3d.scenegraph.MeshData;
import com.ardor3d.scenegraph.Node;
import com.ardor3d.scenegraph.Spatial;
import com.ardor3d.util.export.Savable;
import com.ardor3d.util.export.binary.BinaryExporter;
import com.ardor3d.util.export.binary.BinaryImporter;
import com.ardor3d.util.geom.BufferUtils;
import com.ardor3d.util.geom.VertMap;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
/**
* Methods for parsing Collada data related to animation, skinning and morphing.
*/
public class ColladaAnimUtils {
private static final Logger logger = Logger.getLogger(ColladaAnimUtils.class.getName());
private final ColladaStorage _colladaStorage;
private final DataCache _dataCache;
private final ColladaDOMUtil _colladaDOMUtil;
private final ColladaMeshUtils _colladaMeshUtils;
public ColladaAnimUtils(final ColladaStorage colladaStorage, final DataCache dataCache,
final ColladaDOMUtil colladaDOMUtil, final ColladaMeshUtils colladaMeshUtils) {
_colladaStorage = colladaStorage;
_dataCache = dataCache;
_colladaDOMUtil = colladaDOMUtil;
_colladaMeshUtils = colladaMeshUtils;
}
/**
* Retrieve a name to use for the skin node based on the element names.
*
* @param ic
* instance_controller element.
* @param controller
* controller element
* @return name.
* @see SkinData#SkinData(String)
*/
private String getSkinStoreName(final Element ic, final Element controller) {
final String controllerName = controller.getAttributeValue("name", (String) null) != null ? controller
.getAttributeValue("name", (String) null) : controller.getAttributeValue("id", (String) null);
final String instanceControllerName = ic.getAttributeValue("name", (String) null) != null ? ic
.getAttributeValue("name", (String) null) : ic.getAttributeValue("sid", (String) null);
final String storeName = (controllerName != null ? controllerName : "")
+ (controllerName != null && instanceControllerName != null ? " : " : "")
+ (instanceControllerName != null ? instanceControllerName : "");
return storeName;
}
/**
* Copy the render states from our source Spatial to the destination Spatial. Does not recurse.
*
* @param source
* @param target
*/
private void copyRenderStates(final Spatial source, final Spatial target) {
final EnumMap<StateType, RenderState> states = source.getLocalRenderStates();
for (final RenderState state : states.values()) {
target.setRenderState(state);
}
}
/**
* Clone the given MeshData object via deep copy using the Ardor3D BinaryExporter and BinaryImporter.
*
* @param meshData
* the source to clone.
* @return the clone.
* @throws IOException
* if we have troubles during the clone.
*/
private MeshData copyMeshData(final MeshData meshData) throws IOException {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
final BinaryExporter exporter = new BinaryExporter();
exporter.save(meshData, bos);
bos.flush();
final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
final BinaryImporter importer = new BinaryImporter();
final Savable sav = importer.load(bis);
return (MeshData) sav;
}
/**
* Builds data based on an instance controller element.
*
* @param node
* Ardor3D parent Node
* @param instanceController
*/
void buildController(final Node node, final Element instanceController) {
final Element controller = _colladaDOMUtil.findTargetWithId(instanceController.getAttributeValue("url"));
if (controller == null) {
throw new ColladaException("Unable to find controller with id: "
+ instanceController.getAttributeValue("url"), instanceController);
}
final Element skin = controller.getChild("skin");
if (skin != null) {
buildSkinMeshes(node, instanceController, controller, skin);
} else {
// look for morph... can only be one or the other according to Collada
final Element morph = controller.getChild("morph");
if (morph != null) {
buildMorphMeshes(node, controller, morph);
}
}
}
/**
* Construct skin mesh(es) from the skin element and attach them (under a single new Node) to the given parent Node.
*
* @param ardorParentNode
* Ardor3D Node to attach our skin node to.
* @param instanceController
* the <instance_controller> element. We'll parse the skeleton reference from here.
* @param controller
* the referenced <controller> element. Used for naming purposes.
* @param skin
* our <skin> element.
*/
@SuppressWarnings("unchecked")
private void buildSkinMeshes(final Node ardorParentNode, final Element instanceController,
final Element controller, final Element skin) {
final String skinSource = skin.getAttributeValue("source");
final Element skinNodeEL = _colladaDOMUtil.findTargetWithId(skinSource);
if (skinNodeEL == null || !"geometry".equals(skinNodeEL.getName())) {
throw new ColladaException("Expected a mesh for skin source with url: " + skinSource + " got instead: "
+ skinNodeEL, skin);
}
final Element geometry = skinNodeEL;
final Node meshNode = _colladaMeshUtils.buildMesh(geometry);
if (meshNode != null) {
// Look for skeleton entries in the original <instance_controller> element
final List<Element> skeletonRoots = Lists.newArrayList();
for (final Element sk : instanceController.getChildren("skeleton")) {
final Element skroot = _colladaDOMUtil.findTargetWithId(sk.getText());
if (skroot != null) {
// add as a possible root for when we need to locate a joint by name later.
skeletonRoots.add(skroot);
} else {
throw new ColladaException("Unable to find node with id: " + sk.getText()
+ ", referenced from skeleton " + sk, sk);
}
}
// Read in our joints node
final Element jointsEL = skin.getChild("joints");
if (jointsEL == null) {
throw new ColladaException("skin found without joints.", skin);
}
// Pull out our joint names and bind matrices
final List<String> jointNames = Lists.newArrayList();
final List<Transform> bindMatrices = Lists.newArrayList();
final List<ColladaInputPipe.ParamType> paramTypes = Lists.newArrayList();
for (final Element inputEL : jointsEL.getChildren("input")) {
final ColladaInputPipe pipe = new ColladaInputPipe(_colladaDOMUtil, inputEL);
final ColladaInputPipe.SourceData sd = pipe.getSourceData();
if (pipe.getType() == ColladaInputPipe.Type.JOINT) {
final String[] namesData = sd.stringArray;
for (int i = sd.offset; i < namesData.length; i += sd.stride) {
jointNames.add(namesData[i]);
paramTypes.add(sd.paramType);
}
} else if (pipe.getType() == ColladaInputPipe.Type.INV_BIND_MATRIX) {
final float[] floatData = sd.floatArray;
final FloatBuffer source = BufferUtils.createFloatBufferOnHeap(16);
for (int i = sd.offset; i < floatData.length; i += sd.stride) {
source.rewind();
source.put(floatData, i, 16);
source.flip();
final Matrix4 mat = new Matrix4().fromFloatBuffer(source);
bindMatrices.add(new Transform().fromHomogeneousMatrix(mat));
}
}
}
// Use the skeleton information from the instance_controller to set the parent array locations on the
// joints.
Skeleton ourSkeleton = null; // TODO: maybe not the best way. iterate
final int[] order = new int[jointNames.size()];
for (int i = 0; i < jointNames.size(); i++) {
final String name = jointNames.get(i);
final ParamType paramType = paramTypes.get(i);
final String searcher = paramType == ParamType.idref_param ? "id" : "sid";
Element found = null;
for (final Element root : skeletonRoots) {
if (name.equals(root.getAttributeValue(searcher))) {
found = root;
} else if (paramType == ParamType.idref_param) {
found = _colladaDOMUtil.findTargetWithId(name);
} else {
found = (Element) _colladaDOMUtil.selectSingleNode(root, ".//*[@sid='" + name + "']");
}
// Last resorts (bad exporters)
if (found == null) {
found = _colladaDOMUtil.findTargetWithId(name);
}
if (found == null) {
found = (Element) _colladaDOMUtil.selectSingleNode(root, ".//*[@name='" + name + "']");
}
if (found != null) {
break;
}
}
if (found == null) {
if (paramType == ParamType.idref_param) {
found = _colladaDOMUtil.findTargetWithId(name);
} else {
found = (Element) _colladaDOMUtil.selectSingleNode(geometry, "/*//visual_scene//*[@sid='"
+ name + "']");
}
// Last resorts (bad exporters)
if (found == null) {
found = _colladaDOMUtil.findTargetWithId(name);
}
if (found == null) {
found = (Element) _colladaDOMUtil.selectSingleNode(geometry, "/*//visual_scene//*[@name='"
+ name + "']");
}
if (found == null) {
throw new ColladaException("Unable to find joint with " + searcher + ": " + name, skin);
}
}
final Joint joint = _dataCache.getElementJointMapping().get(found);
if (joint == null) {
logger.warning("unable to parse joint for: " + found.getName() + " " + name);
return;
}
joint.setInverseBindPose(bindMatrices.get(i));
ourSkeleton = _dataCache.getJointSkeletonMapping().get(joint);
order[i] = joint.getIndex();
}
// Make our skeleton pose
SkeletonPose skPose = _dataCache.getSkeletonPoseMapping().get(ourSkeleton);
if (skPose == null) {
skPose = new SkeletonPose(ourSkeleton);
_dataCache.getSkeletonPoseMapping().put(ourSkeleton, skPose);
// attach any attachment points found for the skeleton's joints
addAttachments(skPose);
// Skeleton's default to bind position, so update the global transforms.
skPose.updateTransforms();
}
// Read in our vertex_weights node
final Element weightsEL = skin.getChild("vertex_weights");
if (weightsEL == null) {
throw new ColladaException("skin found without vertex_weights.", skin);
}
// Pull out our per vertex joint indices and weights
final List<Short> jointIndices = Lists.newArrayList();
final List<Float> jointWeights = Lists.newArrayList();
int indOff = 0, weightOff = 0;
int maxOffset = 0;
for (final Element inputEL : weightsEL.getChildren("input")) {
final ColladaInputPipe pipe = new ColladaInputPipe(_colladaDOMUtil, inputEL);
final ColladaInputPipe.SourceData sd = pipe.getSourceData();
if (pipe.getOffset() > maxOffset) {
maxOffset = pipe.getOffset();
}
if (pipe.getType() == ColladaInputPipe.Type.JOINT) {
indOff = pipe.getOffset();
final String[] namesData = sd.stringArray;
for (int i = sd.offset; i < namesData.length; i += sd.stride) {
// XXX: the Collada spec says this could be -1?
final String name = namesData[i];
final int index = jointNames.indexOf(name);
if (index >= 0) {
jointIndices.add((short) index);
} else {
throw new ColladaException("Unknown joint accessed: " + name, inputEL);
}
}
} else if (pipe.getType() == ColladaInputPipe.Type.WEIGHT) {
weightOff = pipe.getOffset();
final float[] floatData = sd.floatArray;
for (int i = sd.offset; i < floatData.length; i += sd.stride) {
jointWeights.add(floatData[i]);
}
}
}
// Pull our values array
int firstIndex = 0, count = 0;
final int[] vals = _colladaDOMUtil.parseIntArray(weightsEL.getChild("v"));
try {
count = weightsEL.getAttribute("count").getIntValue();
} catch (final DataConversionException e) {
throw new ColladaException("Unable to parse count attribute.", weightsEL);
}
// use the vals to fill our vert weight map
final int[][] vertWeightMap = new int[count][];
int index = 0;
for (final int length : _colladaDOMUtil.parseIntArray(weightsEL.getChild("vcount"))) {
final int[] entry = new int[(maxOffset + 1) * length];
vertWeightMap[index++] = entry;
System.arraycopy(vals, (maxOffset + 1) * firstIndex, entry, 0, entry.length);
firstIndex += length;
}
// Create a record for the global ColladaStorage.
final String storeName = getSkinStoreName(instanceController, controller);
final SkinData skinDataStore = new SkinData(storeName);
// add pose to store
skinDataStore.setPose(skPose);
// Create a base Node for our skin meshes
final Node skinNode = new Node(meshNode.getName());
// copy Node render states across.
copyRenderStates(meshNode, skinNode);
// add node to store
skinDataStore.setSkinBaseNode(skinNode);
// Grab the bind_shape_matrix from skin
final Element bindShapeMatrixEL = skin.getChild("bind_shape_matrix");
final Transform bindShapeMatrix = new Transform();
if (bindShapeMatrixEL != null) {
final double[] array = _colladaDOMUtil.parseDoubleArray(bindShapeMatrixEL);
bindShapeMatrix.fromHomogeneousMatrix(new Matrix4().fromArray(array));
}
// Visit our Node and pull out any Mesh children. Turn them into SkinnedMeshes
for (final Spatial spat : meshNode.getChildren()) {
if (spat instanceof Mesh && ((Mesh) spat).getMeshData().getVertexCount() > 0) {
final Mesh sourceMesh = (Mesh) spat;
final SkinnedMesh skMesh = new SkinnedMesh(sourceMesh.getName());
skMesh.setCurrentPose(skPose);
// copy material info mapping for later use
final String material = _dataCache.getMeshMaterialMap().get(sourceMesh);
_dataCache.getMeshMaterialMap().put(skMesh, material);
// copy mesh render states across.
copyRenderStates(sourceMesh, skMesh);
// copy hints across
skMesh.getSceneHints().set(sourceMesh.getSceneHints());
try {
// Use source mesh as bind pose data in the new SkinnedMesh
final MeshData bindPose = copyMeshData(sourceMesh.getMeshData());
skMesh.setBindPoseData(bindPose);
// Apply our BSM
if (!bindShapeMatrix.isIdentity()) {
bindPose.transformVertices(bindShapeMatrix);
if (bindPose.getNormalBuffer() != null) {
bindPose.transformNormals(bindShapeMatrix, true);
}
}
// TODO: This is only needed for CPU skinning... consider a way of making it optional.
// Copy bind pose to mesh data to setup for CPU skinning
final MeshData meshData = copyMeshData(skMesh.getBindPoseData());
meshData.getVertexCoords().setVboAccessMode(VBOAccessMode.StreamDraw);
if (meshData.getNormalCoords() != null) {
meshData.getNormalCoords().setVboAccessMode(VBOAccessMode.StreamDraw);
}
skMesh.setMeshData(meshData);
} catch (final IOException e) {
e.printStackTrace();
throw new ColladaException("Unable to copy skeleton bind pose data.", geometry);
}
// Grab the MeshVertPairs from Global for this mesh.
final Collection<MeshVertPairs> vertPairsList = _dataCache.getVertMappings().get(geometry);
MeshVertPairs pairsMap = null;
if (vertPairsList != null) {
for (final MeshVertPairs pairs : vertPairsList) {
if (pairs.getMesh() == sourceMesh) {
pairsMap = pairs;
break;
}
}
}
if (pairsMap == null) {
throw new ColladaException("Unable to locate pair map for geometry.", geometry);
}
// Check for a remapping, if we optimized geometry
final VertMap vertMap = _dataCache.getMeshVertMap().get(sourceMesh);
// Use pairs map and vertWeightMap to build our weights and joint indices.
{
// count number of weights used
int maxWeightsPerVert = 0;
int weightCount;
for (final int originalIndex : pairsMap.getIndices()) {
weightCount = 0;
// get weights and joints at original index and add weights up to get divisor sum
// we'll assume 0's for vertices with no matching weight.
if (vertWeightMap.length > originalIndex) {
final int[] data = vertWeightMap[originalIndex];
for (int i = 0; i < data.length; i += maxOffset + 1) {
final float weight = jointWeights.get(data[i + weightOff]);
if (weight != 0) {
weightCount++;
}
}
if (weightCount > maxWeightsPerVert) {
maxWeightsPerVert = weightCount;
}
}
}
final int verts = skMesh.getMeshData().getVertexCount();
final FloatBuffer weightBuffer = BufferUtils.createFloatBuffer(verts * maxWeightsPerVert);
final ShortBuffer jointIndexBuffer = BufferUtils.createShortBuffer(verts * maxWeightsPerVert);
int j;
float sum = 0;
final float[] weights = new float[maxWeightsPerVert];
final short[] indices = new short[maxWeightsPerVert];
int originalIndex;
for (int x = 0; x < verts; x++) {
if (vertMap != null) {
originalIndex = pairsMap.getIndices()[vertMap.getFirstOldIndex(x)];
} else {
originalIndex = pairsMap.getIndices()[x];
}
j = 0;
sum = 0;
// get weights and joints at original index and add weights up to get divisor sum
// we'll assume 0's for vertices with no matching weight.
if (vertWeightMap.length > originalIndex) {
final int[] data = vertWeightMap[originalIndex];
for (int i = 0; i < data.length; i += maxOffset + 1) {
final float weight = jointWeights.get(data[i + weightOff]);
if (weight != 0) {
weights[j] = jointWeights.get(data[i + weightOff]);
indices[j] = (short) order[jointIndices.get(data[i + indOff])];
sum += weights[j++];
}
}
}
// add extra padding as needed
while (j < maxWeightsPerVert) {
weights[j] = 0;
indices[j++] = 0;
}
// add weights to weightBuffer / sum
for (final float w : weights) {
weightBuffer.put(sum != 0 ? w / sum : 0);
}
// add joint indices to jointIndexBuffer
jointIndexBuffer.put(indices);
}
final float[] totalWeights = new float[weightBuffer.capacity()];
weightBuffer.flip();
weightBuffer.get(totalWeights);
skMesh.setWeights(totalWeights);
final short[] totalIndices = new short[jointIndexBuffer.capacity()];
jointIndexBuffer.flip();
jointIndexBuffer.get(totalIndices);
skMesh.setJointIndices(totalIndices);
skMesh.setWeightsPerVert(maxWeightsPerVert);
}
// add to the skinNode.
skinNode.attachChild(skMesh);
// Manually apply our bind pose to the skin mesh.
skMesh.applyPose();
// Update the model bounding.
skMesh.updateModelBound();
// add mesh to store
skinDataStore.getSkins().add(skMesh);
}
}
// add to Node
ardorParentNode.attachChild(skinNode);
// Add skin record to storage.
_colladaStorage.getSkins().add(skinDataStore);
}
}
private void addAttachments(final SkeletonPose skPose) {
final Skeleton skeleton = skPose.getSkeleton();
for (final Joint joint : skeleton.getJoints()) {
if (_dataCache.getAttachmentPoints().containsKey(joint)) {
for (final AttachmentPoint point : _dataCache.getAttachmentPoints().get(joint)) {
point.setJointIndex(joint.getIndex());
skPose.addPoseListener(point);
}
}
}
}
/**
* Construct morph mesh(es) from the <morph> element and attach them (under a single new Node) to the given parent
* Node.
*
* Note: This method current does not do anything but attach the referenced mesh since Ardor3D does not yet support
* morph target animation.
*
* @param ardorParentNode
* Ardor3D Node to attach our morph mesh to.
* @param controller
* the referenced <controller> element. Used for naming purposes.
* @param morph
* our <morph> element
*/
private void buildMorphMeshes(final Node ardorParentNode, final Element controller, final Element morph) {
final String skinSource = morph.getAttributeValue("source");
final Element skinNode = _colladaDOMUtil.findTargetWithId(skinSource);
if (skinNode == null || !"geometry".equals(skinNode.getName())) {
throw new ColladaException("Expected a mesh for morph source with url: " + skinSource
+ " (line number is referring morph)", morph);
}
final Element geometry = skinNode;
final Spatial baseMesh = _colladaMeshUtils.buildMesh(geometry);
// TODO: support morph animations someday.
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Morph target animation not yet supported.");
}
// Just add mesh.
if (baseMesh != null) {
ardorParentNode.attachChild(baseMesh);
}
}
/**
* Parse all animations in library_animations
*
* @param colladaRoot
*/
public void parseLibraryAnimations(final Element colladaRoot) {
final Element libraryAnimations = colladaRoot.getChild("library_animations");
if (libraryAnimations == null || libraryAnimations.getChildren().isEmpty()) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("No animations found in collada file!");
}
return;
}
final AnimationItem animationItemRoot = new AnimationItem("Animation Root");
_colladaStorage.setAnimationItemRoot(animationItemRoot);
final Multimap<Element, TargetChannel> channelMap = ArrayListMultimap.create();
parseAnimations(channelMap, libraryAnimations, animationItemRoot);
for (final Element key : channelMap.keySet()) {
buildAnimations(key, channelMap.get(key));
}
}
/**
* Merge all animation channels into Ardor jointchannels
*
* @param entry
*/
@SuppressWarnings("unchecked")
private void buildAnimations(final Element parentElement, final Collection<TargetChannel> targetList) {
final List<Element> elementTransforms = new ArrayList<Element>();
for (final Element child : parentElement.getChildren()) {
if (_dataCache.getTransformTypes().contains(child.getName())) {
elementTransforms.add(child);
}
}
final List<TransformElement> transformList = getNodeTransformList(elementTransforms);
AnimationItem animationItemRoot = null;
for (final TargetChannel targetChannel : targetList) {
if (animationItemRoot == null) {
animationItemRoot = targetChannel.animationItemRoot;
}
final String source = targetChannel.source;
// final Target target = targetChannel.target;
final Element targetNode = targetChannel.targetNode;
final int targetIndex = elementTransforms.indexOf(targetNode);
if (logger.isLoggable(Level.FINE)) {
logger.fine(parentElement.getName() + "(" + parentElement.getAttributeValue("name") + ") -> "
+ targetNode.getName() + "(" + targetIndex + ")");
}
final EnumMap<Type, ColladaInputPipe> pipes = Maps.newEnumMap(Type.class);
final Element samplerElement = _colladaDOMUtil.findTargetWithId(source);
for (final Element inputElement : samplerElement.getChildren("input")) {
final ColladaInputPipe pipe = new ColladaInputPipe(_colladaDOMUtil, inputElement);
pipes.put(pipe.getType(), pipe);
}
// get input (which is TIME for now)
final ColladaInputPipe inputPipe = pipes.get(Type.INPUT);
final ColladaInputPipe.SourceData sdIn = inputPipe.getSourceData();
final float[] time = sdIn.floatArray;
targetChannel.time = time;
if (logger.isLoggable(Level.FINE)) {
logger.fine("inputPipe: " + Arrays.toString(time));
}
// get output data
final ColladaInputPipe outputPipe = pipes.get(Type.OUTPUT);
final ColladaInputPipe.SourceData sdOut = outputPipe.getSourceData();
final float[] animationData = sdOut.floatArray;
targetChannel.animationData = animationData;
if (logger.isLoggable(Level.FINE)) {
logger.fine("outputPipe: " + Arrays.toString(animationData));
}
// TODO: Need to add support for other interpolation types.
// get target array from transform list
final TransformElement transformElement = transformList.get(targetIndex);
final double[] array = transformElement.getArray();
targetChannel.array = array;
final int stride = sdOut.stride;
targetChannel.stride = stride;
targetChannel.currentPos = 0;
}
final List<Float> finalTimeList = Lists.newArrayList();
final List<Transform> finalTransformList = Lists.newArrayList();
final List<TargetChannel> workingChannels = Lists.newArrayList();
for (;;) {
float lowestTime = Float.MAX_VALUE;
boolean found = false;
for (final TargetChannel targetChannel : targetList) {
if (targetChannel.currentPos < targetChannel.time.length) {
final float time = targetChannel.time[targetChannel.currentPos];
if (time < lowestTime) {
lowestTime = time;
}
found = true;
}
}
if (!found) {
break;
}
workingChannels.clear();
for (final TargetChannel targetChannel : targetList) {
if (targetChannel.currentPos < targetChannel.time.length) {
final float time = targetChannel.time[targetChannel.currentPos];
if (time == lowestTime) {
workingChannels.add(targetChannel);
}
}
}
for (final TargetChannel targetChannel : workingChannels) {
final Target target = targetChannel.target;
final float[] animationData = targetChannel.animationData;
final double[] array = targetChannel.array;
// set the correct values depending on accessor
final int position = targetChannel.currentPos * targetChannel.stride;
if (target.accessorType == AccessorType.None) {
for (int j = 0; j < array.length; j++) {
array[j] = animationData[position + j];
}
} else {
if (target.accessorType == AccessorType.Vector) {
array[target.accessorIndexX] = animationData[position];
} else if (target.accessorType == AccessorType.Matrix) {
array[target.accessorIndexY * 4 + target.accessorIndexX] = animationData[position];
}
}
targetChannel.currentPos++;
}
// bake the transform
final Transform transform = bakeTransforms(transformList);
finalTimeList.add(lowestTime);
finalTransformList.add(transform);
}
final float[] time = new float[finalTimeList.size()];
for (int i = 0; i < finalTimeList.size(); i++) {
time[i] = finalTimeList.get(i);
}
final Transform[] transforms = finalTransformList.toArray(new Transform[finalTransformList.size()]);
AnimationClip animationClip = animationItemRoot.getAnimationClip();
if (animationClip == null) {
animationClip = new AnimationClip(animationItemRoot.getName());
animationItemRoot.setAnimationClip(animationClip);
}
// Make an animation channel - first find if we have a matching joint
Joint joint = _dataCache.getElementJointMapping().get(parentElement);
if (joint == null) {
String nodeName = parentElement.getAttributeValue("name", (String) null);
if (nodeName == null) { // use id if name doesn't exist
nodeName = parentElement.getAttributeValue("id", parentElement.getName());
}
if (nodeName != null) {
joint = _dataCache.getExternalJointMapping().get(nodeName);
}
if (joint == null) {
// no joint still, so make a transform channel.
final TransformChannel transformChannel = new TransformChannel(nodeName, time, transforms);
animationClip.addChannel(transformChannel);
_colladaStorage.getAnimationChannels().add(transformChannel);
return;
}
}
// create joint channel
final JointChannel jointChannel = new JointChannel(joint, time, transforms);
animationClip.addChannel(jointChannel);
_colladaStorage.getAnimationChannels().add(jointChannel);
}
/**
* Stores animation data to use for merging into jointchannels.
*/
private static class TargetChannel {
Target target;
Element targetNode;
String source;
AnimationItem animationItemRoot;
float[] time;
float[] animationData;
double[] array;
int stride;
int currentPos;
public TargetChannel(final Target target, final Element targetNode, final String source,
final AnimationItem animationItemRoot) {
this.target = target;
this.targetNode = targetNode;
this.source = source;
this.animationItemRoot = animationItemRoot;
}
}
/**
* Gather up all animation channels based on what nodes they affect.
*
* @param channelMap
* @param animationRoot
* @param animationItemRoot
*/
@SuppressWarnings("unchecked")
private void parseAnimations(final Multimap<Element, TargetChannel> channelMap, final Element animationRoot,
final AnimationItem animationItemRoot) {
if (animationRoot.getChild("animation") != null) {
Attribute nameAttribute = animationRoot.getAttribute("name");
if (nameAttribute == null) {
nameAttribute = animationRoot.getAttribute("id");
}
final String name = nameAttribute != null ? nameAttribute.getValue() : "Default";
final AnimationItem animationItem = new AnimationItem(name);
animationItemRoot.getChildren().add(animationItem);
for (final Element animationElement : animationRoot.getChildren("animation")) {
parseAnimations(channelMap, animationElement, animationItem);
}
}
if (animationRoot.getChild("channel") != null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("\n-- Parsing animation channels --");
}
final List<Element> channels = animationRoot.getChildren("channel");
for (final Element channel : channels) {
final String source = channel.getAttributeValue("source");
final String targetString = channel.getAttributeValue("target");
if (targetString == null || targetString.isEmpty()) {
return;
}
final Target target = processTargetString(targetString);
if (logger.isLoggable(Level.FINE)) {
logger.fine("channel source: " + target.toString());
}
final Element targetNode = findTargetNode(target);
if (targetNode == null || !_dataCache.getTransformTypes().contains(targetNode.getName())) {
// TODO: pass with warning or exception or nothing?
// throw new ColladaException("No target transform node found for target: " + target, target);
continue;
}
if ("rotate".equals(targetNode.getName())) {
target.accessorType = AccessorType.Vector;
target.accessorIndexX = 3;
}
channelMap.put(targetNode.getParentElement(), new TargetChannel(target, targetNode, source,
animationItemRoot));
}
}
}
/**
* Find a target node based on collada target format.
*
* @param target
* @return
*/
private Element findTargetNode(final Target target) {
Element currentElement = _colladaDOMUtil.findTargetWithId(target.id);
if (currentElement == null) {
throw new ColladaException("No target found with id: " + target.id, target);
}
for (final String sid : target.sids) {
final String query = ".//*[@sid='" + sid + "']";
final Element sidElement = (Element) _colladaDOMUtil.selectSingleNode(currentElement, query);
if (sidElement == null) {
// throw new ColladaException("No element found with sid: " + sid, target);
// TODO: this is a hack to support older 3ds max exports. will be removed and instead use
// the above exception
// logger.warning("No element found with sid: " + sid + ", trying with first child.");
// final List<Element> children = currentElement.getChildren();
// if (!children.isEmpty()) {
// currentElement = children.get(0);
// }
// break;
if (logger.isLoggable(Level.WARNING)) {
logger.warning("No element found with sid: " + sid + ", skipping channel.");
}
return null;
} else {
currentElement = sidElement;
}
}
return currentElement;
}
private static final Map<String, Integer> symbolMap = Maps.newHashMap();
static {
symbolMap.put("ANGLE", 3);
symbolMap.put("TIME", 0);
symbolMap.put("X", 0);
symbolMap.put("Y", 1);
symbolMap.put("Z", 2);
symbolMap.put("W", 3);
symbolMap.put("R", 0);
symbolMap.put("G", 1);
symbolMap.put("B", 2);
symbolMap.put("A", 3);
symbolMap.put("S", 0);
symbolMap.put("T", 1);
symbolMap.put("P", 2);
symbolMap.put("Q", 3);
symbolMap.put("U", 0);
symbolMap.put("V", 1);
symbolMap.put("P", 2);
symbolMap.put("Q", 3);
}
/**
* Break up a target uri string into id, sids and accessors
*
* @param targetString
* @return
*/
private Target processTargetString(final String targetString) {
final Target target = new Target();
int accessorIndex = targetString.indexOf(".");
if (accessorIndex == -1) {
accessorIndex = targetString.indexOf("(");
}
final boolean hasAccessor = accessorIndex != -1;
if (accessorIndex == -1) {
accessorIndex = targetString.length();
}
final String baseString = targetString.substring(0, accessorIndex);
int sidIndex = baseString.indexOf("/");
final boolean hasSid = sidIndex != -1;
if (!hasSid) {
sidIndex = baseString.length();
}
final String id = baseString.substring(0, sidIndex);
target.id = id;
if (hasSid) {
final String sidGroup = baseString.substring(sidIndex + 1, baseString.length());
final StringTokenizer tokenizer = new StringTokenizer(sidGroup, "/");
while (tokenizer.hasMoreTokens()) {
final String sid = tokenizer.nextToken();
target.sids.add(sid);
}
}
if (hasAccessor) {
String accessorString = targetString.substring(accessorIndex, targetString.length());
accessorString = accessorString.replace(".", "");
if (accessorString.length() > 0 && accessorString.charAt(0) == '(') {
int endPara = accessorString.indexOf(")");
final String indexXString = accessorString.substring(1, endPara);
target.accessorIndexX = Integer.parseInt(indexXString);
if (endPara < accessorString.length() - 1) {
final String lastAccessorString = accessorString.substring(endPara + 1, accessorString.length());
endPara = lastAccessorString.indexOf(")");
final String indexYString = lastAccessorString.substring(1, endPara);
target.accessorIndexY = Integer.parseInt(indexYString);
target.accessorType = AccessorType.Matrix;
} else {
target.accessorType = AccessorType.Vector;
}
} else {
target.accessorIndexX = symbolMap.get(accessorString);
target.accessorType = AccessorType.Vector;
}
}
return target;
}
/**
* Convert a list of collada elements into a list of TransformElements
*
* @param transforms
* @return
*/
private List<TransformElement> getNodeTransformList(final List<Element> transforms) {
final List<TransformElement> transformList = Lists.newArrayList();
for (final Element transform : transforms) {
final double[] array = _colladaDOMUtil.parseDoubleArray(transform);
if ("translate".equals(transform.getName())) {
transformList.add(new TransformElement(array, TransformElementType.Translation));
} else if ("rotate".equals(transform.getName())) {
transformList.add(new TransformElement(array, TransformElementType.Rotation));
} else if ("scale".equals(transform.getName())) {
transformList.add(new TransformElement(array, TransformElementType.Scale));
} else if ("matrix".equals(transform.getName())) {
transformList.add(new TransformElement(array, TransformElementType.Matrix));
} else if ("lookat".equals(transform.getName())) {
transformList.add(new TransformElement(array, TransformElementType.Lookat));
} else {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("transform not currently supported: " + transform.getClass().getCanonicalName());
}
}
}
return transformList;
}
/**
* Bake a list of TransformElements into an Ardor3D Transform object.
*
* @param transforms
* @return
*/
private Transform bakeTransforms(final List<TransformElement> transforms) {
final Matrix4 workingMat = Matrix4.fetchTempInstance();
final Matrix4 finalMat = Matrix4.fetchTempInstance();
finalMat.setIdentity();
for (final TransformElement transform : transforms) {
final double[] array = transform.getArray();
final TransformElementType type = transform.getType();
if (type == TransformElementType.Translation) {
workingMat.setIdentity();
workingMat.setColumn(3, new Vector4(array[0], array[1], array[2], 1.0));
finalMat.multiplyLocal(workingMat);
} else if (type == TransformElementType.Rotation) {
if (array[3] != 0) {
workingMat.setIdentity();
final Matrix3 rotate = new Matrix3().fromAngleAxis(array[3] * MathUtils.DEG_TO_RAD, new Vector3(
array[0], array[1], array[2]));
workingMat.set(rotate);
finalMat.multiplyLocal(workingMat);
}
} else if (type == TransformElementType.Scale) {
workingMat.setIdentity();
workingMat.scale(new Vector4(array[0], array[1], array[2], 1), workingMat);
finalMat.multiplyLocal(workingMat);
} else if (type == TransformElementType.Matrix) {
workingMat.fromArray(array);
finalMat.multiplyLocal(workingMat);
} else if (type == TransformElementType.Lookat) {
final Vector3 pos = new Vector3(array[0], array[1], array[2]);
final Vector3 target = new Vector3(array[3], array[4], array[5]);
final Vector3 up = new Vector3(array[6], array[7], array[8]);
final Matrix3 rot = new Matrix3();
rot.lookAt(target.subtractLocal(pos), up);
workingMat.set(rot);
workingMat.setColumn(3, new Vector4(array[0], array[1], array[2], 1.0));
finalMat.multiplyLocal(workingMat);
} else {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("transform not currently supported: " + transform.getClass().getCanonicalName());
}
}
}
return new Transform().fromHomogeneousMatrix(finalMat);
}
/**
* Util for making a readable string out of a xml element hierarchy
*
* @param e
* @param maxDepth
* @return
*/
public static String getElementString(final Element e, final int maxDepth) {
return getElementString(e, maxDepth, true);
}
public static String getElementString(final Element e, final int maxDepth, final boolean showDots) {
final StringBuilder str = new StringBuilder();
getElementString(e, str, 0, maxDepth, showDots);
return str.toString();
}
@SuppressWarnings("unchecked")
private static void getElementString(final Element e, final StringBuilder str, final int depth, final int maxDepth,
final boolean showDots) {
addSpacing(str, depth);
str.append('<');
str.append(e.getName());
str.append(' ');
final List<Attribute> attrs = e.getAttributes();
for (int i = 0; i < attrs.size(); i++) {
final Attribute attr = attrs.get(i);
str.append(attr.getName());
str.append("=\"");
str.append(attr.getValue());
str.append('"');
if (i < attrs.size() - 1) {
str.append(' ');
}
}
if (!e.getChildren().isEmpty() || !"".equals(e.getText())) {
str.append('>');
if (depth < maxDepth) {
str.append('\n');
for (final Element child : (List<Element>) e.getChildren()) {
getElementString(child, str, depth + 1, maxDepth, showDots);
}
if (!"".equals(e.getText())) {
addSpacing(str, depth + 1);
str.append(e.getText());
str.append('\n');
}
} else if (showDots) {
str.append('\n');
addSpacing(str, depth + 1);
str.append("...");
str.append('\n');
}
addSpacing(str, depth);
str.append("</");
str.append(e.getName());
str.append('>');
} else {
str.append("/>");
}
str.append('\n');
}
private static void addSpacing(final StringBuilder str, final int depth) {
for (int i = 0; i < depth; i++) {
str.append(" ");
}
}
private enum AccessorType {
None, Vector, Matrix
}
private static class Target {
public String id;
public List<String> sids = Lists.newArrayList();
public AccessorType accessorType = AccessorType.None;
public int accessorIndexX = -1, accessorIndexY = -1;
@Override
public String toString() {
if (accessorType == AccessorType.None) {
return "Target [accessorType=" + accessorType + ", id=" + id + ", sids=" + sids + "]";
}
return "Target [accessorType=" + accessorType + ", accessorIndexX=" + accessorIndexX + ", accessorIndexY="
+ accessorIndexY + ", id=" + id + ", sids=" + sids + "]";
}
}
}