/* * OBJWriter.java 18 sept. 2008 * * Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.eteks.sweethome3d.j3d; import java.awt.image.RenderedImage; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilterWriter; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.imageio.ImageIO; import javax.media.j3d.Appearance; import javax.media.j3d.ColoringAttributes; import javax.media.j3d.Geometry; import javax.media.j3d.GeometryArray; import javax.media.j3d.GeometryStripArray; import javax.media.j3d.Group; import javax.media.j3d.ImageComponent2D; import javax.media.j3d.IndexedGeometryArray; import javax.media.j3d.IndexedGeometryStripArray; import javax.media.j3d.IndexedLineArray; import javax.media.j3d.IndexedLineStripArray; import javax.media.j3d.IndexedQuadArray; import javax.media.j3d.IndexedTriangleArray; import javax.media.j3d.IndexedTriangleFanArray; import javax.media.j3d.IndexedTriangleStripArray; import javax.media.j3d.LineArray; import javax.media.j3d.LineStripArray; import javax.media.j3d.Link; import javax.media.j3d.Material; import javax.media.j3d.Node; import javax.media.j3d.PolygonAttributes; import javax.media.j3d.QuadArray; import javax.media.j3d.RenderingAttributes; import javax.media.j3d.Shape3D; import javax.media.j3d.TexCoordGeneration; import javax.media.j3d.Texture; import javax.media.j3d.Transform3D; import javax.media.j3d.TransformGroup; import javax.media.j3d.TransparencyAttributes; import javax.media.j3d.TriangleArray; import javax.media.j3d.TriangleFanArray; import javax.media.j3d.TriangleStripArray; import javax.vecmath.Color3f; import javax.vecmath.Point3f; import javax.vecmath.TexCoord2f; import javax.vecmath.Vector3f; import javax.vecmath.Vector4f; /** * An output stream that writes Java 3D nodes at OBJ + MTL format. * <p>Once you wrote nodes, call <code>close</code> method to create the MTL file * and the texture images in the same folder as OBJ file. This feature applies * only to constructor that takes a file as parameter.<br> * Note: this class is compatible with Java 3D 1.3. * @author Emmanuel Puybaret */ public class OBJWriter extends FilterWriter { private final NumberFormat defaultNumberFormat = new DecimalFormat("0.#######", new DecimalFormatSymbols(Locale.US)); private final NumberFormat numberFormat; private final String header; private boolean firstNode = true; private String mtlFileName; private int shapeIndex = 1; private Map<Point3f, Integer> vertexIndices = new HashMap<Point3f, Integer>(); private Map<Vector3f, Integer> normalIndices = new HashMap<Vector3f, Integer>(); private Map<TexCoord2f, Integer> textureCoordinatesIndices = new HashMap<TexCoord2f, Integer>(); private Map<ComparableAppearance, String> appearances = new LinkedHashMap<ComparableAppearance, String>(); private Map<Texture, File> textures = new HashMap<Texture, File>(); /** * Create an OBJ writer for the given file, with no header and default precision. */ public OBJWriter(File objFile) throws FileNotFoundException, IOException { this(objFile, null, -1); } /** * Create an OBJ writer for the given file. * @param objFile the file into which 3D nodes will be written at OBJ format * @param header a header written as a comment at start of the OBJ file and its MTL counterpart * @param maximumFractionDigits the maximum digits count used in fraction part of numbers, * or -1 for default value. Using -1 may cause writing nodes to be twice faster. */ public OBJWriter(File objFile, String header, int maximumFractionDigits) throws FileNotFoundException, IOException { this(objFile.toString(), header, maximumFractionDigits); } /** * Create an OBJ writer for the given file name, with no header and default precision. */ public OBJWriter(String objFileName) throws FileNotFoundException, IOException { this(objFileName, null, -1); } /** * Create an OBJ writer for the given file name. * @param objFileName the name of the file into which 3D nodes will be written at OBJ format * @param header a header written as a comment at start of the OBJ file and its MTL counterpart * @param maximumFractionDigits the maximum digits count used in fraction part of numbers, * or -1 for default value. Using -1 may cause writing nodes to be twice faster. */ public OBJWriter(String objFileName, String header, int maximumFractionDigits) throws FileNotFoundException, IOException { this(new FileOutputStream(objFileName), header, maximumFractionDigits); if (objFileName.toLowerCase().endsWith(".obj")) { this.mtlFileName = objFileName.substring(0, objFileName.length() - 4) + ".mtl"; } else { this.mtlFileName = objFileName + ".mtl"; } // Remove spaces in MTL file name this.mtlFileName = new File(new File(this.mtlFileName).getParent(), new File(this.mtlFileName).getName().replace(' ', '_')).toString(); // Ensure MTL file is using only ASCII codes String name = new File(this.mtlFileName).getName(); for (int i = 0; i < name.length(); i++) { if (name.charAt(i) >= 128) { this.mtlFileName = new File(new File(this.mtlFileName).getParent(), "materials.mtl").toString(); break; } } } /** * Create an OBJ writer that will writes in <code>out</code> stream, * with no header and default precision. */ public OBJWriter(OutputStream out) throws IOException { this(out, null, -1); } /** * Create an OBJ writer that will writes in <code>out</code> stream. * @param out the stream into which 3D nodes will be written at OBJ format * @param header a header written as a comment at start of the stream * @param maximumFractionDigits the maximum digits count used in fraction part of numbers, * or -1 for default value. Using -1 may cause writing nodes to be twice faster. */ public OBJWriter(OutputStream out, String header, int maximumFractionDigits) throws IOException { this(new OutputStreamWriter(new BufferedOutputStream(out), "US-ASCII"), header, maximumFractionDigits); } /** * Create an OBJ writer that will writes in <code>out</code> stream, * with no header and default precision. */ public OBJWriter(Writer out) throws IOException { this(out, null, -1); } /** * Create an OBJ writer that will writes in <code>out</code> stream. * @param out the stream into which 3D nodes will be written at OBJ format * @param header a header written as a comment at start of the stream * @param maximumFractionDigits the maximum digits count used in fraction part of numbers, * or -1 for default value. Using -1 may cause writing nodes to be twice faster. */ public OBJWriter(Writer out, String header, int maximumFractionDigits) throws IOException { super(out); if (maximumFractionDigits >= 0) { this.numberFormat = NumberFormat.getNumberInstance(Locale.US); this.numberFormat.setMinimumFractionDigits(0); this.numberFormat.setMaximumFractionDigits(maximumFractionDigits); } else { this.numberFormat = null; } this.header = header; writeHeader(this.out); } /** * Writes header to <code>writer</code> */ private void writeHeader(Writer writer) throws IOException { if (this.header != null) { if (!this.header.startsWith("#")) { writer.write("# "); } writer.write(this.header.replace("\n", "\n# ")); writer.write("\n"); } } /** * Write a single character in a comment at OBJ format. */ @Override public void write(int c) throws IOException { this.out.write("# "); this.out.write(c); this.out.write("\n"); } /** * Write a portion of an array of characters in a comment at OBJ format. */ @Override public void write(char cbuf[], int off, int len) throws IOException { this.out.write("# "); this.out.write(cbuf, off, len); this.out.write("\n"); } /** * Write a portion of a string in a comment at OBJ format. */ @Override public void write(String str, int off, int len) throws IOException { this.out.write("# "); this.out.write(str, off, len); this.out.write("\n"); } /** * Write a string in a comment at OBJ format. */ @Override public void write(String str) throws IOException { this.out.write("# "); this.out.write(str, 0, str.length()); this.out.write("\n"); } /** * Throws an <code>InterruptedRecorderException</code> exception * if current thread is interrupted. */ private void checkCurrentThreadIsntInterrupted() throws InterruptedIOException { if (Thread.interrupted()) { this.mtlFileName = null; throw new InterruptedIOException("Current thread interrupted"); } } /** * Writes all the 3D shapes children of <code>node</code> at OBJ format. * If there are transformation groups on the path from <code>node</code> to its shapes, * they'll be applied to the coordinates written on output. * The <code>node</code> shouldn't be alive or if it's alive it should have the * capabilities to read its children, the geometries and the appearance of its shapes. * Only geometries which are instances of <code>GeometryArray</code> will be written. * @param node a Java 3D node * @throws IOException if the operation failed * @throws InterruptedIOException if the current thread was interrupted during this operation. * The interrupted status of the current thread is cleared when this exception is thrown. */ public void writeNode(Node node) throws IOException, InterruptedIOException { writeNode(node, null); } /** * Writes all the 3D shapes children of <code>node</code> at OBJ format. * If there are transformation groups on the path from <code>node</code> to its shapes, * they'll be applied to the coordinates written on output. * The <code>node</code> shouldn't be alive or if it's alive, it should have the * capabilities to read its children, the geometries and the appearance of its shapes. * Only geometries which are instances of <code>GeometryArray</code> will be written. * @param node a Java 3D node * @param nodeName the name of the node. This is useful to distinguish the objects * names in output. If this name is <code>null</code> or isn't built * with A-Z, a-z, 0-9 and underscores, it will be ignored. * @throws IOException if the operation failed * @throws InterruptedIOException if the current thread was interrupted during this operation * The interrupted status of the current thread is cleared when this exception is thrown. */ public void writeNode(Node node, String nodeName) throws IOException, InterruptedIOException { if (this.firstNode) { if (this.mtlFileName != null) { this.out.write("mtllib " + new File(this.mtlFileName).getName() + "\n"); } this.firstNode = false; } writeNode(node, nodeName, new Transform3D()); } /** * Writes all the 3D shapes children of <code>node</code> at OBJ format. */ private void writeNode(Node node, String nodeName, Transform3D parentTransformations) throws IOException { if (node instanceof Group) { if (node instanceof TransformGroup) { parentTransformations = new Transform3D(parentTransformations); Transform3D transform = new Transform3D(); ((TransformGroup)node).getTransform(transform); parentTransformations.mul(transform); } // Write all children Enumeration<?> enumeration = ((Group)node).getAllChildren(); while (enumeration.hasMoreElements()) { writeNode((Node)enumeration.nextElement(), nodeName, parentTransformations); } } else if (node instanceof Link) { writeNode(((Link)node).getSharedGroup(), nodeName, parentTransformations); } else if (node instanceof Shape3D) { Shape3D shape = (Shape3D)node; Appearance appearance = shape.getAppearance(); RenderingAttributes renderingAttributes = appearance != null ? appearance.getRenderingAttributes() : null; if (shape.numGeometries() >= 1 && (renderingAttributes == null || renderingAttributes.getVisible())) { // Build a unique human readable object name String objectName = ""; if (accept(nodeName)) { objectName = nodeName + "_"; } String shapeName = null; if (shape.getUserData() instanceof String) { shapeName = (String)shape.getUserData(); } if (accept(shapeName)) { objectName += shapeName + "_"; } objectName += String.valueOf(this.shapeIndex++); // Start a new object at OBJ format this.out.write("g " + objectName + "\n"); TexCoordGeneration texCoordGeneration = null; if (this.mtlFileName != null) { if (appearance != null) { texCoordGeneration = appearance.getTexCoordGeneration(); ComparableAppearance comparableAppearance = new ComparableAppearance(appearance); String appearanceName = this.appearances.get(comparableAppearance); if (appearanceName == null) { // Store appearance try { appearanceName = appearance.getName(); } catch (NoSuchMethodError ex) { // Don't reuse appearance name with Java 3D < 1.4 where getName was added } } if (appearanceName == null || !accept(appearanceName)) { appearanceName = objectName; } else { // Find a unique appearance name among appearances Collection<String> appearanceNames = this.appearances.values(); String baseName = appearanceName + "_" + objectName; for (int i = 0; appearanceNames.contains(appearanceName); i++) { if (i == 0) { appearanceName = baseName; } else { appearanceName = baseName + "_" + i; } } } this.appearances.put(comparableAppearance, appearanceName); Texture texture = appearance.getTexture(); if (texture != null) { File textureFile = this.textures.get(texture); if (textureFile == null) { // Store texture textureFile = new File(this.mtlFileName.substring(0, this.mtlFileName.length() - 4) + "_" + appearanceName + ".png"); this.textures.put(texture, textureFile); } } } this.out.write("usemtl " + appearanceName + "\n"); } } int cullFace = PolygonAttributes.CULL_BACK; boolean backFaceNormalFlip = false; if (appearance != null) { PolygonAttributes polygonAttributes = appearance.getPolygonAttributes(); if (polygonAttributes != null) { cullFace = polygonAttributes.getCullFace(); backFaceNormalFlip = polygonAttributes.getBackFaceNormalFlip(); } } // Write object geometries for (int i = 0, n = shape.numGeometries(); i < n; i++) { writeNodeGeometry(shape.getGeometry(i), parentTransformations, texCoordGeneration, cullFace, backFaceNormalFlip); } } } } /** * Returns <code>true</code> if <code>name</code> contains * only letters, digits and underscores. */ private boolean accept(String name) { if (name == null) { return false; } for (int i = 0; i < name.length(); i++) { char car = name.charAt(i); if (!(car >= 'a' && car <= 'z' || car >= 'A' && car <= 'Z' || car >= '0' && car <= '9' || car == '_')) { return false; } } return true; } /** * Writes a 3D geometry at OBJ format. */ private void writeNodeGeometry(Geometry geometry, Transform3D parentTransformations, TexCoordGeneration texCoordGeneration, int cullFace, boolean backFaceNormalFlip) throws IOException { if (geometry instanceof GeometryArray) { GeometryArray geometryArray = (GeometryArray)geometry; int [] vertexIndexSubstitutes = new int [geometryArray.getVertexCount()]; boolean normalsDefined = (geometryArray.getVertexFormat() & GeometryArray.NORMALS) != 0; StringBuilder normalsBuffer; List<Vector3f> addedNormals; if (normalsDefined) { normalsBuffer = new StringBuilder(geometryArray.getVertexCount() * 3 * 10); addedNormals = new ArrayList<Vector3f>(); } else { normalsBuffer = null; addedNormals = null; } int [] normalIndexSubstitutes = new int [geometryArray.getVertexCount()]; int [] oppositeSideNormalIndexSubstitutes; if (cullFace == PolygonAttributes.CULL_NONE) { oppositeSideNormalIndexSubstitutes = new int [geometryArray.getVertexCount()]; } else { oppositeSideNormalIndexSubstitutes = null; } boolean textureCoordinatesDefined = (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0; int [] textureCoordinatesIndexSubstitutes = new int [geometryArray.getVertexCount()]; boolean textureCoordinatesGenerated = false; Vector4f planeS = null; Vector4f planeT = null; if (texCoordGeneration != null) { textureCoordinatesGenerated = texCoordGeneration.getGenMode() == TexCoordGeneration.OBJECT_LINEAR && texCoordGeneration.getEnable() && !(geometryArray instanceof IndexedLineArray) && !(geometryArray instanceof IndexedLineStripArray) && !(geometryArray instanceof LineArray) && !(geometryArray instanceof LineStripArray); if (textureCoordinatesGenerated) { planeS = new Vector4f(); planeT = new Vector4f(); texCoordGeneration.getPlaneS(planeS); texCoordGeneration.getPlaneT(planeT); } } checkCurrentThreadIsntInterrupted(); if ((geometryArray.getVertexFormat() & GeometryArray.BY_REFERENCE) != 0) { if ((geometryArray.getVertexFormat() & GeometryArray.INTERLEAVED) != 0) { float [] vertexData = geometryArray.getInterleavedVertices(); int vertexSize = vertexData.length / geometryArray.getVertexCount(); // Write vertices coordinates for (int index = 0, i = vertexSize - 3, n = geometryArray.getVertexCount(); index < n; index++, i += vertexSize) { Point3f vertex = new Point3f(vertexData [i], vertexData [i + 1], vertexData [i + 2]); writeVertex(parentTransformations, vertex, index, vertexIndexSubstitutes); } // Write texture coordinates if (texCoordGeneration != null) { if (textureCoordinatesGenerated) { for (int index = 0, i = vertexSize - 3, n = geometryArray.getVertexCount(); index < n; index++, i += vertexSize) { TexCoord2f textureCoordinates = generateTextureCoordinates( vertexData [i], vertexData [i + 1], vertexData [i + 2], planeS, planeT); writeTextureCoordinates(textureCoordinates, index, textureCoordinatesIndexSubstitutes); } } } else if (textureCoordinatesDefined) { for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += vertexSize) { TexCoord2f textureCoordinates = new TexCoord2f(vertexData [i], vertexData [i + 1]); writeTextureCoordinates(textureCoordinates, index, textureCoordinatesIndexSubstitutes); } } // Write normals if (normalsDefined) { for (int index = 0, i = vertexSize - 6, n = geometryArray.getVertexCount(); normalsDefined && index < n; index++, i += vertexSize) { Vector3f normal = new Vector3f(vertexData [i], vertexData [i + 1], vertexData [i + 2]); normalsDefined = writeNormal(normalsBuffer, parentTransformations, normal, index, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, addedNormals, cullFace, backFaceNormalFlip); } } } else { // Write vertices coordinates float [] vertexCoordinates = geometryArray.getCoordRefFloat(); for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) { Point3f vertex = new Point3f(vertexCoordinates [i], vertexCoordinates [i + 1], vertexCoordinates [i + 2]); writeVertex(parentTransformations, vertex, index, vertexIndexSubstitutes); } // Write texture coordinates if (texCoordGeneration != null) { if (textureCoordinatesGenerated) { for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) { TexCoord2f textureCoordinates = generateTextureCoordinates( vertexCoordinates [i], vertexCoordinates [i + 1], vertexCoordinates [i + 2], planeS, planeT); writeTextureCoordinates(textureCoordinates, index, textureCoordinatesIndexSubstitutes); } } } else if (textureCoordinatesDefined) { float [] textureCoordinatesArray = geometryArray.getTexCoordRefFloat(0); for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 2) { TexCoord2f textureCoordinates = new TexCoord2f(textureCoordinatesArray [i], textureCoordinatesArray [i + 1]); writeTextureCoordinates(textureCoordinates, index, textureCoordinatesIndexSubstitutes); } } // Write normals if (normalsDefined) { float [] normalCoordinates = geometryArray.getNormalRefFloat(); for (int index = 0, i = 0, n = geometryArray.getVertexCount(); normalsDefined && index < n; index++, i += 3) { Vector3f normal = new Vector3f(normalCoordinates [i], normalCoordinates [i + 1], normalCoordinates [i + 2]); normalsDefined = writeNormal(normalsBuffer, parentTransformations, normal, index, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, addedNormals, cullFace, backFaceNormalFlip); } } } } else { // Write vertices coordinates for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) { Point3f vertex = new Point3f(); geometryArray.getCoordinate(index, vertex); writeVertex(parentTransformations, vertex, index, vertexIndexSubstitutes); } // Write texture coordinates if (texCoordGeneration != null) { if (textureCoordinatesGenerated) { for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) { Point3f vertex = new Point3f(); geometryArray.getCoordinate(index, vertex); TexCoord2f textureCoordinates = generateTextureCoordinates( vertex.x, vertex.y, vertex.z, planeS, planeT); writeTextureCoordinates(textureCoordinates, index, textureCoordinatesIndexSubstitutes); } } } else if (textureCoordinatesDefined) { for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) { TexCoord2f textureCoordinates = new TexCoord2f(); geometryArray.getTextureCoordinate(0, index, textureCoordinates); writeTextureCoordinates(textureCoordinates, index, textureCoordinatesIndexSubstitutes); } } // Write normals if (normalsDefined) { for (int index = 0, n = geometryArray.getVertexCount(); normalsDefined && index < n; index++) { Vector3f normal = new Vector3f(); geometryArray.getNormal(index, normal); normalsDefined = writeNormal(normalsBuffer, parentTransformations, normal, index, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, addedNormals, cullFace, backFaceNormalFlip); } } } if (normalsDefined) { // Write normals only if they all contain valid values out.write(normalsBuffer.toString()); } else if (addedNormals != null) { // Remove ignored normals for (Vector3f normal : addedNormals) { this.normalIndices.remove(normal); } } checkCurrentThreadIsntInterrupted(); // Write lines, triangles or quadrilaterals depending on the geometry if (geometryArray instanceof IndexedGeometryArray) { if (geometryArray instanceof IndexedLineArray) { IndexedLineArray lineArray = (IndexedLineArray)geometryArray; for (int i = 0, n = lineArray.getIndexCount(); i < n; i += 2) { writeIndexedLine(lineArray, i, i + 1, vertexIndexSubstitutes, textureCoordinatesIndexSubstitutes); } } else if (geometryArray instanceof IndexedTriangleArray) { IndexedTriangleArray triangleArray = (IndexedTriangleArray)geometryArray; for (int i = 0, n = triangleArray.getIndexCount(); i < n; i += 3) { writeIndexedTriangle(triangleArray, i, i + 1, i + 2, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } } else if (geometryArray instanceof IndexedQuadArray) { IndexedQuadArray quadArray = (IndexedQuadArray)geometryArray; for (int i = 0, n = quadArray.getIndexCount(); i < n; i += 4) { writeIndexedQuadrilateral(quadArray, i, i + 1, i + 2, i + 3, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } } else if (geometryArray instanceof IndexedGeometryStripArray) { IndexedGeometryStripArray geometryStripArray = (IndexedGeometryStripArray)geometryArray; int [] stripIndexCounts = new int [geometryStripArray.getNumStrips()]; geometryStripArray.getStripIndexCounts(stripIndexCounts); int initialIndex = 0; if (geometryStripArray instanceof IndexedLineStripArray) { for (int strip = 0; strip < stripIndexCounts.length; strip++) { for (int i = initialIndex, n = initialIndex + stripIndexCounts [strip] - 1; i < n; i++) { writeIndexedLine(geometryStripArray, i, i + 1, vertexIndexSubstitutes, textureCoordinatesIndexSubstitutes); } initialIndex += stripIndexCounts [strip]; } } else if (geometryStripArray instanceof IndexedTriangleStripArray) { for (int strip = 0; strip < stripIndexCounts.length; strip++) { for (int i = initialIndex, n = initialIndex + stripIndexCounts [strip] - 2, j = 0; i < n; i++, j++) { if (j % 2 == 0) { writeIndexedTriangle(geometryStripArray, i, i + 1, i + 2, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } else { // Vertices of odd triangles are in reverse order writeIndexedTriangle(geometryStripArray, i, i + 2, i + 1, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } } initialIndex += stripIndexCounts [strip]; } } else if (geometryStripArray instanceof IndexedTriangleFanArray) { for (int strip = 0; strip < stripIndexCounts.length; strip++) { for (int i = initialIndex, n = initialIndex + stripIndexCounts [strip] - 2; i < n; i++) { writeIndexedTriangle(geometryStripArray, initialIndex, i + 1, i + 2, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } initialIndex += stripIndexCounts [strip]; } } } } else { if (geometryArray instanceof LineArray) { LineArray lineArray = (LineArray)geometryArray; for (int i = 0, n = lineArray.getVertexCount(); i < n; i += 2) { writeLine(lineArray, i, i + 1, vertexIndexSubstitutes, textureCoordinatesIndexSubstitutes); } } else if (geometryArray instanceof TriangleArray) { TriangleArray triangleArray = (TriangleArray)geometryArray; for (int i = 0, n = triangleArray.getVertexCount(); i < n; i += 3) { writeTriangle(triangleArray, i, i + 1, i + 2, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } } else if (geometryArray instanceof QuadArray) { QuadArray quadArray = (QuadArray)geometryArray; for (int i = 0, n = quadArray.getVertexCount(); i < n; i += 4) { writeQuadrilateral(quadArray, i, i + 1, i + 2, i + 3, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } } else if (geometryArray instanceof GeometryStripArray) { GeometryStripArray geometryStripArray = (GeometryStripArray)geometryArray; int [] stripVertexCounts = new int [geometryStripArray.getNumStrips()]; geometryStripArray.getStripVertexCounts(stripVertexCounts); int initialIndex = 0; if (geometryStripArray instanceof LineStripArray) { for (int strip = 0; strip < stripVertexCounts.length; strip++) { for (int i = initialIndex, n = initialIndex + stripVertexCounts [strip] - 1; i < n; i++) { writeLine(geometryStripArray, i, i + 1, vertexIndexSubstitutes, textureCoordinatesIndexSubstitutes); } initialIndex += stripVertexCounts [strip]; } } else if (geometryStripArray instanceof TriangleStripArray) { for (int strip = 0; strip < stripVertexCounts.length; strip++) { for (int i = initialIndex, n = initialIndex + stripVertexCounts [strip] - 2, j = 0; i < n; i++, j++) { if (j % 2 == 0) { writeTriangle(geometryStripArray, i, i + 1, i + 2, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } else { // Vertices of odd triangles are in reverse order writeTriangle(geometryStripArray, i, i + 2, i + 1, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } } initialIndex += stripVertexCounts [strip]; } } else if (geometryStripArray instanceof TriangleFanArray) { for (int strip = 0; strip < stripVertexCounts.length; strip++) { for (int i = initialIndex, n = initialIndex + stripVertexCounts [strip] - 2; i < n; i++) { writeTriangle(geometryStripArray, initialIndex, i + 1, i + 2, vertexIndexSubstitutes, normalIndexSubstitutes, oppositeSideNormalIndexSubstitutes, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, cullFace); } initialIndex += stripVertexCounts [strip]; } } } } } } /** * Returns texture coordinates generated with <code>texCoordGeneration</code> computed * as described in <code>TexCoordGeneration</code> javadoc. */ private TexCoord2f generateTextureCoordinates(float x, float y, float z, Vector4f planeS, Vector4f planeT) { return new TexCoord2f(x * planeS.x + y * planeS.y + z * planeS.z + planeS.w, x * planeT.x + y * planeT.y + z * planeT.z + planeT.w); } /** * Applies to <code>vertex</code> the given transformation, and writes it in * a line v at OBJ format, if the vertex wasn't written yet. */ private void writeVertex(Transform3D transformationToParent, Point3f vertex, int index, int [] vertexIndexSubstitutes) throws IOException { transformationToParent.transform(vertex); Integer vertexIndex = this.vertexIndices.get(vertex); if (vertexIndex == null) { vertexIndexSubstitutes [index] = this.vertexIndices.size() + 1; this.vertexIndices.put(vertex, vertexIndexSubstitutes [index]); // Write only once unique vertices this.out.write("v " + format(vertex.x) + " " + format(vertex.y) + " " + format(vertex.z) + "\n"); } else { vertexIndexSubstitutes [index] = vertexIndex; } } /** * Formats a float number to a string as fast as possible depending on the * format chosen in constructor. */ private String format(float number) { if (this.numberFormat != null) { return this.numberFormat.format(number); } else { String numberString = String.valueOf((float)number); if (numberString.indexOf('E') != -1) { // Avoid scientific notation return this.defaultNumberFormat.format(number); } else { return numberString; } } } /** * Applies to <code>normal</code> the given transformation, and appends to <code>normalsBuffer</code> * its values in a line vn at OBJ format, if the normal wasn't written yet. * @return <code>true</code> if the written normal doens't contain any NaN value */ private boolean writeNormal(StringBuilder normalsBuffer, Transform3D transformationToParent, Vector3f normal, int index, int [] normalIndexSubstitutes, int [] oppositeSideNormalIndexSubstitutes, List<Vector3f> addedNormals, int cullFace, boolean backFaceNormalFlip) throws IOException { if (Float.isNaN(normal.x) || Float.isNaN(normal.y) || Float.isNaN(normal.z)) { return false; } if (backFaceNormalFlip) { normal.negate(); } if (normal.x != 0 || normal.y != 0 || normal.z != 0) { transformationToParent.transform(normal); normal.normalize(); } Integer normalIndex = this.normalIndices.get(normal); if (normalIndex == null) { normalIndexSubstitutes [index] = this.normalIndices.size() + 1; this.normalIndices.put(normal, normalIndexSubstitutes [index]); addedNormals.add(normal); // Write only once unique normals normalsBuffer.append("vn " + format(normal.x) + " " + format(normal.y) + " " + format(normal.z) + "\n"); } else { normalIndexSubstitutes [index] = normalIndex; } if (cullFace == PolygonAttributes.CULL_NONE) { Vector3f oppositeNormal = new Vector3f(); oppositeNormal.negate(normal); // Fill opposite side normal index substitutes array return writeNormal(normalsBuffer, transformationToParent, oppositeNormal, index, oppositeSideNormalIndexSubstitutes, null, addedNormals, PolygonAttributes.CULL_FRONT, false); } else { return true; } } /** * Writes <code>textureCoordinates</code> in a line vt at OBJ format, * if the texture coordinates wasn't written yet. */ private void writeTextureCoordinates(TexCoord2f textureCoordinates, int index, int [] textureCoordinatesIndexSubstitutes) throws IOException { Integer textureCoordinatesIndex = this.textureCoordinatesIndices.get(textureCoordinates); if (textureCoordinatesIndex == null) { textureCoordinatesIndexSubstitutes [index] = this.textureCoordinatesIndices.size() + 1; this.textureCoordinatesIndices.put(textureCoordinates, textureCoordinatesIndexSubstitutes [index]); // Write only once unique texture coordinates this.out.write("vt " + format(textureCoordinates.x) + " " + format(textureCoordinates.y) + " 0\n"); } else { textureCoordinatesIndexSubstitutes [index] = textureCoordinatesIndex; } } /** * Writes the line indices given at vertexIndex1, vertexIndex2, * in a line l at OBJ format. */ private void writeIndexedLine(IndexedGeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int [] vertexIndexSubstitutes, int [] textureCoordinatesIndexSubstitutes) throws IOException { if ((geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { this.out.write("l " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) + "\n"); } else { this.out.write("l " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "\n"); } } /** * Writes the triangle indices given at vertexIndex1, vertexIndex2, vertexIndex3, * in a line f at OBJ format. */ private void writeIndexedTriangle(IndexedGeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, int [] vertexIndexSubstitutes, int [] normalIndexSubstitutes, int [] oppositeSideNormalIndexSubstitutes, boolean normalsDefined, int [] textureCoordinatesIndexSubstitutes, boolean textureCoordinatesGenerated, int cullFace) throws IOException { if (cullFace == PolygonAttributes.CULL_FRONT) { // Reverse vertex order int tmp = vertexIndex1; vertexIndex1 = vertexIndex3; vertexIndex3 = tmp; } if (textureCoordinatesGenerated || (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { if (normalsDefined) { this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex3)]) + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) + "\n"); } else { this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex3)]) + "\n"); } } else { if (normalsDefined) { this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) + "\n"); } else { this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "\n"); } } if (cullFace == PolygonAttributes.CULL_NONE) { // Use opposite side normal index substitutes array writeIndexedTriangle(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, vertexIndexSubstitutes, oppositeSideNormalIndexSubstitutes, null, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, PolygonAttributes.CULL_FRONT); } } /** * Writes the quadrilateral indices given at vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4, * in a line f at OBJ format. */ private void 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) throws IOException { if (cullFace == PolygonAttributes.CULL_FRONT) { // Reverse vertex order int tmp = vertexIndex2; vertexIndex2 = vertexIndex3; vertexIndex3 = tmp; tmp = vertexIndex1; vertexIndex1 = vertexIndex4; vertexIndex4 = tmp; } if (textureCoordinatesGenerated || (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { if (normalsDefined) { this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex3)]) + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex4)]) + "/" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex4)]) + "\n"); } else { this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex2)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex3)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) + "/" + (textureCoordinatesIndexSubstitutes [geometryArray.getTextureCoordinateIndex(0, vertexIndex4)]) + "\n"); } } else { if (normalsDefined) { this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex2)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex3)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) + "//" + (normalIndexSubstitutes [geometryArray.getNormalIndex(vertexIndex4)]) + "\n"); } else { this.out.write("f " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex1)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex2)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex3)]) + " " + (vertexIndexSubstitutes [geometryArray.getCoordinateIndex(vertexIndex4)]) + "\n"); } } if (cullFace == PolygonAttributes.CULL_NONE) { // Use opposite side normal index substitutes array writeIndexedQuadrilateral(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4, vertexIndexSubstitutes, oppositeSideNormalIndexSubstitutes, null, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, PolygonAttributes.CULL_FRONT); } } /** * Writes the line indices given at vertexIndex1, vertexIndex2, * in a line l at OBJ format. */ private void writeLine(GeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int [] vertexIndexSubstitutes, int [] textureCoordinatesIndexSubstitutes) throws IOException { if ((geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { this.out.write("l " + (vertexIndexSubstitutes [vertexIndex1]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) + "\n"); } else { this.out.write("l " + (vertexIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + "\n"); } } /** * Writes the triangle indices given at vertexIndex1, vertexIndex2, vertexIndex3, * in a line f at OBJ format. */ private void writeTriangle(GeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, int [] vertexIndexSubstitutes, int [] normalIndexSubstitutes, int [] oppositeSideNormalIndexSubstitutes, boolean normalsDefined, int [] textureCoordinatesIndexSubstitutes, boolean textureCoordinatesGenerated, int cullFace) throws IOException { if (cullFace == PolygonAttributes.CULL_FRONT) { // Reverse vertex order int tmp = vertexIndex1; vertexIndex1 = vertexIndex3; vertexIndex3 = tmp; } if (textureCoordinatesGenerated || (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { if (normalsDefined) { this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) + "/" + (normalIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) + "/" + (normalIndexSubstitutes [vertexIndex2]) + " " + (vertexIndexSubstitutes [vertexIndex3]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex3]) + "/" + (normalIndexSubstitutes [vertexIndex3]) + "\n"); } else { this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) + " " + (vertexIndexSubstitutes [vertexIndex3]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex3]) + "\n"); } } else { if (normalsDefined) { this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) + "//" + (normalIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + "//" + (normalIndexSubstitutes [vertexIndex2]) + " " + (vertexIndexSubstitutes [vertexIndex3]) + "//" + (normalIndexSubstitutes [vertexIndex3]) + "\n"); } else { this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + " " + (vertexIndexSubstitutes [vertexIndex3]) + "\n"); } } if (cullFace == PolygonAttributes.CULL_NONE) { // Use opposite side normal index substitutes array writeTriangle(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, vertexIndexSubstitutes, oppositeSideNormalIndexSubstitutes, null, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, PolygonAttributes.CULL_FRONT); } } /** * Writes the quadrilateral indices given at vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4, * in a line f at OBJ format. */ private void 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) throws IOException { if (cullFace == PolygonAttributes.CULL_FRONT) { // Reverse vertex order int tmp = vertexIndex2; vertexIndex2 = vertexIndex3; vertexIndex3 = tmp; tmp = vertexIndex1; vertexIndex1 = vertexIndex4; vertexIndex4 = tmp; } if (textureCoordinatesGenerated || (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0) { if (normalsDefined) { this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) + "/" + (normalIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) + "/" + (normalIndexSubstitutes [vertexIndex2]) + " " + (vertexIndexSubstitutes [vertexIndex3]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex3]) + "/" + (normalIndexSubstitutes [vertexIndex3]) + " " + (vertexIndexSubstitutes [vertexIndex4]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex4]) + "/" + (normalIndexSubstitutes [vertexIndex4]) + "\n"); } else { this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex2]) + " " + (vertexIndexSubstitutes [vertexIndex3]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex3]) + " " + (vertexIndexSubstitutes [vertexIndex4]) + "/" + (textureCoordinatesIndexSubstitutes [vertexIndex4]) + "\n"); } } else { if (normalsDefined) { this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) + "//" + (normalIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + "//" + (normalIndexSubstitutes [vertexIndex2]) + " " + (vertexIndexSubstitutes [vertexIndex3]) + "//" + (normalIndexSubstitutes [vertexIndex3]) + " " + (vertexIndexSubstitutes [vertexIndex4]) + "//" + (normalIndexSubstitutes [vertexIndex4]) + "\n"); } else { this.out.write("f " + (vertexIndexSubstitutes [vertexIndex1]) + " " + (vertexIndexSubstitutes [vertexIndex2]) + " " + (vertexIndexSubstitutes [vertexIndex3]) + " " + (vertexIndexSubstitutes [vertexIndex4]) + "\n"); } } if (cullFace == PolygonAttributes.CULL_NONE) { // Use opposite side normal index substitutes array writeQuadrilateral(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4, vertexIndexSubstitutes, oppositeSideNormalIndexSubstitutes, null, normalsDefined, textureCoordinatesIndexSubstitutes, textureCoordinatesGenerated, PolygonAttributes.CULL_FRONT); } } /** * Closes this writer and writes MTL file and its texture images, * if this writer was created from a file. * @throws IOException if this writer couldn't be closed * or couldn't write MTL and texture files couldn't be written * @throws InterruptedIOException if the current thread was interrupted during this operation * The interrupted status of the current thread is cleared when this exception is thrown. */ @Override public void close() throws IOException, InterruptedIOException { super.close(); if (this.mtlFileName != null) { writeAppearancesToMTLFile(); } } /** * Exports a set of appearance to a MTL file built from OBJ file name. */ private void writeAppearancesToMTLFile() throws IOException { Writer writer = null; try { writer = new OutputStreamWriter( new BufferedOutputStream(new FileOutputStream(this.mtlFileName)), "ISO-8859-1"); writeHeader(writer); for (Map.Entry<ComparableAppearance, String> appearanceEntry : this.appearances.entrySet()) { checkCurrentThreadIsntInterrupted(); Appearance appearance = appearanceEntry.getKey().getAppearance(); String appearanceName = appearanceEntry.getValue(); writer.write("\nnewmtl " + appearanceName + "\n"); Material material = appearance.getMaterial(); if (material != null) { if (material instanceof OBJMaterial && ((OBJMaterial)material).isIlluminationModelSet()) { writer.write("illum " + ((OBJMaterial)material).getIlluminationModel() + "\n"); } else if (material.getShininess() > 1) { writer.write("illum 2\n"); } else if (material.getLightingEnable()) { writer.write("illum 1\n"); } else { writer.write("illum 0\n"); } Color3f color = new Color3f(); material.getAmbientColor(color); writer.write("Ka " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); material.getDiffuseColor(color); writer.write("Kd " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); material.getSpecularColor(color); writer.write("Ks " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); writer.write("Ns " + format(material.getShininess()) + "\n"); if (material instanceof OBJMaterial) { OBJMaterial objMaterial = (OBJMaterial)material; if (objMaterial.isOpticalDensitySet()) { writer.write("Ni " + format(objMaterial.getOpticalDensity()) + "\n"); } if (objMaterial.isSharpnessSet()) { writer.write("sharpness " + format(objMaterial.getSharpness()) + "\n"); } } } else { ColoringAttributes coloringAttributes = appearance.getColoringAttributes(); if (coloringAttributes != null) { writer.write("illum 0\n"); Color3f color = new Color3f(); coloringAttributes.getColor(color); writer.write("Ka " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); writer.write("Kd " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); writer.write("Ks " + format(color.x) + " " + format(color.y) + " " + format(color.z) + "\n"); } } TransparencyAttributes transparency = appearance.getTransparencyAttributes(); if (transparency != null) { if (!(material instanceof OBJMaterial)) { writer.write("Ni 1\n"); } writer.write("d " + format(1f - transparency.getTransparency()) + "\n"); } Texture texture = appearance.getTexture(); if (texture != null) { writer.write("map_Kd " + this.textures.get(texture).getName() + "\n"); } } for (Map.Entry<Texture, File> textureEntry : this.textures.entrySet()) { Texture texture = textureEntry.getKey(); ImageComponent2D imageComponent = (ImageComponent2D)texture.getImage(0); RenderedImage image = imageComponent.getRenderedImage(); ImageIO.write(image, "png", textureEntry.getValue()); } } finally { if (writer != null) { writer.close(); } } } /** * Writes <code>node</code> in an entry at OBJ format of the given zip file * along with its MTL file and texture images. */ public static void writeNodeInZIPFile(Node node, File zipFile, int compressionLevel, String entryName, String header) throws IOException { // Create a temporary folder File tempFolder = null; for (int i = 0; i < 10 && tempFolder == null; i++) { tempFolder = File.createTempFile("obj", "tmp"); tempFolder.delete(); if (!tempFolder.mkdirs()) { tempFolder = null; } } if (tempFolder == null) { throw new IOException("Couldn't create a temporary folder"); } ZipOutputStream zipOut = null; try { // Write model in an OBJ file OBJWriter writer = new OBJWriter(new File(tempFolder, entryName), header, -1); writer.writeNode(node); writer.close(); // Create a ZIP file containing temp folder files (OBJ + MTL + texture files) zipOut = new ZipOutputStream(new FileOutputStream(zipFile)); zipOut.setLevel(compressionLevel); for (File tempFile : tempFolder.listFiles()) { if (tempFile.isFile()) { InputStream tempIn = null; try { zipOut.putNextEntry(new ZipEntry(tempFile.getName())); tempIn = new FileInputStream(tempFile); byte [] buffer = new byte [8096]; int size; while ((size = tempIn.read(buffer)) != -1) { zipOut.write(buffer, 0, size); } zipOut.closeEntry(); } finally { if (tempIn != null) { tempIn.close(); } } } } } finally { if (zipOut != null) { zipOut.close(); } // Empty tempFolder for (File tempFile : tempFolder.listFiles()) { if (tempFile.isFile()) { tempFile.delete(); } } tempFolder.delete(); } } /** * An <code>Appearance</code> wrapper able to compare * if two appearances are equal for MTL format. */ private static class ComparableAppearance { private Appearance appearance; public ComparableAppearance(Appearance appearance) { this.appearance = appearance; } public Appearance getAppearance() { return this.appearance; } /** * Returns <code>true</code> if this appearance and the one of <code>obj</code> * describe the same colors, transparency and texture. */ @Override public boolean equals(Object obj) { if (obj instanceof ComparableAppearance) { Appearance appearance2 = ((ComparableAppearance)obj).appearance; // Compare coloring attributes ColoringAttributes coloringAttributes1 = this.appearance.getColoringAttributes(); ColoringAttributes coloringAttributes2 = appearance2.getColoringAttributes(); if ((coloringAttributes1 == null) ^ (coloringAttributes2 == null)) { return false; } else if (coloringAttributes1 != coloringAttributes2) { Color3f color1 = new Color3f(); Color3f color2 = new Color3f(); coloringAttributes1.getColor(color1); coloringAttributes2.getColor(color2); if (!color1.equals(color2)) { return false; } } // Compare material colors Material material1 = this.appearance.getMaterial(); Material material2 = appearance2.getMaterial(); if ((material1 == null) ^ (material2 == null)) { return false; } else if (material1 != material2) { Color3f color1 = new Color3f(); Color3f color2 = new Color3f(); material1.getAmbientColor(color1); material2.getAmbientColor(color2); if (!color1.equals(color2)) { return false; } else { material1.getDiffuseColor(color1); material2.getDiffuseColor(color2); if (!color1.equals(color2)) { return false; } else { material1.getEmissiveColor(color1); material2.getEmissiveColor(color2); if (!color1.equals(color2)) { return false; } else { material1.getSpecularColor(color1); material2.getSpecularColor(color2); if (!color1.equals(color2)) { return false; } else if (material1.getShininess() != material2.getShininess()) { return false; } else if (material1.getClass() != material2.getClass()) { return false; } else if (material1.getClass() == OBJMaterial.class) { OBJMaterial objMaterial1 = (OBJMaterial)material1; OBJMaterial objMaterial2 = (OBJMaterial)material2; if (objMaterial1.isOpticalDensitySet() ^ objMaterial2.isOpticalDensitySet()) { return false; } else if (objMaterial1.isOpticalDensitySet() && objMaterial2.isOpticalDensitySet() && objMaterial1.getOpticalDensity() != objMaterial2.getOpticalDensity()) { return false; } else if (objMaterial1.isIlluminationModelSet() ^ objMaterial2.isIlluminationModelSet()) { return false; } else if (objMaterial1.isIlluminationModelSet() && objMaterial2.isIlluminationModelSet() && objMaterial1.getIlluminationModel() != objMaterial2.getIlluminationModel()) { return false; } else if (objMaterial1.isSharpnessSet() ^ objMaterial2.isSharpnessSet()) { return false; } else if (objMaterial1.isSharpnessSet() && objMaterial2.isSharpnessSet() && objMaterial1.getSharpness() != objMaterial2.getSharpness()) { return false; } } } } } } // Compare transparency TransparencyAttributes transparency1 = this.appearance.getTransparencyAttributes(); TransparencyAttributes transparency2 = appearance2.getTransparencyAttributes(); if ((transparency1 == null) ^ (transparency2 == null)) { return false; } else if (transparency1 != transparency2) { if (transparency1.getTransparency() != transparency2.getTransparency()) { return false; } } // Compare texture Texture texture1 = this.appearance.getTexture(); Texture texture2 = appearance2.getTexture(); if ((texture1 == null) ^ (texture2 == null)) { return false; } else if (texture1 != texture2) { if (texture1.getImage(0) != texture2.getImage(0)) { return false; } } // Compare name try { String name1 = this.appearance.getName(); String name2 = appearance2.getName(); if ((name1 == null) ^ (name2 == null)) { return false; } else if (name1 != name2 && !name1.equals(name2)) { return false; } } catch (NoSuchMethodError ex) { // Don't compares name with Java 3D < 1.4 where getName was added } } return true; } return false; } @Override public int hashCode() { int code = 0; ColoringAttributes coloringAttributes = appearance.getColoringAttributes(); if (coloringAttributes != null) { Color3f color = new Color3f(); coloringAttributes.getColor(color); code += color.hashCode(); } Material material = this.appearance.getMaterial(); if (material != null) { Color3f color = new Color3f(); material.getAmbientColor(color); code += color.hashCode(); material.getDiffuseColor(color); code += color.hashCode(); material.getEmissiveColor(color); code += color.hashCode(); material.getSpecularColor(color); code += color.hashCode(); code += Float.floatToIntBits(material.getShininess()); } TransparencyAttributes transparency = this.appearance.getTransparencyAttributes(); if (transparency != null) { code += Float.floatToIntBits(transparency.getTransparency()); } Texture texture = this.appearance.getTexture(); if (texture != null) { code += texture.getImage(0).hashCode(); } try { String name = this.appearance.getName(); if (name != null) { code += name.hashCode(); } } catch (NoSuchMethodError ex) { // Don't take name into account with Java 3D < 1.4 where getName was added } } return code; } } }