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}