001/**
002 * Copyright (c) 2008-2014 Ardor Labs, Inc.
003 *
004 * This file is part of Ardor3D.
005 *
006 * Ardor3D is free software: you can redistribute it and/or modify it
007 * under the terms of its license which may be found in the accompanying
008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
009 */
010
011package com.ardor3d.extension.model.collada.jdom;
012
013import java.io.IOException;
014import java.nio.FloatBuffer;
015import java.util.ArrayList;
016import java.util.Collections;
017import java.util.EnumSet;
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.NoSuchElementException;
022import java.util.Set;
023import java.util.StringTokenizer;
024import java.util.logging.Logger;
025
026import org.jdom2.Attribute;
027import org.jdom2.DataConversionException;
028import org.jdom2.DefaultJDOMFactory;
029import org.jdom2.Document;
030import org.jdom2.Element;
031import org.jdom2.JDOMFactory;
032import org.jdom2.Namespace;
033import org.jdom2.Text;
034import org.jdom2.input.SAXBuilder;
035import org.jdom2.input.sax.SAXHandler;
036import org.jdom2.input.sax.SAXHandlerFactory;
037import org.xml.sax.SAXException;
038
039import com.ardor3d.extension.animation.skeletal.Joint;
040import com.ardor3d.extension.model.collada.jdom.data.AssetData;
041import com.ardor3d.extension.model.collada.jdom.data.ColladaStorage;
042import com.ardor3d.extension.model.collada.jdom.data.DataCache;
043import com.ardor3d.extension.model.collada.jdom.plugin.ColladaExtraPlugin;
044import com.ardor3d.scenegraph.MeshData;
045import com.ardor3d.scenegraph.Node;
046import com.ardor3d.util.geom.GeometryTool;
047import com.ardor3d.util.geom.GeometryTool.MatchCondition;
048import com.ardor3d.util.resource.RelativeResourceLocator;
049import com.ardor3d.util.resource.ResourceLocator;
050import com.ardor3d.util.resource.ResourceLocatorTool;
051import com.ardor3d.util.resource.ResourceSource;
052
053/**
054 * Main class for importing Collada files.
055 * <p>
056 * Example usages: new ColladaImporter().load(resource); new
057 * ColladaImporter().loadTextures(false).modelLocator(locator).load(resource);
058 * </p>
059 */
060public class ColladaImporter {
061    private boolean _loadTextures = true;
062    private boolean _flipTransparency = false;
063    private boolean _loadAnimations = true;
064    private ResourceLocator _textureLocator;
065    private ResourceLocator _modelLocator;
066    private boolean _compressTextures = false;
067    private boolean _optimizeMeshes = false;
068    private final EnumSet<MatchCondition> _optimizeSettings = EnumSet.of(MatchCondition.UVs, MatchCondition.Normal,
069            MatchCondition.Color);
070    private Map<String, Joint> _externalJointMapping;
071    private final List<ColladaExtraPlugin> _extraPlugins = new ArrayList<>();
072
073    public boolean isLoadTextures() {
074        return _loadTextures;
075    }
076
077    public ColladaImporter setLoadTextures(final boolean loadTextures) {
078        _loadTextures = loadTextures;
079        return this;
080    }
081
082    public boolean isCompressTextures() {
083        return _compressTextures;
084    }
085
086    public ColladaImporter setCompressTextures(final boolean compressTextures) {
087        _compressTextures = compressTextures;
088        return this;
089    }
090
091    public boolean isLoadAnimations() {
092        return _loadAnimations;
093    }
094
095    public ColladaImporter setLoadAnimations(final boolean loadAnimations) {
096        _loadAnimations = loadAnimations;
097        return this;
098    }
099
100    public boolean isFlipTransparency() {
101        return _flipTransparency;
102    }
103
104    public void addExtraPlugin(final ColladaExtraPlugin plugin) {
105        _extraPlugins.add(plugin);
106    }
107
108    public void clearExtraPlugins() {
109        _extraPlugins.clear();
110    }
111
112    /**
113     * @param flipTransparency
114     *            if true, invert the value of any "transparency" entries found - required for some exporters.
115     * @return this importer, for chaining
116     */
117    public ColladaImporter setFlipTransparency(final boolean flipTransparency) {
118        _flipTransparency = flipTransparency;
119        return this;
120    }
121
122    public ResourceLocator getTextureLocator() {
123        return _textureLocator;
124    }
125
126    public ColladaImporter setTextureLocator(final ResourceLocator textureLocator) {
127        _textureLocator = textureLocator;
128        return this;
129    }
130
131    public ColladaImporter setExternalJoints(final Map<String, Joint> map) {
132        _externalJointMapping = map;
133        return this;
134    }
135
136    public Map<String, Joint> getExternalJoints() {
137        return _externalJointMapping;
138    }
139
140    public ResourceLocator getModelLocator() {
141        return _modelLocator;
142    }
143
144    public ColladaImporter setModelLocator(final ResourceLocator modelLocator) {
145        _modelLocator = modelLocator;
146        return this;
147    }
148
149    public boolean isOptimizeMeshes() {
150        return _optimizeMeshes;
151    }
152
153    public void setOptimizeMeshes(final boolean optimizeMeshes) {
154        _optimizeMeshes = optimizeMeshes;
155    }
156
157    public Set<MatchCondition> getOptimizeSettings() {
158        return Collections.unmodifiableSet(EnumSet.copyOf(_optimizeSettings));
159    }
160
161    public void setOptimizeSettings(final MatchCondition... optimizeSettings) {
162        _optimizeSettings.clear();
163        for (final MatchCondition cond : optimizeSettings) {
164            _optimizeSettings.add(cond);
165        }
166    }
167
168    /**
169     * Reads a Collada file from the given resource and returns it as a ColladaStorage object.
170     *
171     * @param resource
172     *            the name of the resource to find. ResourceLocatorTool will be used with TYPE_MODEL to find the
173     *            resource.
174     * @return a ColladaStorage data object containing the Collada scene and other useful elements.
175     * @throws IOException
176     *             if the resource can not be located or loaded for some reason.
177     */
178    public ColladaStorage load(final String resource) throws IOException {
179        return load(resource, new GeometryTool());
180    }
181
182    /**
183     * Reads a Collada file from the given resource and returns it as a ColladaStorage object.
184     *
185     * @param resource
186     *            the name of the resource to find. ResourceLocatorTool will be used with TYPE_MODEL to find the
187     *            resource.
188     * @param geometryTool
189     *            the geometry tool used to minimize the vertex count.
190     * @return a ColladaStorage data object containing the Collada scene and other useful elements.
191     * @throws IOException
192     *             if the resource can not be located or loaded for some reason.
193     */
194    public ColladaStorage load(final String resource, final GeometryTool geometryTool) throws IOException {
195        final ResourceSource source;
196        if (_modelLocator == null) {
197            source = ResourceLocatorTool.locateResource(ResourceLocatorTool.TYPE_MODEL, resource);
198        } else {
199            source = _modelLocator.locateResource(resource);
200        }
201
202        if (source == null) {
203            throw new IOException("Unable to locate '" + resource + "'");
204        }
205
206        return load(source, geometryTool);
207    }
208
209    /**
210     * Reads a Collada file from the given resource and returns it as a ColladaStorage object.
211     *
212     * @param resource
213     *            the name of the resource to find.
214     * @return a ColladaStorage data object containing the Collada scene and other useful elements.
215     * @throws IOException
216     *             if the resource can not be loaded for some reason.
217     */
218    public ColladaStorage load(final ResourceSource resource) throws IOException {
219        return load(resource, new GeometryTool());
220    }
221
222    /**
223     * Reads a Collada file from the given resource and returns it as a ColladaStorage object.
224     *
225     * @param resource
226     *            the name of the resource to find.
227     * @param geometryTool
228     *            the geometry tool used to minimize the vertex count.
229     * @return a ColladaStorage data object containing the Collada scene and other useful elements.
230     * @throws IOException
231     *             if the resource can not be loaded for some reason.
232     */
233    public ColladaStorage load(final ResourceSource resource, final GeometryTool geometryTool) throws IOException {
234        final ColladaStorage colladaStorage = new ColladaStorage();
235        final DataCache dataCache = new DataCache();
236        if (_externalJointMapping != null) {
237            dataCache.getExternalJointMapping().putAll(_externalJointMapping);
238        }
239        final ColladaDOMUtil colladaDOMUtil = new ColladaDOMUtil(dataCache);
240        final ColladaMaterialUtils colladaMaterialUtils = new ColladaMaterialUtils(this, dataCache, colladaDOMUtil);
241        final ColladaMeshUtils colladaMeshUtils = new ColladaMeshUtils(dataCache, colladaDOMUtil, colladaMaterialUtils,
242                _optimizeMeshes, _optimizeSettings, geometryTool);
243        final ColladaAnimUtils colladaAnimUtils = new ColladaAnimUtils(colladaStorage, dataCache, colladaDOMUtil,
244                colladaMeshUtils);
245        final ColladaNodeUtils colladaNodeUtils = new ColladaNodeUtils(this, dataCache, colladaDOMUtil,
246                colladaMaterialUtils, colladaMeshUtils, colladaAnimUtils);
247
248        try {
249            // Pull in the DOM tree of the Collada resource.
250            final Element collada = readCollada(resource, dataCache);
251
252            // if we don't specify a texture locator, add a temporary texture locator at the location of this model
253            // resource..
254            final boolean addLocator = _textureLocator == null;
255
256            final RelativeResourceLocator loc;
257            if (addLocator) {
258                loc = new RelativeResourceLocator(resource);
259                ResourceLocatorTool.addResourceLocator(ResourceLocatorTool.TYPE_TEXTURE, loc);
260            } else {
261                loc = null;
262            }
263
264            final AssetData assetData = colladaNodeUtils.parseAsset(collada.getChild("asset"));
265
266            // Collada may or may not have a scene, so this can return null.
267            final Node scene = colladaNodeUtils.getVisualScene(collada);
268
269            if (_loadAnimations) {
270                colladaAnimUtils.parseLibraryAnimations(collada);
271            }
272
273            // reattach attachments to scene
274            if (scene != null) {
275                colladaNodeUtils.reattachAttachments(scene);
276            }
277
278            // set our scene into storage
279            colladaStorage.setScene(scene);
280
281            // set our asset data into storage
282            colladaStorage.setAssetData(assetData);
283
284            // drop our added locator if needed.
285            if (addLocator) {
286                ResourceLocatorTool.removeResourceLocator(ResourceLocatorTool.TYPE_TEXTURE, loc);
287            }
288
289            // copy across our mesh colors - only for objects with multiple channels
290            final Map<MeshData, List<FloatBuffer>> colors = new HashMap<>();
291            final Map<MeshData, List<FloatBuffer>> temp = dataCache.getParsedVertexColors();
292            for (final Map.Entry<MeshData, List<FloatBuffer>> tempEntry : temp.entrySet()) {
293                final MeshData key = tempEntry.getKey();
294                // only copy multiple channels since their data is lost
295                final List<FloatBuffer> val = tempEntry.getValue();
296                if (val != null && val.size() > 1) {
297                    // defensive copy, not sure that it's really necessary
298                    colors.put(key, new ArrayList<>(val));
299                }
300            }
301            colladaStorage.setParsedVertexColors(colors);
302
303            // copy across our mesh material info
304            colladaStorage.setMeshMaterialInfo(dataCache.getMeshMaterialMap());
305            colladaStorage.setMaterialMap(dataCache.getMaterialInfoMap());
306
307            // return storage
308            return colladaStorage;
309        } catch (final Exception e) {
310            throw new IOException("Unable to load collada resource from URL: " + resource, e);
311        }
312    }
313
314    /**
315     * Reads the whole Collada DOM tree from the given resource and returns its root element. Exceptions may be thrown
316     * by underlying tools; these will be wrapped in a RuntimeException and rethrown.
317     *
318     * @param resource
319     *            the ResourceSource to read the resource from
320     * @param dataCache
321     *            the data cache
322     * @return the Collada root element
323     */
324    private Element readCollada(final ResourceSource resource, final DataCache dataCache) {
325        try {
326            final JDOMFactory jdomFac = new ArdorFactory(dataCache);
327            final SAXBuilder builder = new SAXBuilder(null, new SAXHandlerFactory() {
328                @Override
329                public SAXHandler createSAXHandler(final JDOMFactory factory) {
330                    return new SAXHandler(jdomFac) {
331                        @Override
332                        public void startPrefixMapping(final String prefix, final String uri) throws SAXException {
333                            // Just kill what's usually done here...
334                        }
335                    };
336                }
337            }, jdomFac);
338
339            final Document doc = builder.build(resource.openStream());
340            final Element collada = doc.getRootElement();
341
342            // ColladaDOMUtil.stripNamespace(collada);
343
344            return collada;
345        } catch (final Exception e) {
346            throw new RuntimeException("Unable to load collada resource from source: " + resource, e);
347        }
348    }
349
350    private enum BufferType {
351        None, Float, Double, Int, String, P
352    }
353
354    /**
355     * A JDOMFactory that normalizes all text (strips extra whitespace etc), preparses all arrays and hashes all
356     * elements based on their id/sid.
357     */
358    private static final class ArdorFactory extends DefaultJDOMFactory {
359        private final Logger logger = Logger.getLogger(ArdorFactory.class.getName());
360
361        private final DataCache dataCache;
362        private Element currentElement;
363        private BufferType bufferType = BufferType.None;
364        private int count = 0;
365        private final List<String> list = new ArrayList<>();
366
367        ArdorFactory(final DataCache dataCache) {
368            this.dataCache = dataCache;
369        }
370
371        @Override
372        public Text text(final int line, final int col, final String text) {
373            try {
374                switch (bufferType) {
375                    case Float: {
376                        final String normalizedText = Text.normalizeString(text);
377                        if (normalizedText.length() == 0) {
378                            return new Text("");
379                        }
380                        final StringTokenizer tokenizer = new StringTokenizer(normalizedText, " ");
381                        final float[] floatArray = new float[count];
382                        for (int i = 0; i < count; i++) {
383                            floatArray[i] = parseFloat(tokenizer.nextToken());
384                        }
385
386                        dataCache.getFloatArrays().put(currentElement, floatArray);
387
388                        return new Text("");
389                    }
390                    case Double: {
391                        final String normalizedText = Text.normalizeString(text);
392                        if (normalizedText.length() == 0) {
393                            return new Text("");
394                        }
395                        final StringTokenizer tokenizer = new StringTokenizer(normalizedText, " ");
396                        final double[] doubleArray = new double[count];
397                        for (int i = 0; i < count; i++) {
398                            doubleArray[i] = Double.parseDouble(tokenizer.nextToken().replace(",", "."));
399                        }
400
401                        dataCache.getDoubleArrays().put(currentElement, doubleArray);
402
403                        return new Text("");
404                    }
405                    case Int: {
406                        final String normalizedText = Text.normalizeString(text);
407                        if (normalizedText.length() == 0) {
408                            return new Text("");
409                        }
410                        final StringTokenizer tokenizer = new StringTokenizer(normalizedText, " ");
411                        final int[] intArray = new int[count];
412                        int i = 0;
413                        while (tokenizer.hasMoreTokens()) {
414                            intArray[i++] = Integer.parseInt(tokenizer.nextToken());
415                        }
416
417                        dataCache.getIntArrays().put(currentElement, intArray);
418
419                        return new Text("");
420                    }
421                    case P: {
422                        list.clear();
423                        final String normalizedText = Text.normalizeString(text);
424                        if (normalizedText.length() == 0) {
425                            return new Text("");
426                        }
427                        final StringTokenizer tokenizer = new StringTokenizer(normalizedText, " ");
428                        while (tokenizer.hasMoreTokens()) {
429                            list.add(tokenizer.nextToken());
430                        }
431                        final int listSize = list.size();
432                        final int[] intArray = new int[listSize];
433                        for (int i = 0; i < listSize; i++) {
434                            intArray[i] = Integer.parseInt(list.get(i));
435                        }
436
437                        dataCache.getIntArrays().put(currentElement, intArray);
438
439                        return new Text("");
440                    }
441                    default:
442                        break;
443                }
444            } catch (final NoSuchElementException e) {
445                throw new ColladaException(
446                        "Number of values in collada array does not match its count attribute: " + count, e);
447            }
448            return new Text(Text.normalizeString(text));
449        }
450
451        @Override
452        public void setAttribute(final Element parent, final Attribute a) {
453            if ("id".equals(a.getName())) {
454                if (dataCache.getIdCache().containsKey(a.getValue())) {
455                    logger.warning("id already exists in id cache: " + a.getValue());
456                }
457                dataCache.getIdCache().put(a.getValue(), parent);
458            } else if ("sid".equals(a.getName())) {
459                dataCache.getSidCache().put(a.getValue(), parent);
460            } else if ("count".equals(a.getName())) {
461                try {
462                    count = a.getIntValue();
463                } catch (final DataConversionException e) {
464                    e.printStackTrace();
465                }
466            }
467
468            super.setAttribute(parent, a);
469        }
470
471        @Override
472        public Element element(final int line, final int col, final String name, final Namespace namespace) {
473            currentElement = super.element(line, col, name);
474            handleTypes(name);
475            return currentElement;
476        }
477
478        @Override
479        public Element element(final int line, final int col, final String name, final String prefix,
480                final String uri) {
481            currentElement = super.element(line, col, name);
482            handleTypes(name);
483            return currentElement;
484        }
485
486        @Override
487        public Element element(final int line, final int col, final String name, final String uri) {
488            currentElement = super.element(line, col, name);
489            handleTypes(name);
490            return currentElement;
491        }
492
493        @Override
494        public Element element(final int line, final int col, final String name) {
495            currentElement = super.element(line, col, name);
496            handleTypes(name);
497            return currentElement;
498        }
499
500        private void handleTypes(final String name) {
501            if ("float_array".equals(name)) {
502                bufferType = BufferType.Float;
503            } else if ("double_array".equals(name)) {
504                bufferType = BufferType.Double;
505            } else if ("int_array".equals(name)) {
506                bufferType = BufferType.Int;
507            } else if ("p".equals(name)) {
508                bufferType = BufferType.P;
509            } else {
510                bufferType = BufferType.None;
511            }
512        }
513    }
514
515    /**
516     * Parse a numeric value. Commas are replaced by dot automatically. Also handle special values : INF, -INF, NaN
517     *
518     * @param candidate
519     *            the string to parse
520     * @return float the float representing the numeric value
521     */
522    public static float parseFloat(String candidate) {
523        candidate = candidate.replace(',', '.');
524        if (candidate.contains("-INF")) {
525            return Float.NEGATIVE_INFINITY;
526        } else if (candidate.contains("INF")) {
527            return Float.POSITIVE_INFINITY;
528        }
529        return Float.parseFloat(candidate);
530    }
531
532    public boolean readExtra(final Element extra, final Object... params) {
533        for (final ColladaExtraPlugin plugin : _extraPlugins) {
534            if (plugin.processExtra(extra, params)) {
535                return true;
536            }
537        }
538        return false;
539    }
540}