001/**
002 * Copyright (c) 2008-2014 Ardor Labs, Inc.
003 *
004 * This file is part of Ardor3D.
005 *
006 * Ardor3D is free software: you can redistribute it and/or modify it
007 * under the terms of its license which may be found in the accompanying
008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
009 */
010
011package com.ardor3d.extension.model.collada.jdom;
012
013import java.io.ByteArrayInputStream;
014import java.io.ByteArrayOutputStream;
015import java.io.IOException;
016import java.nio.FloatBuffer;
017import java.nio.ShortBuffer;
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.EnumMap;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.StringTokenizer;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.jdom2.Attribute;
030import org.jdom2.DataConversionException;
031import org.jdom2.Element;
032
033import com.ardor3d.extension.animation.skeletal.AttachmentPoint;
034import com.ardor3d.extension.animation.skeletal.Joint;
035import com.ardor3d.extension.animation.skeletal.Skeleton;
036import com.ardor3d.extension.animation.skeletal.SkeletonPose;
037import com.ardor3d.extension.animation.skeletal.SkinnedMesh;
038import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
039import com.ardor3d.extension.animation.skeletal.clip.JointChannel;
040import com.ardor3d.extension.animation.skeletal.clip.TransformChannel;
041import com.ardor3d.extension.model.collada.jdom.ColladaInputPipe.ParamType;
042import com.ardor3d.extension.model.collada.jdom.ColladaInputPipe.Type;
043import com.ardor3d.extension.model.collada.jdom.data.AnimationItem;
044import com.ardor3d.extension.model.collada.jdom.data.ColladaStorage;
045import com.ardor3d.extension.model.collada.jdom.data.DataCache;
046import com.ardor3d.extension.model.collada.jdom.data.MeshVertPairs;
047import com.ardor3d.extension.model.collada.jdom.data.SkinData;
048import com.ardor3d.extension.model.collada.jdom.data.TransformElement;
049import com.ardor3d.extension.model.collada.jdom.data.TransformElement.TransformElementType;
050import com.ardor3d.math.MathUtils;
051import com.ardor3d.math.Matrix3;
052import com.ardor3d.math.Matrix4;
053import com.ardor3d.math.Transform;
054import com.ardor3d.math.Vector3;
055import com.ardor3d.math.Vector4;
056import com.ardor3d.renderer.state.RenderState;
057import com.ardor3d.renderer.state.RenderState.StateType;
058import com.ardor3d.scenegraph.AbstractBufferData.VBOAccessMode;
059import com.ardor3d.scenegraph.Mesh;
060import com.ardor3d.scenegraph.MeshData;
061import com.ardor3d.scenegraph.Node;
062import com.ardor3d.scenegraph.Spatial;
063import com.ardor3d.util.export.Savable;
064import com.ardor3d.util.export.binary.BinaryExporter;
065import com.ardor3d.util.export.binary.BinaryImporter;
066import com.ardor3d.util.geom.BufferUtils;
067import com.ardor3d.util.geom.VertMap;
068
069/**
070 * Methods for parsing Collada data related to animation, skinning and morphing.
071 */
072public class ColladaAnimUtils {
073    private static final Logger logger = Logger.getLogger(ColladaAnimUtils.class.getName());
074
075    private final ColladaStorage _colladaStorage;
076    private final DataCache _dataCache;
077    private final ColladaDOMUtil _colladaDOMUtil;
078    private final ColladaMeshUtils _colladaMeshUtils;
079
080    public ColladaAnimUtils(final ColladaStorage colladaStorage, final DataCache dataCache,
081            final ColladaDOMUtil colladaDOMUtil, final ColladaMeshUtils colladaMeshUtils) {
082        _colladaStorage = colladaStorage;
083        _dataCache = dataCache;
084        _colladaDOMUtil = colladaDOMUtil;
085        _colladaMeshUtils = colladaMeshUtils;
086    }
087
088    /**
089     * Retrieve a name to use for the skin node based on the element names.
090     *
091     * @param ic
092     *            instance_controller element.
093     * @param controller
094     *            controller element
095     * @return name.
096     * @see SkinData#SkinData(String)
097     */
098    private String getSkinStoreName(final Element ic, final Element controller) {
099        final String controllerName = controller.getAttributeValue("name", (String) null) != null
100                ? controller.getAttributeValue("name", (String) null)
101                : controller.getAttributeValue("id", (String) null);
102        final String instanceControllerName = ic.getAttributeValue("name", (String) null) != null
103                ? ic.getAttributeValue("name", (String) null)
104                : ic.getAttributeValue("sid", (String) null);
105        final String storeName = (controllerName != null ? controllerName : "")
106                + (controllerName != null && instanceControllerName != null ? " : " : "")
107                + (instanceControllerName != null ? instanceControllerName : "");
108        return storeName;
109    }
110
111    /**
112     * Copy the render states from our source Spatial to the destination Spatial. Does not recurse.
113     *
114     * @param source
115     *            the source
116     * @param target
117     *            the target
118     */
119    private void copyRenderStates(final Spatial source, final Spatial target) {
120        final EnumMap<StateType, RenderState> states = source.getLocalRenderStates();
121        for (final RenderState state : states.values()) {
122            target.setRenderState(state);
123        }
124    }
125
126    /**
127     * Clone the given MeshData object via deep copy using the Ardor3D BinaryExporter and BinaryImporter.
128     *
129     * @param meshData
130     *            the source to clone.
131     * @return the clone.
132     * @throws IOException
133     *             if we have troubles during the clone.
134     */
135    private MeshData copyMeshData(final MeshData meshData) throws IOException {
136        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
137        final BinaryExporter exporter = new BinaryExporter();
138        exporter.save(meshData, bos);
139        bos.flush();
140        final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
141        final BinaryImporter importer = new BinaryImporter();
142        final Savable sav = importer.load(bis);
143        return (MeshData) sav;
144    }
145
146    /**
147     * Builds data based on an instance controller element.
148     *
149     * @param node
150     *            Ardor3D parent Node
151     * @param instanceController
152     *            the instance controller
153     */
154    void buildController(final Node node, final Element instanceController) {
155        final Element controller = _colladaDOMUtil.findTargetWithId(instanceController.getAttributeValue("url"));
156
157        if (controller == null) {
158            throw new ColladaException(
159                    "Unable to find controller with id: " + instanceController.getAttributeValue("url"),
160                    instanceController);
161        }
162
163        final Element skin = controller.getChild("skin");
164        if (skin != null) {
165            buildSkinMeshes(node, instanceController, controller, skin);
166        } else {
167            // look for morph... can only be one or the other according to Collada
168            final Element morph = controller.getChild("morph");
169            if (morph != null) {
170                buildMorphMeshes(node, controller, morph);
171            }
172        }
173    }
174
175    /**
176     * Construct skin mesh(es) from the skin element and attach them (under a single new Node) to the given parent Node.
177     *
178     * @param ardorParentNode
179     *            Ardor3D Node to attach our skin node to.
180     * @param instanceController
181     *            the instance_controller element. We'll parse the skeleton reference from here.
182     * @param controller
183     *            the referenced controller element. Used for naming purposes.
184     * @param skin
185     *            our skin element.
186     */
187    private void buildSkinMeshes(final Node ardorParentNode, final Element instanceController, final Element controller,
188            final Element skin) {
189        final String skinSource = skin.getAttributeValue("source");
190
191        final Element skinNodeEL = _colladaDOMUtil.findTargetWithId(skinSource);
192        if (skinNodeEL == null || !"geometry".equals(skinNodeEL.getName())) {
193            throw new ColladaException(
194                    "Expected a mesh for skin source with url: " + skinSource + " got instead: " + skinNodeEL, skin);
195        }
196
197        final Element geometry = skinNodeEL;
198
199        final Node meshNode = _colladaMeshUtils.buildMesh(geometry);
200        if (meshNode != null) {
201            // Look for skeleton entries in the original <instance_controller> element
202            final List<Element> skeletonRoots = new ArrayList<>();
203            for (final Element sk : instanceController.getChildren("skeleton")) {
204                final Element skroot = _colladaDOMUtil.findTargetWithId(sk.getText());
205                if (skroot != null) {
206                    // add as a possible root for when we need to locate a joint by name later.
207                    skeletonRoots.add(skroot);
208                } else {
209                    throw new ColladaException(
210                            "Unable to find node with id: " + sk.getText() + ", referenced from skeleton " + sk, sk);
211                }
212            }
213
214            // Read in our joints node
215            final Element jointsEL = skin.getChild("joints");
216            if (jointsEL == null) {
217                throw new ColladaException("skin found without joints.", skin);
218            }
219
220            // Pull out our joint names and bind matrices
221            final List<String> jointNames = new ArrayList<>();
222            final List<Transform> bindMatrices = new ArrayList<>();
223            final List<ColladaInputPipe.ParamType> paramTypes = new ArrayList<>();
224
225            for (final Element inputEL : jointsEL.getChildren("input")) {
226                final ColladaInputPipe pipe = new ColladaInputPipe(_colladaDOMUtil, inputEL);
227                final ColladaInputPipe.SourceData sd = pipe.getSourceData();
228                if (pipe.getType() == ColladaInputPipe.Type.JOINT) {
229                    final String[] namesData = sd.stringArray;
230                    for (int i = sd.offset; i < namesData.length; i += sd.stride) {
231                        jointNames.add(namesData[i]);
232                        paramTypes.add(sd.paramType);
233                    }
234                } else if (pipe.getType() == ColladaInputPipe.Type.INV_BIND_MATRIX) {
235                    final float[] floatData = sd.floatArray;
236                    final FloatBuffer source = BufferUtils.createFloatBufferOnHeap(16);
237                    for (int i = sd.offset; i < floatData.length; i += sd.stride) {
238                        source.rewind();
239                        source.put(floatData, i, 16);
240                        source.flip();
241                        final Matrix4 mat = new Matrix4().fromFloatBuffer(source);
242                        bindMatrices.add(new Transform().fromHomogeneousMatrix(mat));
243                    }
244                }
245            }
246
247            // Use the skeleton information from the instance_controller to set the parent array locations on the
248            // joints.
249            Skeleton ourSkeleton = null; // TODO: maybe not the best way. iterate
250            final int[] order = new int[jointNames.size()];
251            for (int i = 0; i < jointNames.size(); i++) {
252                final String name = jointNames.get(i);
253                final ParamType paramType = paramTypes.get(i);
254                final String searcher = paramType == ParamType.idref_param ? "id" : "sid";
255                Element found = null;
256                for (final Element root : skeletonRoots) {
257                    if (name.equals(root.getAttributeValue(searcher))) {
258                        found = root;
259                    } else if (paramType == ParamType.idref_param) {
260                        found = _colladaDOMUtil.findTargetWithId(name);
261                    } else {
262                        found = (Element) _colladaDOMUtil.selectSingleNode(root, ".//*[@sid='" + name + "']");
263                    }
264
265                    // Last resorts (bad exporters)
266                    if (found == null) {
267                        found = _colladaDOMUtil.findTargetWithId(name);
268                    }
269                    if (found == null) {
270                        found = (Element) _colladaDOMUtil.selectSingleNode(root, ".//*[@name='" + name + "']");
271                    }
272
273                    if (found != null) {
274                        break;
275                    }
276                }
277                if (found == null) {
278                    if (paramType == ParamType.idref_param) {
279                        found = _colladaDOMUtil.findTargetWithId(name);
280                    } else {
281                        found = (Element) _colladaDOMUtil.selectSingleNode(geometry,
282                                "/*//visual_scene//*[@sid='" + name + "']");
283                    }
284
285                    // Last resorts (bad exporters)
286                    if (found == null) {
287                        found = _colladaDOMUtil.findTargetWithId(name);
288                    }
289                    if (found == null) {
290                        found = (Element) _colladaDOMUtil.selectSingleNode(geometry,
291                                "/*//visual_scene//*[@name='" + name + "']");
292                    }
293
294                    if (found == null) {
295                        throw new ColladaException("Unable to find joint with " + searcher + ": " + name, skin);
296                    }
297                }
298
299                final Joint joint = _dataCache.getElementJointMapping().get(found);
300                if (joint == null) {
301                    logger.warning("unable to parse joint for: " + found.getName() + " " + name);
302                    return;
303                }
304                joint.setInverseBindPose(bindMatrices.get(i));
305
306                ourSkeleton = _dataCache.getJointSkeletonMapping().get(joint);
307                order[i] = joint.getIndex();
308            }
309
310            // Make our skeleton pose
311            SkeletonPose skPose = _dataCache.getSkeletonPoseMapping().get(ourSkeleton);
312            if (skPose == null) {
313                skPose = new SkeletonPose(ourSkeleton);
314                _dataCache.getSkeletonPoseMapping().put(ourSkeleton, skPose);
315
316                // attach any attachment points found for the skeleton's joints
317                addAttachments(skPose);
318
319                // Skeleton's default to bind position, so update the global transforms.
320                skPose.updateTransforms();
321            }
322
323            // Read in our vertex_weights node
324            final Element weightsEL = skin.getChild("vertex_weights");
325            if (weightsEL == null) {
326                throw new ColladaException("skin found without vertex_weights.", skin);
327            }
328
329            // Pull out our per vertex joint indices and weights
330            final List<Short> jointIndices = new ArrayList<>();
331            final List<Float> jointWeights = new ArrayList<>();
332            int indOff = 0, weightOff = 0;
333
334            int maxOffset = 0;
335            for (final Element inputEL : weightsEL.getChildren("input")) {
336                final ColladaInputPipe pipe = new ColladaInputPipe(_colladaDOMUtil, inputEL);
337                final ColladaInputPipe.SourceData sd = pipe.getSourceData();
338                if (pipe.getOffset() > maxOffset) {
339                    maxOffset = pipe.getOffset();
340                }
341                if (pipe.getType() == ColladaInputPipe.Type.JOINT) {
342                    indOff = pipe.getOffset();
343                    final String[] namesData = sd.stringArray;
344                    for (int i = sd.offset; i < namesData.length; i += sd.stride) {
345                        // XXX: the Collada spec says this could be -1?
346                        final String name = namesData[i];
347                        final int index = jointNames.indexOf(name);
348                        if (index >= 0) {
349                            jointIndices.add((short) index);
350                        } else {
351                            throw new ColladaException("Unknown joint accessed: " + name, inputEL);
352                        }
353                    }
354                } else if (pipe.getType() == ColladaInputPipe.Type.WEIGHT) {
355                    weightOff = pipe.getOffset();
356                    final float[] floatData = sd.floatArray;
357                    for (int i = sd.offset; i < floatData.length; i += sd.stride) {
358                        jointWeights.add(floatData[i]);
359                    }
360                }
361            }
362
363            // Pull our values array
364            int firstIndex = 0, count = 0;
365            final int[] vals = _colladaDOMUtil.parseIntArray(weightsEL.getChild("v"));
366            try {
367                count = weightsEL.getAttribute("count").getIntValue();
368            } catch (final DataConversionException e) {
369                throw new ColladaException("Unable to parse count attribute.", weightsEL);
370            }
371            // use the vals to fill our vert weight map
372            final int[][] vertWeightMap = new int[count][];
373            int index = 0;
374            for (final int length : _colladaDOMUtil.parseIntArray(weightsEL.getChild("vcount"))) {
375                final int[] entry = new int[(maxOffset + 1) * length];
376                vertWeightMap[index++] = entry;
377
378                System.arraycopy(vals, (maxOffset + 1) * firstIndex, entry, 0, entry.length);
379
380                firstIndex += length;
381            }
382
383            // Create a record for the global ColladaStorage.
384            final String storeName = getSkinStoreName(instanceController, controller);
385            final SkinData skinDataStore = new SkinData(storeName);
386            // add pose to store
387            skinDataStore.setPose(skPose);
388
389            // Create a base Node for our skin meshes
390            final Node skinNode = new Node(meshNode.getName());
391            // copy Node render states across.
392            copyRenderStates(meshNode, skinNode);
393            // add node to store
394            skinDataStore.setSkinBaseNode(skinNode);
395
396            // Grab the bind_shape_matrix from skin
397            final Element bindShapeMatrixEL = skin.getChild("bind_shape_matrix");
398            final Transform bindShapeMatrix = new Transform();
399            if (bindShapeMatrixEL != null) {
400                final double[] array = _colladaDOMUtil.parseDoubleArray(bindShapeMatrixEL);
401                bindShapeMatrix.fromHomogeneousMatrix(new Matrix4().fromArray(array));
402            }
403
404            // Visit our Node and pull out any Mesh children. Turn them into SkinnedMeshes
405            for (final Spatial spat : meshNode.getChildren()) {
406                if (spat instanceof Mesh && ((Mesh) spat).getMeshData().getVertexCount() > 0) {
407                    final Mesh sourceMesh = (Mesh) spat;
408                    final SkinnedMesh skMesh = new SkinnedMesh(sourceMesh.getName());
409                    skMesh.setCurrentPose(skPose);
410
411                    // copy material info mapping for later use
412                    final String material = _dataCache.getMeshMaterialMap().get(sourceMesh);
413                    _dataCache.getMeshMaterialMap().put(skMesh, material);
414
415                    // copy mesh render states across.
416                    copyRenderStates(sourceMesh, skMesh);
417
418                    // copy hints across
419                    skMesh.getSceneHints().set(sourceMesh.getSceneHints());
420
421                    try {
422                        // Use source mesh as bind pose data in the new SkinnedMesh
423                        final MeshData bindPose = copyMeshData(sourceMesh.getMeshData());
424                        skMesh.setBindPoseData(bindPose);
425
426                        // Apply our BSM
427                        if (!bindShapeMatrix.isIdentity()) {
428                            bindPose.transformVertices(bindShapeMatrix);
429                            if (bindPose.getNormalBuffer() != null) {
430                                bindPose.transformNormals(bindShapeMatrix, true);
431                            }
432                        }
433
434                        // TODO: This is only needed for CPU skinning... consider a way of making it optional.
435                        // Copy bind pose to mesh data to setup for CPU skinning
436                        final MeshData meshData = copyMeshData(skMesh.getBindPoseData());
437                        meshData.getVertexCoords().setVboAccessMode(VBOAccessMode.StreamDraw);
438                        if (meshData.getNormalCoords() != null) {
439                            meshData.getNormalCoords().setVboAccessMode(VBOAccessMode.StreamDraw);
440                        }
441                        skMesh.setMeshData(meshData);
442                    } catch (final IOException e) {
443                        e.printStackTrace();
444                        throw new ColladaException("Unable to copy skeleton bind pose data.", geometry);
445                    }
446
447                    // Grab the MeshVertPairs from Global for this mesh.
448                    final Collection<MeshVertPairs> vertPairsList = _dataCache.getVertMappings().get(geometry);
449                    MeshVertPairs pairsMap = null;
450                    if (vertPairsList != null) {
451                        for (final MeshVertPairs pairs : vertPairsList) {
452                            if (pairs.getMesh() == sourceMesh) {
453                                pairsMap = pairs;
454                                break;
455                            }
456                        }
457                    }
458
459                    if (pairsMap == null) {
460                        throw new ColladaException("Unable to locate pair map for geometry.", geometry);
461                    }
462
463                    // Check for a remapping, if we optimized geometry
464                    final VertMap vertMap = _dataCache.getMeshVertMap().get(sourceMesh);
465
466                    // Use pairs map and vertWeightMap to build our weights and joint indices.
467                    {
468                        // count number of weights used
469                        int maxWeightsPerVert = 0;
470                        int weightCount;
471                        for (final int originalIndex : pairsMap.getIndices()) {
472                            weightCount = 0;
473
474                            // get weights and joints at original index and add weights up to get divisor sum
475                            // we'll assume 0's for vertices with no matching weight.
476                            if (vertWeightMap.length > originalIndex) {
477                                final int[] data = vertWeightMap[originalIndex];
478                                for (int i = 0; i < data.length; i += maxOffset + 1) {
479                                    final float weight = jointWeights.get(data[i + weightOff]);
480                                    if (weight != 0) {
481                                        weightCount++;
482                                    }
483                                }
484                                if (weightCount > maxWeightsPerVert) {
485                                    maxWeightsPerVert = weightCount;
486                                }
487                            }
488                        }
489
490                        final int verts = skMesh.getMeshData().getVertexCount();
491                        final FloatBuffer weightBuffer = BufferUtils.createFloatBuffer(verts * maxWeightsPerVert);
492                        final ShortBuffer jointIndexBuffer = BufferUtils.createShortBuffer(verts * maxWeightsPerVert);
493                        int j;
494                        float sum = 0;
495                        final float[] weights = new float[maxWeightsPerVert];
496                        final short[] indices = new short[maxWeightsPerVert];
497                        int originalIndex;
498                        for (int x = 0; x < verts; x++) {
499                            if (vertMap != null) {
500                                originalIndex = pairsMap.getIndices()[vertMap.getFirstOldIndex(x)];
501                            } else {
502                                originalIndex = pairsMap.getIndices()[x];
503                            }
504
505                            j = 0;
506                            sum = 0;
507
508                            // get weights and joints at original index and add weights up to get divisor sum
509                            // we'll assume 0's for vertices with no matching weight.
510                            if (vertWeightMap.length > originalIndex) {
511                                final int[] data = vertWeightMap[originalIndex];
512                                for (int i = 0; i < data.length; i += maxOffset + 1) {
513                                    final float weight = jointWeights.get(data[i + weightOff]);
514                                    if (weight != 0) {
515                                        weights[j] = jointWeights.get(data[i + weightOff]);
516                                        indices[j] = (short) order[jointIndices.get(data[i + indOff])];
517                                        sum += weights[j++];
518                                    }
519                                }
520                            }
521                            // add extra padding as needed
522                            while (j < maxWeightsPerVert) {
523                                weights[j] = 0;
524                                indices[j++] = 0;
525                            }
526                            // add weights to weightBuffer / sum
527                            for (final float w : weights) {
528                                weightBuffer.put(sum != 0 ? w / sum : 0);
529                            }
530                            // add joint indices to jointIndexBuffer
531                            jointIndexBuffer.put(indices);
532                        }
533
534                        final float[] totalWeights = new float[weightBuffer.capacity()];
535                        weightBuffer.flip();
536                        weightBuffer.get(totalWeights);
537                        skMesh.setWeights(totalWeights);
538
539                        final short[] totalIndices = new short[jointIndexBuffer.capacity()];
540                        jointIndexBuffer.flip();
541                        jointIndexBuffer.get(totalIndices);
542                        skMesh.setJointIndices(totalIndices);
543
544                        skMesh.setWeightsPerVert(maxWeightsPerVert);
545                    }
546
547                    // add to the skinNode.
548                    skinNode.attachChild(skMesh);
549
550                    // Manually apply our bind pose to the skin mesh.
551                    skMesh.applyPose();
552
553                    // Update the model bounding.
554                    skMesh.updateModelBound();
555
556                    // add mesh to store
557                    skinDataStore.getSkins().add(skMesh);
558                }
559            }
560
561            // add to Node
562            ardorParentNode.attachChild(skinNode);
563
564            // Add skin record to storage.
565            _colladaStorage.getSkins().add(skinDataStore);
566        }
567    }
568
569    private void addAttachments(final SkeletonPose skPose) {
570        final Skeleton skeleton = skPose.getSkeleton();
571        for (final Joint joint : skeleton.getJoints()) {
572            final List<AttachmentPoint> attachmentPoints = _dataCache.getAttachmentPoints().get(joint);
573            if (attachmentPoints != null) {
574                for (final AttachmentPoint point : attachmentPoints) {
575                    point.setJointIndex(joint.getIndex());
576                    skPose.addPoseListener(point);
577                }
578            }
579        }
580    }
581
582    /**
583     * Construct morph mesh(es) from the morph element and attach them (under a single new Node) to the given parent
584     * Node.
585     *
586     * Note: This method current does not do anything but attach the referenced mesh since Ardor3D does not yet support
587     * morph target animation.
588     *
589     * @param ardorParentNode
590     *            Ardor3D Node to attach our morph mesh to.
591     * @param controller
592     *            the referenced controller element. Used for naming purposes.
593     * @param morph
594     *            our morph element
595     */
596    private void buildMorphMeshes(final Node ardorParentNode, final Element controller, final Element morph) {
597        final String skinSource = morph.getAttributeValue("source");
598
599        final Element skinNode = _colladaDOMUtil.findTargetWithId(skinSource);
600        if (skinNode == null || !"geometry".equals(skinNode.getName())) {
601            throw new ColladaException(
602                    "Expected a mesh for morph source with url: " + skinSource + " (line number is referring morph)",
603                    morph);
604        }
605
606        final Element geometry = skinNode;
607
608        final Spatial baseMesh = _colladaMeshUtils.buildMesh(geometry);
609
610        // TODO: support morph animations someday.
611        if (logger.isLoggable(Level.WARNING)) {
612            logger.warning("Morph target animation not yet supported.");
613        }
614
615        // Just add mesh.
616        if (baseMesh != null) {
617            ardorParentNode.attachChild(baseMesh);
618        }
619    }
620
621    /**
622     * Parse all animations in library_animations
623     *
624     * @param colladaRoot
625     *            the collada root element
626     */
627    public void parseLibraryAnimations(final Element colladaRoot) {
628        final Element libraryAnimations = colladaRoot.getChild("library_animations");
629
630        if (libraryAnimations == null || libraryAnimations.getChildren().isEmpty()) {
631            if (logger.isLoggable(Level.WARNING)) {
632                logger.warning("No animations found in collada file!");
633            }
634            return;
635        }
636
637        final AnimationItem animationItemRoot = new AnimationItem("Animation Root");
638        _colladaStorage.setAnimationItemRoot(animationItemRoot);
639
640        final Map<Element, List<TargetChannel>> channelMap = new HashMap<>();
641
642        parseAnimations(channelMap, libraryAnimations, animationItemRoot);
643
644        for (final Map.Entry<Element, List<TargetChannel>> channelMapEntry : channelMap.entrySet()) {
645            if (channelMapEntry.getValue() != null) {
646                buildAnimations(channelMapEntry.getKey(), channelMapEntry.getValue());
647            }
648        }
649    }
650
651    /**
652     * Merge all animation channels into Ardor joint channels
653     *
654     * @param parentElement
655     *            the parent element
656     * @param targetList
657     *            the target list
658     */
659    private void buildAnimations(final Element parentElement, final Collection<TargetChannel> targetList) {
660
661        final List<Element> elementTransforms = new ArrayList<>();
662        for (final Element child : parentElement.getChildren()) {
663            if (_dataCache.getTransformTypes().contains(child.getName())) {
664                elementTransforms.add(child);
665            }
666        }
667        final List<TransformElement> transformList = getNodeTransformList(elementTransforms);
668
669        AnimationItem animationItemRoot = null;
670        for (final TargetChannel targetChannel : targetList) {
671            if (animationItemRoot == null) {
672                animationItemRoot = targetChannel.animationItemRoot;
673            }
674            final String source = targetChannel.source;
675            // final Target target = targetChannel.target;
676            final Element targetNode = targetChannel.targetNode;
677
678            final int targetIndex = elementTransforms.indexOf(targetNode);
679            if (logger.isLoggable(Level.FINE)) {
680                logger.fine(parentElement.getName() + "(" + parentElement.getAttributeValue("name") + ") -> "
681                        + targetNode.getName() + "(" + targetIndex + ")");
682            }
683
684            final EnumMap<Type, ColladaInputPipe> pipes = new EnumMap<>(Type.class);
685
686            final Element samplerElement = _colladaDOMUtil.findTargetWithId(source);
687            for (final Element inputElement : samplerElement.getChildren("input")) {
688                final ColladaInputPipe pipe = new ColladaInputPipe(_colladaDOMUtil, inputElement);
689                pipes.put(pipe.getType(), pipe);
690            }
691
692            // get input (which is TIME for now)
693            final ColladaInputPipe inputPipe = pipes.get(Type.INPUT);
694            final ColladaInputPipe.SourceData sdIn = inputPipe.getSourceData();
695            final float[] time = sdIn.floatArray;
696            targetChannel.time = time;
697            if (logger.isLoggable(Level.FINE)) {
698                logger.fine("inputPipe: " + Arrays.toString(time));
699            }
700
701            // get output data
702            final ColladaInputPipe outputPipe = pipes.get(Type.OUTPUT);
703            final ColladaInputPipe.SourceData sdOut = outputPipe.getSourceData();
704            final float[] animationData = sdOut.floatArray;
705            targetChannel.animationData = animationData;
706            if (logger.isLoggable(Level.FINE)) {
707                logger.fine("outputPipe: " + Arrays.toString(animationData));
708            }
709
710            // TODO: Need to add support for other interpolation types.
711
712            // get target array from transform list
713            final TransformElement transformElement = transformList.get(targetIndex);
714            final double[] array = transformElement.getArray();
715            targetChannel.array = array;
716
717            final int stride = sdOut.stride;
718            targetChannel.stride = stride;
719
720            targetChannel.currentPos = 0;
721        }
722
723        final List<Float> finalTimeList = new ArrayList<>();
724        final List<Transform> finalTransformList = new ArrayList<>();
725        final List<TargetChannel> workingChannels = new ArrayList<>();
726        for (;;) {
727            float lowestTime = Float.MAX_VALUE;
728            boolean found = false;
729            for (final TargetChannel targetChannel : targetList) {
730                if (targetChannel.currentPos < targetChannel.time.length) {
731                    final float time = targetChannel.time[targetChannel.currentPos];
732                    if (time < lowestTime) {
733                        lowestTime = time;
734                    }
735                    found = true;
736                }
737            }
738            if (!found) {
739                break;
740            }
741
742            workingChannels.clear();
743            for (final TargetChannel targetChannel : targetList) {
744                if (targetChannel.currentPos < targetChannel.time.length) {
745                    final float time = targetChannel.time[targetChannel.currentPos];
746                    if (time == lowestTime) {
747                        workingChannels.add(targetChannel);
748                    }
749                }
750            }
751
752            for (final TargetChannel targetChannel : workingChannels) {
753                final Target target = targetChannel.target;
754                final float[] animationData = targetChannel.animationData;
755                final double[] array = targetChannel.array;
756
757                // set the correct values depending on accessor
758                final int position = targetChannel.currentPos * targetChannel.stride;
759                if (target.accessorType == AccessorType.None) {
760                    for (int j = 0; j < array.length; j++) {
761                        array[j] = animationData[position + j];
762                    }
763                } else {
764                    if (target.accessorType == AccessorType.Vector) {
765                        array[target.accessorIndexX] = animationData[position];
766                    } else if (target.accessorType == AccessorType.Matrix) {
767                        array[target.accessorIndexY * 4 + target.accessorIndexX] = animationData[position];
768                    }
769                }
770                targetChannel.currentPos++;
771            }
772
773            // bake the transform
774            final Transform transform = bakeTransforms(transformList);
775            finalTimeList.add(lowestTime);
776            finalTransformList.add(transform);
777        }
778
779        if (animationItemRoot != null) {
780
781            final float[] time = new float[finalTimeList.size()];
782            for (int i = 0; i < finalTimeList.size(); i++) {
783                time[i] = finalTimeList.get(i);
784            }
785            final Transform[] transforms = finalTransformList.toArray(new Transform[finalTransformList.size()]);
786
787            AnimationClip animationClip = animationItemRoot.getAnimationClip();
788            if (animationClip == null) {
789                animationClip = new AnimationClip(animationItemRoot.getName());
790                animationItemRoot.setAnimationClip(animationClip);
791            }
792
793            // Make an animation channel - first find if we have a matching joint
794            Joint joint = _dataCache.getElementJointMapping().get(parentElement);
795            if (joint == null) {
796                String nodeName = parentElement.getAttributeValue("name", (String) null);
797                if (nodeName == null) { // use id if name doesn't exist
798                    nodeName = parentElement.getAttributeValue("id", parentElement.getName());
799                }
800                if (nodeName != null) {
801                    joint = _dataCache.getExternalJointMapping().get(nodeName);
802                }
803
804                if (joint == null) {
805                    // no joint still, so make a transform channel.
806                    final TransformChannel transformChannel = new TransformChannel(nodeName, time, transforms);
807                    animationClip.addChannel(transformChannel);
808                    _colladaStorage.getAnimationChannels().add(transformChannel);
809                    return;
810                }
811            }
812
813            // create joint channel
814            final JointChannel jointChannel = new JointChannel(joint, time, transforms);
815            animationClip.addChannel(jointChannel);
816            _colladaStorage.getAnimationChannels().add(jointChannel);
817        }
818    }
819
820    /**
821     * Stores animation data to use for merging into jointchannels.
822     */
823    private static class TargetChannel {
824        Target target;
825        Element targetNode;
826        String source;
827        AnimationItem animationItemRoot;
828
829        float[] time;
830        float[] animationData;
831        double[] array;
832        int stride;
833        int currentPos;
834
835        public TargetChannel(final Target target, final Element targetNode, final String source,
836                final AnimationItem animationItemRoot) {
837            this.target = target;
838            this.targetNode = targetNode;
839            this.source = source;
840            this.animationItemRoot = animationItemRoot;
841        }
842    }
843
844    /**
845     * Gather up all animation channels based on what nodes they affect.
846     *
847     * @param channelMap
848     *            the channel map
849     * @param animationRoot
850     *            the animation root
851     * @param animationItemRoot
852     *            the animation item root
853     */
854    private void parseAnimations(final Map<Element, List<TargetChannel>> channelMap, final Element animationRoot,
855            final AnimationItem animationItemRoot) {
856        if (animationRoot.getChild("animation") != null) {
857            Attribute nameAttribute = animationRoot.getAttribute("name");
858            if (nameAttribute == null) {
859                nameAttribute = animationRoot.getAttribute("id");
860            }
861            final String name = nameAttribute != null ? nameAttribute.getValue() : "Default";
862
863            final AnimationItem animationItem = new AnimationItem(name);
864            animationItemRoot.getChildren().add(animationItem);
865
866            for (final Element animationElement : animationRoot.getChildren("animation")) {
867                parseAnimations(channelMap, animationElement, animationItem);
868            }
869        }
870        if (animationRoot.getChild("channel") != null) {
871            if (logger.isLoggable(Level.FINE)) {
872                logger.fine("\n-- Parsing animation channels --");
873            }
874            final List<Element> channels = animationRoot.getChildren("channel");
875            for (final Element channel : channels) {
876                final String source = channel.getAttributeValue("source");
877
878                final String targetString = channel.getAttributeValue("target");
879                if (targetString == null || targetString.isEmpty()) {
880                    return;
881                }
882
883                final Target target = processTargetString(targetString);
884                if (logger.isLoggable(Level.FINE)) {
885                    logger.fine("channel source: " + target.toString());
886                }
887                final Element targetNode = findTargetNode(target);
888                if (targetNode == null || !_dataCache.getTransformTypes().contains(targetNode.getName())) {
889                    // TODO: pass with warning or exception or nothing?
890                    // throw new ColladaException("No target transform node found for target: " + target, target);
891                    continue;
892                }
893                if ("rotate".equals(targetNode.getName())) {
894                    target.accessorType = AccessorType.Vector;
895                    target.accessorIndexX = 3;
896                }
897
898                final TargetChannel targetChannel = new TargetChannel(target, targetNode, source, animationItemRoot);
899                channelMap.compute(targetNode.getParentElement(),
900                        (final Element currentKey, final List<TargetChannel> oldValue) -> {
901                            final List<TargetChannel> newValue;
902                            if (oldValue == null) {
903                                newValue = new ArrayList<>();
904                            } else {
905                                newValue = oldValue;
906                            }
907                            newValue.add(targetChannel);
908                            return newValue;
909                        });
910            }
911        }
912    }
913
914    /**
915     * Find a target node based on collada target format.
916     *
917     * @param target
918     *            the target
919     * @return the target node if any
920     */
921    private Element findTargetNode(final Target target) {
922        Element currentElement = _colladaDOMUtil.findTargetWithId(target.id);
923        if (currentElement == null) {
924            throw new ColladaException("No target found with id: " + target.id, target);
925        }
926
927        for (final String sid : target.sids) {
928            final String query = ".//*[@sid='" + sid + "']";
929            final Element sidElement = (Element) _colladaDOMUtil.selectSingleNode(currentElement, query);
930            if (sidElement == null) {
931                // throw new ColladaException("No element found with sid: " + sid, target);
932
933                // TODO: this is a hack to support older 3ds max exports. will be removed and instead use
934                // the above exception
935                // logger.warning("No element found with sid: " + sid + ", trying with first child.");
936                // final List<Element> children = currentElement.getChildren();
937                // if (!children.isEmpty()) {
938                // currentElement = children.get(0);
939                // }
940                // break;
941
942                if (logger.isLoggable(Level.WARNING)) {
943                    logger.warning("No element found with sid: " + sid + ", skipping channel.");
944                }
945                return null;
946            } else {
947                currentElement = sidElement;
948            }
949        }
950
951        return currentElement;
952    }
953
954    private static final Map<String, Integer> symbolMap = new HashMap<>();
955    static {
956        symbolMap.put("ANGLE", 3);
957        symbolMap.put("TIME", 0);
958
959        symbolMap.put("X", 0);
960        symbolMap.put("Y", 1);
961        symbolMap.put("Z", 2);
962        symbolMap.put("W", 3);
963
964        symbolMap.put("R", 0);
965        symbolMap.put("G", 1);
966        symbolMap.put("B", 2);
967        symbolMap.put("A", 3);
968
969        symbolMap.put("S", 0);
970        symbolMap.put("T", 1);
971        symbolMap.put("P", 2);
972        symbolMap.put("Q", 3);
973
974        symbolMap.put("U", 0);
975        symbolMap.put("V", 1);
976        symbolMap.put("P", 2);
977        symbolMap.put("Q", 3);
978    }
979
980    /**
981     * Break up a target uri string into id, sids and accessors
982     *
983     * @param targetString
984     *            the target string
985     * @return the target
986     */
987    private Target processTargetString(final String targetString) {
988        final Target target = new Target();
989
990        int accessorIndex = targetString.indexOf(".");
991        if (accessorIndex == -1) {
992            accessorIndex = targetString.indexOf("(");
993        }
994        final boolean hasAccessor = accessorIndex != -1;
995        if (accessorIndex == -1) {
996            accessorIndex = targetString.length();
997        }
998
999        final String baseString = targetString.substring(0, accessorIndex);
1000
1001        int sidIndex = baseString.indexOf("/");
1002        final boolean hasSid = sidIndex != -1;
1003        if (!hasSid) {
1004            sidIndex = baseString.length();
1005        }
1006
1007        final String id = baseString.substring(0, sidIndex);
1008        target.id = id;
1009
1010        if (hasSid) {
1011            final String sidGroup = baseString.substring(sidIndex + 1, baseString.length());
1012
1013            final StringTokenizer tokenizer = new StringTokenizer(sidGroup, "/");
1014            while (tokenizer.hasMoreTokens()) {
1015                final String sid = tokenizer.nextToken();
1016                target.sids.add(sid);
1017            }
1018        }
1019
1020        if (hasAccessor) {
1021            String accessorString = targetString.substring(accessorIndex, targetString.length());
1022            accessorString = accessorString.replace(".", "");
1023
1024            if (accessorString.length() > 0 && accessorString.charAt(0) == '(') {
1025                int endPara = accessorString.indexOf(")");
1026                final String indexXString = accessorString.substring(1, endPara);
1027                target.accessorIndexX = Integer.parseInt(indexXString);
1028                if (endPara < accessorString.length() - 1) {
1029                    final String lastAccessorString = accessorString.substring(endPara + 1, accessorString.length());
1030                    endPara = lastAccessorString.indexOf(")");
1031                    final String indexYString = lastAccessorString.substring(1, endPara);
1032                    target.accessorIndexY = Integer.parseInt(indexYString);
1033                    target.accessorType = AccessorType.Matrix;
1034                } else {
1035                    target.accessorType = AccessorType.Vector;
1036                }
1037            } else {
1038                target.accessorIndexX = symbolMap.get(accessorString);
1039                target.accessorType = AccessorType.Vector;
1040            }
1041        }
1042
1043        return target;
1044    }
1045
1046    /**
1047     * Convert a list of collada elements into a list of TransformElements
1048     *
1049     * @param transforms
1050     *            the element transforms
1051     * @return the list of TransformElements
1052     */
1053    private List<TransformElement> getNodeTransformList(final List<Element> transforms) {
1054        final List<TransformElement> transformList = new ArrayList<>();
1055
1056        for (final Element transform : transforms) {
1057            final double[] array = _colladaDOMUtil.parseDoubleArray(transform);
1058
1059            if ("translate".equals(transform.getName())) {
1060                transformList.add(new TransformElement(array, TransformElementType.Translation));
1061            } else if ("rotate".equals(transform.getName())) {
1062                transformList.add(new TransformElement(array, TransformElementType.Rotation));
1063            } else if ("scale".equals(transform.getName())) {
1064                transformList.add(new TransformElement(array, TransformElementType.Scale));
1065            } else if ("matrix".equals(transform.getName())) {
1066                transformList.add(new TransformElement(array, TransformElementType.Matrix));
1067            } else if ("lookat".equals(transform.getName())) {
1068                transformList.add(new TransformElement(array, TransformElementType.Lookat));
1069            } else {
1070                if (logger.isLoggable(Level.WARNING)) {
1071                    logger.warning("transform not currently supported: " + transform.getClass().getCanonicalName());
1072                }
1073            }
1074        }
1075
1076        return transformList;
1077    }
1078
1079    /**
1080     * Bake a list of TransformElements into an Ardor3D Transform object.
1081     *
1082     * @param transforms
1083     *            the list of TransformElements
1084     * @return an Ardor3D Transform object
1085     */
1086    private Transform bakeTransforms(final List<TransformElement> transforms) {
1087        final Matrix4 workingMat = Matrix4.fetchTempInstance();
1088        final Matrix4 finalMat = Matrix4.fetchTempInstance();
1089        finalMat.setIdentity();
1090        for (final TransformElement transform : transforms) {
1091            final double[] array = transform.getArray();
1092            final TransformElementType type = transform.getType();
1093            if (type == TransformElementType.Translation) {
1094                workingMat.setIdentity();
1095                workingMat.setColumn(3, new Vector4(array[0], array[1], array[2], 1.0));
1096                finalMat.multiplyLocal(workingMat);
1097            } else if (type == TransformElementType.Rotation) {
1098                if (array[3] != 0) {
1099                    workingMat.setIdentity();
1100                    final Matrix3 rotate = new Matrix3().fromAngleAxis(array[3] * MathUtils.DEG_TO_RAD,
1101                            new Vector3(array[0], array[1], array[2]));
1102                    workingMat.set(rotate);
1103                    finalMat.multiplyLocal(workingMat);
1104                }
1105            } else if (type == TransformElementType.Scale) {
1106                workingMat.setIdentity();
1107                workingMat.scale(new Vector4(array[0], array[1], array[2], 1), workingMat);
1108                finalMat.multiplyLocal(workingMat);
1109            } else if (type == TransformElementType.Matrix) {
1110                workingMat.fromArray(array);
1111                finalMat.multiplyLocal(workingMat);
1112            } else if (type == TransformElementType.Lookat) {
1113                final Vector3 pos = new Vector3(array[0], array[1], array[2]);
1114                final Vector3 target = new Vector3(array[3], array[4], array[5]);
1115                final Vector3 up = new Vector3(array[6], array[7], array[8]);
1116                final Matrix3 rot = new Matrix3();
1117                rot.lookAt(target.subtractLocal(pos), up);
1118                workingMat.set(rot);
1119                workingMat.setColumn(3, new Vector4(array[0], array[1], array[2], 1.0));
1120                finalMat.multiplyLocal(workingMat);
1121            } else {
1122                if (logger.isLoggable(Level.WARNING)) {
1123                    logger.warning("transform not currently supported: " + transform.getClass().getCanonicalName());
1124                }
1125            }
1126        }
1127        return new Transform().fromHomogeneousMatrix(finalMat);
1128    }
1129
1130    /**
1131     * Util for making a readable string out of a xml element hierarchy
1132     *
1133     * @param e
1134     *            the element
1135     * @param maxDepth
1136     *            the maximum depth
1137     * @return the element string
1138     */
1139    public static String getElementString(final Element e, final int maxDepth) {
1140        return getElementString(e, maxDepth, true);
1141    }
1142
1143    public static String getElementString(final Element e, final int maxDepth, final boolean showDots) {
1144        final StringBuilder str = new StringBuilder();
1145        getElementString(e, str, 0, maxDepth, showDots);
1146        return str.toString();
1147    }
1148
1149    private static void getElementString(final Element e, final StringBuilder str, final int depth, final int maxDepth,
1150            final boolean showDots) {
1151        addSpacing(str, depth);
1152        str.append('<');
1153        str.append(e.getName());
1154        str.append(' ');
1155        final List<Attribute> attrs = e.getAttributes();
1156        for (int i = 0; i < attrs.size(); i++) {
1157            final Attribute attr = attrs.get(i);
1158            str.append(attr.getName());
1159            str.append("=\"");
1160            str.append(attr.getValue());
1161            str.append('"');
1162            if (i < attrs.size() - 1) {
1163                str.append(' ');
1164            }
1165        }
1166        if (!e.getChildren().isEmpty() || !"".equals(e.getText())) {
1167            str.append('>');
1168            if (depth < maxDepth) {
1169                str.append('\n');
1170                for (final Element child : e.getChildren()) {
1171                    getElementString(child, str, depth + 1, maxDepth, showDots);
1172                }
1173                if (!"".equals(e.getText())) {
1174                    addSpacing(str, depth + 1);
1175                    str.append(e.getText());
1176                    str.append('\n');
1177                }
1178            } else if (showDots) {
1179                str.append('\n');
1180                addSpacing(str, depth + 1);
1181                str.append("...");
1182                str.append('\n');
1183            }
1184            addSpacing(str, depth);
1185            str.append("</");
1186            str.append(e.getName());
1187            str.append('>');
1188        } else {
1189            str.append("/>");
1190        }
1191        str.append('\n');
1192    }
1193
1194    private static void addSpacing(final StringBuilder str, final int depth) {
1195        for (int i = 0; i < depth; i++) {
1196            str.append("  ");
1197        }
1198    }
1199
1200    private enum AccessorType {
1201        None, Vector, Matrix
1202    }
1203
1204    private static class Target {
1205        public String id;
1206        public List<String> sids = new ArrayList<>();
1207        public AccessorType accessorType = AccessorType.None;
1208        public int accessorIndexX = -1, accessorIndexY = -1;
1209
1210        @Override
1211        public String toString() {
1212            if (accessorType == AccessorType.None) {
1213                return "Target [accessorType=" + accessorType + ", id=" + id + ", sids=" + sids + "]";
1214            }
1215            return "Target [accessorType=" + accessorType + ", accessorIndexX=" + accessorIndexX + ", accessorIndexY="
1216                    + accessorIndexY + ", id=" + id + ", sids=" + sids + "]";
1217        }
1218    }
1219}