1 /* 2 * OBJWriter.java 18 sept. 2008 3 * 4 * Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 package com.eteks.sweethome3d.j3d; 21 22 import java.awt.image.RenderedImage; 23 import java.io.BufferedOutputStream; 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.FileNotFoundException; 27 import java.io.FileOutputStream; 28 import java.io.FilterWriter; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InterruptedIOException; 32 import java.io.OutputStream; 33 import java.io.OutputStreamWriter; 34 import java.io.Writer; 35 import java.net.JarURLConnection; 36 import java.net.URISyntaxException; 37 import java.net.URL; 38 import java.net.URLConnection; 39 import java.text.DecimalFormat; 40 import java.text.DecimalFormatSymbols; 41 import java.text.NumberFormat; 42 import java.util.ArrayList; 43 import java.util.Collection; 44 import java.util.Enumeration; 45 import java.util.HashMap; 46 import java.util.Iterator; 47 import java.util.LinkedHashMap; 48 import java.util.List; 49 import java.util.Locale; 50 import java.util.Map; 51 import java.util.zip.ZipEntry; 52 import java.util.zip.ZipOutputStream; 53 54 import javax.imageio.ImageIO; 55 import javax.imageio.ImageReader; 56 import javax.imageio.stream.ImageInputStream; 57 import javax.media.j3d.Appearance; 58 import javax.media.j3d.ColoringAttributes; 59 import javax.media.j3d.Geometry; 60 import javax.media.j3d.GeometryArray; 61 import javax.media.j3d.GeometryStripArray; 62 import javax.media.j3d.Group; 63 import javax.media.j3d.ImageComponent2D; 64 import javax.media.j3d.IndexedGeometryArray; 65 import javax.media.j3d.IndexedGeometryStripArray; 66 import javax.media.j3d.IndexedLineArray; 67 import javax.media.j3d.IndexedLineStripArray; 68 import javax.media.j3d.IndexedQuadArray; 69 import javax.media.j3d.IndexedTriangleArray; 70 import javax.media.j3d.IndexedTriangleFanArray; 71 import javax.media.j3d.IndexedTriangleStripArray; 72 import javax.media.j3d.LineArray; 73 import javax.media.j3d.LineStripArray; 74 import javax.media.j3d.Link; 75 import javax.media.j3d.Material; 76 import javax.media.j3d.Node; 77 import javax.media.j3d.PolygonAttributes; 78 import javax.media.j3d.QuadArray; 79 import javax.media.j3d.RenderingAttributes; 80 import javax.media.j3d.Shape3D; 81 import javax.media.j3d.TexCoordGeneration; 82 import javax.media.j3d.Texture; 83 import javax.media.j3d.TextureAttributes; 84 import javax.media.j3d.Transform3D; 85 import javax.media.j3d.TransformGroup; 86 import javax.media.j3d.TransparencyAttributes; 87 import javax.media.j3d.TriangleArray; 88 import javax.media.j3d.TriangleFanArray; 89 import javax.media.j3d.TriangleStripArray; 90 import javax.vecmath.Color3f; 91 import javax.vecmath.Point3f; 92 import javax.vecmath.TexCoord2f; 93 import javax.vecmath.Vector3f; 94 import javax.vecmath.Vector4f; 95 96 /** 97 * An output stream that writes Java 3D nodes at OBJ + MTL format. 98 * <p>Once you wrote nodes, call <code>close</code> method to create the MTL file 99 * and the texture images in the same folder as OBJ file. This feature applies 100 * only to constructor that takes a file as parameter.<br> 101 * Note: this class is compatible with Java 3D 1.3. 102 * @author Emmanuel Puybaret 103 */ 104 public class OBJWriter extends FilterWriter { 105 private final NumberFormat defaultNumberFormat = 106 new DecimalFormat("0.#######", new DecimalFormatSymbols(Locale.US)); 107 private final NumberFormat numberFormat; 108 private final String header; 109 110 private boolean firstNode = true; 111 private String mtlFileName; 112 113 private int shapeIndex = 1; 114 private Map<Point3f, Integer> vertexIndices = new HashMap<Point3f, Integer>(); 115 private Map<Vector3f, Integer> normalIndices = new HashMap<Vector3f, Integer>(); 116 private Map<TexCoord2f, Integer> textureCoordinatesIndices = new HashMap<TexCoord2f, Integer>(); 117 private Map<ComparableAppearance, String> appearances = 118 new LinkedHashMap<ComparableAppearance, String>(); 119 private Map<Texture, File> textures = new HashMap<Texture, File>(); 120 private List<URL> copiedTextures = new ArrayList<URL>(); 121 122 /** 123 * Create an OBJ writer for the given file, with no header and default precision. 124 */ OBJWriter(File objFile)125 public OBJWriter(File objFile) throws FileNotFoundException, IOException { 126 this(objFile, null, -1); 127 } 128 129 /** 130 * Create an OBJ writer for the given file. 131 * @param objFile the file into which 3D nodes will be written at OBJ format 132 * @param header a header written as a comment at start of the OBJ file and its MTL counterpart 133 * @param maximumFractionDigits the maximum digits count used in fraction part of numbers, 134 * or -1 for default value. Using -1 may cause writing nodes to be twice faster. 135 */ OBJWriter(File objFile, String header, int maximumFractionDigits)136 public OBJWriter(File objFile, String header, 137 int maximumFractionDigits) throws FileNotFoundException, IOException { 138 this(objFile.toString(), header, maximumFractionDigits); 139 } 140 141 /** 142 * Create an OBJ writer for the given file name, with no header and default precision. 143 */ OBJWriter(String objFileName)144 public OBJWriter(String objFileName) throws FileNotFoundException, IOException { 145 this(objFileName, null, -1); 146 } 147 148 /** 149 * Create an OBJ writer for the given file name. 150 * @param objFileName the name of the file into which 3D nodes will be written at OBJ format 151 * @param header a header written as a comment at start of the OBJ file and its MTL counterpart 152 * @param maximumFractionDigits the maximum digits count used in fraction part of numbers, 153 * or -1 for default value. Using -1 may cause writing nodes to be twice faster. 154 */ OBJWriter(String objFileName, String header, int maximumFractionDigits)155 public OBJWriter(String objFileName, String header, 156 int maximumFractionDigits) throws FileNotFoundException, IOException { 157 this(new FileOutputStream(objFileName), header, maximumFractionDigits); 158 if (objFileName.toLowerCase().endsWith(".obj")) { 159 this.mtlFileName = objFileName.substring(0, objFileName.length() - 4) + ".mtl"; 160 } else { 161 this.mtlFileName = objFileName + ".mtl"; 162 } 163 // Remove spaces in MTL file name 164 this.mtlFileName = new File(new File(this.mtlFileName).getParent(), 165 new File(this.mtlFileName).getName().replace(' ', '_')).toString(); 166 // Ensure MTL file is using only ASCII codes 167 String name = new File(this.mtlFileName).getName(); 168 for (int i = 0; i < name.length(); i++) { 169 if (name.charAt(i) >= 128) { 170 this.mtlFileName = new File(new File(this.mtlFileName).getParent(), 171 "materials.mtl").toString(); 172 break; 173 } 174 } 175 } 176 177 /** 178 * Create an OBJ writer that will writes in <code>out</code> stream, 179 * with no header and default precision. 180 */ OBJWriter(OutputStream out)181 public OBJWriter(OutputStream out) throws IOException { 182 this(out, null, -1); 183 } 184 185 /** 186 * Create an OBJ writer that will writes in <code>out</code> stream. 187 * @param out the stream into which 3D nodes will be written at OBJ format 188 * @param header a header written as a comment at start of the stream 189 * @param maximumFractionDigits the maximum digits count used in fraction part of numbers, 190 * or -1 for default value. Using -1 may cause writing nodes to be twice faster. 191 */ OBJWriter(OutputStream out, String header, int maximumFractionDigits)192 public OBJWriter(OutputStream out, String header, 193 int maximumFractionDigits) throws IOException { 194 this(new OutputStreamWriter(new BufferedOutputStream(out), "US-ASCII"), header, maximumFractionDigits); 195 } 196 197 /** 198 * Create an OBJ writer that will writes in <code>out</code> stream, 199 * with no header and default precision. 200 */ OBJWriter(Writer out)201 public OBJWriter(Writer out) throws IOException { 202 this(out, null, -1); 203 } 204 205 /** 206 * Create an OBJ writer that will writes in <code>out</code> stream. 207 * @param out the stream into which 3D nodes will be written at OBJ format 208 * @param header a header written as a comment at start of the stream 209 * @param maximumFractionDigits the maximum digits count used in fraction part of numbers, 210 * or -1 for default value. Using -1 may cause writing nodes to be twice faster. 211 */ OBJWriter(Writer out, String header, int maximumFractionDigits)212 public OBJWriter(Writer out, String header, 213 int maximumFractionDigits) throws IOException { 214 super(out); 215 if (maximumFractionDigits >= 0) { 216 this.numberFormat = NumberFormat.getNumberInstance(Locale.US); 217 this.numberFormat.setMinimumFractionDigits(0); 218 this.numberFormat.setMaximumFractionDigits(maximumFractionDigits); 219 } else { 220 this.numberFormat = null; 221 } 222 this.header = header; 223 writeHeader(this.out); 224 } 225 226 /** 227 * Writes header to <code>writer</code> 228 */ writeHeader(Writer writer)229 private void writeHeader(Writer writer) throws IOException { 230 if (this.header != null) { 231 if (!this.header.startsWith("#")) { 232 writer.write("# "); 233 } 234 writer.write(this.header.replace("\n", "\n# ")); 235 writer.write("\n"); 236 } 237 } 238 239 /** 240 * Write a single character in a comment at OBJ format. 241 */ 242 @Override write(int c)243 public void write(int c) throws IOException { 244 this.out.write("# "); 245 this.out.write(c); 246 this.out.write("\n"); 247 } 248 249 /** 250 * Write a portion of an array of characters in a comment at OBJ format. 251 */ 252 @Override write(char cbuf[], int off, int len)253 public void write(char cbuf[], int off, int len) throws IOException { 254 this.out.write("# "); 255 this.out.write(cbuf, off, len); 256 this.out.write("\n"); 257 } 258 259 /** 260 * Write a portion of a string in a comment at OBJ format. 261 */ 262 @Override write(String str, int off, int len)263 public void write(String str, int off, int len) throws IOException { 264 this.out.write("# "); 265 this.out.write(str, off, len); 266 this.out.write("\n"); 267 } 268 269 /** 270 * Write a string in a comment at OBJ format. 271 */ 272 @Override write(String str)273 public void write(String str) throws IOException { 274 this.out.write("# "); 275 this.out.write(str, 0, str.length()); 276 this.out.write("\n"); 277 } 278 279 /** 280 * Throws an <code>InterruptedRecorderException</code> exception 281 * if current thread is interrupted. 282 */ checkCurrentThreadIsntInterrupted()283 private void checkCurrentThreadIsntInterrupted() throws InterruptedIOException { 284 if (Thread.interrupted()) { 285 this.mtlFileName = null; 286 throw new InterruptedIOException("Current thread interrupted"); 287 } 288 } 289 290 /** 291 * Writes all the 3D shapes children of <code>node</code> at OBJ format. 292 * If there are transformation groups on the path from <code>node</code> to its shapes, 293 * they'll be applied to the coordinates written on output. 294 * The <code>node</code> shouldn't be alive or if it's alive it should have the 295 * capabilities to read its children, the geometries and the appearance of its shapes. 296 * Only geometries which are instances of <code>GeometryArray</code> will be written. 297 * @param node a Java 3D node 298 * @throws IOException if the operation failed 299 * @throws InterruptedIOException if the current thread was interrupted during this operation. 300 * The interrupted status of the current thread is cleared when this exception is thrown. 301 */ writeNode(Node node)302 public void writeNode(Node node) throws IOException, InterruptedIOException { 303 writeNode(node, null); 304 } 305 306 /** 307 * Writes all the 3D shapes children of <code>node</code> at OBJ format. 308 * If there are transformation groups on the path from <code>node</code> to its shapes, 309 * they'll be applied to the coordinates written on output. 310 * The <code>node</code> shouldn't be alive or if it's alive, it should have the 311 * capabilities to read its children, the geometries and the appearance of its shapes. 312 * Only geometries which are instances of <code>GeometryArray</code> will be written. 313 * @param node a Java 3D node 314 * @param nodeName the name of the node. This is useful to distinguish the objects 315 * names in output. If this name is <code>null</code> or isn't built 316 * with A-Z, a-z, 0-9 and underscores, it will be ignored. 317 * @throws IOException if the operation failed 318 * @throws InterruptedIOException if the current thread was interrupted during this operation 319 * The interrupted status of the current thread is cleared when this exception is thrown. 320 */ writeNode(Node node, String nodeName)321 public void writeNode(Node node, String nodeName) throws IOException, InterruptedIOException { 322 if (this.firstNode) { 323 if (this.mtlFileName != null) { 324 this.out.write("mtllib " + new File(this.mtlFileName).getName() + "\n"); 325 } 326 this.firstNode = false; 327 } 328 329 writeNode(node, nodeName, new Transform3D()); 330 } 331 332 /** 333 * Writes all the 3D shapes children of <code>node</code> at OBJ format. 334 */ writeNode(Node node, String nodeName, Transform3D parentTransformations)335 private void writeNode(Node node, String nodeName, Transform3D parentTransformations) throws IOException { 336 if (node instanceof Group) { 337 if (node instanceof TransformGroup) { 338 parentTransformations = new Transform3D(parentTransformations); 339 Transform3D transform = new Transform3D(); 340 ((TransformGroup)node).getTransform(transform); 341 parentTransformations.mul(transform); 342 } 343 // Write all children 344 Enumeration<?> enumeration = ((Group)node).getAllChildren(); 345 while (enumeration.hasMoreElements()) { 346 writeNode((Node)enumeration.nextElement(), nodeName, parentTransformations); 347 } 348 } else if (node instanceof Link) { 349 writeNode(((Link)node).getSharedGroup(), nodeName, parentTransformations); 350 } else if (node instanceof Shape3D) { 351 Shape3D shape = (Shape3D)node; 352 Appearance appearance = shape.getAppearance(); 353 RenderingAttributes renderingAttributes = appearance != null 354 ? appearance.getRenderingAttributes() : null; 355 if (shape.numGeometries() >= 1 356 && (renderingAttributes == null 357 || renderingAttributes.getVisible())) { 358 // Build a unique human readable object name 359 String objectName = ""; 360 if (accept(nodeName)) { 361 objectName = nodeName + "_"; 362 } 363 364 String shapeName = null; 365 if (shape.getUserData() instanceof String) { 366 shapeName = (String)shape.getUserData(); 367 } 368 if (accept(shapeName)) { 369 objectName += shapeName + "_"; 370 } 371 372 objectName += String.valueOf(this.shapeIndex++); 373 374 // Start a new object at OBJ format 375 this.out.write("g " + objectName + "\n"); 376 377 TexCoordGeneration texCoordGeneration = null; 378 Transform3D textureTransform = new Transform3D(); 379 if (this.mtlFileName != null) { 380 if (appearance != null) { 381 texCoordGeneration = appearance.getTexCoordGeneration(); 382 TextureAttributes textureAttributes = appearance.getTextureAttributes(); 383 if (textureAttributes != null) { 384 textureAttributes.getTextureTransform(textureTransform); 385 } 386 ComparableAppearance comparableAppearance = new ComparableAppearance(appearance); 387 String appearanceName = this.appearances.get(comparableAppearance); 388 if (appearanceName == null) { 389 // Store appearance 390 try { 391 appearanceName = appearance.getName(); 392 } catch (NoSuchMethodError ex) { 393 // Don't reuse appearance name with Java 3D < 1.4 where getName was added 394 } 395 if (appearanceName == null || !accept(appearanceName)) { 396 appearanceName = objectName; 397 } else { 398 // Find a unique appearance name among appearances 399 Collection<String> appearanceNames = this.appearances.values(); 400 String baseName = appearanceName + "_" + objectName; 401 for (int i = 0; appearanceNames.contains(appearanceName); i++) { 402 if (i == 0) { 403 appearanceName = baseName; 404 } else { 405 appearanceName = baseName + "_" + i; 406 } 407 } 408 } 409 this.appearances.put(comparableAppearance, appearanceName); 410 411 Texture texture = appearance.getTexture(); 412 if (texture != null) { 413 File textureFile = this.textures.get(texture); 414 if (textureFile == null) { 415 String fileExtension = "png"; 416 URL textureUrl = (URL)texture.getUserData(); 417 if (textureUrl instanceof URL) { 418 InputStream in = null; 419 try { 420 // Find the format of the texture image 421 in = openStream(textureUrl); 422 ImageInputStream imageIn = ImageIO.createImageInputStream(in); 423 Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageIn); 424 if (imageReaders.hasNext()) { 425 ImageReader reader = (ImageReader)imageReaders.next(); 426 fileExtension = reader.getFormatName().toLowerCase(); 427 // Store the URL to copy its image content directly when appearances are saved 428 this.copiedTextures.add(textureUrl); 429 } 430 } catch (IOException ex) { 431 if (in != null) { 432 in.close(); 433 } 434 } 435 } 436 437 // Find a unique texture file name which is not case sensitive 438 String textureFileBaseName = this.mtlFileName.substring(0, this.mtlFileName.length() - 4) 439 + "_" + appearanceName; 440 Collection<File> textureFiles = this.textures.values(); 441 boolean fileExists = true; 442 for (int i = 0; fileExists; i++) { 443 if (i == 0) { 444 textureFile = new File(textureFileBaseName + "." + fileExtension); 445 } else { 446 textureFile = new File(textureFileBaseName + "_" + i + "." + fileExtension); 447 } 448 449 fileExists = false; 450 for (File file : textureFiles) { 451 if (textureFile.getName().equalsIgnoreCase(file.getName())) { 452 fileExists = true; 453 break; 454 } 455 } 456 } 457 // Store texture 458 this.textures.put(texture, textureFile); 459 } 460 } 461 } 462 this.out.write("usemtl " + appearanceName + "\n"); 463 } 464 } 465 466 int cullFace = PolygonAttributes.CULL_BACK; 467 boolean backFaceNormalFlip = false; 468 if (appearance != null) { 469 PolygonAttributes polygonAttributes = appearance.getPolygonAttributes(); 470 if (polygonAttributes != null) { 471 cullFace = polygonAttributes.getCullFace(); 472 backFaceNormalFlip = polygonAttributes.getBackFaceNormalFlip(); 473 } 474 } 475 476 // Write object geometries 477 for (int i = 0, n = shape.numGeometries(); i < n; i++) { 478 writeNodeGeometry(shape.getGeometry(i), parentTransformations, texCoordGeneration, 479 textureTransform, cullFace, backFaceNormalFlip); 480 } 481 } 482 } 483 } 484 485 /** 486 * Returns an input stream to read the given URL. 487 */ openStream(URL url)488 private InputStream openStream(URL url) throws IOException { 489 URLConnection connection = url.openConnection(); 490 if (System.getProperty("os.name").startsWith("Windows") 491 && (connection instanceof JarURLConnection)) { 492 JarURLConnection urlConnection = (JarURLConnection)connection; 493 URL jarFileUrl = urlConnection.getJarFileURL(); 494 if (jarFileUrl.getProtocol().equalsIgnoreCase("file")) { 495 try { 496 File file; 497 try { 498 file = new File(jarFileUrl.toURI()); 499 } catch (IllegalArgumentException ex) { 500 // Try a second way to be able to access to files on Windows servers 501 file = new File(jarFileUrl.getPath()); 502 } 503 if (file.canWrite()) { 504 // Refuse to use caches to be able to delete the writable files accessed with jar protocol under Windows, 505 // as suggested in http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6962459 506 connection.setUseCaches(false); 507 } 508 } catch (URISyntaxException ex) { 509 IOException ex2 = new IOException(); 510 ex2.initCause(ex); 511 throw ex2; 512 } 513 } 514 } 515 return connection.getInputStream(); 516 } 517 518 /** 519 * Returns <code>true</code> if <code>name</code> contains 520 * only letters, digits and underscores. 521 */ accept(String name)522 private boolean accept(String name) { 523 if (name == null) { 524 return false; 525 } 526 for (int i = 0; i < name.length(); i++) { 527 char car = name.charAt(i); 528 if (!(car >= 'a' && car <= 'z' 529 || car >= 'A' && car <= 'Z' 530 || car >= '0' && car <= '9' 531 || car == '_')) { 532 return false; 533 } 534 } 535 return true; 536 } 537 538 /** 539 * Writes a 3D geometry at OBJ format. 540 */ writeNodeGeometry(Geometry geometry, Transform3D parentTransformations, TexCoordGeneration texCoordGeneration, Transform3D textureTransform, int cullFace, boolean backFaceNormalFlip)541 private void writeNodeGeometry(Geometry geometry, 542 Transform3D parentTransformations, 543 TexCoordGeneration texCoordGeneration, 544 Transform3D textureTransform, 545 int cullFace, 546 boolean backFaceNormalFlip) throws IOException { 547 if (geometry instanceof GeometryArray) { 548 GeometryArray geometryArray = (GeometryArray)geometry; 549 550 int [] vertexIndexSubstitutes = new int [geometryArray.getVertexCount()]; 551 552 boolean normalsDefined = (geometryArray.getVertexFormat() & GeometryArray.NORMALS) != 0; 553 StringBuilder normalsBuffer; 554 List<Vector3f> addedNormals; 555 if (normalsDefined) { 556 normalsBuffer = new StringBuilder(geometryArray.getVertexCount() * 3 * 10); 557 addedNormals = new ArrayList<Vector3f>(); 558 } else { 559 normalsBuffer = null; 560 addedNormals = null; 561 } 562 int [] normalIndexSubstitutes = new int [geometryArray.getVertexCount()]; 563 int [] oppositeSideNormalIndexSubstitutes; 564 if (cullFace == PolygonAttributes.CULL_NONE) { 565 oppositeSideNormalIndexSubstitutes = new int [geometryArray.getVertexCount()]; 566 } else { 567 oppositeSideNormalIndexSubstitutes = null; 568 } 569 570 boolean textureCoordinatesDefined = (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0; 571 int [] textureCoordinatesIndexSubstitutes = new int [geometryArray.getVertexCount()]; 572 573 boolean textureCoordinatesGenerated = false; 574 Vector4f planeS = null; 575 Vector4f planeT = null; 576 if (texCoordGeneration != null) { 577 textureCoordinatesGenerated = texCoordGeneration.getGenMode() == TexCoordGeneration.OBJECT_LINEAR 578 && texCoordGeneration.getEnable() 579 && !(geometryArray instanceof IndexedLineArray) 580 && !(geometryArray instanceof IndexedLineStripArray) 581 && !(geometryArray instanceof LineArray) 582 && !(geometryArray instanceof LineStripArray); 583 if (textureCoordinatesGenerated) { 584 planeS = new Vector4f(); 585 planeT = new Vector4f(); 586 texCoordGeneration.getPlaneS(planeS); 587 texCoordGeneration.getPlaneT(planeT); 588 } 589 } 590 591 checkCurrentThreadIsntInterrupted(); 592 593 if ((geometryArray.getVertexFormat() & GeometryArray.BY_REFERENCE) != 0) { 594 if ((geometryArray.getVertexFormat() & GeometryArray.INTERLEAVED) != 0) { 595 float [] vertexData = geometryArray.getInterleavedVertices(); 596 int vertexSize = vertexData.length / geometryArray.getVertexCount(); 597 // Write vertices coordinates 598 for (int index = 0, i = vertexSize - 3, n = geometryArray.getVertexCount(); 599 index < n; index++, i += vertexSize) { 600 Point3f vertex = new Point3f(vertexData [i], vertexData [i + 1], vertexData [i + 2]); 601 writeVertex(parentTransformations, vertex, index, vertexIndexSubstitutes); 602 } 603 // Write texture coordinates 604 if (texCoordGeneration != null) { 605 if (textureCoordinatesGenerated) { 606 for (int index = 0, i = vertexSize - 3, n = geometryArray.getVertexCount(); 607 index < n; index++, i += vertexSize) { 608 TexCoord2f textureCoordinates = generateTextureCoordinates( 609 vertexData [i], vertexData [i + 1], vertexData [i + 2], planeS, planeT); 610 writeTextureCoordinates(textureCoordinates, textureTransform, index, textureCoordinatesIndexSubstitutes); 611 } 612 } 613 } else if (textureCoordinatesDefined) { 614 for (int index = 0, i = 0, n = geometryArray.getVertexCount(); 615 index < n; index++, i += vertexSize) { 616 TexCoord2f textureCoordinates = new TexCoord2f(vertexData [i], vertexData [i + 1]); 617 writeTextureCoordinates(textureCoordinates, textureTransform, index, textureCoordinatesIndexSubstitutes); 618 } 619 } 620 // Write normals 621 if (normalsDefined) { 622 for (int index = 0, i = vertexSize - 6, n = geometryArray.getVertexCount(); 623 normalsDefined && index < n; index++, i += vertexSize) { 624 Vector3f normal = new Vector3f(vertexData [i], vertexData [i + 1], vertexData [i + 2]); 625 normalsDefined = writeNormal(normalsBuffer, parentTransformations, normal, index, normalIndexSubstitutes, 626 oppositeSideNormalIndexSubstitutes, addedNormals, cullFace, backFaceNormalFlip); 627 } 628 } 629 } else { 630 // Write vertices coordinates 631 float [] vertexCoordinates = geometryArray.getCoordRefFloat(); 632 for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) { 633 Point3f vertex = new Point3f(vertexCoordinates [i], vertexCoordinates [i + 1], vertexCoordinates [i + 2]); 634 writeVertex(parentTransformations, vertex, index, 635 vertexIndexSubstitutes); 636 } 637 // Write texture coordinates 638 if (texCoordGeneration != null) { 639 if (textureCoordinatesGenerated) { 640 for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) { 641 TexCoord2f textureCoordinates = generateTextureCoordinates( 642 vertexCoordinates [i], vertexCoordinates [i + 1], vertexCoordinates [i + 2], planeS, planeT); 643 writeTextureCoordinates(textureCoordinates, textureTransform, index, textureCoordinatesIndexSubstitutes); 644 } 645 } 646 } else if (textureCoordinatesDefined) { 647 float [] textureCoordinatesArray = geometryArray.getTexCoordRefFloat(0); 648 for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 2) { 649 TexCoord2f textureCoordinates = new TexCoord2f(textureCoordinatesArray [i], textureCoordinatesArray [i + 1]); 650 writeTextureCoordinates(textureCoordinates, textureTransform, index, textureCoordinatesIndexSubstitutes); 651 } 652 } 653 // Write normals 654 if (normalsDefined) { 655 float [] normalCoordinates = geometryArray.getNormalRefFloat(); 656 for (int index = 0, i = 0, n = geometryArray.getVertexCount(); normalsDefined && index < n; index++, i += 3) { 657 Vector3f normal = new Vector3f(normalCoordinates [i], normalCoordinates [i + 1], normalCoordinates [i + 2]); 658 normalsDefined = writeNormal(normalsBuffer, parentTransformations, normal, index, normalIndexSubstitutes, 659 oppositeSideNormalIndexSubstitutes, addedNormals, cullFace, backFaceNormalFlip); 660 } 661 } 662 } 663 } else { 664 // Write vertices coordinates 665 for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) { 666 Point3f vertex = new Point3f(); 667 geometryArray.getCoordinate(index, vertex); 668 writeVertex(parentTransformations, vertex, index, 669 vertexIndexSubstitutes); 670 } 671 // Write texture coordinates 672 if (texCoordGeneration != null) { 673 if (textureCoordinatesGenerated) { 674 for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) { 675 Point3f vertex = new Point3f(); 676 geometryArray.getCoordinate(index, vertex); 677 TexCoord2f textureCoordinates = generateTextureCoordinates( 678 vertex.x, vertex.y, vertex.z, planeS, planeT); 679 writeTextureCoordinates(textureCoordinates, textureTransform, index, textureCoordinatesIndexSubstitutes); 680 } 681 } 682 } else if (textureCoordinatesDefined) { 683 for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) { 684 TexCoord2f textureCoordinates = new TexCoord2f(); 685 geometryArray.getTextureCoordinate(0, index, textureCoordinates); 686 writeTextureCoordinates(textureCoordinates, textureTransform, index, textureCoordinatesIndexSubstitutes); 687 } 688 } 689 // Write normals 690 if (normalsDefined) { 691 for (int index = 0, n = geometryArray.getVertexCount(); normalsDefined && index < n; index++) { 692 Vector3f normal = new Vector3f(); 693 geometryArray.getNormal(index, normal); 694 normalsDefined = writeNormal(normalsBuffer, parentTransformations, normal, index, normalIndexSubstitutes, 695 oppositeSideNormalIndexSubstitutes, addedNormals, cullFace, backFaceNormalFlip); 696 } 697 } 698 } 699 700 if (normalsDefined) { 701 // Write normals only if they all contain valid values 702 out.write(normalsBuffer.toString()); 703 } else if (addedNormals != null) { 704 // Remove ignored normals 705 for (Vector3f normal : addedNormals) { 706 this.normalIndices.remove(normal); 707 } 708 } 709 710 checkCurrentThreadIsntInterrupted(); 711 712 // Write lines, triangles or quadrilaterals depending on the geometry 713 if (geometryArray instanceof IndexedGeometryArray) { 714 if (geometryArray instanceof IndexedLineArray) { 715 IndexedLineArray lineArray = (IndexedLineArray)geometryArray; 716 for (int i = 0, n = lineArray.getIndexCount(); i < n; i += 2) { 717 writeIndexedLine(lineArray, i, i + 1, vertexIndexSubstitutes, textureCoordinatesIndexSubstitutes); 718 } 719 } else if (geometryArray instanceof IndexedTriangleArray) { 720 IndexedTriangleArray triangleArray = (IndexedTriangleArray)geometryArray; 721 for (int i = 0, n = triangleArray.getIndexCount(); i < n; i += 3) { 722 writeIndexedTriangle(triangleArray, i, i + 1, i + 2, 723 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 724 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 725 } 726 } else if (geometryArray instanceof IndexedQuadArray) { 727 IndexedQuadArray quadArray = (IndexedQuadArray)geometryArray; 728 for (int i = 0, n = quadArray.getIndexCount(); i < n; i += 4) { 729 writeIndexedQuadrilateral(quadArray, i, i + 1, i + 2, i + 3, 730 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 731 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 732 } 733 } else if (geometryArray instanceof IndexedGeometryStripArray) { 734 IndexedGeometryStripArray geometryStripArray = (IndexedGeometryStripArray)geometryArray; 735 int [] stripIndexCounts = new int [geometryStripArray.getNumStrips()]; 736 geometryStripArray.getStripIndexCounts(stripIndexCounts); 737 int initialIndex = 0; 738 739 if (geometryStripArray instanceof IndexedLineStripArray) { 740 for (int strip = 0; strip < stripIndexCounts.length; strip++) { 741 for (int i = initialIndex, n = initialIndex + stripIndexCounts [strip] - 1; i < n; i++) { 742 writeIndexedLine(geometryStripArray, i, i + 1, 743 vertexIndexSubstitutes, textureCoordinatesIndexSubstitutes); 744 } 745 initialIndex += stripIndexCounts [strip]; 746 } 747 } else if (geometryStripArray instanceof IndexedTriangleStripArray) { 748 for (int strip = 0; strip < stripIndexCounts.length; strip++) { 749 for (int i = initialIndex, n = initialIndex + stripIndexCounts [strip] - 2, j = 0; i < n; i++, j++) { 750 if (j % 2 == 0) { 751 writeIndexedTriangle(geometryStripArray, i, i + 1, i + 2, 752 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 753 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 754 } else { // Vertices of odd triangles are in reverse order 755 writeIndexedTriangle(geometryStripArray, i, i + 2, i + 1, 756 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 757 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 758 } 759 } 760 initialIndex += stripIndexCounts [strip]; 761 } 762 } else if (geometryStripArray instanceof IndexedTriangleFanArray) { 763 for (int strip = 0; strip < stripIndexCounts.length; strip++) { 764 for (int i = initialIndex, n = initialIndex + stripIndexCounts [strip] - 2; i < n; i++) { 765 writeIndexedTriangle(geometryStripArray, initialIndex, i + 1, i + 2, 766 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 767 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 768 } 769 initialIndex += stripIndexCounts [strip]; 770 } 771 } 772 } 773 } else { 774 if (geometryArray instanceof LineArray) { 775 LineArray lineArray = (LineArray)geometryArray; 776 for (int i = 0, n = lineArray.getVertexCount(); i < n; i += 2) { 777 writeLine(lineArray, i, i + 1, vertexIndexSubstitutes, textureCoordinatesIndexSubstitutes); 778 } 779 } else if (geometryArray instanceof TriangleArray) { 780 TriangleArray triangleArray = (TriangleArray)geometryArray; 781 for (int i = 0, n = triangleArray.getVertexCount(); i < n; i += 3) { 782 writeTriangle(triangleArray, i, i + 1, i + 2, 783 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 784 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 785 } 786 } else if (geometryArray instanceof QuadArray) { 787 QuadArray quadArray = (QuadArray)geometryArray; 788 for (int i = 0, n = quadArray.getVertexCount(); i < n; i += 4) { 789 writeQuadrilateral(quadArray, i, i + 1, i + 2, i + 3, 790 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 791 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 792 } 793 } else if (geometryArray instanceof GeometryStripArray) { 794 GeometryStripArray geometryStripArray = (GeometryStripArray)geometryArray; 795 int [] stripVertexCounts = new int [geometryStripArray.getNumStrips()]; 796 geometryStripArray.getStripVertexCounts(stripVertexCounts); 797 int initialIndex = 0; 798 799 if (geometryStripArray instanceof LineStripArray) { 800 for (int strip = 0; strip < stripVertexCounts.length; strip++) { 801 for (int i = initialIndex, n = initialIndex + stripVertexCounts [strip] - 1; i < n; i++) { 802 writeLine(geometryStripArray, i, i + 1, vertexIndexSubstitutes, textureCoordinatesIndexSubstitutes); 803 } 804 initialIndex += stripVertexCounts [strip]; 805 } 806 } else if (geometryStripArray instanceof TriangleStripArray) { 807 for (int strip = 0; strip < stripVertexCounts.length; strip++) { 808 for (int i = initialIndex, n = initialIndex + stripVertexCounts [strip] - 2, j = 0; i < n; i++, j++) { 809 if (j % 2 == 0) { 810 writeTriangle(geometryStripArray, i, i + 1, i + 2, 811 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 812 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 813 } else { // Vertices of odd triangles are in reverse order 814 writeTriangle(geometryStripArray, i, i + 2, i + 1, 815 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 816 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 817 } 818 } 819 initialIndex += stripVertexCounts [strip]; 820 } 821 } else if (geometryStripArray instanceof TriangleFanArray) { 822 for (int strip = 0; strip < stripVertexCounts.length; strip++) { 823 for (int i = initialIndex, n = initialIndex + stripVertexCounts [strip] - 2; i < n; i++) { 824 writeTriangle(geometryStripArray, initialIndex, i + 1, i + 2, 825 vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, 826 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); 827 } 828 initialIndex += stripVertexCounts [strip]; 829 } 830 } 831 } 832 } 833 } 834 } 835 836 /** 837 * Returns texture coordinates generated with <code>texCoordGeneration</code> computed 838 * as described in <code>TexCoordGeneration</code> javadoc. 839 */ generateTextureCoordinates(float x, float y, float z, Vector4f planeS, Vector4f planeT)840 private TexCoord2f generateTextureCoordinates(float x, float y, float z, 841 Vector4f planeS, 842 Vector4f planeT) { 843 return new TexCoord2f(x * planeS.x + y * planeS.y + z * planeS.z + planeS.w, 844 x * planeT.x + y * planeT.y + z * planeT.z + planeT.w); 845 } 846 847 /** 848 * Applies to <code>vertex</code> the given transformation, and writes it in 849 * a line v at OBJ format, if the vertex wasn't written yet. 850 */ writeVertex(Transform3D transformationToParent, Point3f vertex, int index, int [] vertexIndexSubstitutes)851 private void writeVertex(Transform3D transformationToParent, 852 Point3f vertex, int index, 853 int [] vertexIndexSubstitutes) throws IOException { 854 transformationToParent.transform(vertex); 855 Integer vertexIndex = this.vertexIndices.get(vertex); 856 if (vertexIndex == null) { 857 vertexIndexSubstitutes [index] = this.vertexIndices.size() + 1; 858 this.vertexIndices.put(vertex, vertexIndexSubstitutes [index]); 859 // Write only once unique vertices 860 this.out.write("v " + format(vertex.x) 861 + " " + format(vertex.y) 862 + " " + format(vertex.z) + "\n"); 863 } else { 864 vertexIndexSubstitutes [index] = vertexIndex; 865 } 866 } 867 868 /** 869 * Formats a float number to a string as fast as possible depending on the 870 * format chosen in constructor. 871 */ format(float number)872 private String format(float number) { 873 if (this.numberFormat != null) { 874 return this.numberFormat.format(number); 875 } else { 876 String numberString = String.valueOf((float)number); 877 if (numberString.indexOf('E') != -1) { 878 // Avoid scientific notation 879 return this.defaultNumberFormat.format(number); 880 } else { 881 return numberString; 882 } 883 } 884 } 885 886 /** 887 * Applies to <code>normal</code> the given transformation, and appends to <code>normalsBuffer</code> 888 * its values in a line vn at OBJ format, if the normal wasn't written yet. 889 * @return <code>true</code> if the written normal doens't contain any NaN value 890 */ writeNormal(StringBuilder normalsBuffer, Transform3D transformationToParent, Vector3f normal, int index, int [] normalIndexSubstitutes, int [] oppositeSideNormalIndexSubstitutes, List<Vector3f> addedNormals, int cullFace, boolean backFaceNormalFlip)891 private boolean writeNormal(StringBuilder normalsBuffer, 892 Transform3D transformationToParent, 893 Vector3f normal, int index, 894 int [] normalIndexSubstitutes, 895 int [] oppositeSideNormalIndexSubstitutes, 896 List<Vector3f> addedNormals, 897 int cullFace, boolean backFaceNormalFlip) throws IOException { 898 if (Float.isNaN(normal.x) || Float.isNaN(normal.y) || Float.isNaN(normal.z)) { 899 return false; 900 } 901 if (backFaceNormalFlip) { 902 normal.negate(); 903 } 904 if (normal.x != 0 || normal.y != 0 || normal.z != 0) { 905 transformationToParent.transform(normal); 906 normal.normalize(); 907 } 908 Integer normalIndex = this.normalIndices.get(normal); 909 if (normalIndex == null) { 910 normalIndexSubstitutes [index] = this.normalIndices.size() + 1; 911 this.normalIndices.put(normal, normalIndexSubstitutes [index]); 912 addedNormals.add(normal); 913 // Write only once unique normals 914 normalsBuffer.append("vn " + format(normal.x) 915 + " " + format(normal.y) 916 + " " + format(normal.z) + "\n"); 917 } else { 918 normalIndexSubstitutes [index] = normalIndex; 919 } 920 921 if (cullFace == PolygonAttributes.CULL_NONE) { 922 Vector3f oppositeNormal = new Vector3f(); 923 oppositeNormal.negate(normal); 924 // Fill opposite side normal index substitutes array 925 return writeNormal(normalsBuffer, transformationToParent, oppositeNormal, index, oppositeSideNormalIndexSubstitutes, 926 null, addedNormals, PolygonAttributes.CULL_FRONT, false); 927 } else { 928 return true; 929 } 930 } 931 932 /** 933 * Writes <code>textureCoordinates</code> in a line vt at OBJ format, 934 * if the texture coordinates wasn't written yet. 935 */ writeTextureCoordinates(TexCoord2f textureCoordinates, Transform3D textureTransform, int index, int [] textureCoordinatesIndexSubstitutes)936 private void writeTextureCoordinates(TexCoord2f textureCoordinates, Transform3D textureTransform, 937 int index, int [] textureCoordinatesIndexSubstitutes) throws IOException { 938 if (textureTransform.getBestType() != Transform3D.IDENTITY) { 939 Point3f transformedCoordinates = new Point3f(textureCoordinates.x, textureCoordinates.y, 0); 940 textureTransform.transform(transformedCoordinates); 941 textureCoordinates = new TexCoord2f(transformedCoordinates.x, transformedCoordinates.y); 942 } 943 Integer textureCoordinatesIndex = this.textureCoordinatesIndices.get(textureCoordinates); 944 if (textureCoordinatesIndex == null) { 945 textureCoordinatesIndexSubstitutes [index] = this.textureCoordinatesIndices.size() + 1; 946 this.textureCoordinatesIndices.put(textureCoordinates, textureCoordinatesIndexSubstitutes [index]); 947 // Write only once unique texture coordinates 948 this.out.write("vt " + format(textureCoordinates.x) 949 + " " + format(textureCoordinates.y) + " 0\n"); 950 } else { 951 textureCoordinatesIndexSubstitutes [index] = textureCoordinatesIndex; 952 } 953 } 954 955 /** 956 * Writes the line indices given at vertexIndex1, vertexIndex2, 957 * in a line l at OBJ format. 958 */ writeIndexedLine(IndexedGeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int [] vertexIndexSubstitutes, int [] textureCoordinatesIndexSubstitutes)959 private void writeIndexedLine(IndexedGeometryArray geometryArray, 960 int vertexIndex1, int vertexIndex2, 961 int [] vertexIndexSubstitutes, 962 int [] textureCoordinatesIndexSubstitutes) throws IOException { 963 if ((geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { 964 this.out.write("l " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 965 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) 966 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 967 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) + "\n"); 968 } else { 969 this.out.write("l " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 970 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "\n"); 971 } 972 } 973 974 /** 975 * Writes the triangle indices given at vertexIndex1, vertexIndex2, vertexIndex3, 976 * in a line f at OBJ format. 977 */ writeIndexedTriangle(IndexedGeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, int [] vertexIndexSubstitutes, int [] normalIndexSubstitutes, int [] oppositeSideNormalIndexSubstitutes, boolean normalsDefined, int [] textureCoordinatesIndexSubstitutes, boolean textureCoordinatesGenerated, int cullFace)978 private void writeIndexedTriangle(IndexedGeometryArray geometryArray, 979 int vertexIndex1, int vertexIndex2, int vertexIndex3, 980 int [] vertexIndexSubstitutes, 981 int [] normalIndexSubstitutes, 982 int [] oppositeSideNormalIndexSubstitutes, 983 boolean normalsDefined, 984 int [] textureCoordinatesIndexSubstitutes, 985 boolean textureCoordinatesGenerated, int cullFace) throws IOException { 986 if (cullFace == PolygonAttributes.CULL_FRONT) { 987 // Reverse vertex order 988 int tmp = vertexIndex1; 989 vertexIndex1 = vertexIndex3; 990 vertexIndex3 = tmp; 991 } 992 993 if (textureCoordinatesGenerated) { 994 if (normalsDefined) { 995 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 996 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 997 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) 998 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 999 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1000 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) 1001 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1002 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1003 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) + "\n"); 1004 } else { 1005 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1006 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1007 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1008 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1009 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1010 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "\n"); 1011 } 1012 } else if ((geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { 1013 if (normalsDefined) { 1014 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1015 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) 1016 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) 1017 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1018 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) 1019 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) 1020 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1021 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex3)]) 1022 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) + "\n"); 1023 } else { 1024 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1025 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) 1026 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1027 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) 1028 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1029 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex3)]) + "\n"); 1030 } 1031 } else { 1032 if (normalsDefined) { 1033 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1034 + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) 1035 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1036 + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) 1037 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1038 + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) + "\n"); 1039 } else { 1040 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1041 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1042 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "\n"); 1043 } 1044 } 1045 1046 if (cullFace == PolygonAttributes.CULL_NONE) { 1047 // Use opposite side normal index substitutes array 1048 writeIndexedTriangle(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, 1049 vertexIndexSubstitutes, oppositeSideNormalIndexSubstitutes, null, 1050 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, PolygonAttributes.CULL_FRONT); 1051 } 1052 } 1053 1054 /** 1055 * Writes the quadrilateral indices given at vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4, 1056 * in a line f at OBJ format. 1057 */ writeIndexedQuadrilateral(IndexedGeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, int vertexIndex4, int [] vertexIndexSubstitutes, int [] normalIndexSubstitutes, int [] oppositeSideNormalIndexSubstitutes, boolean normalsDefined, int [] textureCoordinatesIndexSubstitutes, boolean textureCoordinatesGenerated, int cullFace)1058 private void writeIndexedQuadrilateral(IndexedGeometryArray geometryArray, 1059 int vertexIndex1, int vertexIndex2, int vertexIndex3, int vertexIndex4, 1060 int [] vertexIndexSubstitutes, 1061 int [] normalIndexSubstitutes, 1062 int [] oppositeSideNormalIndexSubstitutes, 1063 boolean normalsDefined, 1064 int [] textureCoordinatesIndexSubstitutes, 1065 boolean textureCoordinatesGenerated, int cullFace) throws IOException { 1066 if (cullFace == PolygonAttributes.CULL_FRONT) { 1067 // Reverse vertex order 1068 int tmp = vertexIndex2; 1069 vertexIndex2 = vertexIndex3; 1070 vertexIndex3 = tmp; 1071 tmp = vertexIndex1; 1072 vertexIndex1 = vertexIndex4; 1073 vertexIndex4 = tmp; 1074 } 1075 1076 if (textureCoordinatesGenerated) { 1077 if (normalsDefined) { 1078 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1079 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1080 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) 1081 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1082 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1083 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) 1084 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1085 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1086 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) 1087 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) 1088 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) 1089 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex4)]) + "\n"); 1090 } else { 1091 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1092 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1093 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1094 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1095 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1096 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1097 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) 1098 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) + "\n"); 1099 } 1100 } else if ((geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { 1101 if (normalsDefined) { 1102 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1103 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) 1104 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) 1105 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1106 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) 1107 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) 1108 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1109 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex3)]) 1110 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) 1111 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) 1112 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex4)]) 1113 + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex4)]) + "\n"); 1114 } else { 1115 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1116 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) 1117 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1118 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) 1119 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1120 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex3)]) 1121 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) 1122 + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex4)]) + "\n"); 1123 } 1124 } else { 1125 if (normalsDefined) { 1126 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1127 + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) 1128 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1129 + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) 1130 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1131 + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) 1132 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) 1133 + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex4)]) + "\n"); 1134 } else { 1135 this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) 1136 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) 1137 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) 1138 + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) + "\n"); 1139 } 1140 } 1141 1142 if (cullFace == PolygonAttributes.CULL_NONE) { 1143 // Use opposite side normal index substitutes array 1144 writeIndexedQuadrilateral(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4, 1145 vertexIndexSubstitutes, oppositeSideNormalIndexSubstitutes, null, 1146 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, PolygonAttributes.CULL_FRONT); 1147 } 1148 } 1149 1150 /** 1151 * Writes the line indices given at vertexIndex1, vertexIndex2, 1152 * in a line l at OBJ format. 1153 */ writeLine(GeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int [] vertexIndexSubstitutes, int [] textureCoordinatesIndexSubstitutes)1154 private void writeLine(GeometryArray geometryArray, 1155 int vertexIndex1, int vertexIndex2, 1156 int [] vertexIndexSubstitutes, 1157 int [] textureCoordinatesIndexSubstitutes) throws IOException { 1158 if ((geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { 1159 this.out.write("l " + (vertexIndexSubstitutes [vertexIndex1]) 1160 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) 1161 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1162 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) + "\n"); 1163 } else { 1164 this.out.write("l " + (vertexIndexSubstitutes [vertexIndex1]) 1165 + " " + (vertexIndexSubstitutes [vertexIndex2]) + "\n"); 1166 } 1167 } 1168 1169 /** 1170 * Writes the triangle indices given at vertexIndex1, vertexIndex2, vertexIndex3, 1171 * in a line f at OBJ format. 1172 */ writeTriangle(GeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, int [] vertexIndexSubstitutes, int [] normalIndexSubstitutes, int [] oppositeSideNormalIndexSubstitutes, boolean normalsDefined, int [] textureCoordinatesIndexSubstitutes, boolean textureCoordinatesGenerated, int cullFace)1173 private void writeTriangle(GeometryArray geometryArray, 1174 int vertexIndex1, int vertexIndex2, int vertexIndex3, 1175 int [] vertexIndexSubstitutes, 1176 int [] normalIndexSubstitutes, 1177 int [] oppositeSideNormalIndexSubstitutes, 1178 boolean normalsDefined, 1179 int [] textureCoordinatesIndexSubstitutes, 1180 boolean textureCoordinatesGenerated, int cullFace) throws IOException { 1181 if (cullFace == PolygonAttributes.CULL_FRONT) { 1182 // Reverse vertex order 1183 int tmp = vertexIndex1; 1184 vertexIndex1 = vertexIndex3; 1185 vertexIndex3 = tmp; 1186 } 1187 1188 if (textureCoordinatesGenerated 1189 || (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { 1190 if (normalsDefined) { 1191 this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) 1192 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) 1193 + "/" + (normalIndexSubstitutes [vertexIndex1]) 1194 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1195 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) 1196 + "/" + (normalIndexSubstitutes [vertexIndex2]) 1197 + " " + (vertexIndexSubstitutes [vertexIndex3]) 1198 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex3]) 1199 + "/" + (normalIndexSubstitutes [vertexIndex3]) + "\n"); 1200 } else { 1201 this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) 1202 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) 1203 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1204 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) 1205 + " " + (vertexIndexSubstitutes [vertexIndex3]) 1206 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex3]) + "\n"); 1207 } 1208 } else { 1209 if (normalsDefined) { 1210 this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) 1211 + "//" + (normalIndexSubstitutes [vertexIndex1]) 1212 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1213 + "//" + (normalIndexSubstitutes [vertexIndex2]) 1214 + " " + (vertexIndexSubstitutes [vertexIndex3]) 1215 + "//" + (normalIndexSubstitutes [vertexIndex3]) + "\n"); 1216 } else { 1217 this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) 1218 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1219 + " " + (vertexIndexSubstitutes [vertexIndex3]) + "\n"); 1220 } 1221 } 1222 1223 if (cullFace == PolygonAttributes.CULL_NONE) { 1224 // Use opposite side normal index substitutes array 1225 writeTriangle(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, 1226 vertexIndexSubstitutes, oppositeSideNormalIndexSubstitutes, null, 1227 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, PolygonAttributes.CULL_FRONT); 1228 } 1229 } 1230 1231 /** 1232 * Writes the quadrilateral indices given at vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4, 1233 * in a line f at OBJ format. 1234 */ writeQuadrilateral(GeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, int vertexIndex4, int [] vertexIndexSubstitutes, int [] normalIndexSubstitutes, int [] oppositeSideNormalIndexSubstitutes, boolean normalsDefined, int [] textureCoordinatesIndexSubstitutes, boolean textureCoordinatesGenerated, int cullFace)1235 private void writeQuadrilateral(GeometryArray geometryArray, 1236 int vertexIndex1, int vertexIndex2, int vertexIndex3, int vertexIndex4, 1237 int [] vertexIndexSubstitutes, 1238 int [] normalIndexSubstitutes, 1239 int [] oppositeSideNormalIndexSubstitutes, 1240 boolean normalsDefined, 1241 int [] textureCoordinatesIndexSubstitutes, 1242 boolean textureCoordinatesGenerated, int cullFace) throws IOException { 1243 if (cullFace == PolygonAttributes.CULL_FRONT) { 1244 // Reverse vertex order 1245 int tmp = vertexIndex2; 1246 vertexIndex2 = vertexIndex3; 1247 vertexIndex3 = tmp; 1248 tmp = vertexIndex1; 1249 vertexIndex1 = vertexIndex4; 1250 vertexIndex4 = tmp; 1251 } 1252 1253 if (textureCoordinatesGenerated 1254 || (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { 1255 if (normalsDefined) { 1256 this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) 1257 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) 1258 + "/" + (normalIndexSubstitutes [vertexIndex1]) 1259 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1260 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) 1261 + "/" + (normalIndexSubstitutes [vertexIndex2]) 1262 + " " + (vertexIndexSubstitutes [vertexIndex3]) 1263 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex3]) 1264 + "/" + (normalIndexSubstitutes [vertexIndex3]) 1265 + " " + (vertexIndexSubstitutes [vertexIndex4]) 1266 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex4]) 1267 + "/" + (normalIndexSubstitutes [vertexIndex4]) + "\n"); 1268 } else { 1269 this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) 1270 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) 1271 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1272 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) 1273 + " " + (vertexIndexSubstitutes [vertexIndex3]) 1274 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex3]) 1275 + " " + (vertexIndexSubstitutes [vertexIndex4]) 1276 + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex4]) + "\n"); 1277 } 1278 } else { 1279 if (normalsDefined) { 1280 this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) 1281 + "//" + (normalIndexSubstitutes [vertexIndex1]) 1282 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1283 + "//" + (normalIndexSubstitutes [vertexIndex2]) 1284 + " " + (vertexIndexSubstitutes [vertexIndex3]) 1285 + "//" + (normalIndexSubstitutes [vertexIndex3]) 1286 + " " + (vertexIndexSubstitutes [vertexIndex4]) 1287 + "//" + (normalIndexSubstitutes [vertexIndex4]) + "\n"); 1288 } else { 1289 this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) 1290 + " " + (vertexIndexSubstitutes [vertexIndex2]) 1291 + " " + (vertexIndexSubstitutes [vertexIndex3]) 1292 + " " + (vertexIndexSubstitutes [vertexIndex4]) + "\n"); 1293 } 1294 } 1295 1296 if (cullFace == PolygonAttributes.CULL_NONE) { 1297 // Use opposite side normal index substitutes array 1298 writeQuadrilateral(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4, 1299 vertexIndexSubstitutes, oppositeSideNormalIndexSubstitutes, null, 1300 normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, PolygonAttributes.CULL_FRONT); 1301 } 1302 } 1303 1304 /** 1305 * Closes this writer and writes MTL file and its texture images, 1306 * if this writer was created from a file. 1307 * @throws IOException if this writer couldn't be closed 1308 * or couldn't write MTL and texture files couldn't be written 1309 * @throws InterruptedIOException if the current thread was interrupted during this operation 1310 * The interrupted status of the current thread is cleared when this exception is thrown. 1311 */ 1312 @Override close()1313 public void close() throws IOException, InterruptedIOException { 1314 super.close(); 1315 if (this.mtlFileName != null) { 1316 writeAppearancesToMTLFile(); 1317 } 1318 } 1319 1320 /** 1321 * Exports a set of appearance to a MTL file built from OBJ file name. 1322 */ writeAppearancesToMTLFile()1323 private void writeAppearancesToMTLFile() throws IOException { 1324 Writer writer = null; 1325 try { 1326 writer = new OutputStreamWriter( 1327 new BufferedOutputStream(new FileOutputStream(this.mtlFileName)), "ISO-8859-1"); 1328 writeHeader(writer); 1329 for (Map.Entry<ComparableAppearance, String> appearanceEntry : this.appearances.entrySet()) { 1330 checkCurrentThreadIsntInterrupted(); 1331 1332 Appearance appearance = appearanceEntry.getKey().getAppearance(); 1333 String appearanceName = appearanceEntry.getValue(); 1334 writer.write("\nnewmtl " + appearanceName + "\n"); 1335 Material material = appearance.getMaterial(); 1336 if (material != null) { 1337 if (material instanceof OBJMaterial 1338 && ((OBJMaterial)material).isIlluminationModelSet()) { 1339 writer.write("illum " + ((OBJMaterial)material).getIlluminationModel() + "\n"); 1340 } else if (material.getShininess() > 1) { 1341 writer.write("illum 2\n"); 1342 } else if (material.getLightingEnable()) { 1343 writer.write("illum 1\n"); 1344 } else { 1345 writer.write("illum 0\n"); 1346 } 1347 Color3f color = new Color3f(); 1348 material.getAmbientColor(color); 1349 writer.write("Ka " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); 1350 material.getDiffuseColor(color); 1351 writer.write("Kd " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); 1352 material.getSpecularColor(color); 1353 writer.write("Ks " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); 1354 writer.write("Ns " + format(material.getShininess()) + "\n"); 1355 if (material instanceof OBJMaterial) { 1356 OBJMaterial objMaterial = (OBJMaterial)material; 1357 if (objMaterial.isOpticalDensitySet()) { 1358 writer.write("Ni " + format(objMaterial.getOpticalDensity()) + "\n"); 1359 } 1360 if (objMaterial.isSharpnessSet()) { 1361 writer.write("sharpness " + format(objMaterial.getSharpness()) + "\n"); 1362 } 1363 } 1364 } else { 1365 ColoringAttributes coloringAttributes = appearance.getColoringAttributes(); 1366 if (coloringAttributes != null) { 1367 writer.write("illum 0\n"); 1368 Color3f color = new Color3f(); 1369 coloringAttributes.getColor(color); 1370 writer.write("Ka " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); 1371 writer.write("Kd " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); 1372 writer.write("Ks " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); 1373 } 1374 } 1375 TransparencyAttributes transparency = appearance.getTransparencyAttributes(); 1376 if (transparency != null) { 1377 if (!(material instanceof OBJMaterial)) { 1378 writer.write("Ni 1\n"); 1379 } 1380 writer.write("d " + format(1f - transparency.getTransparency()) + "\n"); 1381 } 1382 Texture texture = appearance.getTexture(); 1383 if (texture != null) { 1384 writer.write("map_Kd " + this.textures.get(texture).getName() + "\n"); 1385 } 1386 } 1387 1388 for (Map.Entry<Texture, File> textureEntry : this.textures.entrySet()) { 1389 Texture texture = textureEntry.getKey(); 1390 Object textureUrl = texture.getUserData(); 1391 if (this.copiedTextures.contains(textureUrl)) { 1392 // Copy texture image file directly 1393 InputStream in = null; 1394 OutputStream out = null; 1395 try { 1396 in = openStream((URL)textureUrl); 1397 out = new FileOutputStream(textureEntry.getValue()); 1398 byte [] buffer = new byte [8192]; 1399 int size; 1400 while ((size = in.read(buffer)) != -1) { 1401 out.write(buffer, 0, size); 1402 } 1403 } finally { 1404 if (in != null) { 1405 in.close(); 1406 } 1407 if (out != null) { 1408 out.close(); 1409 } 1410 } 1411 } else { 1412 ImageComponent2D imageComponent = (ImageComponent2D)texture.getImage(0); 1413 RenderedImage image = imageComponent.getRenderedImage(); 1414 ImageIO.write(image, "png", textureEntry.getValue()); 1415 } 1416 } 1417 } finally { 1418 if (writer != null) { 1419 writer.close(); 1420 } 1421 } 1422 } 1423 1424 /** 1425 * Writes <code>node</code> in an entry at OBJ format of the given zip file 1426 * along with its MTL file and texture images. 1427 */ writeNodeInZIPFile(Node node, File zipFile, int compressionLevel, String entryName, String header)1428 public static void writeNodeInZIPFile(Node node, 1429 File zipFile, 1430 int compressionLevel, 1431 String entryName, 1432 String header) throws IOException { 1433 writeNodeInZIPFile(node, null, zipFile, compressionLevel, entryName, header); 1434 } 1435 1436 /** 1437 * Writes <code>node</code> in an entry at OBJ format of the given zip file 1438 * along with its MTL file and texture images. 1439 * Once saved, <code>materialAppearances</code> will contain the appearances matching 1440 * each material saved in the MTL file. Material names used as keys maybe be different 1441 * of the appearance names to respect MTL specifications. 1442 */ writeNodeInZIPFile(Node node, Map<String, Appearance> materialAppearances, File zipFile, int compressionLevel, String entryName, String header)1443 public static void writeNodeInZIPFile(Node node, 1444 Map<String, Appearance> materialAppearances, 1445 File zipFile, 1446 int compressionLevel, 1447 String entryName, 1448 String header) throws IOException { 1449 // Create a temporary folder 1450 File tempFolder = null; 1451 for (int i = 0; i < 10 && tempFolder == null; i++) { 1452 tempFolder = File.createTempFile("obj", "tmp"); 1453 tempFolder.delete(); 1454 if (!tempFolder.mkdirs()) { 1455 tempFolder = null; 1456 } 1457 } 1458 if (tempFolder == null) { 1459 throw new IOException("Couldn't create a temporary folder"); 1460 } 1461 1462 ZipOutputStream zipOut = null; 1463 try { 1464 // Write model in an OBJ file 1465 OBJWriter writer = new OBJWriter(new File(tempFolder, entryName), header, -1); 1466 writer.writeNode(node); 1467 writer.close(); 1468 // Create a ZIP file containing temp folder files (OBJ + MTL + texture files) 1469 zipOut = new ZipOutputStream(new FileOutputStream(zipFile)); 1470 zipOut.setLevel(compressionLevel); 1471 for (File tempFile : tempFolder.listFiles()) { 1472 if (tempFile.isFile()) { 1473 InputStream tempIn = null; 1474 try { 1475 zipOut.putNextEntry(new ZipEntry(tempFile.getName())); 1476 tempIn = new FileInputStream(tempFile); 1477 byte [] buffer = new byte [8096]; 1478 int size; 1479 while ((size = tempIn.read(buffer)) != -1) { 1480 zipOut.write(buffer, 0, size); 1481 } 1482 zipOut.closeEntry(); 1483 } finally { 1484 if (tempIn != null) { 1485 tempIn.close(); 1486 } 1487 } 1488 } 1489 } 1490 1491 if (materialAppearances != null) { 1492 // Update material / appearance map 1493 for (Map.Entry<ComparableAppearance, String> appearanceEntry : writer.appearances.entrySet()) { 1494 materialAppearances.put(appearanceEntry.getValue(), appearanceEntry.getKey().getAppearance()); 1495 } 1496 } 1497 } finally { 1498 if (zipOut != null) { 1499 zipOut.close(); 1500 } 1501 // Empty tempFolder 1502 for (File tempFile : tempFolder.listFiles()) { 1503 if (tempFile.isFile()) { 1504 tempFile.delete(); 1505 } 1506 } 1507 tempFolder.delete(); 1508 } 1509 } 1510 1511 1512 /** 1513 * An <code>Appearance</code> wrapper able to compare 1514 * if two appearances are equal for MTL format. 1515 */ 1516 private static class ComparableAppearance { 1517 private Appearance appearance; 1518 ComparableAppearance(Appearance appearance)1519 public ComparableAppearance(Appearance appearance) { 1520 this.appearance = appearance; 1521 } 1522 getAppearance()1523 public Appearance getAppearance() { 1524 return this.appearance; 1525 } 1526 1527 /** 1528 * Returns <code>true</code> if this appearance and the one of <code>obj</code> 1529 * describe the same colors, transparency and texture. 1530 */ 1531 @Override equals(Object obj)1532 public boolean equals(Object obj) { 1533 if (obj instanceof ComparableAppearance) { 1534 Appearance appearance2 = ((ComparableAppearance)obj).appearance; 1535 // Compare coloring attributes 1536 ColoringAttributes coloringAttributes1 = this.appearance.getColoringAttributes(); 1537 ColoringAttributes coloringAttributes2 = appearance2.getColoringAttributes(); 1538 if ((coloringAttributes1 == null) ^ (coloringAttributes2 == null)) { 1539 return false; 1540 } else if (coloringAttributes1 != coloringAttributes2) { 1541 Color3f color1 = new Color3f(); 1542 Color3f color2 = new Color3f(); 1543 coloringAttributes1.getColor(color1); 1544 coloringAttributes2.getColor(color2); 1545 if (!color1.equals(color2)) { 1546 return false; 1547 } 1548 } 1549 // Compare material colors 1550 Material material1 = this.appearance.getMaterial(); 1551 Material material2 = appearance2.getMaterial(); 1552 if ((material1 == null) ^ (material2 == null)) { 1553 return false; 1554 } else if (material1 != material2) { 1555 Color3f color1 = new Color3f(); 1556 Color3f color2 = new Color3f(); 1557 material1.getAmbientColor(color1); 1558 material2.getAmbientColor(color2); 1559 if (!color1.equals(color2)) { 1560 return false; 1561 } else { 1562 material1.getDiffuseColor(color1); 1563 material2.getDiffuseColor(color2); 1564 if (!color1.equals(color2)) { 1565 return false; 1566 } else { 1567 material1.getEmissiveColor(color1); 1568 material2.getEmissiveColor(color2); 1569 if (!color1.equals(color2)) { 1570 return false; 1571 } else { 1572 material1.getSpecularColor(color1); 1573 material2.getSpecularColor(color2); 1574 if (!color1.equals(color2)) { 1575 return false; 1576 } else if (material1.getShininess() != material2.getShininess()) { 1577 return false; 1578 } else if (material1.getClass() != material2.getClass()) { 1579 return false; 1580 } else if (material1.getClass() == OBJMaterial.class) { 1581 OBJMaterial objMaterial1 = (OBJMaterial)material1; 1582 OBJMaterial objMaterial2 = (OBJMaterial)material2; 1583 if (objMaterial1.isOpticalDensitySet() ^ objMaterial2.isOpticalDensitySet()) { 1584 return false; 1585 } else if (objMaterial1.isOpticalDensitySet() && objMaterial2.isOpticalDensitySet() 1586 && objMaterial1.getOpticalDensity() != objMaterial2.getOpticalDensity()) { 1587 return false; 1588 } else if (objMaterial1.isIlluminationModelSet() ^ objMaterial2.isIlluminationModelSet()) { 1589 return false; 1590 } else if (objMaterial1.isIlluminationModelSet() && objMaterial2.isIlluminationModelSet() 1591 && objMaterial1.getIlluminationModel() != objMaterial2.getIlluminationModel()) { 1592 return false; 1593 } else if (objMaterial1.isSharpnessSet() ^ objMaterial2.isSharpnessSet()) { 1594 return false; 1595 } else if (objMaterial1.isSharpnessSet() && objMaterial2.isSharpnessSet() 1596 && objMaterial1.getSharpness() != objMaterial2.getSharpness()) { 1597 return false; 1598 } 1599 } 1600 } 1601 } 1602 } 1603 } 1604 // Compare transparency 1605 TransparencyAttributes transparency1 = this.appearance.getTransparencyAttributes(); 1606 TransparencyAttributes transparency2 = appearance2.getTransparencyAttributes(); 1607 if ((transparency1 == null) ^ (transparency2 == null)) { 1608 return false; 1609 } else if (transparency1 != transparency2) { 1610 if (transparency1.getTransparency() != transparency2.getTransparency()) { 1611 return false; 1612 } 1613 } 1614 // Compare texture 1615 Texture texture1 = this.appearance.getTexture(); 1616 Texture texture2 = appearance2.getTexture(); 1617 if ((texture1 == null) ^ (texture2 == null)) { 1618 return false; 1619 } else if (texture1 != texture2) { 1620 if (texture1.getImage(0) != texture2.getImage(0)) { 1621 return false; 1622 } 1623 } 1624 // Compare name 1625 try { 1626 String name1 = this.appearance.getName(); 1627 String name2 = appearance2.getName(); 1628 if ((name1 == null) ^ (name2 == null)) { 1629 return false; 1630 } else if (name1 != name2 1631 && !name1.equals(name2)) { 1632 return false; 1633 } 1634 } catch (NoSuchMethodError ex) { 1635 // Don't compare name with Java 3D < 1.4 where getName was added 1636 } 1637 1638 return true; 1639 } 1640 return false; 1641 } 1642 1643 @Override hashCode()1644 public int hashCode() { 1645 int code = 0; 1646 ColoringAttributes coloringAttributes = appearance.getColoringAttributes(); 1647 if (coloringAttributes != null) { 1648 Color3f color = new Color3f(); 1649 coloringAttributes.getColor(color); 1650 code += color.hashCode(); 1651 } 1652 Material material = this.appearance.getMaterial(); 1653 if (material != null) { 1654 Color3f color = new Color3f(); 1655 material.getAmbientColor(color); 1656 code += color.hashCode(); 1657 material.getDiffuseColor(color); 1658 code += color.hashCode(); 1659 material.getEmissiveColor(color); 1660 code += color.hashCode(); 1661 material.getSpecularColor(color); 1662 code += color.hashCode(); 1663 code += Float.floatToIntBits(material.getShininess()); 1664 } 1665 TransparencyAttributes transparency = this.appearance.getTransparencyAttributes(); 1666 if (transparency != null) { 1667 code += Float.floatToIntBits(transparency.getTransparency()); 1668 } 1669 Texture texture = this.appearance.getTexture(); 1670 if (texture != null) { 1671 code += texture.getImage(0).hashCode(); 1672 } 1673 try { 1674 String name = this.appearance.getName(); 1675 if (name != null) { 1676 code += name.hashCode(); 1677 } 1678 } catch (NoSuchMethodError ex) { 1679 // Don't take name into account with Java 3D < 1.4 where getName was added 1680 } 1681 return code; 1682 } 1683 } 1684 } 1685