Java tutorial
/** * 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 + "]"; } } }