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