diff options
author | neothemachine <[email protected]> | 2012-12-05 19:19:31 +0100 |
---|---|---|
committer | neothemachine <[email protected]> | 2012-12-05 19:19:31 +0100 |
commit | 7ca002eb1d1510767cd524a800710cf3898ba699 (patch) | |
tree | 7b3bd1dec9f58e32d083b287f44569526761bb32 /ardor3d-collada/src/main/java/com | |
parent | 105f890b5e750aaa5ec63e768e77eca2af8631a4 (diff) | |
parent | 9dd02f103042cb8a196f8a3ed2278da443e345bf (diff) |
Merge branch 'kill_trunk' into dependencies
Diffstat (limited to 'ardor3d-collada/src/main/java/com')
22 files changed, 5137 insertions, 0 deletions
diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaAnimUtils.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaAnimUtils.java new file mode 100644 index 0000000..379651e --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaAnimUtils.java @@ -0,0 +1,1174 @@ +/** + * 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.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); + + // 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); + } + } + + /** + * 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.startsWith("(")) { + 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 : 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 + "]"; + } + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaDOMUtil.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaDOMUtil.java new file mode 100644 index 0000000..2f67140 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaDOMUtil.java @@ -0,0 +1,378 @@ +/** + * 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.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jdom2.Attribute; +import org.jdom2.CDATA; +import org.jdom2.Comment; +import org.jdom2.DataConversionException; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.ProcessingInstruction; +import org.jdom2.Text; +import org.jdom2.xpath.XPath; + +import com.ardor3d.extension.model.collada.jdom.data.DataCache; +import com.ardor3d.math.ColorRGBA; + +/** + * Utility methods for parsing Collada data related to node hierarchy and arrays, using XPath or arrays extracted during + * sax parsing. + */ +public class ColladaDOMUtil { + private final Logger logger = Logger.getLogger(ColladaDOMUtil.class.getName()); + + private final DataCache _dataCache; + + public ColladaDOMUtil(final DataCache dataCache) { + _dataCache = dataCache; + } + + /** + * Find element with specific id + * + * @param baseUrl + * url specifying target id + * @return element with specific id or null if not found + */ + public Element findTargetWithId(final String baseUrl) { + return _dataCache.getIdCache().get(parseUrl(baseUrl)); + } + + /** + * Find element with specific sid + * + * @param baseUrl + * url specifying target sid + * @return element with specific id or null if not found + */ + public Element findTargetWithSid(final String baseUrl) { + return _dataCache.getSidCache().get(parseUrl(baseUrl)); + } + + /** + * Select nodes through an XPath query and return all hits as a List + * + * @param element + * root element to start search on + * @param query + * XPath expression + * @return the list of selected items, which may be of types: {@link Element}, {@link Attribute}, {@link Text}, + * {@link CDATA}, {@link Comment}, {@link ProcessingInstruction}, Boolean, Double, or String. + */ + public List<?> selectNodes(final Element element, final String query) { + final XPath xPathExpression = getXPathExpression(query); + + try { + return xPathExpression.selectNodes(element); + } catch (final JDOMException e) { + e.printStackTrace(); + } + return Collections.emptyList(); + } + + /** + * Select nodes through an XPath query and returns the first hit + * + * @param element + * root element to start search on + * @param query + * XPath expression + * @return the first selected item, which may be of types: {@link Element}, {@link Attribute}, {@link Text}, + * {@link CDATA}, {@link Comment}, {@link ProcessingInstruction}, Boolean, Double, String, or + * <code>null</code> if no item was selected. + */ + public Object selectSingleNode(final Element element, final String query) { + final XPath xPathExpression = getXPathExpression(query); + + try { + return xPathExpression.selectSingleNode(element); + } catch (final JDOMException e) { + e.printStackTrace(); + } + return null; + } + + private String parseUrl(String baseUrl) { + if (baseUrl.startsWith("#")) { + baseUrl = baseUrl.substring(1); + } + return baseUrl; + } + + /** + * Compiles and return an XPath expression. Expressions are cached. + * + * @param query + * XPath query to compile + * @return new XPath expression object + */ + private XPath getXPathExpression(final String query) { + + if (_dataCache.getxPathExpressions().containsKey(query)) { + return _dataCache.getxPathExpressions().get(query); + } + + XPath xPathExpression = null; + try { + xPathExpression = XPath.newInstance(query); + } catch (final JDOMException e) { + e.printStackTrace(); + } + + _dataCache.getxPathExpressions().put(query, xPathExpression); + + return xPathExpression; + } + + /** + * Parses the text under a node and returns it as a float array. + * + * @param node + * node to parse content from + * @return parsed float array + */ + public float[] parseFloatArray(final Element node) { + if (_dataCache.getFloatArrays().containsKey(node)) { + return _dataCache.getFloatArrays().get(node); + } + + final String content = node.getText(); + + final List<String> list = new ArrayList<String>(); + final StringTokenizer tokenizer = new StringTokenizer(content, " "); + while (tokenizer.hasMoreTokens()) { + list.add(tokenizer.nextToken()); + } + final int listSize = list.size(); + final float[] floatArray = new float[listSize]; + for (int i = 0; i < listSize; i++) { + floatArray[i] = Float.parseFloat(list.get(i).replace(",", ".")); + } + + _dataCache.getFloatArrays().put(node, floatArray); + + return floatArray; + } + + /** + * Parses the text under a node and returns it as a double array. + * + * @param node + * node to parse content from + * @return parsed double array + */ + public double[] parseDoubleArray(final Element node) { + if (_dataCache.getDoubleArrays().containsKey(node)) { + return _dataCache.getDoubleArrays().get(node); + } + + final String content = node.getText(); + + final List<String> list = new ArrayList<String>(); + final StringTokenizer tokenizer = new StringTokenizer(content, " "); + while (tokenizer.hasMoreTokens()) { + list.add(tokenizer.nextToken()); + } + final int listSize = list.size(); + final double[] doubleArray = new double[listSize]; + for (int i = 0; i < listSize; i++) { + doubleArray[i] = Double.parseDouble(list.get(i).replace(",", ".")); + } + + _dataCache.getDoubleArrays().put(node, doubleArray); + + return doubleArray; + } + + /** + * Parses the text under a node and returns it as an int array. + * + * @param node + * node to parse content from + * @return parsed int array + */ + public int[] parseIntArray(final Element node) { + if (_dataCache.getIntArrays().containsKey(node)) { + return _dataCache.getIntArrays().get(node); + } + + final String content = node.getText(); + + final List<String> list = new ArrayList<String>(); + final StringTokenizer tokenizer = new StringTokenizer(content, " "); + while (tokenizer.hasMoreTokens()) { + list.add(tokenizer.nextToken()); + } + final int listSize = list.size(); + final int[] intArray = new int[listSize]; + for (int i = 0; i < listSize; i++) { + intArray[i] = Integer.parseInt(list.get(i)); + } + + _dataCache.getIntArrays().put(node, intArray); + + return intArray; + } + + /** + * Parses the text under a node and returns it as a boolean array. + * + * @param node + * node to parse content from + * @return parsed boolean array + */ + public boolean[] parseBooleanArray(final Element node) { + if (_dataCache.getDoubleArrays().containsKey(node)) { + return _dataCache.getBooleanArrays().get(node); + } + + final String content = node.getText(); + + final List<String> list = new ArrayList<String>(); + final StringTokenizer tokenizer = new StringTokenizer(content, " "); + while (tokenizer.hasMoreTokens()) { + list.add(tokenizer.nextToken()); + } + final int listSize = list.size(); + final boolean[] booleanArray = new boolean[listSize]; + for (int i = 0; i < listSize; i++) { + booleanArray[i] = Boolean.parseBoolean(list.get(i)); + } + + _dataCache.getBooleanArrays().put(node, booleanArray); + + return booleanArray; + } + + /** + * Parses the text under a node and returns it as a string array. + * + * @param node + * node to parse content from + * @return parsed string array + */ + public String[] parseStringArray(final Element node) { + if (_dataCache.getStringArrays().containsKey(node)) { + return _dataCache.getStringArrays().get(node); + } + + final String content = node.getText(); + + final List<String> list = new ArrayList<String>(); + final StringTokenizer tokenizer = new StringTokenizer(content, " "); + while (tokenizer.hasMoreTokens()) { + list.add(tokenizer.nextToken()); + } + final String[] stringArray = list.toArray(new String[list.size()]); + + _dataCache.getStringArrays().put(node, stringArray); + + return stringArray; + } + + /** + * Strips the namespace from all nodes in a tree. + * + * @param rootElement + * Root of strip operation + */ + public void stripNamespace(final Element rootElement) { + rootElement.setNamespace(null); + + @SuppressWarnings("unchecked") + final List<Element> children = rootElement.getChildren(); + final Iterator<Element> i = children.iterator(); + while (i.hasNext()) { + final Element child = i.next(); + stripNamespace(child); + } + } + + /** + * Parse an int value in an attribute. + * + * @param input + * Element containing the attribute + * @param attributeName + * Attribute name to parse a value for + * @return parsed integer + */ + public int getAttributeIntValue(final Element input, final String attributeName, final int defaultVal) { + final Attribute attribute = input.getAttribute(attributeName); + if (attribute != null) { + try { + return attribute.getIntValue(); + } catch (final DataConversionException e) { + logger.log(Level.WARNING, "Could not parse int value", e); + } + } + return defaultVal; + } + + /** + * Convert a Collada color description into an Ardor3D ColorRGBA + * + * @param colorDescription + * Collada color description + * @return Ardor3d ColorRGBA + */ + public ColorRGBA getColor(final String colorDescription) { + if (colorDescription == null) { + throw new ColladaException("Null color description not allowed", null); + } + + final String[] values = _dataCache.getPattern().split(colorDescription.replace(",", ".")); + + if (values.length < 3 || values.length > 4) { + throw new ColladaException("Expected color definition of length 3 or 4 - got " + values.length + + " for description: " + colorDescription, colorDescription); + } + + try { + return new ColorRGBA(Float.parseFloat(values[0]), Float.parseFloat(values[1]), Float.parseFloat(values[2]), + values.length == 4 ? Float.parseFloat(values[3]) : 1.0f); + } catch (final NumberFormatException e) { + throw new ColladaException("Unable to parse float number", colorDescription, e); + } + } + + /** + * Find Element with semantic POSITION under an element with inputs + * + * @param v + * @return + */ + @SuppressWarnings("unchecked") + public Element getPositionSource(final Element v) { + for (final Element input : v.getChildren("input")) { + if ("POSITION".equals(input.getAttributeValue("semantic"))) { + final Element n = findTargetWithId(input.getAttributeValue("source")); + if (n != null && "source".equals(n.getName())) { + return n; + } + } + } + + // changed this to throw an exception instead - otherwise, there will just be a nullpointer exception + // outside. This provides much more information about what went wrong / Petter + // return null; + throw new ColladaException("Unable to find POSITION semantic for inputs under DaeVertices", v); + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaException.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaException.java new file mode 100644 index 0000000..b4d4927 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaException.java @@ -0,0 +1,31 @@ +/**
+ * 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;
+
+/**
+ * Customer exception thrown when something unexpected is encountered in a Collada file.
+ */
+public class ColladaException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public ColladaException(final String message, final Object source) {
+ super(ColladaException.createMessage(message, source));
+ }
+
+ public ColladaException(final String msg, final Object source, final Throwable cause) {
+ super(ColladaException.createMessage(msg, source), cause);
+ }
+
+ private static String createMessage(final String message, final Object source) {
+ return "Collada problem for source: " + (source != null ? source.toString() : "null") + ": " + message;
+ }
+}
diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaImporter.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaImporter.java new file mode 100644 index 0000000..89906c1 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaImporter.java @@ -0,0 +1,500 @@ +/** + * 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.IOException; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.logging.Logger; + +import org.jdom2.Attribute; +import org.jdom2.DataConversionException; +import org.jdom2.DefaultJDOMFactory; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMFactory; +import org.jdom2.Namespace; +import org.jdom2.Text; +import org.jdom2.input.SAXBuilder; +import org.jdom2.input.sax.SAXHandler; +import org.jdom2.input.sax.SAXHandlerFactory; +import org.xml.sax.SAXException; + +import com.ardor3d.extension.animation.skeletal.Joint; +import com.ardor3d.extension.model.collada.jdom.data.AssetData; +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.plugin.ColladaExtraPlugin; +import com.ardor3d.scenegraph.MeshData; +import com.ardor3d.scenegraph.Node; +import com.ardor3d.util.geom.GeometryTool.MatchCondition; +import com.ardor3d.util.resource.RelativeResourceLocator; +import com.ardor3d.util.resource.ResourceLocator; +import com.ardor3d.util.resource.ResourceLocatorTool; +import com.ardor3d.util.resource.ResourceSource; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; + +/** + * Main class for importing Collada files. + * <p> + * Example usages: + * <li>new ColladaImporter().load(resource);</li> + * <li>new ColladaImporter().loadTextures(false).modelLocator(locator).load(resource);</li> + * </p> + */ +public class ColladaImporter { + private boolean _loadTextures = true; + private boolean _flipTransparency = false; + private boolean _loadAnimations = true; + private ResourceLocator _textureLocator; + private ResourceLocator _modelLocator; + private boolean _compressTextures = false; + private boolean _optimizeMeshes = false; + private final EnumSet<MatchCondition> _optimizeSettings = EnumSet.of(MatchCondition.UVs, MatchCondition.Normal, + MatchCondition.Color); + private Map<String, Joint> _externalJointMapping; + private final List<ColladaExtraPlugin> _extraPlugins = Lists.newArrayList(); + + public boolean isLoadTextures() { + return _loadTextures; + } + + public ColladaImporter setLoadTextures(final boolean loadTextures) { + _loadTextures = loadTextures; + return this; + } + + public boolean isCompressTextures() { + return _compressTextures; + } + + public ColladaImporter setCompressTextures(final boolean compressTextures) { + _compressTextures = compressTextures; + return this; + } + + public boolean isLoadAnimations() { + return _loadAnimations; + } + + public ColladaImporter setLoadAnimations(final boolean loadAnimations) { + _loadAnimations = loadAnimations; + return this; + } + + public boolean isFlipTransparency() { + return _flipTransparency; + } + + public void addExtraPlugin(final ColladaExtraPlugin plugin) { + _extraPlugins.add(plugin); + } + + public void clearExtraPlugins() { + _extraPlugins.clear(); + } + + /** + * @param flipTransparency + * if true, invert the value of any "transparency" entries found - required for some exporters. + * @return this importer, for chaining + */ + public ColladaImporter setFlipTransparency(final boolean flipTransparency) { + _flipTransparency = flipTransparency; + return this; + } + + public ResourceLocator getTextureLocator() { + return _textureLocator; + } + + public ColladaImporter setTextureLocator(final ResourceLocator textureLocator) { + _textureLocator = textureLocator; + return this; + } + + public ColladaImporter setExternalJoints(final Map<String, Joint> map) { + _externalJointMapping = map; + return this; + } + + public Map<String, Joint> getExternalJoints() { + return _externalJointMapping; + } + + public ResourceLocator getModelLocator() { + return _modelLocator; + } + + public ColladaImporter setModelLocator(final ResourceLocator modelLocator) { + _modelLocator = modelLocator; + return this; + } + + public boolean isOptimizeMeshes() { + return _optimizeMeshes; + } + + public void setOptimizeMeshes(final boolean optimizeMeshes) { + _optimizeMeshes = optimizeMeshes; + } + + public Set<MatchCondition> getOptimizeSettings() { + return ImmutableSet.copyOf(_optimizeSettings); + } + + public void setOptimizeSettings(final MatchCondition... optimizeSettings) { + _optimizeSettings.clear(); + for (final MatchCondition cond : optimizeSettings) { + _optimizeSettings.add(cond); + } + } + + /** + * Reads a Collada file from the given resource and returns it as a ColladaStorage object. + * + * @param resource + * the name of the resource to find. ResourceLocatorTool will be used with TYPE_MODEL to find the + * resource. + * @return a ColladaStorage data object containing the Collada scene and other useful elements. + * @throws IOException + * if the resource can not be located or loaded for some reason. + */ + public ColladaStorage load(final String resource) throws IOException { + final ResourceSource source; + if (_modelLocator == null) { + source = ResourceLocatorTool.locateResource(ResourceLocatorTool.TYPE_MODEL, resource); + } else { + source = _modelLocator.locateResource(resource); + } + + if (source == null) { + throw new IOException("Unable to locate '" + resource + "'"); + } + + return load(source); + } + + /** + * Reads a Collada file from the given resource and returns it as a ColladaStorage object. + * + * @param resource + * the name of the resource to find. + * @return a ColladaStorage data object containing the Collada scene and other useful elements. + * @throws IOException + * if the resource can not be loaded for some reason. + */ + public ColladaStorage load(final ResourceSource resource) throws IOException { + final ColladaStorage colladaStorage = new ColladaStorage(); + final DataCache dataCache = new DataCache(); + if (_externalJointMapping != null) { + dataCache.getExternalJointMapping().putAll(_externalJointMapping); + } + final ColladaDOMUtil colladaDOMUtil = new ColladaDOMUtil(dataCache); + final ColladaMaterialUtils colladaMaterialUtils = new ColladaMaterialUtils(this, dataCache, colladaDOMUtil); + final ColladaMeshUtils colladaMeshUtils = new ColladaMeshUtils(dataCache, colladaDOMUtil, colladaMaterialUtils, + _optimizeMeshes, _optimizeSettings); + final ColladaAnimUtils colladaAnimUtils = new ColladaAnimUtils(colladaStorage, dataCache, colladaDOMUtil, + colladaMeshUtils); + final ColladaNodeUtils colladaNodeUtils = new ColladaNodeUtils(dataCache, colladaDOMUtil, colladaMaterialUtils, + colladaMeshUtils, colladaAnimUtils); + + try { + // Pull in the DOM tree of the Collada resource. + final Element collada = readCollada(resource, dataCache); + + // if we don't specify a texture locator, add a temporary texture locator at the location of this model + // resource.. + final boolean addLocator = _textureLocator == null; + + final RelativeResourceLocator loc; + if (addLocator) { + loc = new RelativeResourceLocator(resource); + ResourceLocatorTool.addResourceLocator(ResourceLocatorTool.TYPE_TEXTURE, loc); + } else { + loc = null; + } + + final AssetData assetData = colladaNodeUtils.parseAsset(collada.getChild("asset")); + + // Collada may or may not have a scene, so this can return null. + final Node scene = colladaNodeUtils.getVisualScene(collada); + + if (_loadAnimations) { + colladaAnimUtils.parseLibraryAnimations(collada); + } + + // set our scene into storage + colladaStorage.setScene(scene); + + // set our asset data into storage + colladaStorage.setAssetData(assetData); + + // drop our added locator if needed. + if (addLocator) { + ResourceLocatorTool.removeResourceLocator(ResourceLocatorTool.TYPE_TEXTURE, loc); + } + + // copy across our mesh colors - only for objects with multiple channels + final Multimap<MeshData, FloatBuffer> colors = ArrayListMultimap.create(); + final Multimap<MeshData, FloatBuffer> temp = dataCache.getParsedVertexColors(); + for (final MeshData key : temp.keySet()) { + // only copy multiple channels since their data is lost + final Collection<FloatBuffer> val = temp.get(key); + if (val != null && val.size() > 1) { + colors.putAll(key, val); + } + } + colladaStorage.setParsedVertexColors(colors); + + // copy across our mesh material info + colladaStorage.setMeshMaterialInfo(dataCache.getMeshMaterialMap()); + colladaStorage.setMaterialMap(dataCache.getMaterialInfoMap()); + + // return storage + return colladaStorage; + } catch (final Exception e) { + throw new IOException("Unable to load collada resource from URL: " + resource, e); + } + } + + /** + * Reads the whole Collada DOM tree from the given resource and returns its root element. Exceptions may be thrown + * by underlying tools; these will be wrapped in a RuntimeException and rethrown. + * + * @param resource + * the ResourceSource to read the resource from + * @return the Collada root element + */ + private Element readCollada(final ResourceSource resource, final DataCache dataCache) { + try { + final JDOMFactory jdomFac = new ArdorFactory(dataCache); + final SAXBuilder builder = new SAXBuilder(null, new SAXHandlerFactory() { + @Override + public SAXHandler createSAXHandler(final JDOMFactory factory) { + return new SAXHandler(jdomFac) { + @Override + public void startPrefixMapping(final String prefix, final String uri) throws SAXException { + // Just kill what's usually done here... + } + }; + } + }, jdomFac); + + final Document doc = builder.build(resource.openStream()); + final Element collada = doc.getRootElement(); + + // ColladaDOMUtil.stripNamespace(collada); + + return collada; + } catch (final Exception e) { + throw new RuntimeException("Unable to load collada resource from source: " + resource, e); + } + } + + private enum BufferType { + None, Float, Double, Int, String, P + } + + /** + * A JDOMFactory that normalizes all text (strips extra whitespace etc), preparses all arrays and hashes all + * elements based on their id/sid. + */ + private static final class ArdorFactory extends DefaultJDOMFactory { + private final Logger logger = Logger.getLogger(ArdorFactory.class.getName()); + + private final DataCache dataCache; + private Element currentElement; + private BufferType bufferType = BufferType.None; + private int count = 0; + private final List<String> list = new ArrayList<String>(); + + ArdorFactory(final DataCache dataCache) { + this.dataCache = dataCache; + } + + @Override + public Text text(final int line, final int col, final String text) { + try { + switch (bufferType) { + case Float: { + final String normalizedText = Text.normalizeString(text); + if (normalizedText.length() == 0) { + return new Text(""); + } + final StringTokenizer tokenizer = new StringTokenizer(normalizedText, " "); + final float[] floatArray = new float[count]; + for (int i = 0; i < count; i++) { + floatArray[i] = parseFloat(tokenizer.nextToken()); + } + + dataCache.getFloatArrays().put(currentElement, floatArray); + + return new Text(""); + } + case Double: { + final String normalizedText = Text.normalizeString(text); + if (normalizedText.length() == 0) { + return new Text(""); + } + final StringTokenizer tokenizer = new StringTokenizer(normalizedText, " "); + final double[] doubleArray = new double[count]; + for (int i = 0; i < count; i++) { + doubleArray[i] = Double.parseDouble(tokenizer.nextToken().replace(",", ".")); + } + + dataCache.getDoubleArrays().put(currentElement, doubleArray); + + return new Text(""); + } + case Int: { + final String normalizedText = Text.normalizeString(text); + if (normalizedText.length() == 0) { + return new Text(""); + } + final StringTokenizer tokenizer = new StringTokenizer(normalizedText, " "); + final int[] intArray = new int[count]; + int i = 0; + while (tokenizer.hasMoreTokens()) { + intArray[i++] = Integer.parseInt(tokenizer.nextToken()); + } + + dataCache.getIntArrays().put(currentElement, intArray); + + return new Text(""); + } + case P: { + list.clear(); + final String normalizedText = Text.normalizeString(text); + if (normalizedText.length() == 0) { + return new Text(""); + } + final StringTokenizer tokenizer = new StringTokenizer(normalizedText, " "); + while (tokenizer.hasMoreTokens()) { + list.add(tokenizer.nextToken()); + } + final int listSize = list.size(); + final int[] intArray = new int[listSize]; + for (int i = 0; i < listSize; i++) { + intArray[i] = Integer.parseInt(list.get(i)); + } + + dataCache.getIntArrays().put(currentElement, intArray); + + return new Text(""); + } + } + } catch (final NoSuchElementException e) { + throw new ColladaException("Number of values in collada array does not match its count attribute: " + + count, e); + } + return new Text(Text.normalizeString(text)); + } + + @Override + public void setAttribute(final Element parent, final Attribute a) { + if ("id".equals(a.getName())) { + if (dataCache.getIdCache().containsKey(a.getValue())) { + logger.warning("id already exists in id cache: " + a.getValue()); + } + dataCache.getIdCache().put(a.getValue(), parent); + } else if ("sid".equals(a.getName())) { + dataCache.getSidCache().put(a.getValue(), parent); + } else if ("count".equals(a.getName())) { + try { + count = a.getIntValue(); + } catch (final DataConversionException e) { + e.printStackTrace(); + } + } + + super.setAttribute(parent, a); + } + + @Override + public Element element(final int line, final int col, final String name, final Namespace namespace) { + currentElement = super.element(line, col, name); + handleTypes(name); + return currentElement; + } + + @Override + public Element element(final int line, final int col, final String name, final String prefix, final String uri) { + currentElement = super.element(line, col, name); + handleTypes(name); + return currentElement; + } + + @Override + public Element element(final int line, final int col, final String name, final String uri) { + currentElement = super.element(line, col, name); + handleTypes(name); + return currentElement; + } + + @Override + public Element element(final int line, final int col, final String name) { + currentElement = super.element(line, col, name); + handleTypes(name); + return currentElement; + } + + private void handleTypes(final String name) { + if ("float_array".equals(name)) { + bufferType = BufferType.Float; + } else if ("double_array".equals(name)) { + bufferType = BufferType.Double; + } else if ("int_array".equals(name)) { + bufferType = BufferType.Int; + } else if ("p".equals(name)) { + bufferType = BufferType.P; + } else { + bufferType = BufferType.None; + } + } + } + + /** + * Parse a numeric value. Commas are replaced by dot automaticly. Also handle special values : INF, -INF, NaN + * + * @param number + * string + * @return float + */ + public static float parseFloat(String candidate) { + candidate = candidate.replace(',', '.'); + if (candidate.contains("-INF")) { + return Float.NEGATIVE_INFINITY; + } else if (candidate.contains("INF")) { + return Float.POSITIVE_INFINITY; + } + return Float.parseFloat(candidate); + } + + public boolean readExtra(final Element extra, final Object... params) { + for (final ColladaExtraPlugin plugin : _extraPlugins) { + if (plugin.processExtra(extra, params)) { + return true; + } + } + return false; + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaInputPipe.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaInputPipe.java new file mode 100644 index 0000000..3f3a87e --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaInputPipe.java @@ -0,0 +1,250 @@ +/** + * 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.nio.FloatBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jdom2.Element; + +import com.ardor3d.extension.model.collada.jdom.data.DataCache; +import com.ardor3d.scenegraph.FloatBufferData; +import com.ardor3d.scenegraph.MeshData; +import com.ardor3d.util.geom.BufferUtils; + +/** + * The purpose of this class is to tie a <source> and accessor together to pull out data. + */ +public class ColladaInputPipe { + private static final Logger logger = Logger.getLogger(ColladaInputPipe.class.getName()); + + private final int _offset; + private final int _set; + private final Element _source; + private int _paramCount; + private SourceData _sourceData = null; + private Type _type; + private FloatBuffer _buffer; + private int _texCoord = 0; + + public enum Type { + VERTEX, POSITION, NORMAL, TEXCOORD, COLOR, JOINT, WEIGHT, TEXTANGENT, TEXBINORMAL, // + INV_BIND_MATRIX, INPUT, IN_TANGENT, OUT_TANGENT, OUTPUT, INTERPOLATION, UNKNOWN + } + + static class SourceData { + int count; + int stride; + int offset; + + ParamType paramType; + float[] floatArray; + boolean[] boolArray; + int[] intArray; + String[] stringArray; + + @Override + public String toString() { + switch (paramType) { + case bool_param: + return "SourceData [boolArray=" + Arrays.toString(boolArray) + "]"; + case float_param: + return "SourceData [floatArray=" + Arrays.toString(floatArray) + "]"; + case idref_param: + return "SourceData [idrefArray=" + Arrays.toString(stringArray) + "]"; + case int_param: + return "SourceData [intArray=" + Arrays.toString(intArray) + "]"; + case name_param: + return "SourceData [nameArray=" + Arrays.toString(stringArray) + "]"; + default: + return "Unknown paramType"; + } + } + } + + public enum ParamType { + float_param, bool_param, int_param, name_param, idref_param + } + + @SuppressWarnings("unchecked") + public ColladaInputPipe(final ColladaDOMUtil colladaDOMUtil, final Element input) { + // Setup our type + try { + _type = Type.valueOf(input.getAttributeValue("semantic")); + } catch (final Exception ex) { + ColladaInputPipe.logger.warning("Unknown input type: " + input.getAttributeValue("semantic")); + _type = Type.UNKNOWN; + } + + // Locate our source + final Element n = colladaDOMUtil.findTargetWithId(input.getAttributeValue("source")); + if (n == null) { + throw new ColladaException("Input source not found: " + input.getAttributeValue("source"), input); + } + + if ("source".equals(n.getName())) { + _source = n; + } else if ("vertices".equals(n.getName())) { + _source = colladaDOMUtil.getPositionSource(n); + } else { + throw new ColladaException("Input source not found: " + input.getAttributeValue("source"), input); + } + + // TODO: Need to go through the params and see if they have a name set, and skip values if not when + // parsing the array? + + _sourceData = new SourceData(); + if (_source.getChild("float_array") != null) { + _sourceData.floatArray = colladaDOMUtil.parseFloatArray(_source.getChild("float_array")); + _sourceData.paramType = ParamType.float_param; + } else if (_source.getChild("bool_array") != null) { + _sourceData.boolArray = colladaDOMUtil.parseBooleanArray(_source.getChild("bool_array")); + _sourceData.paramType = ParamType.bool_param; + } else if (_source.getChild("int_array") != null) { + _sourceData.intArray = colladaDOMUtil.parseIntArray(_source.getChild("int_array")); + _sourceData.paramType = ParamType.int_param; + } else if (_source.getChild("Name_array") != null) { + _sourceData.stringArray = colladaDOMUtil.parseStringArray(_source.getChild("Name_array")); + _sourceData.paramType = ParamType.name_param; + } else if (_source.getChild("IDREF_array") != null) { + _sourceData.stringArray = colladaDOMUtil.parseStringArray(_source.getChild("IDREF_array")); + _sourceData.paramType = ParamType.idref_param; + } + + // add a hook to our params from the technique_common + final Element accessor = getCommonAccessor(_source); + if (accessor != null) { + if (ColladaInputPipe.logger.isLoggable(Level.FINE)) { + ColladaInputPipe.logger.fine("Creating buffers for: " + _source.getAttributeValue("id")); + } + + final List<Element> params = accessor.getChildren("param"); + _paramCount = params.size(); + + // Might use this info for real later, but use for testing for unsupported param skipping. + boolean skippedParam = false; + for (final Element param : params) { + final String paramName = param.getAttributeValue("name"); + if (paramName == null) { + skippedParam = true; + break; + } + // String paramType = param.getAttributeValue("type"); + } + if (_paramCount > 1 && skippedParam == true) { + ColladaInputPipe.logger.warning("Parameter skipping not yet supported when parsing sources. " + + _source.getAttributeValue("id")); + } + + _sourceData.count = colladaDOMUtil.getAttributeIntValue(accessor, "count", 0); + _sourceData.stride = colladaDOMUtil.getAttributeIntValue(accessor, "stride", 1); + _sourceData.offset = colladaDOMUtil.getAttributeIntValue(accessor, "offset", 0); + } + + // save our offset + _offset = colladaDOMUtil.getAttributeIntValue(input, "offset", 0); + _set = colladaDOMUtil.getAttributeIntValue(input, "set", 0); + + _texCoord = 0; + } + + public int getOffset() { + return _offset; + } + + public int getSet() { + return _set; + } + + public Type getType() { + return _type; + } + + public SourceData getSourceData() { + return _sourceData; + } + + public void setupBuffer(final int numEntries, final MeshData meshData, final DataCache cache) { + // use our source and the number of params to determine our buffer length + // we'll use the params from the common technique accessor: + final int size = _paramCount * numEntries; + switch (_type) { + case POSITION: + _buffer = BufferUtils.createFloatBuffer(size); + meshData.setVertexCoords(new FloatBufferData(_buffer, _paramCount)); + break; + case NORMAL: + _buffer = BufferUtils.createFloatBuffer(size); + meshData.setNormalCoords(new FloatBufferData(_buffer, _paramCount)); + break; + case TEXCOORD: + _buffer = BufferUtils.createFloatBuffer(size); + meshData.setTextureCoords(new FloatBufferData(_buffer, _paramCount), _texCoord); + break; + case COLOR: + _buffer = BufferUtils.createFloatBuffer(size); + meshData.setColorCoords(new FloatBufferData(_buffer, _paramCount)); + cache.getParsedVertexColors().put(meshData, _buffer); + break; + case TEXTANGENT: + _buffer = BufferUtils.createFloatBuffer(size); + meshData.setTangentCoords(new FloatBufferData(_buffer, _paramCount)); + break; + // case TEXBINORMAL: + // _buffer = BufferUtils.createFloatBuffer(size); + // meshData.setTangentBuffer(_buffer); + // break; + default: + } + } + + void pushValues(final int memberIndex) { + if (_buffer == null) { + return; + } + + if (_sourceData == null) { + throw new ColladaException("No source data found in pipe!", _source); + } + + if (memberIndex >= _sourceData.count) { + ColladaInputPipe.logger.warning("Accessed invalid index " + memberIndex + " on source " + _source + + ". Count: " + _sourceData.count); + return; + } + + int index = memberIndex * _sourceData.stride + _sourceData.offset; + final ParamType paramType = _sourceData.paramType; + for (int i = 0; i < _paramCount; i++) { + if (ParamType.float_param == paramType) { + _buffer.put(_sourceData.floatArray[index]); + } else if (ParamType.int_param == paramType) { + _buffer.put(_sourceData.intArray[index]); + } + index++; + } + } + + public void setTexCoord(final int texCoord) { + _texCoord = texCoord; + } + + private Element getCommonAccessor(final Element source) { + final Element techniqueCommon = source.getChild("technique_common"); + if (techniqueCommon != null) { + return techniqueCommon.getChild("accessor"); + } + return null; + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaMaterialUtils.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaMaterialUtils.java new file mode 100644 index 0000000..e4572ff --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaMaterialUtils.java @@ -0,0 +1,586 @@ +/** + * 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.util.HashMap; +import java.util.List; +import java.util.logging.Logger; + +import org.jdom2.Element; + +import com.ardor3d.extension.model.collada.jdom.data.DataCache; +import com.ardor3d.extension.model.collada.jdom.data.MaterialInfo; +import com.ardor3d.extension.model.collada.jdom.data.SamplerTypes; +import com.ardor3d.image.Texture; +import com.ardor3d.image.Texture.ApplyMode; +import com.ardor3d.image.Texture.CombinerFunctionRGB; +import com.ardor3d.image.Texture.CombinerOperandRGB; +import com.ardor3d.image.Texture.CombinerSource; +import com.ardor3d.image.TextureStoreFormat; +import com.ardor3d.math.ColorRGBA; +import com.ardor3d.math.MathUtils; +import com.ardor3d.renderer.queue.RenderBucketType; +import com.ardor3d.renderer.state.BlendState; +import com.ardor3d.renderer.state.MaterialState; +import com.ardor3d.renderer.state.MaterialState.MaterialFace; +import com.ardor3d.renderer.state.RenderState; +import com.ardor3d.renderer.state.TextureState; +import com.ardor3d.scenegraph.Mesh; +import com.ardor3d.util.TextureManager; +import com.ardor3d.util.resource.ResourceSource; + +/** + * Methods for parsing Collada data related to materials. + */ +public class ColladaMaterialUtils { + private static final Logger logger = Logger.getLogger(ColladaMaterialUtils.class.getName()); + + private final ColladaImporter _importer; + private final DataCache _dataCache; + private final ColladaDOMUtil _colladaDOMUtil; + private final boolean _compressTextures, _loadTextures, _flipTransparency; + + public ColladaMaterialUtils(final ColladaImporter importer, final DataCache dataCache, + final ColladaDOMUtil colladaDOMUtil) { + _importer = importer; + _dataCache = dataCache; + _colladaDOMUtil = colladaDOMUtil; + _compressTextures = _importer.isCompressTextures(); + _loadTextures = _importer.isLoadTextures(); + _flipTransparency = _importer.isFlipTransparency(); + } + + /** + * Find and apply the given material to the given Mesh. + * + * @param materialName + * our material name + * @param mesh + * the mesh to apply material to. + */ + public void applyMaterial(final String materialName, final Mesh mesh) { + if (materialName == null) { + logger.warning("materialName is null"); + return; + } + + Element mat = _dataCache.getBoundMaterial(materialName); + if (mat == null) { + logger.warning("material not bound: " + materialName + ", trying search with id."); + mat = _colladaDOMUtil.findTargetWithId(materialName); + } + if (mat == null || !"material".equals(mat.getName())) { + logger.warning("material not found: " + materialName); + return; + } + + final String originalMaterial = mat.getAttributeValue("id"); + MaterialInfo mInfo = null; + if (!_dataCache.getMaterialInfoMap().containsKey(originalMaterial)) { + mInfo = new MaterialInfo(); + mInfo.setMaterialName(originalMaterial); + _dataCache.getMaterialInfoMap().put(originalMaterial, mInfo); + } + _dataCache.getMeshMaterialMap().put(mesh, originalMaterial); + + final Element child = mat.getChild("instance_effect"); + final Element effectNode = _colladaDOMUtil.findTargetWithId(child.getAttributeValue("url")); + if (effectNode == null) { + logger.warning("material effect not found: " + mat.getChild("instance_material").getAttributeValue("url")); + return; + } + + if ("effect".equals(effectNode.getName())) { + /* + * temp cache for textures, we do not want to add textures twice (for example, transparant map might point + * to diffuse texture) + */ + final HashMap<String, Texture> loadedTextures = new HashMap<String, Texture>(); + final Element effect = effectNode; + // XXX: For now, just grab the common technique: + final Element common = effect.getChild("profile_COMMON"); + if (common != null) { + if (mInfo != null) { + mInfo.setProfile("COMMON"); + } + + final Element commonExtra = common.getChild("extra"); + if (commonExtra != null) { + // process with any plugins + _importer.readExtra(commonExtra, mesh); + } + + final Element technique = common.getChild("technique"); + String type = "blinn"; + if (technique.getChild(type) == null) { + type = "phong"; + if (technique.getChild(type) == null) { + type = "lambert"; + if (technique.getChild(type) == null) { + type = "constant"; + if (technique.getChild(type) == null) { + ColladaMaterialUtils.logger.warning("COMMON material has unusuable techinque. " + + child.getAttributeValue("url")); + return; + } + } + } + } + final Element blinnPhongLambert = technique.getChild(type); + if (mInfo != null) { + mInfo.setTechnique(type); + } + final MaterialState mState = new MaterialState(); + + // TODO: implement proper transparency handling + Texture diffuseTexture = null; + ColorRGBA transparent = new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + float transparency = 1.0f; + boolean useTransparency = false; + String opaqueMode = "A_ONE"; + + /* + * place holder for current property, we import material properties in fixed order (for texture order) + */ + Element property = null; + /* Diffuse property */ + property = blinnPhongLambert.getChild("diffuse"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("color".equals(propertyValue.getName())) { + final ColorRGBA color = _colladaDOMUtil.getColor(propertyValue.getText()); + mState.setDiffuse(MaterialFace.FrontAndBack, color); + } else if ("texture".equals(propertyValue.getName()) && _loadTextures) { + diffuseTexture = populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, + "diffuse"); + } + } + /* Ambient property */ + property = blinnPhongLambert.getChild("ambient"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("color".equals(propertyValue.getName())) { + final ColorRGBA color = _colladaDOMUtil.getColor(propertyValue.getText()); + mState.setAmbient(MaterialFace.FrontAndBack, color); + } else if ("texture".equals(propertyValue.getName()) && _loadTextures) { + populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "ambient"); + } + } + /* Transparent property */ + property = blinnPhongLambert.getChild("transparent"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("color".equals(propertyValue.getName())) { + transparent = _colladaDOMUtil.getColor(propertyValue.getText()); + // TODO: use this + useTransparency = true; + } else if ("texture".equals(propertyValue.getName()) && _loadTextures) { + populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "transparent"); + } + opaqueMode = property.getAttributeValue("opaque", "A_ONE"); + } + /* Transparency property */ + property = blinnPhongLambert.getChild("transparency"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("float".equals(propertyValue.getName())) { + transparency = Float.parseFloat(propertyValue.getText().replace(",", ".")); + // TODO: use this + if (_flipTransparency) { + transparency = 1f - transparency; + } + useTransparency = true; + } else if ("texture".equals(propertyValue.getName()) && _loadTextures) { + populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "transparency"); + } + } + /* Emission property */ + property = blinnPhongLambert.getChild("emission"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("color".equals(propertyValue.getName())) { + mState.setEmissive(MaterialFace.FrontAndBack, _colladaDOMUtil.getColor(propertyValue.getText())); + } else if ("texture".equals(propertyValue.getName()) && _loadTextures) { + populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "emissive"); + } + } + /* Specular property */ + property = blinnPhongLambert.getChild("specular"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("color".equals(propertyValue.getName())) { + mState.setSpecular(MaterialFace.FrontAndBack, _colladaDOMUtil.getColor(propertyValue.getText())); + } else if ("texture".equals(propertyValue.getName()) && _loadTextures) { + populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "specular"); + } + } + /* Shininess property */ + property = blinnPhongLambert.getChild("shininess"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("float".equals(propertyValue.getName())) { + float shininess = Float.parseFloat(propertyValue.getText().replace(",", ".")); + if (shininess >= 0.0f && shininess <= 1.0f) { + final float oldShininess = shininess; + shininess *= 128; + logger.finest("Shininess - " + oldShininess + + " - was in the [0,1] range. Scaling to [0, 128] - " + shininess); + } else if (shininess < 0 || shininess > 128) { + final float oldShininess = shininess; + shininess = MathUtils.clamp(shininess, 0, 128); + logger.warning("Shininess must be between 0 and 128. Shininess " + oldShininess + + " was clamped to " + shininess); + } + mState.setShininess(MaterialFace.FrontAndBack, shininess); + } else if ("texture".equals(propertyValue.getName()) && _loadTextures) { + populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "shininess"); + } + } + /* Reflectivity property */ + float reflectivity = 1.0f; + property = blinnPhongLambert.getChild("reflectivity"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("float".equals(propertyValue.getName())) { + reflectivity = Float.parseFloat(propertyValue.getText().replace(",", ".")); + } + } + /* Reflective property. Texture only */ + property = blinnPhongLambert.getChild("reflective"); + if (property != null) { + final Element propertyValue = property.getChildren().get(0); + if ("texture".equals(propertyValue.getName()) && _loadTextures) { + final Texture reflectiveTexture = populateTextureState(mesh, propertyValue, effect, + loadedTextures, mInfo, "reflective"); + + reflectiveTexture.setEnvironmentalMapMode(Texture.EnvironmentalMapMode.SphereMap); + reflectiveTexture.setApply(ApplyMode.Combine); + + reflectiveTexture.setCombineFuncRGB(CombinerFunctionRGB.Interpolate); + // color 1 + reflectiveTexture.setCombineSrc0RGB(CombinerSource.CurrentTexture); + reflectiveTexture.setCombineOp0RGB(CombinerOperandRGB.SourceColor); + // color 2 + reflectiveTexture.setCombineSrc1RGB(CombinerSource.Previous); + reflectiveTexture.setCombineOp1RGB(CombinerOperandRGB.SourceColor); + // interpolate param will come from alpha of constant color + reflectiveTexture.setCombineSrc2RGB(CombinerSource.Constant); + reflectiveTexture.setCombineOp2RGB(CombinerOperandRGB.SourceAlpha); + + reflectiveTexture.setConstantColor(new ColorRGBA(1, 1, 1, reflectivity)); + } + } + + /* + * An extra tag defines some materials not part of the collada standard. Since we're not able to parse + * we simply extract the textures from the element (such that shaders etc can at least pick up on them) + */ + property = technique.getChild("extra"); + if (property != null) { + // process with any plugins + if (!_importer.readExtra(property, mesh)) { + // no plugin processed our mesh, so process ourselves. + getTexturesFromElement(mesh, property, effect, loadedTextures, mInfo); + } + } + + // XXX: There are some issues with clarity on how to use alpha blending in OpenGL FFP. + // The best interpretation I have seen is that if transparent has a texture == diffuse, + // Turn on alpha blending and use diffuse alpha. + + // check to make sure we actually need this. + // testing separately for a transparency of 0.0 is to hack around erroneous exports, since usually + // there is no use in exporting something with 100% transparency. + if ("A_ONE".equals(opaqueMode) && ColorRGBA.WHITE.equals(transparent) && transparency == 1.0 + || transparency == 0.0) { + useTransparency = false; + } + + if (useTransparency) { + if (diffuseTexture != null) { + final BlendState blend = new BlendState(); + blend.setBlendEnabled(true); + blend.setTestEnabled(true); + blend.setSourceFunction(BlendState.SourceFunction.SourceAlpha); + blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha); + mesh.setRenderState(blend); + } else { + final BlendState blend = new BlendState(); + blend.setBlendEnabled(true); + blend.setTestEnabled(true); + transparent.setAlpha(transparent.getAlpha() * transparency); + blend.setConstantColor(transparent); + blend.setSourceFunction(BlendState.SourceFunction.ConstantAlpha); + blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusConstantAlpha); + mesh.setRenderState(blend); + } + + mesh.getSceneHints().setRenderBucketType(RenderBucketType.Transparent); + } + + if (mInfo != null) { + if (useTransparency) { + mInfo.setUseTransparency(useTransparency); + if (diffuseTexture == null) { + mInfo.setTransparency(transparent.getAlpha() * transparency); + } + } + mInfo.setMaterialState(mState); + } + mesh.setRenderState(mState); + } + } else { + ColladaMaterialUtils.logger.warning("material effect not found: " + + mat.getChild("instance_material").getAttributeValue("url")); + } + } + + /** + * Function to searches an xml node for <texture> elements and adds them to the texture state of the mesh. + * + * @param mesh + * the Ardor3D Mesh to add the Texture to. + * @param element + * the xml element to start the search on + * @param effect + * our <instance_effect> element + * @param info + */ + private void getTexturesFromElement(final Mesh mesh, final Element element, final Element effect, + final HashMap<String, Texture> loadedTextures, final MaterialInfo info) { + if ("texture".equals(element.getName()) && _loadTextures) { + populateTextureState(mesh, element, effect, loadedTextures, info, null); + } + @SuppressWarnings("unchecked") + final List<Element> children = element.getChildren(); + if (children != null) { + for (final Element child : children) { + /* recurse on children */ + getTexturesFromElement(mesh, child, effect, loadedTextures, info); + } + } + } + + /** + * Convert a <texture> element to an Ardor3D representation and store in the given state. + * + * @param mesh + * the Ardor3D Mesh to add the Texture to. + * @param daeTexture + * our <texture> element + * @param effect + * our <instance_effect> element + * @return the created Texture. + */ + private Texture populateTextureState(final Mesh mesh, final Element daeTexture, final Element effect, + final HashMap<String, Texture> loadedTextures, final MaterialInfo info, String textureSlot) { + // TODO: Use vert data to determine which texcoords and set to use. + // final String uvName = daeTexture.getAttributeValue("texcoord"); + TextureState tState = (TextureState) mesh.getLocalRenderState(RenderState.StateType.Texture); + if (tState == null) { + tState = new TextureState(); + mesh.setRenderState(tState); + } + + // Use texture attrib to find correct sampler + final String textureReference = daeTexture.getAttributeValue("texture"); + if (textureSlot == null) { + // if we have no texture slot defined (like in the case of an "extra" texture), we'll use the + // textureReference. + textureSlot = textureReference; + } + + /* only add the texture to the state once */ + if (loadedTextures.containsKey(textureReference)) { + final Texture tex = loadedTextures.get(textureReference); + if (info != null) { + info.setTextureSlot(textureSlot, textureReference, tex, null); + } + return tex; + } + + Element node = _colladaDOMUtil.findTargetWithSid(textureReference); + if (node == null) { + // Not sure if this is quite right, but spec seems to indicate looking for global id + node = _colladaDOMUtil.findTargetWithId("#" + textureReference); + } + + if ("newparam".equals(node.getName())) { + node = node.getChildren().get(0); + } + + Element sampler = null; + Element surface = null; + Element image = null; + + Texture.MinificationFilter min = Texture.MinificationFilter.BilinearNoMipMaps; + if ("sampler2D".equals(node.getName())) { + sampler = node; + if (sampler.getChild("minfilter") != null) { + final String minfilter = sampler.getChild("minfilter").getText(); + min = Enum.valueOf(SamplerTypes.MinFilterType.class, minfilter).getArdor3dFilter(); + } + // Use sampler to get correct surface + node = _colladaDOMUtil.findTargetWithSid(sampler.getChild("source").getText()); + // node = resolveSid(effect, sampler.getSource()); + } + + if ("newparam".equals(node.getName())) { + node = node.getChildren().get(0); + } + + if ("surface".equals(node.getName())) { + surface = node; + // image(s) will come from surface. + } else if ("image".equals(node.getName())) { + image = node; + } + + // Ok, a few possibilities here... + Texture texture = null; + String fileName = null; + if (surface == null && image != null) { + // Only an image found (no sampler). Assume 2d texture. Load. + fileName = image.getChild("init_from").getText(); + texture = loadTexture2D(fileName, min); + } else if (surface != null) { + // We have a surface, pull images from that. + if ("2D".equals(surface.getAttributeValue("type"))) { + // look for an init_from with lowest mip and use that. (usually 0) + + // TODO: mip? + final Element lowest = surface.getChildren("init_from").get(0); + // Element lowest = null; + // for (final Element i : (List<Element>) surface.getChildren("init_from")) { + // if (lowest == null || lowest.getMip() > i.getMip()) { + // lowest = i; + // } + // } + + if (lowest == null) { + logger.warning("surface given with no usable init_from: " + surface); + return null; + } + + image = _colladaDOMUtil.findTargetWithId("#" + lowest.getText()); + // image = (DaeImage) root.resolveUrl("#" + lowest.getValue()); + if (image != null) { + fileName = image.getChild("init_from").getText(); + texture = loadTexture2D(fileName, min); + } + + // TODO: add support for mip map levels other than 0. + } + // TODO: add support for the other texture types. + } else { + // No surface OR image... warn. + logger.warning("texture given with no matching <sampler*> or <image> found."); + if (info != null) { + info.setTextureSlot(textureSlot, textureReference, null, null); + } + return null; + } + + if (texture != null) { + if (sampler != null) { + // Apply params from our sampler. + applySampler(sampler, texture); + } + + // Add to texture state. + tState.setTexture(texture, tState.getNumberOfSetTextures()); + loadedTextures.put(textureReference, texture); + if (info != null) { + info.setTextureSlot(textureSlot, textureReference, texture, fileName); + } + } else { + logger.warning("unable to load texture: " + daeTexture); + if (info != null) { + info.setTextureSlot(textureSlot, textureReference, null, fileName); + } + } + + return texture; + } + + private void applySampler(final Element sampler, final Texture texture) { + if (sampler.getChild("minfilter") != null) { + final String minfilter = sampler.getChild("minfilter").getText(); + texture.setMinificationFilter(Enum.valueOf(SamplerTypes.MinFilterType.class, minfilter).getArdor3dFilter()); + } + if (sampler.getChild("magfilter") != null) { + final String magfilter = sampler.getChild("magfilter").getText(); + texture.setMagnificationFilter(Enum.valueOf(SamplerTypes.MagFilterType.class, magfilter).getArdor3dFilter()); + } + if (sampler.getChild("wrap_s") != null) { + final String wrapS = sampler.getChild("wrap_s").getText(); + texture.setWrap(Texture.WrapAxis.S, Enum.valueOf(SamplerTypes.WrapModeType.class, wrapS) + .getArdor3dWrapMode()); + } + if (sampler.getChild("wrap_t") != null) { + final String wrapT = sampler.getChild("wrap_t").getText(); + texture.setWrap(Texture.WrapAxis.T, Enum.valueOf(SamplerTypes.WrapModeType.class, wrapT) + .getArdor3dWrapMode()); + } + if (sampler.getChild("border_color") != null) { + texture.setBorderColor(_colladaDOMUtil.getColor(sampler.getChild("border_color").getText())); + } + } + + @SuppressWarnings("unchecked") + public void bindMaterials(final Element bindMaterial) { + if (bindMaterial == null || bindMaterial.getChildren().isEmpty()) { + return; + } + + for (final Element instance : bindMaterial.getChild("technique_common").getChildren("instance_material")) { + final Element matNode = _colladaDOMUtil.findTargetWithId(instance.getAttributeValue("target")); + if (matNode != null && "material".equals(matNode.getName())) { + _dataCache.bindMaterial(instance.getAttributeValue("symbol"), matNode); + } else { + logger.warning("instance material target not found: " + instance.getAttributeValue("target")); + } + + // TODO: need to store bound vert data as local data. (also unstore on unbind.) + } + } + + @SuppressWarnings("unchecked") + public void unbindMaterials(final Element bindMaterial) { + if (bindMaterial == null || bindMaterial.getChildren().isEmpty()) { + return; + } + for (final Element instance : bindMaterial.getChild("technique_common").getChildren("instance_material")) { + _dataCache.unbindMaterial(instance.getAttributeValue("symbol")); + } + } + + private Texture loadTexture2D(final String path, final Texture.MinificationFilter minFilter) { + if (_dataCache.containsTexture(path)) { + return _dataCache.getTexture(path); + } + + final Texture texture; + if (_importer.getTextureLocator() == null) { + texture = TextureManager.load(path, minFilter, _compressTextures ? TextureStoreFormat.GuessCompressedFormat + : TextureStoreFormat.GuessNoCompressedFormat, true); + } else { + final ResourceSource source = _importer.getTextureLocator().locateResource(path); + texture = TextureManager.load(source, minFilter, + _compressTextures ? TextureStoreFormat.GuessCompressedFormat + : TextureStoreFormat.GuessNoCompressedFormat, true); + } + _dataCache.addTexture(path, texture); + + return texture; + } + +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaMeshUtils.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaMeshUtils.java new file mode 100644 index 0000000..c04e854 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaMeshUtils.java @@ -0,0 +1,616 @@ +/** + * 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.nio.FloatBuffer; +import java.util.EnumSet; +import java.util.LinkedList; +import java.util.logging.Logger; + +import org.jdom2.Element; + +import com.ardor3d.extension.model.collada.jdom.ColladaInputPipe.Type; +import com.ardor3d.extension.model.collada.jdom.data.DataCache; +import com.ardor3d.extension.model.collada.jdom.data.MeshVertPairs; +import com.ardor3d.renderer.IndexMode; +import com.ardor3d.scenegraph.IndexBufferData; +import com.ardor3d.scenegraph.Line; +import com.ardor3d.scenegraph.Mesh; +import com.ardor3d.scenegraph.Node; +import com.ardor3d.scenegraph.Point; +import com.ardor3d.scenegraph.Spatial; +import com.ardor3d.util.geom.BufferUtils; +import com.ardor3d.util.geom.GeometryTool; +import com.ardor3d.util.geom.GeometryTool.MatchCondition; +import com.ardor3d.util.geom.VertMap; + +/** + * Methods for parsing Collada data related to meshes. + */ +public class ColladaMeshUtils { + private static final Logger logger = Logger.getLogger(ColladaMeshUtils.class.getName()); + + private final DataCache _dataCache; + private final ColladaDOMUtil _colladaDOMUtil; + private final ColladaMaterialUtils _colladaMaterialUtils; + private final boolean _optimizeMeshes; + private final EnumSet<MatchCondition> _optimizeSettings; + + public ColladaMeshUtils(final DataCache dataCache, final ColladaDOMUtil colladaDOMUtil, + final ColladaMaterialUtils colladaMaterialUtils, final boolean optimizeMeshes, + final EnumSet<MatchCondition> optimizeSettings) { + _dataCache = dataCache; + _colladaDOMUtil = colladaDOMUtil; + _colladaMaterialUtils = colladaMaterialUtils; + _optimizeMeshes = optimizeMeshes; + _optimizeSettings = EnumSet.copyOf(optimizeSettings); + } + + /** + * Builds geometry from an instance_geometry element. + * + * @param instanceGeometry + * @return our Spatial + */ + public Spatial getGeometryMesh(final Element instanceGeometry) { + final Element geometry = _colladaDOMUtil.findTargetWithId(instanceGeometry.getAttributeValue("url")); + + if (geometry != null) { + return buildMesh(geometry); + } + return null; + } + + /** + * Builds a mesh from a Collada geometry element. Currently supported mesh types: mesh, polygons, polylist, + * triangles, lines. Not supported yet: linestrips, trifans, tristrips. If no meshtype is found, a pointcloud is + * built. + * + * @param colladaGeometry + * @return a Node containing all of the Ardor3D meshes we've parsed from this geometry element. + */ + @SuppressWarnings("unchecked") + public Node buildMesh(final Element colladaGeometry) { + if (colladaGeometry.getChild("mesh") != null) { + final Element cMesh = colladaGeometry.getChild("mesh"); + final Node meshNode = new Node(colladaGeometry.getAttributeValue("name", colladaGeometry.getName())); + + // Grab all mesh types (polygons, triangles, etc.) + // Create each as an Ardor3D Mesh, and attach to node + boolean hasChild = false; + if (cMesh.getChild("polygons") != null) { + for (final Element p : cMesh.getChildren("polygons")) { + final Mesh child = buildMeshPolygons(colladaGeometry, p); + if (child != null) { + if (child.getName() == null) { + child.setName(meshNode.getName() + "_polygons"); + } + meshNode.attachChild(child); + hasChild = true; + } + } + } + if (cMesh.getChild("polylist") != null) { + for (final Element p : cMesh.getChildren("polylist")) { + final Mesh child = buildMeshPolylist(colladaGeometry, p); + if (child != null) { + if (child.getName() == null) { + child.setName(meshNode.getName() + "_polylist"); + } + meshNode.attachChild(child); + hasChild = true; + } + } + } + if (cMesh.getChild("triangles") != null) { + for (final Element t : cMesh.getChildren("triangles")) { + final Mesh child = buildMeshTriangles(colladaGeometry, t); + if (child != null) { + if (child.getName() == null) { + child.setName(meshNode.getName() + "_triangles"); + } + meshNode.attachChild(child); + hasChild = true; + } + } + } + if (cMesh.getChild("lines") != null) { + for (final Element l : cMesh.getChildren("lines")) { + final Line child = buildMeshLines(colladaGeometry, l); + if (child != null) { + if (child.getName() == null) { + child.setName(meshNode.getName() + "_lines"); + } + meshNode.attachChild(child); + hasChild = true; + } + } + } + if (cMesh.getChild("linestrips") != null) { + logger.warning("<linestrips> not currently supported."); + hasChild = true; + // TODO: Add support + } + if (cMesh.getChild("trifans") != null) { + logger.warning("<trifan> not currently supported."); + hasChild = true; + // TODO: Add support + } + if (cMesh.getChild("tristrips") != null) { + logger.warning("<tristrip> not currently supported."); + hasChild = true; + // TODO: Add support + } + + // If we did not find a valid child, the spec says to add verts as a "cloud of points" + if (!hasChild) { + logger.warning("No valid child found, creating 'cloud of points'"); + final Point points = buildPoints(colladaGeometry, cMesh); + if (points != null) { + if (points.getName() == null) { + points.setName(meshNode.getName() + "_points"); + } + meshNode.attachChild(points); + } + } + + return meshNode; + } + return null; + } + + private Point buildPoints(final Element colladaGeometry, final Element mesh) { + if (mesh == null || mesh.getChild("vertices") == null || mesh.getChild("vertices").getChild("input") == null) { + return null; + } + final Point points = new Point(); + points.setName(mesh.getAttributeValue("name", mesh.getName())); + + // Find POSITION vertices source + final Element source = _colladaDOMUtil.getPositionSource(mesh.getChild("vertices")); + if (source == null) { + return null; + } + + if (source.getChild("float_array") != null) { + // Turn into Floatbuffer if we have float array data + final Element floatArray = source.getChild("float_array"); + if ("0".equals(floatArray.getAttributeValue("count"))) { + return null; + } + final FloatBuffer vertices = BufferUtils.createFloatBuffer(_colladaDOMUtil.parseFloatArray(floatArray)); + // Add to points + points.getMeshData().setVertexBuffer(vertices); + } else if (source.getChild("int_array") != null) { + // Turn into Floatbuffer if we have int array data + final Element intArray = source.getChild("int_array"); + if ("0".equals(intArray.getAttributeValue("count"))) { + return null; + } + final int[] data = _colladaDOMUtil.parseIntArray(intArray); + final FloatBuffer vertices = BufferUtils.createFloatBuffer(data.length); + for (final int i : data) { + vertices.put(i); + } + // Add to points + points.getMeshData().setVertexBuffer(vertices); + } + + // Add to vert mapping + final int[] indices = new int[points.getMeshData().getVertexCount()]; + for (int i = 0; i < indices.length; i++) { + indices[i] = i; + } + final MeshVertPairs mvp = new MeshVertPairs(points, indices); + _dataCache.getVertMappings().put(colladaGeometry, mvp); + + if (_optimizeMeshes) { + final VertMap map = GeometryTool.minimizeVerts(points, _optimizeSettings); + _dataCache.setMeshVertMap(points, map); + } + + // Update bound + points.updateModelBound(); + + // return + return points; + } + + @SuppressWarnings("unchecked") + public Mesh buildMeshPolygons(final Element colladaGeometry, final Element polys) { + if (polys == null || polys.getChild("input") == null) { + return null; + } + final Mesh polyMesh = new Mesh(extractName(colladaGeometry, polys)); + polyMesh.getMeshData().setIndexMode(IndexMode.Triangles); + + // Build and set RenderStates for our material + _colladaMaterialUtils.applyMaterial(polys.getAttributeValue("material"), polyMesh); + + final LinkedList<ColladaInputPipe> pipes = new LinkedList<ColladaInputPipe>(); + final int maxOffset = extractPipes(polys, pipes); + final int interval = maxOffset + 1; + + // use interval & sum of sizes of p entries to determine buffer sizes. + int numEntries = 0; + int numIndices = 0; + for (final Element vals : polys.getChildren("p")) { + final int length = _colladaDOMUtil.parseIntArray(vals).length; + numEntries += length; + numIndices += (length / interval - 2) * 3; + } + numEntries /= interval; + + // Construct nio buffers for specified inputs. + for (final ColladaInputPipe pipe : pipes) { + pipe.setupBuffer(numEntries, polyMesh.getMeshData(), _dataCache); + } + + // Add to vert mapping + final int[] indices = new int[numEntries]; + final MeshVertPairs mvp = new MeshVertPairs(polyMesh, indices); + _dataCache.getVertMappings().put(colladaGeometry, mvp); + + // Prepare indices buffer + final IndexBufferData<?> meshIndices = BufferUtils.createIndexBufferData(numIndices, polyMesh.getMeshData() + .getVertexCount() - 1); + polyMesh.getMeshData().setIndices(meshIndices); + + // go through the polygon entries + int firstIndex = 0, vecIndex; + final int[] currentVal = new int[interval]; + for (final Element dia : polys.getChildren("p")) { + // for each p, iterate using max offset + final int[] vals = _colladaDOMUtil.parseIntArray(dia); + + final int first = firstIndex + 0; + System.arraycopy(vals, 0, currentVal, 0, interval); + vecIndex = processPipes(pipes, currentVal); + if (vecIndex != Integer.MIN_VALUE) { + indices[firstIndex + 0] = vecIndex; + } + + int prev = firstIndex + 1; + System.arraycopy(vals, interval, currentVal, 0, interval); + vecIndex = processPipes(pipes, currentVal); + if (vecIndex != Integer.MIN_VALUE) { + indices[firstIndex + 1] = vecIndex; + } + + // first add the first two entries to the buffers. + + // Now go through remaining entries and create a polygon as a triangle fan. + for (int j = 2, max = vals.length / interval; j < max; j++) { + // add first as index + meshIndices.put(first); + // add prev as index + meshIndices.put(prev); + + // set prev to current + prev = firstIndex + j; + // add current to buffers + System.arraycopy(vals, j * interval, currentVal, 0, interval); + vecIndex = processPipes(pipes, currentVal); + if (vecIndex != Integer.MIN_VALUE) { + indices[firstIndex + j] = vecIndex; + } + // add current as index + meshIndices.put(prev); + } + firstIndex += vals.length / interval; + } + + if (_optimizeMeshes) { + final VertMap map = GeometryTool.minimizeVerts(polyMesh, _optimizeSettings); + _dataCache.setMeshVertMap(polyMesh, map); + } + + // update bounds + polyMesh.updateModelBound(); + + // return + return polyMesh; + } + + public Mesh buildMeshPolylist(final Element colladaGeometry, final Element polys) { + if (polys == null || polys.getChild("input") == null) { + return null; + } + final Mesh polyMesh = new Mesh(extractName(colladaGeometry, polys)); + polyMesh.getMeshData().setIndexMode(IndexMode.Triangles); + + // Build and set RenderStates for our material + _colladaMaterialUtils.applyMaterial(polys.getAttributeValue("material"), polyMesh); + + final LinkedList<ColladaInputPipe> pipes = new LinkedList<ColladaInputPipe>(); + final int maxOffset = extractPipes(polys, pipes); + final int interval = maxOffset + 1; + + // use interval & sum of sizes of vcount to determine buffer sizes. + int numEntries = 0; + int numIndices = 0; + for (final int length : _colladaDOMUtil.parseIntArray(polys.getChild("vcount"))) { + numEntries += length; + numIndices += (length - 2) * 3; + } + + // Construct nio buffers for specified inputs. + for (final ColladaInputPipe pipe : pipes) { + pipe.setupBuffer(numEntries, polyMesh.getMeshData(), _dataCache); + } + + // Add to vert mapping + final int[] indices = new int[numEntries]; + final MeshVertPairs mvp = new MeshVertPairs(polyMesh, indices); + _dataCache.getVertMappings().put(colladaGeometry, mvp); + + // Prepare indices buffer + final IndexBufferData<?> meshIndices = BufferUtils.createIndexBufferData(numIndices, polyMesh.getMeshData() + .getVertexCount() - 1); + polyMesh.getMeshData().setIndices(meshIndices); + + // go through the polygon entries + int firstIndex = 0; + int vecIndex; + final int[] vals = _colladaDOMUtil.parseIntArray(polys.getChild("p")); + for (final int length : _colladaDOMUtil.parseIntArray(polys.getChild("vcount"))) { + final int[] currentVal = new int[interval]; + + // first add the first two entries to the buffers. + final int first = firstIndex + 0; + System.arraycopy(vals, (first * interval), currentVal, 0, interval); + vecIndex = processPipes(pipes, currentVal); + if (vecIndex != Integer.MIN_VALUE) { + indices[firstIndex + 0] = vecIndex; + } + + int prev = firstIndex + 1; + System.arraycopy(vals, (prev * interval), currentVal, 0, interval); + vecIndex = processPipes(pipes, currentVal); + if (vecIndex != Integer.MIN_VALUE) { + indices[firstIndex + 1] = vecIndex; + } + + // Now go through remaining entries and create a polygon as a triangle fan. + for (int j = 2, max = length; j < max; j++) { + // add first as index + meshIndices.put(first); + // add prev as index + meshIndices.put(prev); + + // set prev to current + prev = firstIndex + j; + // add current to buffers + System.arraycopy(vals, (prev * interval), currentVal, 0, interval); + vecIndex = processPipes(pipes, currentVal); + if (vecIndex != Integer.MIN_VALUE) { + indices[firstIndex + j] = vecIndex; + } + // add current as index + meshIndices.put(prev); + } + firstIndex += length; + } + + if (_optimizeMeshes) { + final VertMap map = GeometryTool.minimizeVerts(polyMesh, _optimizeSettings); + _dataCache.setMeshVertMap(polyMesh, map); + } + + // update bounds + polyMesh.updateModelBound(); + + // return + return polyMesh; + } + + public Mesh buildMeshTriangles(final Element colladaGeometry, final Element tris) { + if (tris == null || tris.getChild("input") == null || tris.getChild("p") == null) { + return null; + } + final Mesh triMesh = new Mesh(extractName(colladaGeometry, tris)); + triMesh.getMeshData().setIndexMode(IndexMode.Triangles); + + // Build and set RenderStates for our material + _colladaMaterialUtils.applyMaterial(tris.getAttributeValue("material"), triMesh); + + final LinkedList<ColladaInputPipe> pipes = new LinkedList<ColladaInputPipe>(); + final int maxOffset = extractPipes(tris, pipes); + final int interval = maxOffset + 1; + + // use interval & size of p array to determine buffer sizes. + final int[] vals = _colladaDOMUtil.parseIntArray(tris.getChild("p")); + final int numEntries = vals.length / interval; + + // Construct nio buffers for specified inputs. + for (final ColladaInputPipe pipe : pipes) { + pipe.setupBuffer(numEntries, triMesh.getMeshData(), _dataCache); + } + + // Add to vert mapping + final int[] indices = new int[numEntries]; + final MeshVertPairs mvp = new MeshVertPairs(triMesh, indices); + _dataCache.getVertMappings().put(colladaGeometry, mvp); + + // go through the p entry + // for each p, iterate using max offset + final int[] currentVal = new int[interval]; + + // Go through entries and add to buffers. + for (int j = 0, max = numEntries; j < max; j++) { + // add entry to buffers + System.arraycopy(vals, j * interval, currentVal, 0, interval); + final int rVal = processPipes(pipes, currentVal); + if (rVal != Integer.MIN_VALUE) { + indices[j] = rVal; + } + } + + if (_optimizeMeshes) { + final VertMap map = GeometryTool.minimizeVerts(triMesh, _optimizeSettings); + _dataCache.setMeshVertMap(triMesh, map); + } + + triMesh.updateModelBound(); + + return triMesh; + } + + public Line buildMeshLines(final Element colladaGeometry, final Element lines) { + if (lines == null || lines.getChild("input") == null || lines.getChild("p") == null) { + return null; + } + final Line lineMesh = new Line(extractName(colladaGeometry, lines)); + + // Build and set RenderStates for our material + _colladaMaterialUtils.applyMaterial(lines.getAttributeValue("material"), lineMesh); + + final LinkedList<ColladaInputPipe> pipes = new LinkedList<ColladaInputPipe>(); + final int maxOffset = extractPipes(lines, pipes); + final int interval = maxOffset + 1; + + // use interval & size of p array to determine buffer sizes. + final int[] vals = _colladaDOMUtil.parseIntArray(lines.getChild("p")); + final int numEntries = vals.length / interval; + + // Construct nio buffers for specified inputs. + for (final ColladaInputPipe pipe : pipes) { + pipe.setupBuffer(numEntries, lineMesh.getMeshData(), _dataCache); + } + + // Add to vert mapping + final int[] indices = new int[numEntries]; + final MeshVertPairs mvp = new MeshVertPairs(lineMesh, indices); + _dataCache.getVertMappings().put(colladaGeometry, mvp); + + // go through the p entry + // for each p, iterate using max offset + final int[] currentVal = new int[interval]; + + // Go through entries and add to buffers. + for (int j = 0, max = numEntries; j < max; j++) { + // add entry to buffers + System.arraycopy(vals, j * interval, currentVal, 0, interval); + final int rVal = processPipes(pipes, currentVal); + if (rVal != Integer.MIN_VALUE) { + indices[j] = rVal; + } + } + + if (_optimizeMeshes) { + final VertMap map = GeometryTool.minimizeVerts(lineMesh, _optimizeSettings); + _dataCache.setMeshVertMap(lineMesh, map); + } + + lineMesh.updateModelBound(); + + return lineMesh; + } + + /** + * Extract our pipes from the given parent element. + * + * @param inputsParent + * @param pipesStore + * the store for our pipes + * @return the max offset of our pipes. + */ + @SuppressWarnings("unchecked") + private int extractPipes(final Element inputsParent, final LinkedList<ColladaInputPipe> pipesStore) { + int maxOffset = 0; + int texCount = 0; + for (final Element input : inputsParent.getChildren("input")) { + maxOffset = Math.max(maxOffset, _colladaDOMUtil.getAttributeIntValue(input, "offset", 0)); + try { + final Type type = Type.valueOf(input.getAttributeValue("semantic")); + if (type == Type.VERTEX) { + final Element vertexElement = _colladaDOMUtil.findTargetWithId(input.getAttributeValue("source")); + for (final Element vertexInput : vertexElement.getChildren("input")) { + vertexInput.setAttribute("offset", input.getAttributeValue("offset")); + vertexInput.setAttribute("isVertexDefined", "true"); + final ColladaInputPipe pipe = new ColladaInputPipe(_colladaDOMUtil, vertexInput); + if (pipe.getType() == Type.TEXCOORD) { + pipe.setTexCoord(texCount++); + } + pipesStore.add(pipe); + } + } else { + final ColladaInputPipe pipe = new ColladaInputPipe(_colladaDOMUtil, input); + if (pipe.getType() == Type.TEXCOORD) { + pipe.setTexCoord(texCount++); + } + pipesStore.add(pipe); + } + } catch (final Exception ex) { + logger.warning("Unknown input type: " + input.getAttributeValue("semantic")); + continue; + } + } + return maxOffset; + } + + /** + * Push the values at the given indices of currentVal onto the buffers defined in pipes. + * + * @param pipes + * @param currentVal + * @return the vertex index referenced in the given indices based on the pipes. Integer.MIN_VALUE is returned if no + * vertex pipe is found. + */ + private int processPipes(final LinkedList<ColladaInputPipe> pipes, final int[] currentVal) { + // go through our pipes. use the indices in currentVal to pull the correct float val + // from our source and set into our buffer. + int rVal = Integer.MIN_VALUE; + for (final ColladaInputPipe pipe : pipes) { + pipe.pushValues(currentVal[pipe.getOffset()]); + if (pipe.getType() == Type.POSITION) { + rVal = currentVal[pipe.getOffset()]; + } + } + return rVal; + } + + /** + * Extract name from xml element, some exporters don't support 'name' attribute, so we better use the material + * instead of a generic name. + * + * @param element + * @return value from 'name' or 'material' attribute + */ + private String extractName(final Element colladaGeometry, final Element element) { + // Try to get mesh name + String name = element.getAttributeValue("name"); + if (name == null || name.isEmpty()) { + // No mesh name found, try to get mesh id + name = element.getAttributeValue("id"); + } + if (name == null || name.isEmpty()) { + // No mesh name or id found, try to get parent geometry name + name = colladaGeometry.getAttributeValue("name"); + if (name == null || name.isEmpty()) { + // No parent geometry name found, try to get geometry id (mandatory according to spec) + name = colladaGeometry.getAttributeValue("id"); + } + if (name == null) { + name = ""; + } + + // Since we have retrieved the parent geometry name/id, we append the material(if any), + // to make identification unique. + final String materialName = element.getAttributeValue("material"); + if (materialName != null && !materialName.isEmpty()) { + name += "[" + materialName + "]"; + } + } + + return name; + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaNodeUtils.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaNodeUtils.java new file mode 100644 index 0000000..da4535e --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/ColladaNodeUtils.java @@ -0,0 +1,378 @@ +/** + * 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.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.jdom2.Element; + +import com.ardor3d.extension.animation.skeletal.Joint; +import com.ardor3d.extension.animation.skeletal.Skeleton; +import com.ardor3d.extension.model.collada.jdom.data.AssetData; +import com.ardor3d.extension.model.collada.jdom.data.ControllerStore; +import com.ardor3d.extension.model.collada.jdom.data.DataCache; +import com.ardor3d.extension.model.collada.jdom.data.JointNode; +import com.ardor3d.extension.model.collada.jdom.data.NodeType; +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.scenegraph.Node; +import com.ardor3d.scenegraph.Spatial; +import com.google.common.collect.Lists; + +/** + * Methods for parsing Collada data related to scenes and node hierarchy. + */ +public class ColladaNodeUtils { + private static final Logger logger = Logger.getLogger(ColladaNodeUtils.class.getName()); + + private final DataCache _dataCache; + private final ColladaDOMUtil _colladaDOMUtil; + private final ColladaMaterialUtils _colladaMaterialUtils; + private final ColladaMeshUtils _colladaMeshUtils; + private final ColladaAnimUtils _colladaAnimUtils; + + public ColladaNodeUtils(final DataCache dataCache, final ColladaDOMUtil colladaDOMUtil, + final ColladaMaterialUtils colladaMaterialUtils, final ColladaMeshUtils colladaMeshUtils, + final ColladaAnimUtils colladaAnimUtils) { + _dataCache = dataCache; + _colladaDOMUtil = colladaDOMUtil; + _colladaMaterialUtils = colladaMaterialUtils; + _colladaMeshUtils = colladaMeshUtils; + _colladaAnimUtils = colladaAnimUtils; + } + + /** + * Retrieves the scene and returns it as an Ardor3D Node. + * + * @param colladaRoot + * The collada root element + * @return Scene as an Node or null if not found + */ + @SuppressWarnings("unchecked") + public Node getVisualScene(final Element colladaRoot) { + if (colladaRoot.getChild("scene") == null) { + logger.warning("No scene found in collada file!"); + return null; + } + + final Element instance_visual_scene = colladaRoot.getChild("scene").getChild("instance_visual_scene"); + if (instance_visual_scene == null) { + logger.warning("No instance_visual_scene found in collada file!"); + return null; + } + + final Element visualScene = _colladaDOMUtil.findTargetWithId(instance_visual_scene.getAttributeValue("url")); + + if (visualScene != null) { + final Node sceneRoot = new Node( + visualScene.getAttributeValue("name") != null ? visualScene.getAttributeValue("name") + : "Collada Root"); + + // Load each sub node and attach + final JointNode baseJointNode = new JointNode(null); + _dataCache.setRootJointNode(baseJointNode); + for (final Element n : visualScene.getChildren("node")) { + final Node subNode = buildNode(n, baseJointNode); + if (subNode != null) { + sceneRoot.attachChild(subNode); + } + } + + // build a list of joints - one list per skeleton + final List<List<Joint>> jointCollection = Lists.newArrayList(); + for (final JointNode jointChildNode : _dataCache.getRootJointNode().getChildren()) { + final List<Joint> jointList = Lists.newArrayList(); + buildJointLists(jointChildNode, jointList); + jointCollection.add(jointList); + } + + // build a skeleton for each joint list. + for (final List<Joint> jointList : jointCollection) { + final Joint[] joints = jointList.toArray(new Joint[jointList.size()]); + final Skeleton skeleton = new Skeleton(joints[0].getName() + "_skeleton", joints); + logger.fine(skeleton.getName()); + for (final Joint joint : jointList) { + _dataCache.getJointSkeletonMapping().put(joint, skeleton); + logger.fine("- Joint " + joint.getName() + " - index: " + joint.getIndex() + " parent index: " + + joint.getParentIndex()); + } + _dataCache.addSkeleton(skeleton); + } + + // update our world transforms so we can use them to init the default bind matrix of any joints. + sceneRoot.updateWorldTransform(true); + initDefaultJointTransforms(baseJointNode); + + // Setup our skinned mesh objects. + for (final ControllerStore controllerStore : _dataCache.getControllers()) { + _colladaMaterialUtils.bindMaterials(controllerStore.instanceController.getChild("bind_material")); + _colladaAnimUtils.buildController(controllerStore.ardorParentNode, controllerStore.instanceController); + _colladaMaterialUtils.unbindMaterials(controllerStore.instanceController.getChild("bind_material")); + } + + return sceneRoot; + } + return null; + } + + private void buildJointLists(final JointNode jointNode, final List<Joint> jointList) { + final Joint joint = jointNode.getJoint(); + joint.setIndex((short) jointList.size()); + if (jointNode.getParent().getJoint() != null) { + joint.setParentIndex(jointNode.getParent().getJoint().getIndex()); + } else { + joint.setParentIndex(Joint.NO_PARENT); + } + jointList.add(joint); + for (final JointNode jointChildNode : jointNode.getChildren()) { + buildJointLists(jointChildNode, jointList); + } + } + + private void initDefaultJointTransforms(final JointNode jointNode) { + final Joint j = jointNode.getJoint(); + if (j != null && jointNode.getSceneNode() != null) { + j.setInverseBindPose(jointNode.getSceneNode().getWorldTransform().invert(null)); + } + for (final JointNode jointChildNode : jointNode.getChildren()) { + initDefaultJointTransforms(jointChildNode); + } + } + + /** + * Parse an asset element into an AssetData object. + * + * @param asset + * @return + */ + @SuppressWarnings("unchecked") + public AssetData parseAsset(final Element asset) { + final AssetData assetData = new AssetData(); + + for (final Element child : asset.getChildren()) { + if ("contributor".equals(child.getName())) { + parseContributor(assetData, child); + } else if ("created".equals(child.getName())) { + assetData.setCreated(child.getText()); + } else if ("keywords".equals(child.getName())) { + assetData.setKeywords(child.getText()); + } else if ("modified".equals(child.getName())) { + assetData.setModified(child.getText()); + } else if ("revision".equals(child.getName())) { + assetData.setRevision(child.getText()); + } else if ("subject".equals(child.getName())) { + assetData.setSubject(child.getText()); + } else if ("title".equals(child.getName())) { + assetData.setTitle(child.getText()); + } else if ("unit".equals(child.getName())) { + final String name = child.getAttributeValue("name"); + if (name != null) { + assetData.setUnitName(name); + } + final String meter = child.getAttributeValue("meter"); + if (meter != null) { + assetData.setUnitMeter(Float.parseFloat(meter.replace(",", "."))); + } + } else if ("up_axis".equals(child.getName())) { + final String axis = child.getText(); + if ("X_UP".equals(axis)) { + assetData.setUpAxis(new Vector3()); + } else if ("Y_UP".equals(axis)) { + assetData.setUpAxis(Vector3.UNIT_Y); + } else if ("Z_UP".equals(axis)) { + assetData.setUpAxis(Vector3.UNIT_Z); + } + } + } + + return assetData; + } + + @SuppressWarnings("unchecked") + private void parseContributor(final AssetData assetData, final Element contributor) { + for (final Element child : contributor.getChildren()) { + if ("author".equals(child.getName())) { + assetData.setAuthor(child.getText()); + } else if ("authoringTool".equals(child.getName())) { + assetData.setCreated(child.getText()); + } else if ("comments".equals(child.getName())) { + assetData.setComments(child.getText()); + } else if ("copyright".equals(child.getName())) { + assetData.setCopyright(child.getText()); + } else if ("source_data".equals(child.getName())) { + assetData.setSourceData(child.getText()); + } + } + } + + /** + * @param instanceNode + * @return a new Ardor3D node, created from the <node> pointed to by the given <instance_node> element + */ + public Node getNode(final Element instanceNode, final JointNode jointNode) { + final Element node = _colladaDOMUtil.findTargetWithId(instanceNode.getAttributeValue("url")); + + if (node == null) { + throw new ColladaException("No node with id: " + instanceNode.getAttributeValue("url") + " found", + instanceNode); + } + + return buildNode(node, jointNode); + } + + /** + * Recursively parse the node hierarcy. + * + * @param dNode + * @return a new Ardor3D node, created from the given <node> element + */ + @SuppressWarnings("unchecked") + private Node buildNode(final Element dNode, JointNode jointNode) { + NodeType nodeType = NodeType.NODE; + if (dNode.getAttribute("type") != null) { + nodeType = Enum.valueOf(NodeType.class, dNode.getAttributeValue("type")); + } + final JointNode jointChildNode; + if (nodeType == NodeType.JOINT) { + String name = dNode.getAttributeValue("name"); + if (name == null) { + name = dNode.getAttributeValue("id"); + } + if (name == null) { + name = dNode.getAttributeValue("sid"); + } + final Joint joint = new Joint(name); + jointChildNode = new JointNode(joint); + jointChildNode.setParent(jointNode); + jointNode.getChildren().add(jointChildNode); + jointNode = jointChildNode; + + _dataCache.getElementJointMapping().put(dNode, joint); + } else { + jointChildNode = null; + } + + String nodeName = dNode.getAttributeValue("name", (String) null); + if (nodeName == null) { // use id if name doesn't exist + nodeName = dNode.getAttributeValue("id", dNode.getName()); + } + final Node node = new Node(nodeName); + + final List<Element> transforms = new ArrayList<Element>(); + for (final Element child : dNode.getChildren()) { + if (_dataCache.getTransformTypes().contains(child.getName())) { + transforms.add(child); + } + } + + // process any transform information. + if (!transforms.isEmpty()) { + final Transform localTransform = getNodeTransforms(transforms); + + node.setTransform(localTransform); + if (jointChildNode != null) { + jointChildNode.setSceneNode(node); + } + } + + // process any instance geometries + for (final Element instance_geometry : dNode.getChildren("instance_geometry")) { + _colladaMaterialUtils.bindMaterials(instance_geometry.getChild("bind_material")); + + final Spatial mesh = _colladaMeshUtils.getGeometryMesh(instance_geometry); + if (mesh != null) { + node.attachChild(mesh); + } + + _colladaMaterialUtils.unbindMaterials(instance_geometry.getChild("bind_material")); + } + + // process any instance controllers + for (final Element instanceController : dNode.getChildren("instance_controller")) { + _dataCache.getControllers().add(new ControllerStore(node, instanceController)); + } + + // process any instance nodes + for (final Element in : dNode.getChildren("instance_node")) { + final Node subNode = getNode(in, jointNode); + if (subNode != null) { + node.attachChild(subNode); + } + } + + // process any concrete child nodes. + for (final Element n : dNode.getChildren("node")) { + final Node subNode = buildNode(n, jointNode); + if (subNode != null) { + node.attachChild(subNode); + } + } + + // Cache reference + _dataCache.getElementSpatialMapping().put(dNode, node); + + return node; + } + + /** + * Combines a list of transform elements into an Ardor3D Transform object. + * + * @param transforms + * List of transform elements + * @return an Ardor3D Transform object + */ + public Transform getNodeTransforms(final List<Element> transforms) { + final Matrix4 workingMat = Matrix4.fetchTempInstance(); + final Matrix4 finalMat = Matrix4.fetchTempInstance(); + finalMat.setIdentity(); + for (final Element transform : transforms) { + final double[] array = _colladaDOMUtil.parseDoubleArray(transform); + if ("translate".equals(transform.getName())) { + workingMat.setIdentity(); + workingMat.setColumn(3, new Vector4(array[0], array[1], array[2], 1.0)); + finalMat.multiplyLocal(workingMat); + } else if ("rotate".equals(transform.getName())) { + 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 ("scale".equals(transform.getName())) { + workingMat.setIdentity(); + workingMat.scale(new Vector4(array[0], array[1], array[2], 1), workingMat); + finalMat.multiplyLocal(workingMat); + } else if ("matrix".equals(transform.getName())) { + workingMat.fromArray(array); + finalMat.multiplyLocal(workingMat); + } else if ("lookat".equals(transform.getName())) { + 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)); + finalMat.multiplyLocal(workingMat); + } else { + logger.warning("transform not currently supported: " + transform.getClass().getCanonicalName()); + } + } + return new Transform().fromHomogeneousMatrix(finalMat); + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/AnimationItem.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/AnimationItem.java new file mode 100644 index 0000000..43c732b --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/AnimationItem.java @@ -0,0 +1,94 @@ +/**
+ * 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.data;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.ardor3d.annotation.SavableFactory;
+import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+import com.google.common.collect.Lists;
+
+@SavableFactory(factoryMethod = "initSavable")
+public class AnimationItem implements Savable {
+ private final String _name;
+ private final List<AnimationItem> _children = Lists.newArrayList();
+ private AnimationClip _animationClip;
+
+ public AnimationItem(final String name) {
+ _name = name;
+ }
+
+ public AnimationClip getAnimationClip() {
+ return _animationClip;
+ }
+
+ public void setAnimationClip(final AnimationClip animationClip) {
+ _animationClip = animationClip;
+ }
+
+ public String getName() {
+ return _name;
+ }
+
+ public List<AnimationItem> getChildren() {
+ return _children;
+ }
+
+ @Override
+ public String toString() {
+ return "AnimationItem [name=" + _name + (_animationClip != null ? ", " + _animationClip.toString() : "") + "]";
+ }
+
+ // /////////////////
+ // Methods for Savable
+ // /////////////////
+
+ @Override
+ public Class<? extends AnimationItem> getClassTag() {
+ return this.getClass();
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ final String name = capsule.readString("name", "");
+ try {
+ final Field field1 = AnimationClip.class.getDeclaredField("_name");
+ field1.setAccessible(true);
+ field1.set(this, name);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+ _children.clear();
+ _children.addAll(capsule.readSavableList("children", new ArrayList<AnimationItem>()));
+ _animationClip = (AnimationClip) capsule.readSavable("animationClip", null);
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ capsule.write(_name, "name", null);
+ capsule.writeSavableList(_children, "children", null);
+ capsule.write(_animationClip, "animationClip", null);
+ }
+
+ public static AnimationItem initSavable() {
+ return new AnimationItem();
+ }
+
+ private AnimationItem() {
+ this(null);
+ }
+}
diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/AssetData.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/AssetData.java new file mode 100644 index 0000000..9e53f2c --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/AssetData.java @@ -0,0 +1,201 @@ +/** + * 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.data; + +import java.io.IOException; + +import com.ardor3d.math.type.ReadOnlyVector3; +import com.ardor3d.util.export.InputCapsule; +import com.ardor3d.util.export.OutputCapsule; +import com.ardor3d.util.export.Savable; + +/** + * Holds data related to asset info. + */ +public class AssetData implements Savable { + + private String author; + private String authoringTool; + private String comments; + private String copyright; + private String sourceData; + private String created; + private String keywords; + private String modified; + private String revision; + private String subject; + private String title; + private String unitName; + private float unitMeter; + private ReadOnlyVector3 upAxis; + + public String getAuthor() { + return author; + } + + public void setAuthor(final String author) { + this.author = author; + } + + public String getAuthoringTool() { + return authoringTool; + } + + public void setAuthoringTool(final String authoringTool) { + this.authoringTool = authoringTool; + } + + public String getComments() { + return comments; + } + + public void setComments(final String comments) { + this.comments = comments; + } + + public String getCopyright() { + return copyright; + } + + public void setCopyright(final String copyright) { + this.copyright = copyright; + } + + public String getSourceData() { + return sourceData; + } + + public void setSourceData(final String sourceData) { + this.sourceData = sourceData; + } + + public String getCreated() { + return created; + } + + public void setCreated(final String created) { + this.created = created; + } + + public String getKeywords() { + return keywords; + } + + public void setKeywords(final String keywords) { + this.keywords = keywords; + } + + public String getModified() { + return modified; + } + + public void setModified(final String modified) { + this.modified = modified; + } + + public String getRevision() { + return revision; + } + + public void setRevision(final String revision) { + this.revision = revision; + } + + public String getSubject() { + return subject; + } + + public void setSubject(final String subject) { + this.subject = subject; + } + + public String getTitle() { + return title; + } + + public void setTitle(final String title) { + this.title = title; + } + + public String getUnitName() { + return unitName; + } + + public void setUnitName(final String unitName) { + this.unitName = unitName; + } + + public float getUnitMeter() { + return unitMeter; + } + + public void setUnitMeter(final float unitMeter) { + this.unitMeter = unitMeter; + } + + /** + * @return the up axis as defined in the <asset> tag, or null if not existing. + */ + public ReadOnlyVector3 getUpAxis() { + return upAxis; + } + + public void setUpAxis(final ReadOnlyVector3 upAxis) { + this.upAxis = upAxis; + } + + // ///////////////// + // Methods for Savable + // ///////////////// + + @Override + public Class<? extends AssetData> getClassTag() { + return getClass(); + } + + @Override + public void read(final InputCapsule capsule) throws IOException { + author = capsule.readString("author", null); + authoringTool = capsule.readString("authoringTool", null); + comments = capsule.readString("comments", null); + copyright = capsule.readString("copyright", null); + sourceData = capsule.readString("sourceData", null); + created = capsule.readString("created", null); + keywords = capsule.readString("keywords", null); + modified = capsule.readString("modified", null); + revision = capsule.readString("revision", null); + subject = capsule.readString("subject", null); + title = capsule.readString("title", null); + unitName = capsule.readString("unitName", null); + unitMeter = capsule.readFloat("unitMeter", 0f); + upAxis = (ReadOnlyVector3) capsule.readSavable("upAxis", null); + } + + @Override + public void write(final OutputCapsule capsule) throws IOException { + capsule.write(author, "author", null); + capsule.write(authoringTool, "authoringTool", null); + capsule.write(comments, "comments", null); + capsule.write(copyright, "copyright", null); + capsule.write(sourceData, "sourceData", null); + capsule.write(created, "created", null); + capsule.write(keywords, "keywords", null); + capsule.write(modified, "modified", null); + capsule.write(revision, "revision", null); + capsule.write(subject, "subject", null); + capsule.write(title, "title", null); + capsule.write(unitName, "unitName", null); + capsule.write(unitMeter, "unitMeter", 0f); + if (upAxis instanceof Savable) { + capsule.write((Savable) upAxis, "upAxis", null); + } + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/ColladaStorage.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/ColladaStorage.java new file mode 100644 index 0000000..8eafa20 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/ColladaStorage.java @@ -0,0 +1,161 @@ +/** + * 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.data; + +import java.io.IOException; +import java.nio.FloatBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.ardor3d.extension.animation.skeletal.clip.AbstractAnimationChannel; +import com.ardor3d.extension.animation.skeletal.clip.AnimationClip; +import com.ardor3d.extension.animation.skeletal.clip.TransformChannel; +import com.ardor3d.scenegraph.Mesh; +import com.ardor3d.scenegraph.MeshData; +import com.ardor3d.scenegraph.Node; +import com.ardor3d.util.export.InputCapsule; +import com.ardor3d.util.export.OutputCapsule; +import com.ardor3d.util.export.Savable; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; + +/** + * Data storage object meant to hold objects parsed from a Collada file that the user might want to directly access. + */ +public class ColladaStorage implements Savable { + + private Node _scene; + private final List<SkinData> _skins = Lists.newArrayList(); + private AssetData _assetData; + + private final List<AbstractAnimationChannel> _transformChannels = Lists.newArrayList(); + private AnimationItem _animationItemRoot; + + // List of parsed color buffers, useful if collada includes multiple color channels per meshdata object + // Transient because the key is identity - meshdata. + private transient Multimap<MeshData, FloatBuffer> _parsedVertexColors; + + // Map of parsed material information, useful for doing post-manipulation of model information. + // Transient because the key is identity - mesh. + private transient Map<Mesh, String> _meshMaterialInfo; + private transient Map<String, MaterialInfo> _materialMap; + + public void setScene(final Node scene) { + _scene = scene; + } + + /** + * @return a Node representing the parsed "visual scene". + */ + public Node getScene() { + return _scene; + } + + /** + * @return a list of data objects representing each <skin> tag parsed during reading of the visual scene. + */ + public List<SkinData> getSkins() { + return _skins; + } + + public AssetData getAssetData() { + return _assetData; + } + + public void setAssetData(final AssetData assetData) { + _assetData = assetData; + } + + public List<AbstractAnimationChannel> getAnimationChannels() { + return _transformChannels; + } + + /** + * @return the root of our animation library. We can use this to walk through all animations in the Collada file. + */ + public AnimationItem getAnimationItemRoot() { + return _animationItemRoot; + } + + public void setAnimationItemRoot(final AnimationItem animationItemRoot) { + _animationItemRoot = animationItemRoot; + } + + /** + * Extract all animation channels in the Collada file as a single, unified AnimationClip. + * + * @param name + * the name to give our new clip. + * @return the new AnimationClip. + */ + public AnimationClip extractChannelsAsClip(final String name) { + final AnimationClip clip = new AnimationClip(name); + for (final AbstractAnimationChannel channel : getAnimationChannels()) { + clip.addChannel(channel); + } + return clip; + } + + /** + * @return a transient Multimap of MeshData -> List of parsed vertex colors. Only MeshData objects that had multiple + * vertex colors parsed will show up in this map. + */ + public Multimap<MeshData, FloatBuffer> getParsedVertexColors() { + return _parsedVertexColors; + } + + public void setParsedVertexColors(final Multimap<MeshData, FloatBuffer> parsedVertexColors) { + _parsedVertexColors = parsedVertexColors; + } + + public Map<String, MaterialInfo> getMaterialMap() { + return _materialMap; + } + + public void setMaterialMap(final Map<String, MaterialInfo> materialMap) { + _materialMap = materialMap; + } + + public Map<Mesh, String> getMeshMaterialInfo() { + return _meshMaterialInfo; + } + + public void setMeshMaterialInfo(final Map<Mesh, String> meshMaterialInfo) { + _meshMaterialInfo = meshMaterialInfo; + } + + // ///////////////// + // Methods for Savable + // ///////////////// + + @Override + public Class<?> getClassTag() { + return this.getClass(); + } + + @Override + public void read(final InputCapsule capsule) throws IOException { + _assetData = (AssetData) capsule.readSavable("assetData", null); + _scene = (Node) capsule.readSavable("scene", null); + _skins.addAll(capsule.readSavableList("skins", new LinkedList<SkinData>())); + _transformChannels.clear(); + _transformChannels.addAll(capsule.readSavableList("jointChannels", new LinkedList<TransformChannel>())); + } + + @Override + public void write(final OutputCapsule capsule) throws IOException { + capsule.write(_assetData, "assetData", null); + capsule.write(_scene, "scene", null); + capsule.writeSavableList(_skins, "skins", new LinkedList<SkinData>()); + capsule.writeSavableList(_transformChannels, "jointChannels", new LinkedList<TransformChannel>()); + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/ControllerStore.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/ControllerStore.java new file mode 100644 index 0000000..b827add --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/ControllerStore.java @@ -0,0 +1,16 @@ + +package com.ardor3d.extension.model.collada.jdom.data; + +import org.jdom2.Element; + +import com.ardor3d.scenegraph.Node; + +public class ControllerStore { + public final Node ardorParentNode; + public final Element instanceController; + + public ControllerStore(final Node ardorParentNode, final Element instanceController) { + this.ardorParentNode = ardorParentNode; + this.instanceController = instanceController; + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/DataCache.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/DataCache.java new file mode 100644 index 0000000..2ae984f --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/DataCache.java @@ -0,0 +1,229 @@ +/**
+ * 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.data;
+
+import java.nio.FloatBuffer;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.jdom2.Element;
+import org.jdom2.xpath.XPath;
+
+import com.ardor3d.extension.animation.skeletal.Joint;
+import com.ardor3d.extension.animation.skeletal.Skeleton;
+import com.ardor3d.extension.animation.skeletal.SkeletonPose;
+import com.ardor3d.image.Texture;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.scenegraph.MeshData;
+import com.ardor3d.scenegraph.Spatial;
+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;
+
+/**
+ * Performance cache and temp storage during parsing.
+ */
+public class DataCache {
+ private final Map<String, Element> _boundMaterials;
+ private final Map<String, Texture> _textures;
+ private final Map<String, Element> _idCache;
+ private final Map<String, Element> _sidCache;
+ private final Map<String, XPath> _xPathExpressions;
+ private final Pattern _pattern;
+ private final List<String> _transformTypes;
+
+ private final Map<Element, float[]> _floatArrays;
+ private final Map<Element, double[]> _doubleArrays;
+ private final Map<Element, boolean[]> _booleanArrays;
+ private final Map<Element, int[]> _intArrays;
+ private final Map<Element, String[]> _stringArrays;
+
+ private final Multimap<Element, MeshVertPairs> _vertMappings;
+ private final Map<Mesh, VertMap> _meshVertMap;
+ private final Multimap<MeshData, FloatBuffer> _parsedVertexColors;
+ private final Map<String, MaterialInfo> _materialInfoMap;
+ private final Map<Mesh, String> _meshMaterialMap;
+ private final Map<Element, Spatial> _elementSpatialMapping;
+
+ private final Map<Element, Joint> _elementJointMapping;
+ private final Map<String, Joint> _externalJointMapping;
+ private JointNode _rootJointNode;
+ private final Map<Joint, Skeleton> _jointSkeletonMapping;
+ private final Map<Skeleton, SkeletonPose> _skeletonPoseMapping;
+ private final List<Skeleton> _skeletons;
+ private final List<ControllerStore> _controllers;
+
+ public DataCache() {
+ _boundMaterials = Maps.newHashMap();
+ _textures = Maps.newHashMap();
+ _idCache = Maps.newHashMap();
+ _sidCache = Maps.newHashMap();
+ _xPathExpressions = Maps.newHashMap();
+ _pattern = Pattern.compile("\\s");
+
+ _transformTypes = Collections.unmodifiableList(Lists.newArrayList("lookat", "matrix", "rotate", "scale",
+ "scew", "translate"));
+
+ _floatArrays = Maps.newHashMap();
+ _doubleArrays = Maps.newHashMap();
+ _booleanArrays = Maps.newHashMap();
+ _intArrays = Maps.newHashMap();
+ _stringArrays = Maps.newHashMap();
+ _vertMappings = ArrayListMultimap.create();
+ _meshVertMap = Maps.newIdentityHashMap();
+ _parsedVertexColors = ArrayListMultimap.create();
+ _materialInfoMap = Maps.newHashMap();
+ _meshMaterialMap = Maps.newIdentityHashMap();
+
+ _elementSpatialMapping = Maps.newHashMap();
+
+ _elementJointMapping = Maps.newHashMap();
+ _externalJointMapping = Maps.newHashMap();
+ _skeletons = Lists.newArrayList();
+ _jointSkeletonMapping = Maps.newHashMap();
+ _skeletonPoseMapping = Maps.newHashMap();
+ _controllers = Lists.newArrayList();
+ }
+
+ public void bindMaterial(final String ref, final Element material) {
+ if (!_boundMaterials.containsKey(ref)) {
+ _boundMaterials.put(ref, material);
+ }
+ }
+
+ public void unbindMaterial(final String ref) {
+ _boundMaterials.remove(ref);
+ }
+
+ public Element getBoundMaterial(final String ref) {
+ return _boundMaterials.get(ref);
+ }
+
+ public boolean containsTexture(final String path) {
+ return _textures.containsKey(path);
+ }
+
+ public void addTexture(final String path, final Texture texture) {
+ _textures.put(path, texture);
+ }
+
+ public Texture getTexture(final String path) {
+ return _textures.get(path);
+ }
+
+ public Map<String, Element> getIdCache() {
+ return _idCache;
+ }
+
+ public Map<String, Element> getSidCache() {
+ return _sidCache;
+ }
+
+ public Map<String, XPath> getxPathExpressions() {
+ return _xPathExpressions;
+ }
+
+ public Pattern getPattern() {
+ return _pattern;
+ }
+
+ public List<String> getTransformTypes() {
+ return _transformTypes;
+ }
+
+ public Map<Element, float[]> getFloatArrays() {
+ return _floatArrays;
+ }
+
+ public Map<Element, double[]> getDoubleArrays() {
+ return _doubleArrays;
+ }
+
+ public Map<Element, boolean[]> getBooleanArrays() {
+ return _booleanArrays;
+ }
+
+ public Map<Element, int[]> getIntArrays() {
+ return _intArrays;
+ }
+
+ public Map<Element, String[]> getStringArrays() {
+ return _stringArrays;
+ }
+
+ public Multimap<Element, MeshVertPairs> getVertMappings() {
+ return _vertMappings;
+ }
+
+ public Map<Mesh, VertMap> getMeshVertMap() {
+ return _meshVertMap;
+ }
+
+ public Multimap<MeshData, FloatBuffer> getParsedVertexColors() {
+ return _parsedVertexColors;
+ }
+
+ public Map<String, MaterialInfo> getMaterialInfoMap() {
+ return _materialInfoMap;
+ }
+
+ public Map<Mesh, String> getMeshMaterialMap() {
+ return _meshMaterialMap;
+ }
+
+ public Map<Element, Spatial> getElementSpatialMapping() {
+ return _elementSpatialMapping;
+ }
+
+ public Map<Element, Joint> getElementJointMapping() {
+ return _elementJointMapping;
+ }
+
+ public Map<String, Joint> getExternalJointMapping() {
+ return _externalJointMapping;
+ }
+
+ public JointNode getRootJointNode() {
+ return _rootJointNode;
+ }
+
+ public void setRootJointNode(final JointNode rootJointNode) {
+ _rootJointNode = rootJointNode;
+ }
+
+ public Map<Joint, Skeleton> getJointSkeletonMapping() {
+ return _jointSkeletonMapping;
+ }
+
+ public Map<Skeleton, SkeletonPose> getSkeletonPoseMapping() {
+ return _skeletonPoseMapping;
+ }
+
+ public List<ControllerStore> getControllers() {
+ return _controllers;
+ }
+
+ public List<Skeleton> getSkeletons() {
+ return _skeletons;
+ }
+
+ public void addSkeleton(final Skeleton skeleton) {
+ _skeletons.add(skeleton);
+ }
+
+ public void setMeshVertMap(final Mesh geometry, final VertMap map) {
+ _meshVertMap.put(geometry, map);
+ }
+}
diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/JointNode.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/JointNode.java new file mode 100644 index 0000000..66a8c75 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/JointNode.java @@ -0,0 +1,54 @@ +/** + * 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.data; + +import java.util.List; + +import com.ardor3d.extension.animation.skeletal.Joint; +import com.ardor3d.scenegraph.Node; +import com.google.common.collect.Lists; + +public class JointNode { + private JointNode parent; + private final List<JointNode> children = Lists.newArrayList(); + private final Joint joint; + + /** Scene node associated with the Joint. */ + private Node sceneNode; + + public JointNode(final Joint joint) { + this.joint = joint; + } + + public List<JointNode> getChildren() { + return children; + } + + public Joint getJoint() { + return joint; + } + + public JointNode getParent() { + return parent; + } + + public void setParent(final JointNode parent) { + this.parent = parent; + } + + public Node getSceneNode() { + return sceneNode; + } + + public void setSceneNode(final Node node) { + sceneNode = node; + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/MaterialInfo.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/MaterialInfo.java new file mode 100644 index 0000000..91c918d --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/MaterialInfo.java @@ -0,0 +1,98 @@ +/** + * 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.data; + +import java.util.Map; + +import com.ardor3d.image.Texture; +import com.ardor3d.renderer.state.MaterialState; +import com.google.common.collect.Maps; + +public class MaterialInfo { + + private String _materialName; + private String _profile; + private String _technique; + private final Map<String, String> _textureReferences = Maps.newHashMap(); + private final Map<String, Texture> _textures = Maps.newHashMap(); + private final Map<String, String> _textureFileNames = Maps.newHashMap(); + private boolean _useTransparency; + private float _transparency = 1.0f; + + private MaterialState _materialState; + + public void setMaterialName(final String materialName) { + _materialName = materialName; + } + + public String getMaterialName() { + return _materialName; + } + + public void setProfile(final String profile) { + _profile = profile; + } + + public String getProfile() { + return _profile; + } + + public void setTechnique(final String technique) { + _technique = technique; + } + + public String getTechnique() { + return _technique; + } + + public void setTextureSlot(final String textureSlot, final String textureReference, final Texture texture, + final String fileName) { + _textureReferences.put(textureSlot, textureReference); + _textures.put(textureSlot, texture); + _textureFileNames.put(textureSlot, fileName); + } + + public Map<String, String> getTextureReferences() { + return _textureReferences; + } + + public Map<String, Texture> getTextures() { + return _textures; + } + + public Map<String, String> getTextureFileNames() { + return _textureFileNames; + } + + public void setUseTransparency(final boolean useTransparency) { + _useTransparency = useTransparency; + } + + public boolean isUseTransparency() { + return _useTransparency; + } + + public void setMaterialState(final MaterialState state) { + _materialState = state; + } + + public MaterialState getMaterialState() { + return _materialState; + } + + public float getTransparency() { + return _transparency; + } + + public void setTransparency(final float transparency) { + _transparency = transparency; + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/MeshVertPairs.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/MeshVertPairs.java new file mode 100644 index 0000000..95e1de0 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/MeshVertPairs.java @@ -0,0 +1,43 @@ +/** + * 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.data; + +import com.ardor3d.scenegraph.Mesh; + +/** + * A data object matching an Ardor3D Mesh object to the vertex indices it uses from the original <mesh><vertices> + * Collada data. This allows us to match weights, joint and such that are indexed to the original vertex positions to + * their final positions in the Mesh. + */ +public class MeshVertPairs { + /** + * The referenced Ardor3D Mesh. + */ + private final Mesh _mesh; + + /** + * The Collada indices. This array should be as big as the vertex count of the Mesh. + */ + private final int[] _indices; + + public MeshVertPairs(final Mesh mesh, final int[] indices) { + _mesh = mesh; + _indices = indices; + } + + public Mesh getMesh() { + return _mesh; + } + + public int[] getIndices() { + return _indices; + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/NodeType.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/NodeType.java new file mode 100644 index 0000000..93b8224 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/NodeType.java @@ -0,0 +1,18 @@ +/** + * 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.data; + +/** + * The 'type' attribute of a node element. + */ +public enum NodeType { + NODE, JOINT +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/SamplerTypes.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/SamplerTypes.java new file mode 100644 index 0000000..cd9c9cf --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/SamplerTypes.java @@ -0,0 +1,80 @@ +/** + * 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.data; + +import com.ardor3d.image.Texture.MagnificationFilter; +import com.ardor3d.image.Texture.MinificationFilter; +import com.ardor3d.image.Texture.WrapMode; + +/** + * Various enum types to be used when parsing <sampler> Collada tags. + */ +public abstract class SamplerTypes { + + /** + * Enum matching Collada's texture wrapping modes to Ardor3D's. + */ + public enum WrapModeType { + WRAP(WrapMode.Repeat), MIRROR(WrapMode.MirroredRepeat), CLAMP(WrapMode.EdgeClamp), BORDER(WrapMode.BorderClamp), NONE( + WrapMode.BorderClamp); + + final WrapMode _wm; + + private WrapModeType(final WrapMode ardorWrapMode) { + _wm = ardorWrapMode; + } + + public WrapMode getArdor3dWrapMode() { + return _wm; + } + } + + /** + * Enum matching Collada's texture minification modes to Ardor3D's. + */ + public enum MinFilterType { + NONE(MinificationFilter.NearestNeighborNoMipMaps), NEAREST(MinificationFilter.NearestNeighborNoMipMaps), LINEAR( + MinificationFilter.BilinearNoMipMaps), NEAREST_MIPMAP_NEAREST( + MinificationFilter.NearestNeighborNearestMipMap), LINEAR_MIPMAP_NEAREST( + MinificationFilter.BilinearNearestMipMap), NEAREST_MIPMAP_LINEAR( + MinificationFilter.NearestNeighborLinearMipMap), LINEAR_MIPMAP_LINEAR(MinificationFilter.Trilinear); + + final MinificationFilter _mf; + + private MinFilterType(final MinificationFilter ardorFilter) { + _mf = ardorFilter; + } + + public MinificationFilter getArdor3dFilter() { + return _mf; + } + } + + /** + * Enum matching Collada's texture magnification modes to Ardor3D's. + */ + public enum MagFilterType { + NONE(MagnificationFilter.NearestNeighbor), NEAREST(MagnificationFilter.NearestNeighbor), LINEAR( + MagnificationFilter.Bilinear), NEAREST_MIPMAP_NEAREST(MagnificationFilter.NearestNeighbor), LINEAR_MIPMAP_NEAREST( + MagnificationFilter.Bilinear), NEAREST_MIPMAP_LINEAR(MagnificationFilter.NearestNeighbor), LINEAR_MIPMAP_LINEAR( + MagnificationFilter.Bilinear); + + final MagnificationFilter _mf; + + private MagFilterType(final MagnificationFilter ardorFilter) { + _mf = ardorFilter; + } + + public MagnificationFilter getArdor3dFilter() { + return _mf; + } + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/SkinData.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/SkinData.java new file mode 100644 index 0000000..7fea027 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/SkinData.java @@ -0,0 +1,126 @@ +/** + * 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.data; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import com.ardor3d.annotation.SavableFactory; +import com.ardor3d.extension.animation.skeletal.SkeletonPose; +import com.ardor3d.extension.animation.skeletal.SkinnedMesh; +import com.ardor3d.scenegraph.Node; +import com.ardor3d.util.export.InputCapsule; +import com.ardor3d.util.export.OutputCapsule; +import com.ardor3d.util.export.Savable; +import com.google.common.collect.Lists; + +/** + * Data class used to hold references to useful objects created during parsing of a single Collada <skin> tag. + */ +@SavableFactory(factoryMethod = "initSavable") +public class SkinData implements Savable { + + private SkeletonPose _pose; + private Node _skinBaseNode; + private final List<SkinnedMesh> _skins = Lists.newArrayList(); + private final String _name; + + /** + * Construct a new SkinData object. + * + * @param name + * The name for our skin data store. Should be in the format <i>[controller name][ : ][instance + * controller name]</i>. The names of each element should be first the name attribute, if present, or + * second their sid/id, or blank if neither are specified. The " : " should be left out if one or both + * names are blank. If both names are blank, empty string should be used. + */ + public SkinData(final String name) { + _name = name; + } + + public void setPose(final SkeletonPose pose) { + _pose = pose; + } + + /** + * @return the skeletal pose created for this instance of <skin>. If there are multiple skinned meshes parsed as + * part of this skin, they will all share this same pose. + */ + public SkeletonPose getPose() { + return _pose; + } + + public void setSkinBaseNode(final Node skinBaseNode) { + _skinBaseNode = skinBaseNode; + } + + /** + * @return the Node created to hold all SkinnedMesh objects created when parsing this skin. + */ + public Node getSkinBaseNode() { + return _skinBaseNode; + } + + public List<SkinnedMesh> getSkins() { + return _skins; + } + + /** + * @return name + * @see #SkinData(String) + */ + public String getName() { + return _name; + } + + // ///////////////// + // Methods for Savable + // ///////////////// + + @Override + public Class<? extends SkinData> getClassTag() { + return this.getClass(); + } + + @Override + public void read(final InputCapsule capsule) throws IOException { + final String name = capsule.readString("name", ""); + try { + final Field field1 = SkinData.class.getDeclaredField("_name"); + field1.setAccessible(true); + field1.set(this, name); + } catch (final Exception e) { + e.printStackTrace(); + } + _skinBaseNode = (Node) capsule.readSavable("baseNode", null); + _skins.clear(); + _skins.addAll(capsule.readSavableList("skins", new ArrayList<SkinnedMesh>())); + _pose = (SkeletonPose) capsule.readSavable("pose", null); + } + + @Override + public void write(final OutputCapsule capsule) throws IOException { + capsule.write(_name, "name", ""); + capsule.write(_skinBaseNode, "baseNode", null); + capsule.writeSavableList(_skins, "skins", null); + capsule.write(_pose, "pose", null); + } + + public static SkinData initSavable() { + return new SkinData(); + } + + private SkinData() { + this(null); + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/TransformElement.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/TransformElement.java new file mode 100644 index 0000000..85dbfb5 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/data/TransformElement.java @@ -0,0 +1,41 @@ +/** + * 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.data; + +import java.util.Arrays; + +public class TransformElement { + private final double[] _array; + private final TransformElementType _type; + + public enum TransformElementType { + Translation, Rotation, Scale, Matrix, Lookat + } + + public TransformElement(final double[] array, final TransformElementType type) { + super(); + _array = array; + _type = type; + } + + @Override + public String toString() { + return "TransformElement [array=" + Arrays.toString(_array) + ", type=" + _type + "]"; + } + + public double[] getArray() { + return _array; + } + + public TransformElementType getType() { + return _type; + } +} diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/plugin/ColladaExtraPlugin.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/plugin/ColladaExtraPlugin.java new file mode 100644 index 0000000..49ce7bc --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/plugin/ColladaExtraPlugin.java @@ -0,0 +1,19 @@ +/**
+ * 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.plugin;
+
+import org.jdom2.Element;
+
+public interface ColladaExtraPlugin {
+
+ boolean processExtra(Element extra, Object[] params);
+
+}
diff --git a/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/plugin/GoogleEarthPlugin.java b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/plugin/GoogleEarthPlugin.java new file mode 100644 index 0000000..91ec7a8 --- /dev/null +++ b/ardor3d-collada/src/main/java/com/ardor3d/extension/model/collada/jdom/plugin/GoogleEarthPlugin.java @@ -0,0 +1,44 @@ +/**
+ * 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.plugin;
+
+import org.jdom2.Attribute;
+import org.jdom2.Element;
+
+import com.ardor3d.renderer.state.CullState;
+import com.ardor3d.scenegraph.Mesh;
+
+public class GoogleEarthPlugin implements ColladaExtraPlugin {
+
+ @Override
+ public boolean processExtra(final Element extra, final Object[] params) {
+ if (params.length > 0 && params[0] instanceof Mesh) {
+ final Mesh mesh = (Mesh) params[0];
+ // should have a child: <technique profile="GOOGLEEARTH">
+ final Element technique = extra.getChild("technique");
+ if (technique != null) {
+ final Attribute profile = technique.getAttribute("profile");
+ if (profile != null && "GOOGLEEARTH".equalsIgnoreCase(profile.getValue())) {
+ for (final Element child : technique.getChildren()) {
+ // disable back face culling if it's been enabled.
+ if ("double_sided".equalsIgnoreCase(child.getName()) && "1".equals(child.getTextTrim())) {
+ final CullState cs = new CullState();
+ cs.setEnabled(false);
+ mesh.setRenderState(cs);
+ }
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
|