/* * JaamSim Discrete Event Simulation * Copyright (C) 2012 Ausenco Engineering Canada Inc. * Copyright (C) 2015 JaamSim Software Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.collada; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Stack; import java.util.TreeSet; import com.jaamsim.MeshFiles.MeshData; import com.jaamsim.MeshFiles.MeshData.Trans; import com.jaamsim.MeshFiles.VertexMap; import com.jaamsim.math.Color4d; import com.jaamsim.math.Mat4d; import com.jaamsim.math.Quaternion; import com.jaamsim.math.Vec2d; import com.jaamsim.math.Vec3d; import com.jaamsim.math.Vec4d; import com.jaamsim.render.AnimCurve; import com.jaamsim.render.RenderException; import com.jaamsim.ui.LogBox; import com.jaamsim.xml.XmlNode; import com.jaamsim.xml.XmlParser; /** * Inspired by the Collada loader for Sweethome3d by Emmanuel Puybaret / eTeks <info@eteks.com>. */ public class ColParser { private static boolean SHOW_COL_DEBUG = false; private static final Effect DEFAULT_EFFECT; private static final String DEFAULT_MATERIAL_NAME = "JaamSimDefaultMaterial"; private static boolean keepRuntimeData = false; public static void setKeepData(boolean keepData) { keepRuntimeData = keepData; } static { DEFAULT_EFFECT = new Effect(); DEFAULT_EFFECT.diffuse = new ColorTex(); DEFAULT_EFFECT.diffuse.color = new Color4d(0.5, 0.5, 0.5, 1); DEFAULT_EFFECT.transType = MeshData.NO_TRANS; } public static MeshData parse(URI asset) throws RenderException { try { ColParser colParser = new ColParser(asset.toURL()); colParser.processContent(); return colParser.getData(); } catch (Exception e) { LogBox.renderLogException(e); throw new RenderException(e.getMessage()); } } private static void parseAssert(boolean b) { if (!b) { throw new RenderException("Failed Collada parsing assert"); } } static final List<String> DOUBLE_ARRAY_TAGS; static final List<String> INT_ARRAY_TAGS; static final List<String> STRING_ARRAY_TAGS; static final List<String> BOOLEAN_ARRAY_TAGS; static { DOUBLE_ARRAY_TAGS = new ArrayList<>(); DOUBLE_ARRAY_TAGS.add("float_array"); DOUBLE_ARRAY_TAGS.add("float"); DOUBLE_ARRAY_TAGS.add("rotate"); DOUBLE_ARRAY_TAGS.add("translate"); DOUBLE_ARRAY_TAGS.add("scale"); DOUBLE_ARRAY_TAGS.add("lookat"); DOUBLE_ARRAY_TAGS.add("matrix"); DOUBLE_ARRAY_TAGS.add("color"); DOUBLE_ARRAY_TAGS.add("bind_shape_matrix"); INT_ARRAY_TAGS = new ArrayList<>(); INT_ARRAY_TAGS.add("int_array"); INT_ARRAY_TAGS.add("vcount"); INT_ARRAY_TAGS.add("p"); INT_ARRAY_TAGS.add("h"); INT_ARRAY_TAGS.add("v"); STRING_ARRAY_TAGS = new ArrayList<>(); STRING_ARRAY_TAGS.add("Name_array"); STRING_ARRAY_TAGS.add("IDREF_array"); BOOLEAN_ARRAY_TAGS = new ArrayList<>(); BOOLEAN_ARRAY_TAGS.add("boolean_array"); } private static class FaceSubGeo { public VertexMap vMap; int[] indices; public FaceSubGeo(int size) { vMap = new VertexMap(); indices = new int[size]; } } private static class LineSubGeo { public final Vec4d[] verts; public String materialSymbol; public LineSubGeo(int size) { verts = new Vec4d[size]; } } private static class VisualScene { public final ArrayList<SceneNode> nodes = new ArrayList<>(); } private static class Controller { private Mat4d bindSpaceMat; private String geometry; } private static class Geometry { // Note: the face information is lazily baked when it is first referenced because that is the first time we // know which texture coordinate set to use (then error if it is later referenced with different coordinate sets) public final ArrayList<SubMeshDesc> faceSubDescs = new ArrayList<>(); public final ArrayList<LineSubGeo> lineSubGeos = new ArrayList<>(); } private static class AnimSampler { public String inputSource; public String outputSource; public String inTangentSource; public String outTangentSource; public String interpSource; } private static class AnimChannel { public String target; public String actionName; public AnimCurve curve; } private final URL _contextURL; private final HashMap<String, Geometry> _geos = new HashMap<>(); private final HashMap<String, String> _images = new HashMap<>(); // Maps image names to files private final HashMap<String, String> _materials = new HashMap<>(); // Maps materials to effects private final HashMap<String, Effect> _effects = new HashMap<>(); // List of known effects private final HashMap<String, SceneNode> _namedNodes = new HashMap<>(); private final HashMap<String, VisualScene> _visualScenes = new HashMap<>(); private final HashMap<String, Controller> _controllers = new HashMap<>(); private final HashMap<String, AnimSampler> _samplers = new HashMap<>(); private final HashMap<String, ArrayList<String>> _animClips = new HashMap<>(); // This stack is used to track node loops private final Stack<SceneNode> _nodeStack = new Stack<>(); // This list tracks the combinations of sub geometries and effects loaded in the mesh proto and defines an implicit // index into the mesh proto. This should probably be made more explicit later private final ArrayList<SubMeshDesc> _loadedFaceGeos = new ArrayList<>(); private final ArrayList<Effect> _loadedEffects = new ArrayList<>(); private final ArrayList<LineGeoEffectPair> _loadedLineGeos = new ArrayList<>(); private final ArrayList<AnimChannel> _animChannels = new ArrayList<>(); private final MeshData _finalData = new MeshData(keepRuntimeData); private final HashMap<String, Vec4d[]> _vec4dSources = new HashMap<>(); private final HashMap<String, double[][]> _dataSources = new HashMap<>(); private final HashMap<String, String[]> _stringSources = new HashMap<>(); private XmlNode _colladaNode; private XmlParser _parser; public ColParser(URL context) { _contextURL = context; } private XmlNode getNodeFromID(String fragID) { if (fragID.length() < 1 || fragID.charAt(0) != '#') { return null; } return _parser.getNodeByID(fragID.substring(1)); } private void processContent() { _parser = new XmlParser(_contextURL); _parser.setDoubleArrayTags(DOUBLE_ARRAY_TAGS); _parser.setIntArrayTags(INT_ARRAY_TAGS); _parser.setBooleanArrayTags(BOOLEAN_ARRAY_TAGS); _parser.setStringArrayTags(STRING_ARRAY_TAGS); long startTime = System.nanoTime(); _parser.parse(); long parseTime = System.nanoTime(); _colladaNode = _parser.getRootNode().findChildTag("COLLADA", false); parseAssert(_colladaNode != null); processGeos(); long geoTime = System.nanoTime(); processImages(); processMaterials(); processEffects(); processNodes(); processControllers(); processAnimationClips(); processAnimations(); processVisualScenes(); processScene(); long sceneTime = System.nanoTime(); double parseDurMS = (parseTime - startTime)/1000000.0; double geoDurMS = (geoTime - parseTime)/1000000.0; double sceneDurMS = (sceneTime - geoTime)/1000000.0; if (SHOW_COL_DEBUG) { LogBox.formatRenderLog("%s Parse: %.1f Geo: %.1f, Scene: %.1f\n", _contextURL.toString(), parseDurMS, geoDurMS, sceneDurMS); } } private double getScaleFactor() { XmlNode assetNode = _colladaNode.findChildTag("asset", false); if (assetNode == null) return 1; XmlNode unit = assetNode.findChildTag("unit", false); if (unit == null) return 1; String meter = unit.getAttrib("meter"); if (meter == null) return 1; return Double.parseDouble(meter); } private String getUpAxis() { XmlNode assetNode = _colladaNode.findChildTag("asset", false); if (assetNode == null) return "Y_UP"; XmlNode upAxisNode = assetNode.findChildTag("up_axis", false); if (upAxisNode == null) return "Y_UP"; String ret = (String)upAxisNode.getContent(); if (ret == null) { return "Y_UP"; } return ret; } /** * Returns a matrix that rotates which ever axis is specified into the Z axis (as JaamSim treats Z as up) * @return */ private Mat4d getGlobalRot() { String up = getUpAxis(); Mat4d ret = new Mat4d(); if (up.equals("Z_UP")) { return ret; } else if (up.equals("X_UP")) { ret.d00 = 0; ret.d01 = 0; ret.d02 = 1; ret.d10 = -1; ret.d11 = 0; ret.d12 = 0; ret.d20 = 0; ret.d21 = -1; ret.d22 = 0; return ret; } else { // Y_UP ret.d00 = 1; ret.d01 = 0; ret.d02 = 0; ret.d10 = 0; ret.d11 = 0; ret.d12 = -1; ret.d20 = 0; ret.d21 = 1; ret.d22 = 0; return ret; } } private void processScene() { for (AnimChannel chan : _animChannels) { attachChannelToScene(chan); } XmlNode scene = _colladaNode.findChildTag("scene", false); parseAssert(scene != null); XmlNode instVS = scene.findChildTag("instance_visual_scene", false); parseAssert(instVS != null); String vsURL = instVS.getAttrib("url"); parseAssert(vsURL.charAt(0) == '#'); Mat4d globalMat = getGlobalRot(); globalMat.scale3(getScaleFactor()); VisualScene vs = _visualScenes.get(vsURL.substring(1)); MeshData.TreeNode treeRoot = new MeshData.TreeNode(); treeRoot.trans = new MeshData.StaticTrans(globalMat); for (SceneNode sn : vs.nodes) { treeRoot.children.add(buildMeshTree(sn)); } _finalData.setTree(treeRoot); _finalData.finalizeData(); } private MeshData.TreeNode buildMeshTree(SceneNode node) { _nodeStack.push(node); // Create a series of TreeNodes, one for each transform MeshData.TreeNode ret = new MeshData.TreeNode(); MeshData.TreeNode leaf = ret; if (node.transforms.size() == 0) { // No transforms, fill in an identity transform in the output tree ret.trans = new MeshData.StaticTrans(new Mat4d()); } else { for (int i = 0; i < node.transforms.size(); ++i) { leaf.trans = node.transforms.get(i).toMeshDataTrans(); if (i < node.transforms.size()-1) { // If this isn't the last node, create a new one for the next iteration MeshData.TreeNode newNode = new MeshData.TreeNode(); leaf.children.add(newNode); leaf = newNode; } } } for (GeoInstInfo geoInfo : node.subGeo) { addGeoInstToTreeNode(geoInfo, leaf); } // Add instance_node for (String nodeName : node.subInstanceNames) { parseAssert(nodeName.charAt(0) == '#'); SceneNode instNode = _namedNodes.get(nodeName.substring(1)); // Check for reference loops, make sure this node is not currently in the active node stack parseAssert(!_nodeStack.contains(instNode)); parseAssert(instNode != null); node.subNodes.add(instNode); leaf.children.add(buildMeshTree(instNode)); } // Finally add children for (SceneNode nextNode : node.subNodes) { leaf.children.add(buildMeshTree(nextNode)); } _nodeStack.pop(); return ret; } private Effect geoBindingToEffect(Map<String, String> materialMap, String symbol) { if (symbol == DEFAULT_MATERIAL_NAME) { return DEFAULT_EFFECT; } String materialId = materialMap.get(symbol); parseAssert(materialId != null); return getEffectForMat(materialId); } private Effect getEffectForMat(String materialId) { parseAssert(materialId.charAt(0) == '#'); String effectId = _materials.get(materialId.substring(1)); parseAssert(effectId != null); parseAssert(effectId.charAt(0) == '#'); Effect effect = _effects.get(effectId.substring(1)); parseAssert(effect != null); return effect; } private void addGeoInstToTreeNode(GeoInstInfo geoInfo, MeshData.TreeNode node) { parseAssert(geoInfo.geoName.charAt(0) == '#'); Geometry geo = _geos.get(geoInfo.geoName.substring(1)); for (SubMeshDesc subGeo : geo.faceSubDescs) { // Check if this geometry and material pair has been loaded yet Effect effect = geoBindingToEffect(geoInfo.materialMap, subGeo.material); // Check that this instance of the subgeometry uses the same texture coordinate set as any previous if (geoInfo.usedTexSet != null) { if (subGeo.usedTexSet != null) { parseAssert(geoInfo.usedTexSet.intValue() == subGeo.usedTexSet.intValue()); } subGeo.usedTexSet = geoInfo.usedTexSet; } else { subGeo.usedTexSet = 0; } int geoID; if (_loadedFaceGeos.contains(subGeo)) { geoID = _loadedFaceGeos.indexOf(subGeo); } else { geoID = _loadedFaceGeos.size(); _loadedFaceGeos.add(subGeo); // Finally bake the face geometry information into a runtime format FaceSubGeo fsg = getFaceSubGeo(subGeo); _finalData.addSubMesh(fsg.vMap.getVertList(), fsg.indices); } int matID; if (_loadedEffects.contains(effect)) { matID = _loadedEffects.indexOf(effect); } else { matID = _loadedEffects.size(); _loadedEffects.add(effect); _finalData.addMaterial(effect.diffuse.texture, effect.diffuse.relTexture, effect.diffuse.color, effect.ambient, effect.spec, effect.shininess, effect.transType, effect.transColour); } MeshData.AnimMeshInstance inst = new MeshData.AnimMeshInstance(geoID, matID); node.meshInstances.add(inst); } for (LineSubGeo subGeo : geo.lineSubGeos) { // Check if this geometry and material pair has been loaded yet Effect effect = geoBindingToEffect(geoInfo.materialMap, subGeo.materialSymbol); LineGeoEffectPair ge = new LineGeoEffectPair(subGeo, effect); int geoID; if (_loadedLineGeos.contains(ge)) { geoID = _loadedLineGeos.indexOf(ge); } else { geoID = _loadedLineGeos.size(); _loadedLineGeos.add(ge); _finalData.addSubLine(subGeo.verts, effect.diffuse.color); } node.lineInstances.add(new MeshData.AnimLineInstance(geoID)); } } private void processVisualScenes() { XmlNode libScenes = _colladaNode.findChildTag("library_visual_scenes", false); if (libScenes == null) return; // No scenes for (XmlNode child : libScenes.children()) { if (child.getTag().equals("visual_scene")) { processVisualScene(child); } } } private void processVisualScene(XmlNode scene) { String id = scene.getFragID(); VisualScene vs = new VisualScene(); _visualScenes.put(id, vs); for (XmlNode child : scene.children()) { if (child.getTag().equals("node")) { SceneNode node = processNode(child, null); vs.nodes.add(node); } } } private void processControllers() { XmlNode libControllers = _colladaNode.findChildTag("library_controllers", false); if (libControllers == null) return; // No effects for (XmlNode child : libControllers.children()) { if (child.getTag().equals("controller")) { processController(child); } } } private void processController(XmlNode controller) { XmlNode skin = controller.findChildTag("skin", false); String id = controller.getFragID(); if (skin == null) { return; // We do not handle 'morph' controllers for now } Controller cont = new Controller(); String source = skin.getAttrib("source"); parseAssert(source != null); cont.geometry = source; XmlNode bindMat = skin.findChildTag("bind_space_matrix", false); if (bindMat == null) { cont.bindSpaceMat = new Mat4d(); // Default to identity } else { double[] data = (double[])bindMat.getContent(); parseAssert(data.length == 16); cont.bindSpaceMat = new Mat4d(data); } _controllers.put(id, cont); } private void processAnimationClips() { XmlNode libClips = _colladaNode.findChildTag("library_animation_clips", false); if (libClips == null) return; // No animations for (XmlNode child : libClips.children()) { if (child.getTag().equals("animation_clip")) { processAnimationClip(child); } } } private void processAnimationClip(XmlNode clipNode) { String clipName = clipNode.getAttrib("name"); if (clipName == null) { // Fall back to the ID if the name is not present clipName = clipNode.getFragID(); } if (clipName == null) { return; // We can not identify this clip so act like it doesn't exist } ArrayList<String> instanceAnims = new ArrayList<>(); _animClips.put(clipName, instanceAnims); for (XmlNode child : clipNode.children()) { if (child.getTag().equals("instance_animation")) { instanceAnims.add(child.getAttrib("url")); } } } private void processAnimations() { XmlNode libAnims = _colladaNode.findChildTag("library_animations", false); if (libAnims == null) return; // No animations for (XmlNode child : libAnims.children()) { if (child.getTag().equals("animation")) { processAnimation(child, "default"); } } } private void processAnimation(XmlNode animation, String actionName) { // See if this animation has been instanced in a clip, if so that clips name is the action name String animID = animation.getFragID(); if (animID != null) { for (Map.Entry<String, ArrayList<String>> entry : _animClips.entrySet()) { if (entry.getValue().contains("#" + animID)) { actionName = entry.getKey(); } } } for (XmlNode child : animation.children()) { if (child.getTag().equals("animation")) { processAnimation(child, actionName); // Recurse through child animations } if (child.getTag().equals("sampler")) { processSampler(child); } if (child.getTag().equals("channel")) { processChannel(child, actionName); } } } private void processSampler(XmlNode samplerNode) { AnimSampler sampler = new AnimSampler(); String id = samplerNode.getFragID(); for (XmlNode child : samplerNode.children()) { if (!child.getTag().equals("input")) continue; String semantic = child.getAttrib("semantic"); String source = child.getAttrib("source"); if (semantic.equals("INPUT")) sampler.inputSource = source; if (semantic.equals("OUTPUT")) sampler.outputSource = source; if (semantic.equals("IN_TANGENT")) sampler.inTangentSource = source; if (semantic.equals("OUT_TANGENT")) sampler.outTangentSource = source; if (semantic.equals("INTERPOLATION")) sampler.interpSource = source; } _samplers.put(id, sampler); } private void processChannel(XmlNode channelNode, String actionName) { String source = channelNode.getAttrib("source"); String target = channelNode.getAttrib("target"); parseAssert(source.charAt(0) == '#'); AnimSampler sampler = _samplers.get(source.substring(1)); parseAssert(sampler != null); AnimChannel c = new AnimChannel(); c.curve = buildAnimCurve(sampler); c.target = target; c.actionName = actionName; _animChannels.add(c); } private void processImages() { XmlNode libImage = _colladaNode.findChildTag("library_images", false); if (libImage == null) return; // No images for (XmlNode child : libImage.children()) { if (child.getTag().equals("image")) { processImage(child); } } } private void processImage(XmlNode imageNode) { // For now all we care about with images is the init_form contents and the name String id = imageNode.getFragID(); if (id == null) return; // We do not care about images we can not reference XmlNode initFrom = imageNode.findChildTag("init_from", true); if (initFrom == null) { parseAssert(false); return; } String fileName = (String)initFrom.getContent(); parseAssert(fileName != null); _images.put(id, fileName); } // According to the spec, image nodes can be in a lot of places private void scanNodeForImages(XmlNode node) { for (XmlNode child : node.children()) { if (child.getTag().equals("image")) { processImage(child); } } } private void processMaterials() { XmlNode libMats = _colladaNode.findChildTag("library_materials", false); if (libMats == null) return; // No materials for (XmlNode child : libMats.children()) { if (child.getTag().equals("material")) { processMaterial(child); } } } private void processMaterial(XmlNode matNode) { String id = matNode.getFragID(); if (id == null) return; // We do not care about materials we can not reference XmlNode instEffect = matNode.findChildTag("instance_effect", true); if (instEffect == null) { parseAssert(false); return; } String effectURL = instEffect.getAttrib("url"); if (effectURL == null) { parseAssert(false); return; } _materials.put(id, effectURL); } private void processEffects() { XmlNode libEffects = _colladaNode.findChildTag("library_effects", false); if (libEffects == null) return; // No effects for (XmlNode child : libEffects.children()) { if (child.getTag().equals("effect")) { processEffect(child); } } } private void processEffect(XmlNode effectNode) { String id = effectNode.getFragID(); if (id == null) return; // We do not care about materials we can not reference scanNodeForImages(effectNode); XmlNode profCommon = effectNode.findChildTag("profile_COMMON", true); if (profCommon == null) { parseAssert(false); return; // There is no common profile } scanNodeForImages(profCommon); HashMap<String, XmlNode> paramMap = new HashMap<>(); // Start by building a table of all params for (XmlNode child : profCommon.children()) { String tag = child.getTag(); if (tag.equals("newparam")) { String sid = child.getAttrib("sid"); if (sid != null) paramMap.put(sid, child); } } XmlNode technique = profCommon.findChildTag("technique", false); if (technique == null) { parseAssert(false); return; // There is no common profile } scanNodeForImages(technique); // Search technique for the kind of data we care about, for now find blinn, phong or lambert XmlNode diffuse = null; XmlNode ambient = null; XmlNode spec = null; XmlNode shininess = null; XmlNode transparency = null; XmlNode transparent = null; XmlNode blinn = technique.findChildTag("blinn", false); XmlNode phong = technique.findChildTag("phong", false); XmlNode lambert = technique.findChildTag("lambert", false); XmlNode constant = technique.findChildTag("constant", false); if (blinn != null) { diffuse = blinn.findChildTag("diffuse", false); ambient = blinn.findChildTag("ambient", false); spec = blinn.findChildTag("specular", false); shininess = blinn.findChildTag("shininess", false); transparency = blinn.findChildTag("transparency", false); transparent = blinn.findChildTag("transparent", false); } if (phong != null) { diffuse = phong.findChildTag("diffuse", false); ambient = phong.findChildTag("ambient", false); spec = phong.findChildTag("specular", false); shininess = phong.findChildTag("shininess", false); transparency = phong.findChildTag("transparency", false); transparent = phong.findChildTag("transparent", false); } if (lambert != null) { diffuse = lambert.findChildTag("diffuse", false); ambient = lambert.findChildTag("ambient", false); transparency = lambert.findChildTag("transparency", false); transparent = lambert.findChildTag("transparent", false); } if (constant != null) { diffuse = constant.findChildTag("emission", false); transparency = constant.findChildTag("transparency", false); transparent = constant.findChildTag("transparent", false); } // Now either parse diffuse as a color value or texture... Effect effect = new Effect(); ColorTex diffuseCT = null; if (diffuse == null) { diffuseCT = new ColorTex(); diffuseCT.color = new Color4d(); } else { diffuseCT = getColorTex(diffuse, paramMap); } effect.diffuse = diffuseCT; effect.spec = getColor(spec); effect.ambient = getColor(ambient); effect.shininess = getFloat(shininess, 1.0); double alpha = 1.0; if (transparency != null) { XmlNode floatNode = transparency.findChildTag("float", false); parseAssert(floatNode != null); double[] floats = (double[])floatNode.getContent(); parseAssert(floats != null && floats.length >= 1); alpha = floats[0]; } String opaque = null; ColorTex transparentCT = null; if (transparent != null) { opaque = transparent.getAttrib("opaque"); if (opaque != null) { parseAssert((opaque.equals("A_ONE") || opaque.equals("RGB_ZERO"))); } transparentCT = getColorTex(transparent, paramMap); } if (opaque == null) { opaque = "A_ONE"; if (alpha == 0.0) { // This model did not specify a transparency mode and has an alpha of 0 // This should lead to a completely transparent material, but this is an encountered bug // in sketchup exported models, so we will assume they want it to be opaque alpha = 1.0; } } // There is a ton of conditions for us to handle transparency if (transparentCT != null) { if (transparentCT.color != null) { effect.transColour = new Color4d(transparentCT.color); // Solid transparent color if (opaque.equals("A_ONE")) { effect.transType = MeshData.A_ONE_TRANS; } if (opaque.equals("RGB_ZERO")) { effect.transType = MeshData.RGB_ZERO_TRANS; // Handle the weird luminance term for alpha in RGB_ZERO effect.transColour.a = effect.transColour.r * 0.212671 + effect.transColour.g * 0.715160 + effect.transColour.b * 0.072169; } // Bake the transparency term into the colour effect.transColour.r *= alpha; effect.transColour.g *= alpha; effect.transColour.b *= alpha; effect.transColour.a *= alpha; if ((effect.transColour.a >= 0.999 && opaque.equals("A_ONE")) || (effect.transColour.a <= 0.001 && opaque.equals("RGB_ZERO")) ) { effect.transType = MeshData.NO_TRANS; // Some meshes are effectively not transparent despite having the information } } else { // Transparent texture, we only support a very limited sub set of possible collada alpha mapping, // specifically only if the texture used is the alpha channel of the diffuse texture // We do not support trasparent textures and RGB_ZERO mode parseAssert(opaque.equals("A_ONE")); parseAssert(alpha == 1.0); // We do not support variable transparency with transparent textures parseAssert(transparentCT.texture != null); parseAssert(transparentCT.texture.equals(effect.diffuse.texture)); effect.transType = MeshData.DIFF_ALPHA_TRANS; } } else { effect.transType = MeshData.NO_TRANS; } _effects.put(id, effect); } // Parse a Color4d from the xml node private Color4d getColor(XmlNode node) { if (node == null) return new Color4d(); if (node.getNumChildren() != 1) { parseAssert(false); return null; } XmlNode valNode = node.getChild(0); String tag = valNode.getTag(); if (!tag.equals("color")) { return null; } double[] colVals = (double[])valNode.getContent(); parseAssert(colVals != null && colVals.length >= 4); Color4d col = new Color4d(colVals[0], colVals[1], colVals[2], colVals[3]); return col; } // Parse a floating point value from the xml node private double getFloat(XmlNode node, double def) { if (node == null) return def; if (node.getNumChildren() != 1) { parseAssert(false); return def; } XmlNode valNode = node.getChild(0); String tag = valNode.getTag(); parseAssert(tag.equals("float")); double[] vals = (double[])valNode.getContent(); parseAssert(vals != null && vals.length >= 1); return vals[0]; } private ColorTex getColorTex(XmlNode node, HashMap<String, XmlNode> paramMap) { if (node.getNumChildren() != 1) { parseAssert(false); return null; } XmlNode valNode = node.getChild(0); String tag = valNode.getTag(); ColorTex ret = new ColorTex(); if (tag.equals("color")) { double[] colVals = (double[])valNode.getContent(); parseAssert(colVals != null && colVals.length >= 4); Color4d col = new Color4d(colVals[0], colVals[1], colVals[2], colVals[3]); ret.color = col; return ret; } if (!tag.equals("texture")) { parseAssert(false); return null; } // Now we have the fun dealing with COLLADA's incredible indirectness String texName = valNode.getAttrib("texture"); String texCoord = valNode.getAttrib("texcoord"); // Find this sampler in the map XmlNode sampler = paramMap.get(texName); parseAssert(sampler != null); XmlNode sampler2D = sampler.findChildTag("sampler2D", false); parseAssert(sampler2D != null); XmlNode source = sampler2D.findChildTag("source", false); parseAssert(source != null); String surfaceName = (String)source.getContent(); XmlNode surfaceParam = paramMap.get(surfaceName); XmlNode surface = surfaceParam.findChildTag("surface", false); parseAssert(surface != null); parseAssert(surface.getAttrib("type").equals("2D")); XmlNode initFrom = surface.findChildTag("init_from", false); parseAssert(initFrom != null); String imageName = (String)initFrom.getContent(); String img = _images.get(imageName); parseAssert(img != null); try { ret.texture = new URL(_contextURL, img).toURI(); ret.relTexture = img; ret.texCoordName = texCoord; } catch (MalformedURLException ex) { LogBox.renderLogException(ex); parseAssert(false); } catch (URISyntaxException ex) { LogBox.renderLogException(ex); parseAssert(false); } return ret; } private void processGeos() { XmlNode libGeo = _colladaNode.findChildTag("library_geometries", false); if (libGeo == null) return; // No geometries for (XmlNode child : libGeo.children()) { if (child.getTag().equals("geometry")) { processGeo(child); } } } private void processGeo(XmlNode geoNode) { String geoID = geoNode.getFragID(); if (geoID == null) { // This geometry can not be referenced, don't bother return; } Geometry geoData = new Geometry(); for (XmlNode meshNode : geoNode.children()) { if (meshNode.getTag().equals("mesh")) { parseMesh(meshNode, geoData); } } _geos.put(geoID, geoData); } private void processNodes() { XmlNode libNodes = _colladaNode.findChildTag("library_nodes", false); if (libNodes == null) return; // No images for (XmlNode child : libNodes.children()) { if (child.getTag().equals("node")) { processNode(child, null); } } } private SceneNode processNode(XmlNode node, SceneNode parent) { SceneNode sn = new SceneNode(node.getFragID(), node.getAttrib("sid")); if (sn.id != null) _namedNodes.put(sn.id, sn); if (parent != null) { parent.subNodes.add(sn); } // Build up the transformation matrix for this node for (XmlNode child : node.children()) { String childTag = child.getTag(); if (childTag.equals("translate")) { sn.transforms.add(new TranslationTrans(child)); } if (childTag.equals("rotate")) { sn.transforms.add(new RotationTrans(child)); } if (childTag.equals("scale")) { sn.transforms.add(new ScaleTrans(child)); } if (childTag.equals("matrix")) { sn.transforms.add(new MatrixTrans(child)); } } // Now handle sub geometry, sub nodes and instance nodes for (XmlNode child : node.children()) { String childTag = child.getTag(); if (childTag.equals("instance_geometry")) { GeoInstInfo geoInfo = processInstGeo(child); sn.subGeo.add(geoInfo); } if (childTag.equals("instance_controller")) { SceneNode contNode = processInstController(child); sn.subNodes.add(contNode); } if (childTag.equals("instance_node")) { String nodeID = child.getAttrib("url"); parseAssert(nodeID != null); sn.subInstanceNames.add(nodeID); } if (childTag.equals("node")) { processNode(child, sn); } } return sn; } private SceneNode processInstController(XmlNode instCont) { String controllerURL = instCont.getAttrib("url"); parseAssert(controllerURL.charAt(0) == '#'); Controller cont = _controllers.get(controllerURL.substring(1)); GeoInstInfo instInfo = new GeoInstInfo(); instInfo.geoName = cont.geometry; XmlNode bindMat = instCont.findChildTag("bind_material", false); if (bindMat != null) { addMatInfoToGeoInst(bindMat, instInfo); } // Now we need to add in an extra scene not to accommodate the bind space matrix held in the controller SceneNode sn = new SceneNode(null, null); sn.transforms.add(new MatrixTrans(cont.bindSpaceMat)); sn.subGeo.add(instInfo); return sn; } private GeoInstInfo processInstGeo(XmlNode instGeo) { GeoInstInfo instInfo = new GeoInstInfo(); instInfo.geoName = instGeo.getAttrib("url"); XmlNode bindMat = instGeo.findChildTag("bind_material", false); if (bindMat != null) { addMatInfoToGeoInst(bindMat, instInfo); } return instInfo; } private void addMatInfoToGeoInst(XmlNode bindMat, GeoInstInfo instInfo) { XmlNode techCommon = bindMat.findChildTag("technique_common", false); parseAssert(techCommon != null); for (XmlNode instMat : techCommon.children()) { if (!instMat.getTag().equals("instance_material")) { continue; } String symbol = instMat.getAttrib("symbol"); String target = instMat.getAttrib("target"); parseAssert(symbol != null && target != null); instInfo.materialMap.put(symbol, target); Effect eff = getEffectForMat(target); // Make sure that if the asset is requesting a particular texture coordinate set, that it's set 0 (the only one we support) for (XmlNode sub : instMat.children()) { if (!sub.getTag().equals("bind_vertex_input")) { continue; } if (eff.diffuse.texture == null) { // This effect does not use a diffuse texture, we do not care continue; } // Find the texcoord we want for this material String texCoordName = eff.diffuse.texCoordName; String semantic = sub.getAttrib("semantic"); if (texCoordName == null || !texCoordName.equals(semantic)) { // We don't care about this binding continue; } int texSet = 0; String texSetString = sub.getAttrib("input_set"); if (texSetString != null) { texSet = Integer.parseInt(texSetString); } if (instInfo.usedTexSet != null) { // For now only one texture set can be used per instance parseAssert(instInfo.usedTexSet == texSet); } instInfo.usedTexSet = texSet; } } } private void parseMesh(XmlNode mesh, Geometry geoData) { // Now try to parse geometry type for (XmlNode subGeo : mesh.children()) { String geoTag = subGeo.getTag(); if (geoTag.equals("polylist") || geoTag.equals("polygons") || geoTag.equals("triangles")) { generateTriangleGeo(subGeo, geoData); } if (geoTag.equals("lines") || geoTag.equals("linestrip")) { generateLineGeo(subGeo, geoData); } } } private void generateLineGeo(XmlNode subGeo, Geometry geoData) { String geoTag = subGeo.getTag(); SubMeshDesc smd = readGeometryInputs(subGeo); if (geoTag.equals("lines")) { parseLines(smd, subGeo); } if (geoTag.equals("linestrip")) { parseLinestrip(smd, subGeo); } int numVerts = smd.posDesc.indices.length; parseAssert(numVerts % 2 == 0); // Now the SubMeshDesc should be fully populated, and we can actually produce the final triangle arrays LineSubGeo lsg = new LineSubGeo(numVerts); Vec4d[] posData = getVec4dArrayFromSource(smd.posDesc.source); lsg.materialSymbol = subGeo.getAttrib("material"); if (lsg.materialSymbol == null) { lsg.materialSymbol = DEFAULT_MATERIAL_NAME; } for (int i = 0; i < numVerts; ++i) { lsg.verts[i] = posData[smd.posDesc.indices[i]]; lsg.verts[i].w = 1; } geoData.lineSubGeos.add(lsg); } private Vec3d generateNormal(Vec4d p0, Vec4d p1, Vec4d p2, Vec4d t0, Vec4d t1) { t0.sub3(p1, p0); t1.sub3(p2, p0); Vec3d norm = new Vec3d(); norm.cross3(t0, t1); norm.normalize3(); return norm; } private void generateTriangleGeo(XmlNode subGeo, Geometry geoData) { SubMeshDesc smd = getSubMeshDesc(subGeo, geoData); if (smd.posDesc.indices.length == 0) { return; // There is no actual geometry here } String material = subGeo.getAttrib("material"); if (material == null) { material = DEFAULT_MATERIAL_NAME; } smd.material = material; geoData.faceSubDescs.add(smd); } private SubMeshDesc getSubMeshDesc(XmlNode subGeo, Geometry geoData) { String geoTag = subGeo.getTag(); SubMeshDesc smd = readGeometryInputs(subGeo); if (geoTag.equals("triangles")) { parseTriangles(smd, subGeo); } if (geoTag.equals("polylist")) { parsePolylist(smd, subGeo); } if (geoTag.equals("polygons")) { parsePolygons(smd, subGeo); } return smd; } private FaceSubGeo getFaceSubGeo(SubMeshDesc smd) { boolean hasNormal = smd.normDesc != null; int numVerts = smd.posDesc.indices.length; parseAssert(numVerts % 3 == 0); if (numVerts == 0) { return null; } // Now the SubMeshDesc should be fully populated, and we can actually produce the final triangle arrays FaceSubGeo fsg = new FaceSubGeo(numVerts); Vec4d[] posData = getVec4dArrayFromSource(smd.posDesc.source); Vec4d[] normData = null; if (hasNormal) { normData = getVec4dArrayFromSource(smd.normDesc.source); } boolean hasTexCoords = false; DataDesc texSetDesc = null; Vec4d[] texCoordData = null; if (smd.usedTexSet != null) { texSetDesc = smd.texCoordMap.get(smd.usedTexSet); if (texSetDesc != null) { hasTexCoords = true; texCoordData = getVec4dArrayFromSource(texSetDesc.source); } } Vec4d t0 = new Vec4d(); Vec4d t1 = new Vec4d(); Vec3d[] generatedNormals = null; if (!hasNormal) { // Generate one normal per face generatedNormals = new Vec3d[numVerts/3]; for (int i = 0; i < numVerts / 3; ++i) { Vec4d p0 = posData[smd.posDesc.indices[i*3 + 0]]; Vec4d p1 = posData[smd.posDesc.indices[i*3 + 1]]; Vec4d p2 = posData[smd.posDesc.indices[i*3 + 2]]; Vec3d norm = generateNormal(p0, p1, p2, t0, t1); generatedNormals[i] = norm; } } for (int i = 0; i < numVerts; ++i) { Vec3d pos = new Vec3d(posData[smd.posDesc.indices[i]]); Vec3d normal = null; if (hasNormal) { // Make sure the normal is actually present, treat negative indices as missing normals int normInd = smd.normDesc.indices[i]; if (normInd < 0) { // We need to generate one int triInd = i/3; Vec4d p0 = posData[smd.posDesc.indices[triInd*3 + 0]]; Vec4d p1 = posData[smd.posDesc.indices[triInd*3 + 1]]; Vec4d p2 = posData[smd.posDesc.indices[triInd*3 + 2]]; normal = generateNormal(p0, p1, p2, t0, t1); } else { normal = new Vec3d(normData[normInd]); } } else { normal = generatedNormals[i/3]; } Vec2d texCoord = null; if (hasTexCoords) { texCoord = new Vec2d(texCoordData[texSetDesc.indices[i]]); } fsg.indices[i] = fsg.vMap.getVertIndex(pos, normal, texCoord); } return fsg; } private void readVertices(SubMeshDesc smd, int offset, XmlNode vertices) { // Check vertices for inputs for (XmlNode input : vertices.children()) { if (input.getTag() != "input") { continue; } String semantic = input.getAttrib("semantic"); String source = input.getAttrib("source"); if (source == null || semantic == null) throw new ColException("Bad Vertex Input tag: " + input.getFragID() + " in mesh."); if (semantic.equals("POSITION")) { smd.posDesc = new DataDesc(); smd.posDesc.source = source; smd.posDesc.offset = offset; } if (semantic.equals("NORMAL")) { smd.normDesc = new DataDesc(); smd.normDesc.source = source; smd.normDesc.offset = offset; } } } private void parseLines(SubMeshDesc smd, XmlNode subGeo) { int count = Integer.parseInt(subGeo.getAttrib("count")); XmlNode pNode = subGeo.findChildTag("p", false); if (pNode == null) throw new ColException("No 'p' child in 'lines' in mesh."); int[] ps = (int[])pNode.getContent(); if (ps.length < count * 2 * smd.stride) { // Collada error, there is not enough indices for the apparent count, but we will play along all the same count = ps.length / (2 * smd.stride); } smd.posDesc.indices = new int[count*2]; for (int i = 0; i < count * 2; ++i) { int offset = i * smd.stride; smd.posDesc.indices[i] = ps[offset + smd.posDesc.offset]; } } private void parseLinestrip(SubMeshDesc smd, XmlNode subGeo) { int count = Integer.parseInt(subGeo.getAttrib("count")); // There should be 'count' number of 'p' tags in this element int[][] stripIndices = new int[count][]; int numLines = 0; int nextIndex = 0; for (XmlNode child : subGeo.children()) { if (!child.getTag().equals("p")) continue; int[] ps = (int[])child.getContent(); parseAssert(ps != null); parseAssert(nextIndex < count); stripIndices[nextIndex++] = ps; numLines += ps.length - 1; } // We now have a list of list of all indices, split this into lines int nextWriteIndex = 0; smd.posDesc.indices = new int[numLines * 2]; for (int[] strip : stripIndices) { parseAssert(strip.length >= 2); for (int i = 1; i < strip.length; ++i) { smd.posDesc.indices[nextWriteIndex++] = strip[i-1]; smd.posDesc.indices[nextWriteIndex++] = strip[i]; } } } // Fill in the indices for 'smd' private void parseTriangles(SubMeshDesc smd, XmlNode subGeo) { int count = Integer.parseInt(subGeo.getAttrib("count")); XmlNode pNode = subGeo.findChildTag("p", false); if (count == 0) { smd.posDesc.indices = new int[0]; return; } if (pNode == null) throw new ColException("No 'p' child in 'triangles' in mesh."); int[] ps = (int[])pNode.getContent(); parseAssert(ps.length >= count * 3 * smd.stride); smd.posDesc.indices = new int[count*3]; if (smd.normDesc != null) { smd.normDesc.indices = new int[count*3]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices = new int[count*3]; } for (int i = 0; i < count * 3; ++i) { int offset = i * smd.stride; smd.posDesc.indices[i] = ps[offset + smd.posDesc.offset]; if (smd.normDesc != null) { smd.normDesc.indices[i] = ps[offset + smd.normDesc.offset]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices[i] = ps[offset + texDesc.offset]; } } } // Note, this is definitely not correct, but for now assume all polygons are convex private void parsePolylist(SubMeshDesc smd, XmlNode subGeo) { int count = Integer.parseInt(subGeo.getAttrib("count")); XmlNode pNode = subGeo.findChildTag("p", false); if (pNode == null) throw new ColException("No 'p' child in 'polygons' in mesh."); int[] ps = (int[])pNode.getContent(); XmlNode vcountNode = subGeo.findChildTag("vcount", false); int[] vcounts; if (vcountNode != null) vcounts = (int[])vcountNode.getContent(); else vcounts = new int[0]; parseAssert(vcounts.length == count); int totalVerts = 0; int numTriangles = 0; for (int i : vcounts) { if (i == 0) { continue; } totalVerts += i; numTriangles += (i-2); } parseAssert(ps.length >= totalVerts * smd.stride); smd.posDesc.indices = new int[numTriangles * 3]; if (smd.normDesc != null) { smd.normDesc.indices = new int[numTriangles * 3]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices = new int[numTriangles*3]; } int nextWriteVert = 0; int readVertOffset = 0; for (int v : vcounts) { if (v == 0) { continue; } // v is the number of vertices in this polygon parseAssert(v >= 3); for (int i = 0; i < (v-2); ++i) { int vert0 = readVertOffset + i; int vert1 = readVertOffset + i + 1; int vert2 = readVertOffset + v - 1; smd.posDesc.indices[nextWriteVert] = ps[(vert0*smd.stride) + smd.posDesc.offset]; if (smd.normDesc != null) { smd.normDesc.indices[nextWriteVert] = ps[(vert0*smd.stride) + smd.normDesc.offset]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices[nextWriteVert] = ps[(vert0*smd.stride) + texDesc.offset]; } nextWriteVert++; smd.posDesc.indices[nextWriteVert] = ps[(vert1*smd.stride) + smd.posDesc.offset]; if (smd.normDesc != null) { smd.normDesc.indices[nextWriteVert] = ps[(vert1*smd.stride) + smd.normDesc.offset]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices[nextWriteVert] = ps[(vert1*smd.stride) + texDesc.offset]; } nextWriteVert++; smd.posDesc.indices[nextWriteVert] = ps[(vert2*smd.stride) + smd.posDesc.offset]; if (smd.normDesc != null) { smd.normDesc.indices[nextWriteVert] = ps[(vert2*smd.stride) + smd.normDesc.offset]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices[nextWriteVert] = ps[(vert2*smd.stride) + texDesc.offset]; } nextWriteVert++; } readVertOffset += v; } } // Note, this is definitely not correct, but for now assume all polygons are convex private void parsePolygons(SubMeshDesc smd, XmlNode subGeo) { int numTriangles = 0; // Find the number of triangles, for this we will need to iterate over all the polygons for (XmlNode n : subGeo.children()) { // Note: we do not support 'ph' tags (polygons with holes) if (!n.getTag().equals("p")) { continue; } int[] ps = (int[])n.getContent(); int numVerts = ps.length / smd.stride; parseAssert( (ps.length % smd.stride) == 0); parseAssert(numVerts >= 3); numTriangles += numVerts - 2; } smd.posDesc.indices = new int[numTriangles * 3]; if (smd.normDesc != null) { smd.normDesc.indices = new int[numTriangles * 3]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices = new int[numTriangles*3]; } int nextWriteVert = 0; for (XmlNode n : subGeo.children()) { // Note: we do not support 'ph' tags (polygons with holes) if (!n.getTag().equals("p")) { continue; } int[] ps = (int[])n.getContent(); for(int i = 0; i < (ps.length / smd.stride) - 2; ++i) { int vert0 = 0; int vert1 = i + 1; int vert2 = i + 2; smd.posDesc.indices[nextWriteVert] = ps[(vert0*smd.stride) + smd.posDesc.offset]; if (smd.normDesc != null) { smd.normDesc.indices[nextWriteVert] = ps[(vert0*smd.stride) + smd.normDesc.offset]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices[nextWriteVert] = ps[(vert0*smd.stride) + texDesc.offset]; } nextWriteVert++; smd.posDesc.indices[nextWriteVert] = ps[(vert1*smd.stride) + smd.posDesc.offset]; if (smd.normDesc != null) { smd.normDesc.indices[nextWriteVert] = ps[(vert1*smd.stride) + smd.normDesc.offset]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices[nextWriteVert] = ps[(vert1*smd.stride) + texDesc.offset]; } nextWriteVert++; smd.posDesc.indices[nextWriteVert] = ps[(vert2*smd.stride) + smd.posDesc.offset]; if (smd.normDesc != null) { smd.normDesc.indices[nextWriteVert] = ps[(vert2*smd.stride) + smd.normDesc.offset]; } for (DataDesc texDesc : smd.texCoordMap.values()) { texDesc.indices[nextWriteVert] = ps[(vert2*smd.stride) + texDesc.offset]; } nextWriteVert++; } } } private SubMeshDesc readGeometryInputs(XmlNode subGeo) { SubMeshDesc smd = new SubMeshDesc(); int maxOffset = 0; for (XmlNode input : subGeo.children()) { if (!input.getTag().equals("input")) { continue; } String semantic = input.getAttrib("semantic"); String source = input.getAttrib("source"); if (source == null || semantic == null) throw new ColException("Bad Geometry Input tag: " + input.getFragID()); int offset = Integer.parseInt(input.getAttrib("offset")); if (offset > maxOffset) { maxOffset = offset; } if (semantic.equals("VERTEX")) { XmlNode vertices = getNodeFromID(source); readVertices(smd, offset, vertices); } if (semantic.equals("NORMAL")) { smd.normDesc = new DataDesc(); smd.normDesc.source = source; smd.normDesc.offset = offset; } String setString = input.getAttrib("set"); int set = 0; if (setString != null) set = Integer.parseInt(setString); if (semantic.equals("TEXCOORD") || semantic.equals("TEXCOORD0")) { DataDesc texDesc = new DataDesc(); texDesc.source = source; texDesc.offset = offset; smd.texCoordMap.put(set, texDesc); } } if (smd.posDesc == null) { throw new ColException("Could not find positions for mesh."); } smd.stride = maxOffset+1; return smd; } private static class SourceInfo { int stride; int offset; int count; int dataCount; Object dataArray; } private SourceInfo getInfoFromSource(String id, String arrayName) { SourceInfo ret = new SourceInfo(); // Okay, this source hasn't be accessed yet XmlNode sourceNode = getNodeFromID(id); if (sourceNode == null) { throw new ColException("Could not find node with id: " + id); } XmlNode dataNode = sourceNode.findChildTag(arrayName, false); if (dataNode == null) { throw new ColException("No float array in source: " + id); } ret.dataCount = Integer.parseInt(dataNode.getAttrib("count")); ret.dataArray = dataNode.getContent(); XmlNode techCommon = sourceNode.findChildTag("technique_common", false); if (techCommon == null) { throw new ColException("No technique_common in source: " + id); } XmlNode accessor = techCommon.findChildTag("accessor", false); if (accessor == null) { throw new ColException("No accessor in source: " + id); } ret.stride = Integer.parseInt(accessor.getAttrib("stride")); ret.count = Integer.parseInt(accessor.getAttrib("count")); String offsetString = accessor.getAttrib("offset"); if (offsetString == null) { ret.offset = 0; } else { ret.offset = Integer.parseInt(offsetString); } parseAssert(ret.dataCount >= ret.count * ret.stride); return ret; } /** * Return a meaningful list of Vectors from data source 'id' * @param id * @return */ private Vec4d[] getVec4dArrayFromSource(String id) { Vec4d[] cached = _vec4dSources.get(id); if (cached != null) return cached; double[][] data = getDataArrayFromSource(id); // convert to vec4ds Vec4d[] ret = new Vec4d[data.length]; for (int i = 0; i < data.length; ++i) { double[] ds = data[i]; switch (ds.length) { case 1: ret[i] = new Vec4d(ds[0], 0, 0, 1); break; case 2: ret[i] = new Vec4d(ds[0], ds[1], 0, 1); break; case 3: ret[i] = new Vec4d(ds[0], ds[1], ds[2], 1); break; case 4: ret[i] = new Vec4d(ds[0], ds[1], ds[2], ds[3]); break; default: throw new RenderException(String.format("Invalid number of elements in data Vector: %d", ds.length)); } } _vec4dSources.put(id, ret); return ret; } private double[][] getDataArrayFromSource(String id) { // First check the cache double[][] cached = _dataSources.get(id); if (cached != null) { return cached; } SourceInfo info = getInfoFromSource(id, "float_array"); double[][] ret = new double[info.count][]; double[] values = null; try { values = (double[])info.dataArray; } catch (ClassCastException ex) { parseAssert(false); } int valueOffset = info.offset; for (int i = 0; i < info.count; ++i) { ret[i] = new double[info.stride]; for (int j = 0; j < info.stride; j++) { ret[i][j] = values[valueOffset + j]; } valueOffset += info.stride; } _dataSources.put(id, ret); return ret; } private String[] getStringArrayFromSource(String id) { // First check the cache String[] cached = _stringSources.get(id); if (cached != null) { return cached; } SourceInfo info = getInfoFromSource(id, "Name_array"); String[] ret = new String[info.count]; String[] values = null; try { values = (String[])info.dataArray; } catch (ClassCastException ex) { parseAssert(false); } int valueOffset = info.offset; for (int i = 0; i < info.count; ++i) { ret[i] = values[valueOffset]; valueOffset += info.stride; } _stringSources.put(id, ret); return ret; } private AnimCurve buildAnimCurve(AnimSampler samp) { AnimCurve.ColCurve colData = new AnimCurve.ColCurve(); double[][] inTemp = getDataArrayFromSource(samp.inputSource); colData.in = new double[inTemp.length]; for (int i = 0; i < inTemp.length; ++i) { colData.in[i] = inTemp[i][0]; } colData.out = getDataArrayFromSource(samp.outputSource); colData.interp = getStringArrayFromSource(samp.interpSource); SourceInfo outInfo = getInfoFromSource(samp.outputSource, "float_array"); colData.numComponents = outInfo.stride; if (samp.inTangentSource != null) { colData.inTan = getDataArrayFromSource(samp.inTangentSource); } if (samp.outTangentSource != null) { colData.outTan = getDataArrayFromSource(samp.outTangentSource); } parseAssert(colData.in.length == colData.out.length); parseAssert(colData.in.length == colData.interp.length); parseAssert(colData.inTan == null || colData.in.length == colData.inTan.length); parseAssert(colData.outTan == null || colData.in.length == colData.outTan.length); AnimCurve ret = AnimCurve.buildFromColCurve(colData); parseAssert(ret != null); return ret; } // Scan the scene, find the transform targeted and attach the AnimCurve to that transform private void attachChannelToScene(AnimChannel ch) { // Start by parsing the target int dotInd = ch.target.indexOf('.'); if (dotInd == -1) dotInd = ch.target.length(); int braceInd = ch.target.indexOf('('); if (braceInd == -1) braceInd = ch.target.length(); int targetInd = Math.min(dotInd, braceInd); String target = ch.target.substring(targetInd); String[] pathSegments = ch.target.substring(0, targetInd).split("/"); parseAssert(pathSegments.length > 0); SceneNode rootNode = _namedNodes.get(pathSegments[0]); Object currentNode = rootNode; for (int i = 1; i < pathSegments.length; ++i) { // Now scan the scene tree for the rest of the path parseAssert(currentNode instanceof SceneNode); SceneNode sn = (SceneNode)currentNode; currentNode = findSubNodeBySID(sn, pathSegments[i]); parseAssert(currentNode != null); } // We have now scanned the path and have the final node, use the target to find the actual curve to apply to parseAssert(currentNode instanceof SceneTrans); // The node at the end of the chain must be a transform SceneTrans trans = (SceneTrans)currentNode; trans.attachCurve(ch.curve, target, ch.actionName); } // Perform a breadth first tree scan of this sn for any sub object or Transform with this sid // This returns an Object, because valid results are both Transforms and SceneNodes. The caller // will need to cast into the appropriate type private Object findSubNodeBySID(SceneNode sn, String sid) { if (sid.equals(sn.sid)) { return sn; } // Start with the transforms for (SceneTrans trans : sn.transforms) { if (sid.equals(trans.sid)) { return trans; } } // Now the children for (SceneNode child : sn.subNodes) { if (sid.equals(child.sid)) { return child; } } // Now descend for (SceneNode child : sn.subNodes) { Object childRes = findSubNodeBySID(child, sid); if (childRes != null) { return childRes; } } return null; } public MeshData getData() { return _finalData; } /** * This data structure is useful for turning COLLADA data arrays into flat arrays to be processed. * @author matt.chudleigh * */ private static class DataDesc { // The fragID of the source public String source; // The offset in the index array public int offset; int[] indices; } private static class SubMeshDesc { public DataDesc posDesc; public DataDesc normDesc; public HashMap<Integer, DataDesc> texCoordMap = new HashMap<>(); public Integer usedTexSet = null; public int stride; public String material; } /** * Information for a geometry instance (as opposed to a geometry) * for now this is simply a map from material binding symbols to actual material IDs * and a geometry name */ private static class GeoInstInfo { public String geoName; public final Map<String, String> materialMap = new HashMap<>(); public Integer usedTexSet = null; } private static class AnimAction { public AnimCurve[] attachedCurves; public int[] attachedIndex; } /** * Base class for scene node transforms * @author matt.chudleigh * */ private static abstract class SceneTrans { public final String sid; protected final Vec3d commonVect; protected SceneTrans(String sid) { this.sid = sid; commonVect = new Vec3d(); } protected HashMap<String, AnimAction> actions = new HashMap<>(); protected abstract Mat4d getStaticMat(); protected abstract MeshData.Trans toAnimatedTransform(); public MeshData.Trans toMeshDataTrans() { boolean stat = true; for (AnimAction act : actions.values()) { for (AnimCurve ac : act.attachedCurves) { if (ac != null) { stat = false; } } } // If there are no attached curves, we can output a static mesh if (stat) { return new MeshData.StaticTrans(getStaticMat()); } return toAnimatedTransform(); } public abstract void attachCurve(AnimCurve curve, String target, String actionName); protected boolean attachCommonCurves(AnimCurve curve, String tar, String actionName) { AnimAction act = actions.get(actionName); if (tar.equals(".X") || tar.equals("(0)")) { act.attachedCurves[0] = curve; act.attachedIndex[0] = 0; return true; } if (tar.equals(".Y") || tar.equals("(1)")) { act.attachedCurves[1] = curve; act.attachedIndex[1] = 0; return true; } if (tar.equals(".Z") || tar.equals("(2)")) { act.attachedCurves[2] = curve; act.attachedIndex[2] = 0; return true; } return false; } protected Vec3d getAnimatedVectAtTime(double time, String actionName) { Vec3d ret = new Vec3d(commonVect); AnimAction act = actions.get(actionName); if (act == null) { return ret; } if (act.attachedCurves[0] != null) { ret.x = act.attachedCurves[0].getValueForTime(time)[act.attachedIndex[0]]; } if (act.attachedCurves[1] != null) { ret.y = act.attachedCurves[1].getValueForTime(time)[act.attachedIndex[1]]; } if (act.attachedCurves[2] != null) { ret.z = act.attachedCurves[2].getValueForTime(time)[act.attachedIndex[2]]; } return ret; } protected double[] getKeyTimes(String actionName) { AnimAction act = actions.get(actionName); TreeSet<Double> times = new TreeSet<>(); for (AnimCurve ac : act.attachedCurves) { if (ac != null) { for (double time : ac.times) { times.add(time); } } } double[] ret = new double[times.size()]; int index = 0; for (Double time : times) { ret[index++] = time; } // Sanity check for (int i = 0; i < ret.length - 1; ++i) { parseAssert(ret[0] < ret[1]); } return ret; } // Return a list of times with (oversample-1) additional points interspersed in each gap protected double[] oversampleTime(double[] originalTimes, int oversample) { double[] times = new double[(originalTimes.length-1)*oversample + 1]; for (int i = 0; i < originalTimes.length - 1; ++i) { double cur = originalTimes[i]; double next = originalTimes[i+1]; for (int j = 0; j < oversample; ++j) { double scale = (double)j / (double)oversample; times[i*oversample +j] = cur*(1-scale) + next*scale; } } times[times.length-1] = originalTimes[originalTimes.length-1]; return times; } } private static class TranslationTrans extends SceneTrans { public TranslationTrans(XmlNode transNode) { super(transNode.getAttrib("sid")); double[] vals = (double[])transNode.getContent(); parseAssert(vals != null && vals.length >= 3); commonVect.set3(vals[0], vals[1], vals[2]); } @Override public Mat4d getStaticMat() { Mat4d ret = new Mat4d(); ret.setTranslate3(commonVect); return ret; } @Override public void attachCurve(AnimCurve curve, String target, String actionName) { AnimAction act = actions.get(actionName); if (act == null) { // First curve bound for this action act = new AnimAction(); act.attachedCurves = new AnimCurve[3]; act.attachedIndex = new int[3]; actions.put(actionName, act); } String tar = target.toUpperCase(); if (attachCommonCurves(curve, tar, actionName)) { return; } if (target.equals("")) { // For an empty target, attach to all curves for (int i = 0; i < 3; ++i) { act.attachedCurves[i] = curve; act.attachedIndex[i] = i; } return; } parseAssert(false); } @Override protected Trans toAnimatedTransform() { Set<String> actionNames = actions.keySet(); double[][] timesArray = new double[actionNames.size()][]; Mat4d[][] matsArray = new Mat4d[actionNames.size()][]; String[] names = new String[actionNames.size()]; int actionInd = 0; for (String actionName : actions.keySet()) { double[] times = getKeyTimes(actionName); Mat4d[] mats = new Mat4d[times.length]; for (int i = 0; i < times.length; ++i) { Vec3d animTranslation = getAnimatedVectAtTime(times[i], actionName); mats[i] = new Mat4d(); mats[i].setTranslate3(animTranslation); } timesArray[actionInd] = times; matsArray[actionInd] = mats; names[actionInd] = actionName; ++actionInd; } return new MeshData.AnimTrans(timesArray, matsArray, names, getStaticMat()); } } private static class RotationTrans extends SceneTrans { private final double angle; public RotationTrans(XmlNode rotNode) { super(rotNode.getAttrib("sid")); double[] vals = (double[])rotNode.getContent(); parseAssert(vals != null && vals.length >= 4); commonVect.set3(vals[0], vals[1], vals[2]); angle = (float)Math.toRadians(vals[3]); } @Override public Mat4d getStaticMat() { Quaternion rot = new Quaternion(); rot.setAxisAngle(commonVect, angle); Mat4d ret = new Mat4d(); ret.setRot3(rot); return ret; } @Override public void attachCurve(AnimCurve curve, String target, String actionName) { AnimAction act = actions.get(actionName); if (act == null) { // First curve bound for this action act = new AnimAction(); act.attachedCurves = new AnimCurve[4]; act.attachedIndex = new int[4]; actions.put(actionName, act); } String tar = target.toUpperCase(); if (attachCommonCurves(curve, tar, actionName)) { return; } if (tar.equals(".ANGLE") || tar.equals("(3)")) { act.attachedCurves[3] = curve; return; } if (target.equals("")) { // For an empty target, attach to all curves for (int i = 0; i < 4; ++i) { act.attachedCurves[i] = curve; act.attachedIndex[i] = i; } return; } parseAssert(false); } @Override protected Trans toAnimatedTransform() { Set<String> actionNames = actions.keySet(); double[][] timesArray = new double[actionNames.size()][]; Mat4d[][] matsArray = new Mat4d[actionNames.size()][]; String[] names = new String[actionNames.size()]; int actionInd = 0; for (Entry<String, AnimAction> eachAction : actions.entrySet()) { final String actionName = eachAction.getKey(); double[] originalTimes = getKeyTimes(actionName); AnimAction act = eachAction.getValue(); // Add new sample points because linearly interpolating a rotation matrix usually does not work correctly double[] times = oversampleTime(originalTimes, 4); Mat4d[] mats = new Mat4d[times.length]; for (int i = 0; i < times.length; ++i) { double animAngle = Math.toRadians(angle); if (act.attachedCurves[3] != null) { animAngle = Math.toRadians(act.attachedCurves[3].getValueForTime(times[i])[act.attachedIndex[3]]); } Vec3d animAxis = getAnimatedVectAtTime(times[i], actionName); Quaternion rot = new Quaternion(); rot.setAxisAngle(animAxis, animAngle); mats[i] = new Mat4d(); mats[i].setRot3(rot); } timesArray[actionInd] = times; matsArray[actionInd] = mats; names[actionInd] = actionName; } return new MeshData.AnimTrans(timesArray, matsArray, names, getStaticMat()); } } private static class ScaleTrans extends SceneTrans { public ScaleTrans(XmlNode scaleNode) { super(scaleNode.getAttrib("sid")); double[] vals = (double[])scaleNode.getContent(); parseAssert(vals != null && vals.length >= 3); commonVect.set3(vals[0], vals[1], vals[2]); } @Override public Mat4d getStaticMat() { Mat4d ret = new Mat4d(); ret.d00 = commonVect.x; ret.d11 = commonVect.y; ret.d22 = commonVect.z; return ret; } @Override public void attachCurve(AnimCurve curve, String target, String actionName) { AnimAction act = actions.get(actionName); if (act == null) { // First curve bound for this action act = new AnimAction(); act.attachedCurves = new AnimCurve[3]; act.attachedIndex = new int[3]; actions.put(actionName, act); } String tar = target.toUpperCase(); if (attachCommonCurves(curve, tar, actionName)) { return; } if (target.equals("")) { // For an empty target, attach to all curves for (int i = 0; i < 3; ++i) { act.attachedCurves[i] = curve; act.attachedIndex[i] = i; } return; } parseAssert(false); } @Override protected Trans toAnimatedTransform() { Set<String> actionNames = actions.keySet(); double[][] timesArray = new double[actionNames.size()][]; Mat4d[][] matsArray = new Mat4d[actionNames.size()][]; String[] names = new String[actionNames.size()]; int actionInd = 0; for (String actionName : actions.keySet()) { double[] times = getKeyTimes(actionName); Mat4d[] mats = new Mat4d[times.length]; for (int i = 0; i < times.length; ++i) { Vec3d animScale = getAnimatedVectAtTime(times[i], actionName); mats[i] = new Mat4d(); mats[i].d00 = animScale.x; mats[i].d11 = animScale.y; mats[i].d22 = animScale.z; } timesArray[actionInd] = times; matsArray[actionInd] = mats; names[actionInd] = actionName; } return new MeshData.AnimTrans(timesArray, matsArray, names, getStaticMat()); } } private static class MatrixTrans extends SceneTrans { private final Mat4d matrix; public MatrixTrans(Mat4d mat) { super(null); matrix = new Mat4d(mat); } public MatrixTrans(XmlNode matNode) { super(matNode.getAttrib("sid")); double[] vals = (double[])matNode.getContent(); parseAssert(vals != null && vals.length >= 16); matrix = new Mat4d(vals); } @Override public Mat4d getStaticMat() { return new Mat4d(matrix); } @Override public void attachCurve(AnimCurve curve, String target, String actionName) { if (!target.equals("") || curve.numComponents != 16) { throw new RenderException("Currently only support animating whole matrices"); } AnimAction act = actions.get(actionName); if (act != null) { // TODO warn we are over-writing an action here } // We only support whole matrix animation and single curve per action currently, so the last curve // bound to an action will dominate act = new AnimAction(); act.attachedCurves = new AnimCurve[1]; act.attachedCurves[0] = curve; actions.put(actionName, act); } @Override protected Trans toAnimatedTransform() { Set<String> actionNames = actions.keySet(); double[][] timesArray = new double[actionNames.size()][]; Mat4d[][] matsArray = new Mat4d[actionNames.size()][]; String[] names = new String[actionNames.size()]; int actionInd = 0; for (Entry<String, AnimAction> eachAction : actions.entrySet()) { String actionName = eachAction.getKey(); AnimAction act = eachAction.getValue(); double[] times = getKeyTimes(actionName); Mat4d[] mats = new Mat4d[times.length]; // Fill in the mats for (int i = 0; i < times.length; ++i) { double[] matVals = act.attachedCurves[0].getValueForTime(times[i]); mats[i] = new Mat4d(matVals); } names[actionInd] = actionName; timesArray[actionInd] = times; matsArray[actionInd] = mats; actionInd++; } return new MeshData.AnimTrans(timesArray, matsArray, names, getStaticMat()); } } /** * SceneNode is basically a container for Collada "node" tags, this is needed to allow the system to walk the * node tree and properly honour the instance nodes. * @author matt.chudleigh */ private static class SceneNode { public final String id; public final String sid; public final ArrayList<SceneTrans> transforms = new ArrayList<>(); public final ArrayList<SceneNode> subNodes = new ArrayList<>(); public final ArrayList<String> subInstanceNames = new ArrayList<>(); public final ArrayList<GeoInstInfo> subGeo = new ArrayList<>(); SceneNode(String id, String sid) { this.id = id; this.sid = sid; } } /** * A union like data structure, this is either a color value or texture (it should always be one or the other, but never both) * @author matt.chudleigh * */ private static class ColorTex { public Color4d color; public URI texture; public String relTexture; public String texCoordName; } private static class Effect { // Only hold diffuse colour for now public ColorTex diffuse; public Color4d ambient; public Color4d spec; public double shininess; public int transType; public Color4d transColour; } private static class LineGeoEffectPair { public LineSubGeo geo; public Effect effect; @Override public int hashCode() { return geo.hashCode() ^ effect.hashCode(); } public LineGeoEffectPair(LineSubGeo g, Effect e) { this.geo = g; this.effect = e; } @Override public boolean equals(Object o) { if (o == null) return false; if (!(o instanceof LineGeoEffectPair)) return false; LineGeoEffectPair other = (LineGeoEffectPair)o; return other.geo == geo && other.effect == effect; } } }