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}