ObjWriter.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.geometry.io.euclidean.threed.obj;

  18. import java.io.Writer;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.HashMap;
  22. import java.util.Iterator;
  23. import java.util.LinkedHashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.function.DoubleFunction;
  27. import java.util.stream.Stream;

  28. import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
  29. import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
  30. import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
  31. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  32. import org.apache.commons.geometry.euclidean.threed.mesh.Mesh;
  33. import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
  34. import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;

  35. /** Class for writing OBJ files containing 3D polygon geometries.
  36.  */
  37. public final class ObjWriter extends AbstractTextFormatWriter {

  38.     /** Space character. */
  39.     private static final char SPACE = ' ';

  40.     /** Number of vertices written to the output. */
  41.     private int vertexCount;

  42.     /** Number of normals written to the output. */
  43.     private int normalCount;

  44.     /** Create a new instance that writes output with the given writer.
  45.      * @param writer writer used to write output
  46.      */
  47.     public ObjWriter(final Writer writer) {
  48.         super(writer);
  49.     }

  50.     /** Get the number of vertices written to the output.
  51.      * @return the number of vertices written to the output.
  52.      */
  53.     public int getVertexCount() {
  54.         return vertexCount;
  55.     }

  56.     /** Get the number of vertex normals written to the output.
  57.      * @return the number of vertex normals written to the output.
  58.      */
  59.     public int getVertexNormalCount() {
  60.         return normalCount;
  61.     }

  62.     /** Write an OBJ comment with the given value.
  63.      * @param comment comment to write
  64.      * @throws java.io.UncheckedIOException if an I/O error occurs
  65.      */
  66.     public void writeComment(final String comment) {
  67.         for (final String line : comment.split("\\R")) {
  68.             write(ObjConstants.COMMENT_CHAR);
  69.             write(SPACE);
  70.             write(line);
  71.             writeNewLine();
  72.         }
  73.     }

  74.     /** Write an object name to the output. This is metadata for the file and
  75.      * does not affect the geometry, although it may affect how the file content
  76.      * is read by other programs.
  77.      * @param objectName the name to write
  78.      * @throws java.io.UncheckedIOException if an I/O error occurs
  79.      */
  80.     public void writeObjectName(final String objectName) {
  81.         writeKeywordLine(ObjConstants.OBJECT_KEYWORD, objectName);
  82.     }

  83.     /** Write a group name to the output. This is metadata for the file and
  84.      * does not affect the geometry, although it may affect how the file content
  85.      * is read by other programs.
  86.      * @param groupName the name to write
  87.      * @throws java.io.UncheckedIOException if an I/O error occurs
  88.      */
  89.     public void writeGroupName(final String groupName) {
  90.         writeKeywordLine(ObjConstants.GROUP_KEYWORD, groupName);
  91.     }

  92.     /** Write a vertex and return the 0-based index of the vertex in the output.
  93.      * @param vertex vertex to write
  94.      * @return 0-based index of the written vertex
  95.      * @throws java.io.UncheckedIOException if an I/O error occurs
  96.      */
  97.     public int writeVertex(final Vector3D vertex) {
  98.         return writeVertexLine(createVectorString(vertex));
  99.     }

  100.     /** Write a vertex normal and return the 0-based index of the normal in the output.
  101.      * @param normal normal to write
  102.      * @return 0-based index of the written normal
  103.      * @throws java.io.UncheckedIOException if an I/O error occurs
  104.      */
  105.     public int writeVertexNormal(final Vector3D normal) {
  106.         return writeVertexNormalLine(createVectorString(normal));
  107.     }

  108.     /** Write a face with the given 0-based vertex indices.
  109.      * @param vertexIndices 0-based vertex indices for the face
  110.      * @throws IllegalArgumentException if fewer than 3 vertex indices are given
  111.      * @throws IndexOutOfBoundsException if any vertex index is computed to be outside of
  112.      *      the bounds of the elements written so far
  113.      * @throws java.io.UncheckedIOException if an I/O error occurs
  114.      */
  115.     public void writeFace(final int... vertexIndices) {
  116.         writeFaceWithOffsets(0, vertexIndices, 0, null);
  117.     }

  118.     /** Write a face with the given 0-based vertex indices and 0-based normal index. The normal
  119.      * index is applied to all face vertices.
  120.      * @param vertexIndices 0-based vertex indices
  121.      * @param normalIndex 0-based normal index
  122.      * @throws IndexOutOfBoundsException if any vertex or normal index is computed to be outside of
  123.      *      the bounds of the elements written so far
  124.      * @throws java.io.UncheckedIOException if an I/O error occurs
  125.      */
  126.     public void writeFace(final int[] vertexIndices, final int normalIndex) {
  127.         final int[] normalIndices = new int[vertexIndices.length];
  128.         Arrays.fill(normalIndices, normalIndex);

  129.         writeFaceWithOffsets(0, vertexIndices, 0, normalIndices);
  130.     }

  131.     /** Write a face with the given vertex and normal indices. Indices are 0-based.
  132.      * The {@code normalIndices} argument may be null, but if present, must contain the
  133.      * same number of indices as {@code vertexIndices}.
  134.      * @param vertexIndices 0-based vertex indices; may not be null
  135.      * @param normalIndices 0-based normal indices; may be null but if present must contain
  136.      *      the same number of indices as {@code vertexIndices}
  137.      * @throws IllegalArgumentException if fewer than 3 vertex indices are given or {@code normalIndices}
  138.      *      is not null but has a different length than {@code vertexIndices}
  139.      * @throws IndexOutOfBoundsException if any vertex or normal index is computed to be outside of
  140.      *      the bounds of the elements written so far
  141.      * @throws java.io.UncheckedIOException if an I/O error occurs
  142.      */
  143.     public void writeFace(final int[] vertexIndices, final int[] normalIndices) {
  144.         writeFaceWithOffsets(0, vertexIndices, 0, normalIndices);
  145.     }

  146.     /** Write the boundaries present in the given boundary source using a {@link MeshBuffer}
  147.      * with an unlimited size.
  148.      * @param src boundary source containing the boundaries to write to the output
  149.      * @throws IllegalArgumentException if any boundary in the argument is infinite
  150.      * @throws java.io.UncheckedIOException if an I/O error occurs
  151.      * @see #meshBuffer(int)
  152.      * @see #writeMesh(Mesh)
  153.      */
  154.     public void writeBoundaries(final BoundarySource3D src) {
  155.         writeBoundaries(src, -1);
  156.     }

  157.     /** Write the boundaries present in the given boundary source using a {@link MeshBuffer} with
  158.      * the given {@code batchSize}.
  159.      * @param src boundary source containing the boundaries to write to the output
  160.      * @param batchSize batch size to use for the mesh buffer; pass {@code -1} to use a buffer
  161.      *      of unlimited size
  162.      * @throws IllegalArgumentException if any boundary in the argument is infinite
  163.      * @throws java.io.UncheckedIOException if an I/O error occurs
  164.      * @see #meshBuffer(int)
  165.      * @see #writeMesh(Mesh)
  166.      */
  167.     public void writeBoundaries(final BoundarySource3D src, final int batchSize) {
  168.         final MeshBuffer buffer = meshBuffer(batchSize);

  169.         try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
  170.             final Iterator<PlaneConvexSubset> it = stream.iterator();
  171.             while (it.hasNext()) {
  172.                 buffer.add(it.next());
  173.             }
  174.         }

  175.         buffer.flush();
  176.     }

  177.     /** Write a mesh to the output. All vertices and faces are written exactly as found. For example,
  178.      * if a vertex is duplicated in the argument, it will also be duplicated in the output.
  179.      * @param mesh the mesh to write
  180.      * @throws java.io.UncheckedIOException if an I/O error occurs
  181.      */
  182.     public void writeMesh(final Mesh<?> mesh) {
  183.         final int vertexOffset = vertexCount;

  184.         for (final Vector3D vertex : mesh.vertices()) {
  185.             writeVertex(vertex);
  186.         }

  187.         for (final Mesh.Face face : mesh.faces()) {
  188.             writeFaceWithOffsets(vertexOffset, face.getVertexIndices(), 0, null);
  189.         }
  190.     }

  191.     /** Create a new {@link MeshBuffer} instance with an unlimited batch size, meaning that
  192.      * no vertex definitions are duplicated in the mesh output. This produces the most compact
  193.      * mesh but at the most of higher memory usage during writing.
  194.      * @return new mesh buffer instance
  195.      */
  196.     public MeshBuffer meshBuffer() {
  197.         return meshBuffer(-1);
  198.     }

  199.     /** Create a new {@link MeshBuffer} instance with the given batch size. The batch size determines
  200.      * how many faces will be stored in the buffer before being flushed. Faces stored in the buffer
  201.      * share duplicate vertices, reducing the number of vertices required in the file. The {@code batchSize}
  202.      * is therefore a trade-off between higher memory usage (high batch size) and a higher probability of duplicate
  203.      * vertices present in the output (low batch size). A batch size of {@code -1} indicates an unlimited
  204.      * batch size.
  205.      * @param batchSize number of faces to store in the buffer before automatically flushing to the
  206.      *      output
  207.      * @return new mesh buffer instance
  208.      */
  209.     public MeshBuffer meshBuffer(final int batchSize) {
  210.         return new MeshBuffer(batchSize);
  211.     }

  212.     /** Write a face with the given offsets and indices. The offsets are added to each
  213.      * index before being written.
  214.      * @param vertexOffset vertex offset value
  215.      * @param vertexIndices 0-based vertex indices for the face
  216.      * @param normalOffset normal offset value
  217.      * @param normalIndices 0-based normal indices for the face; may be null if no normal are
  218.      *      defined for the face
  219.      * @throws IllegalArgumentException if fewer than 3 vertex indices are given or {@code normalIndices}
  220.      *      is not null but has a different length than {@code vertexIndices}
  221.      * @throws IndexOutOfBoundsException if any vertex or normal index is computed to be outside of
  222.      *      the bounds of the elements written so far
  223.      * @throws java.io.UncheckedIOException if an I/O error occurs
  224.      */
  225.     private void writeFaceWithOffsets(final int vertexOffset, final int[] vertexIndices,
  226.             final int normalOffset, final int[] normalIndices) {
  227.         if (vertexIndices.length < EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
  228.             throw new IllegalArgumentException("Face must have more than " + EuclideanUtils.TRIANGLE_VERTEX_COUNT +
  229.                     " vertices; found " + vertexIndices.length);
  230.         } else if (normalIndices != null && normalIndices.length != vertexIndices.length) {
  231.             throw new IllegalArgumentException("Face normal index count must equal vertex index count; expected " +
  232.                     vertexIndices.length + " but was " + normalIndices.length);
  233.         }

  234.         write(ObjConstants.FACE_KEYWORD);

  235.         int vertexIdx;
  236.         int normalIdx;
  237.         for (int i = 0; i < vertexIndices.length; ++i) {
  238.             vertexIdx = vertexIndices[i] + vertexOffset;
  239.             if (vertexIdx < 0 || vertexIdx >= vertexCount) {
  240.                 throw new IndexOutOfBoundsException("Vertex index out of bounds: " + vertexIdx);
  241.             }

  242.             write(SPACE);
  243.             write(vertexIdx + 1); // convert to OBJ 1-based convention

  244.             if (normalIndices != null) {
  245.                 normalIdx = normalIndices[i] + normalOffset;
  246.                 if (normalIdx < 0 || normalIdx >= normalCount) {
  247.                     throw new IndexOutOfBoundsException("Normal index out of bounds: " + normalIdx);
  248.                 }

  249.                 // two separator chars since there is no texture coordinate
  250.                 write(ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR);
  251.                 write(ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR);

  252.                 write(normalIdx + 1); // convert to OBJ 1-based convention
  253.             }
  254.         }

  255.         writeNewLine();
  256.     }

  257.     /** Create the OBJ string representation of the given vector.
  258.      * @param vec vector to convert to a string
  259.      * @return string representation of the given vector
  260.      */
  261.     private String createVectorString(final Vector3D vec) {
  262.         final DoubleFunction<String> fmt = getDoubleFormat();

  263.         final StringBuilder sb = new StringBuilder();
  264.         sb.append(fmt.apply(vec.getX()))
  265.             .append(SPACE)
  266.             .append(fmt.apply(vec.getY()))
  267.             .append(SPACE)
  268.             .append(fmt.apply(vec.getZ()));

  269.         return sb.toString();
  270.     }

  271.     /** Write a vertex line containing the given string content.
  272.      * @param content vertex string content
  273.      * @return the 0-based index of the added vertex
  274.      * @throws java.io.UncheckedIOException if an I/O error occurs
  275.      */
  276.     private int writeVertexLine(final String content) {
  277.         writeKeywordLine(ObjConstants.VERTEX_KEYWORD, content);
  278.         return vertexCount++;
  279.     }

  280.     /** Write a vertex normal line containing the given string content.
  281.      * @param content vertex normal string content
  282.      * @return the 0-based index of the added vertex normal
  283.      * @throws java.io.UncheckedIOException if an I/O error occurs
  284.      */
  285.     private int writeVertexNormalLine(final String content) {
  286.         writeKeywordLine(ObjConstants.VERTEX_NORMAL_KEYWORD, content);
  287.         return normalCount++;
  288.     }

  289.     /** Write a line of content prefixed with the given OBJ keyword.
  290.      * @param keyword OBJ keyword
  291.      * @param content line content
  292.      * @throws java.io.UncheckedIOException if an I/O error occurs
  293.      */
  294.     private void writeKeywordLine(final String keyword, final String content) {
  295.         write(keyword);
  296.         write(SPACE);
  297.         write(content);
  298.         writeNewLine();
  299.     }

  300.     /** Class used to produce OBJ mesh content from sequences of facets. As facets are added to the buffer
  301.      * their vertices and normals are converted to OBJ vertex and normal definition strings. Vertices and normals
  302.      * that produce equal definition strings are shared among all of the facets in the buffer. This process
  303.      * converts the facet sequence into a compact mesh suitable for writing as OBJ file content.
  304.      *
  305.      * <p>Ideally, no vertices or normals would be duplicated in an OBJ file. However, when working with very large
  306.      * geometries it may not be desirable to store values in memory before writing to the output. This
  307.      * is where the {@code batchSize} property comes into play. The {@code batchSize} represents the maximum
  308.      * number of faces that the buffer will store before automatically flushing its contents to the output and
  309.      * resetting its state. This reduces the amount of memory used by the buffer at the cost of increasing the
  310.      * likelihood of duplicate vertices and/or normals in the output.</p>
  311.      */
  312.     public final class MeshBuffer {

  313.         /** Maximum number of faces that will be stored in the buffer before automatically flushing. */
  314.         private final int batchSize;

  315.         /** Map of vertex definition strings to their local index. */
  316.         private final Map<String, Integer> vertexMap = new LinkedHashMap<>();

  317.         /** Map of vertex normals to their local index. */
  318.         private final Map<String, Integer> normalMap = new LinkedHashMap<>();

  319.         /** List of local face vertex indices. */
  320.         private final List<int[]> faceVertices;

  321.         /** Map of local face indices to their local normal index. */
  322.         private final Map<Integer, Integer> faceToNormalMap = new HashMap<>();

  323.         /** Construct a new mesh buffer instance with the given batch size.
  324.          * @param batchSize batch size; set to -1 to indicate an unlimited size
  325.          */
  326.         MeshBuffer(final int batchSize) {
  327.             this.batchSize = batchSize;
  328.             this.faceVertices = batchSize > -1 ?
  329.                     new ArrayList<>(batchSize) :
  330.                     new ArrayList<>();
  331.         }

  332.         /** Add a facet to this buffer. If {@code batchSize} is greater than {@code -1} and the number
  333.          * of currently stored faces is greater than or equal to {@code batchSize}, then the buffer
  334.          * content is written to the output and the buffer state is reset.
  335.          * @param facet facet to add
  336.          * @throws java.io.UncheckedIOException if an I/O error occurs
  337.          */
  338.         public void add(final FacetDefinition facet) {
  339.             addFace(facet.getVertices(), facet.getNormal());
  340.         }

  341.         /** Add a boundary to this buffer. If {@code batchSize} is greater than {@code -1} and the number
  342.          * of currently stored faces is greater than or equal to {@code batchSize}, then the buffer
  343.          * content is written to the output and the buffer state is reset.
  344.          * @param boundary boundary to add
  345.          * @throws IllegalArgumentException if the boundary is infinite
  346.          * @throws java.io.UncheckedIOException if an I/O error occurs
  347.          */
  348.         public void add(final PlaneConvexSubset boundary) {
  349.             if (boundary.isInfinite()) {
  350.                 throw new IllegalArgumentException("OBJ input geometry cannot be infinite: " + boundary);
  351.             } else if (!boundary.isEmpty()) {
  352.                 addFace(boundary.getVertices(), null);
  353.             }
  354.         }

  355.         /** Add a vertex to the buffer.
  356.          * @param vertex vertex to add
  357.          * @return the index of the vertex in the buffer
  358.          */
  359.         public int addVertex(final Vector3D vertex) {
  360.             return addToMap(vertex, vertexMap);
  361.         }

  362.         /** Add a normal to the buffer.
  363.          * @param normal normal to add
  364.          * @return the index of the normal in the buffer
  365.          */
  366.         public int addNormal(final Vector3D normal) {
  367.             return addToMap(normal, normalMap);
  368.         }

  369.         /** Flush the buffer content to the output and reset its state.
  370.          * @throws java.io.UncheckedIOException if an I/O error occurs
  371.          */
  372.         public void flush() {
  373.             final int vertexOffset = vertexCount;
  374.             final int normalOffset = normalCount;

  375.             // write vertices
  376.             for (final String vertexStr : vertexMap.keySet()) {
  377.                 writeVertexLine(vertexStr);
  378.             }

  379.             // write normals
  380.             for (final String normalStr : normalMap.keySet()) {
  381.                 writeVertexNormalLine(normalStr);
  382.             }

  383.             // write faces
  384.             Integer normalIndex;
  385.             int[] normalIndices;
  386.             int faceIndex = 0;
  387.             for (final int[] vertexIndices : faceVertices) {
  388.                 normalIndex = faceToNormalMap.get(faceIndex);
  389.                 if (normalIndex != null) {
  390.                     normalIndices = new int[vertexIndices.length];
  391.                     Arrays.fill(normalIndices, normalIndex);
  392.                 } else {
  393.                     normalIndices = null;
  394.                 }

  395.                 writeFaceWithOffsets(vertexOffset, vertexIndices, normalOffset, normalIndices);

  396.                 ++faceIndex;
  397.             }

  398.             reset();
  399.         }

  400.         /** Convert the given vector to on OBJ definition string and add it to the
  401.          * map if not yet present. The mapped index of the vector is returned.
  402.          * @param vec vector to add
  403.          * @param map map to add the vector to
  404.          * @return the index the vector entry is mapped to
  405.          */
  406.         private int addToMap(final Vector3D vec, final Map<String, Integer> map) {
  407.             final String str = createVectorString(vec);

  408.             return map.computeIfAbsent(str, k -> map.size());
  409.         }

  410.         /** Add a face to the buffer. If {@code batchSize} is greater than {@code -1} and the number
  411.          * of currently stored faces is greater than or equal to {@code batchSize}, then the buffer
  412.          * content is written to the output and the buffer state is reset.
  413.          * @param vertices face vertices
  414.          * @param normal face normal; may be null
  415.          * @throws java.io.UncheckedIOException if an I/O error occurs
  416.          */
  417.         private void addFace(final List<Vector3D> vertices, final Vector3D normal) {
  418.             final int faceIndex = faceVertices.size();

  419.             final int[] vertexIndices = new int[vertices.size()];

  420.             int i = -1;
  421.             for (final Vector3D vertex : vertices) {
  422.                 vertexIndices[++i] = addVertex(vertex);
  423.             }
  424.             faceVertices.add(vertexIndices);

  425.             if (normal != null) {
  426.                 faceToNormalMap.put(faceIndex, addNormal(normal));
  427.             }

  428.             if (batchSize > -1 && faceVertices.size() >= batchSize) {
  429.                 flush();
  430.             }
  431.         }

  432.         /** Reset the buffer state.
  433.          */
  434.         private void reset() {
  435.             vertexMap.clear();
  436.             normalMap.clear();
  437.             faceVertices.clear();
  438.             faceToNormalMap.clear();
  439.         }
  440.     }
  441. }