View Javadoc

    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
    19  import java.io.Writer;
    20  import java.util.ArrayList;
    21  import java.util.Arrays;
    22  import java.util.HashMap;
    23  import java.util.Iterator;
    24  import java.util.LinkedHashMap;
    25  import java.util.List;
    26  import java.util.Map;
    27  import java.util.function.DoubleFunction;
    28  import java.util.stream.Stream;
    29
    30  import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
    31  import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
    32  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
    33  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    34  import org.apache.commons.geometry.euclidean.threed.mesh.Mesh;
    35  import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
    36  import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
    37
    38  /** Class for writing OBJ files containing 3D polygon geometries.
    39   */
    40  public final class ObjWriter extends AbstractTextFormatWriter {
    41
    42      /** Space character. */
    43      private static final char SPACE = ' ';
    44
    45      /** Number of vertices written to the output. */
    46      private int vertexCount;
    47
    48      /** Number of normals written to the output. */
    49      private int normalCount;
    50
    51      /** Create a new instance that writes output with the given writer.
    52       * @param writer writer used to write output
    53       */
    54      public ObjWriter(final Writer writer) {
    55          super(writer);
    56      }
    57
    58      /** Get the number of vertices written to the output.
    59       * @return the number of vertices written to the output.
    60       */
    61      public int getVertexCount() {
    62          return vertexCount;
    63      }
    64
    65      /** Get the number of vertex normals written to the output.
    66       * @return the number of vertex normals written to the output.
    67       */
    68      public int getVertexNormalCount() {
    69          return normalCount;
    70      }
    71
    72      /** Write an OBJ comment with the given value.
    73       * @param comment comment to write
    74       * @throws java.io.UncheckedIOException if an I/O error occurs
    75       */
    76      public void writeComment(final String comment) {
    77          for (final String line : comment.split("\\R")) {
    78              write(ObjConstants.COMMENT_CHAR);
    79              write(SPACE);
    80              write(line);
    81              writeNewLine();
    82          }
    83      }
    84
    85      /** Write an object name to the output. This is metadata for the file and
    86       * does not affect the geometry, although it may affect how the file content
    87       * is read by other programs.
    88       * @param objectName the name to write
    89       * @throws java.io.UncheckedIOException if an I/O error occurs
    90       */
    91      public void writeObjectName(final String objectName) {
    92          writeKeywordLine(ObjConstants.OBJECT_KEYWORD, objectName);
    93      }
    94
    95      /** Write a group name to the output. This is metadata for the file and
    96       * does not affect the geometry, although it may affect how the file content
    97       * is read by other programs.
    98       * @param groupName the name to write
    99       * @throws java.io.UncheckedIOException if an I/O error occurs
    100      */
    101     public void writeGroupName(final String groupName) {
    102         writeKeywordLine(ObjConstants.GROUP_KEYWORD, groupName);
    103     }
    104
    105     /** Write a vertex and return the 0-based index of the vertex in the output.
    106      * @param vertex vertex to write
    107      * @return 0-based index of the written vertex
    108      * @throws java.io.UncheckedIOException if an I/O error occurs
    109      */
    110     public int writeVertex(final Vector3D vertex) {
    111         return writeVertexLine(createVectorString(vertex));
    112     }
    113
    114     /** Write a vertex normal and return the 0-based index of the normal in the output.
    115      * @param normal normal to write
    116      * @return 0-based index of the written normal
    117      * @throws java.io.UncheckedIOException if an I/O error occurs
    118      */
    119     public int writeVertexNormal(final Vector3D normal) {
    120         return writeVertexNormalLine(createVectorString(normal));
    121     }
    122
    123     /** Write a face with the given 0-based vertex indices.
    124      * @param vertexIndices 0-based vertex indices for the face
    125      * @throws IllegalArgumentException if fewer than 3 vertex indices are given
    126      * @throws IndexOutOfBoundsException if any vertex index is computed to be outside of
    127      *      the bounds of the elements written so far
    128      * @throws java.io.UncheckedIOException if an I/O error occurs
    129      */
    130     public void writeFace(final int... vertexIndices) {
    131         writeFaceWithOffsets(0, vertexIndices, 0, null);
    132     }
    133
    134     /** Write a face with the given 0-based vertex indices and 0-based normal index. The normal
    135      * index is applied to all face vertices.
    136      * @param vertexIndices 0-based vertex indices
    137      * @param normalIndex 0-based normal index
    138      * @throws IndexOutOfBoundsException if any vertex or normal index is computed to be outside of
    139      *      the bounds of the elements written so far
    140      * @throws java.io.UncheckedIOException if an I/O error occurs
    141      */
    142     public void writeFace(final int[] vertexIndices, final int normalIndex) {
    143         final int[] normalIndices = new int[vertexIndices.length];
    144         Arrays.fill(normalIndices, normalIndex);
    145
    146         writeFaceWithOffsets(0, vertexIndices, 0, normalIndices);
    147     }
    148
    149     /** Write a face with the given vertex and normal indices. Indices are 0-based.
    150      * The {@code normalIndices} argument may be null, but if present, must contain the
    151      * same number of indices as {@code vertexIndices}.
    152      * @param vertexIndices 0-based vertex indices; may not be null
    153      * @param normalIndices 0-based normal indices; may be null but if present must contain
    154      *      the same number of indices as {@code vertexIndices}
    155      * @throws IllegalArgumentException if fewer than 3 vertex indices are given or {@code normalIndices}
    156      *      is not null but has a different length than {@code vertexIndices}
    157      * @throws IndexOutOfBoundsException if any vertex or normal index is computed to be outside of
    158      *      the bounds of the elements written so far
    159      * @throws java.io.UncheckedIOException if an I/O error occurs
    160      */
    161     public void writeFace(final int[] vertexIndices, final int[] normalIndices) {
    162         writeFaceWithOffsets(0, vertexIndices, 0, normalIndices);
    163     }
    164
    165     /** Write the boundaries present in the given boundary source using a {@link MeshBuffer}
    166      * with an unlimited size.
    167      * @param src boundary source containing the boundaries to write to the output
    168      * @throws IllegalArgumentException if any boundary in the argument is infinite
    169      * @throws java.io.UncheckedIOException if an I/O error occurs
    170      * @see #meshBuffer(int)
    171      * @see #writeMesh(Mesh)
    172      */
    173     public void writeBoundaries(final BoundarySource3D src) {
    174         writeBoundaries(src, -1);
    175     }
    176
    177     /** Write the boundaries present in the given boundary source using a {@link MeshBuffer} with
    178      * the given {@code batchSize}.
    179      * @param src boundary source containing the boundaries to write to the output
    180      * @param batchSize batch size to use for the mesh buffer; pass {@code -1} to use a buffer
    181      *      of unlimited size
    182      * @throws IllegalArgumentException if any boundary in the argument is infinite
    183      * @throws java.io.UncheckedIOException if an I/O error occurs
    184      * @see #meshBuffer(int)
    185      * @see #writeMesh(Mesh)
    186      */
    187     public void writeBoundaries(final BoundarySource3D src, final int batchSize) {
    188         final MeshBuffer buffer = meshBuffer(batchSize);
    189
    190         try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
    191             final Iterator<PlaneConvexSubset> it = stream.iterator();
    192             while (it.hasNext()) {
    193                 buffer.add(it.next());
    194             }
    195         }
    196
    197         buffer.flush();
    198     }
    199
    200     /** Write a mesh to the output. All vertices and faces are written exactly as found. For example,
    201      * if a vertex is duplicated in the argument, it will also be duplicated in the output.
    202      * @param mesh the mesh to write
    203      * @throws java.io.UncheckedIOException if an I/O error occurs
    204      */
    205     public void writeMesh(final Mesh<?> mesh) {
    206         final int vertexOffset = vertexCount;
    207
    208         for (final Vector3D vertex : mesh.vertices()) {
    209             writeVertex(vertex);
    210         }
    211
    212         for (final Mesh.Face face : mesh.faces()) {
    213             writeFaceWithOffsets(vertexOffset, face.getVertexIndices(), 0, null);
    214         }
    215     }
    216
    217     /** Create a new {@link MeshBuffer} instance with an unlimited batch size, meaning that
    218      * no vertex definitions are duplicated in the mesh output. This produces the most compact
    219      * mesh but at the most of higher memory usage during writing.
    220      * @return new mesh buffer instance
    221      */
    222     public MeshBuffer meshBuffer() {
    223         return meshBuffer(-1);
    224     }
    225
    226     /** Create a new {@link MeshBuffer} instance with the given batch size. The batch size determines
    227      * how many faces will be stored in the buffer before being flushed. Faces stored in the buffer
    228      * share duplicate vertices, reducing the number of vertices required in the file. The {@code batchSize}
    229      * is therefore a trade-off between higher memory usage (high batch size) and a higher probability of duplicate
    230      * vertices present in the output (low batch size). A batch size of {@code -1} indicates an unlimited
    231      * batch size.
    232      * @param batchSize number of faces to store in the buffer before automatically flushing to the
    233      *      output
    234      * @return new mesh buffer instance
    235      */
    236     public MeshBuffer meshBuffer(final int batchSize) {
    237         return new MeshBuffer(batchSize);
    238     }
    239
    240     /** Write a face with the given offsets and indices. The offsets are added to each
    241      * index before being written.
    242      * @param vertexOffset vertex offset value
    243      * @param vertexIndices 0-based vertex indices for the face
    244      * @param normalOffset normal offset value
    245      * @param normalIndices 0-based normal indices for the face; may be null if no normal are
    246      *      defined for the face
    247      * @throws IllegalArgumentException if fewer than 3 vertex indices are given or {@code normalIndices}
    248      *      is not null but has a different length than {@code vertexIndices}
    249      * @throws IndexOutOfBoundsException if any vertex or normal index is computed to be outside of
    250      *      the bounds of the elements written so far
    251      * @throws java.io.UncheckedIOException if an I/O error occurs
    252      */
    253     private void writeFaceWithOffsets(final int vertexOffset, final int[] vertexIndices,
    254             final int normalOffset, final int[] normalIndices) {
    255         if (vertexIndices.length < EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
    256             throw new IllegalArgumentException("Face must have more than " + EuclideanUtils.TRIANGLE_VERTEX_COUNT +
    257                     " vertices; found " + vertexIndices.length);
    258         } else if (normalIndices != null && normalIndices.length != vertexIndices.length) {
    259             throw new IllegalArgumentException("Face normal index count must equal vertex index count; expected " +
    260                     vertexIndices.length + " but was " + normalIndices.length);
    261         }
    262
    263         write(ObjConstants.FACE_KEYWORD);
    264
    265         int vertexIdx;
    266         int normalIdx;
    267         for (int i = 0; i < vertexIndices.length; ++i) {
    268             vertexIdx = vertexIndices[i] + vertexOffset;
    269             if (vertexIdx < 0 || vertexIdx >= vertexCount) {
    270                 throw new IndexOutOfBoundsException("Vertex index out of bounds: " + vertexIdx);
    271             }
    272
    273             write(SPACE);
    274             write(vertexIdx + 1); // convert to OBJ 1-based convention
    275
    276             if (normalIndices != null) {
    277                 normalIdx = normalIndices[i] + normalOffset;
    278                 if (normalIdx < 0 || normalIdx >= normalCount) {
    279                     throw new IndexOutOfBoundsException("Normal index out of bounds: " + normalIdx);
    280                 }
    281
    282                 // two separator chars since there is no texture coordinate
    283                 write(ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR);
    284                 write(ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR);
    285
    286                 write(normalIdx + 1); // convert to OBJ 1-based convention
    287             }
    288         }
    289
    290         writeNewLine();
    291     }
    292
    293     /** Create the OBJ string representation of the given vector.
    294      * @param vec vector to convert to a string
    295      * @return string representation of the given vector
    296      */
    297     private String createVectorString(final Vector3D vec) {
    298         final DoubleFunction<String> fmt = getDoubleFormat();
    299
    300         final StringBuilder sb = new StringBuilder();
    301         sb.append(fmt.apply(vec.getX()))
    302             .append(SPACE)
    303             .append(fmt.apply(vec.getY()))
    304             .append(SPACE)
    305             .append(fmt.apply(vec.getZ()));
    306
    307         return sb.toString();
    308     }
    309
    310     /** Write a vertex line containing the given string content.
    311      * @param content vertex string content
    312      * @return the 0-based index of the added vertex
    313      * @throws java.io.UncheckedIOException if an I/O error occurs
    314      */
    315     private int writeVertexLine(final String content) {
    316         writeKeywordLine(ObjConstants.VERTEX_KEYWORD, content);
    317         return vertexCount++;
    318     }
    319
    320     /** Write a vertex normal line containing the given string content.
    321      * @param content vertex normal string content
    322      * @return the 0-based index of the added vertex normal
    323      * @throws java.io.UncheckedIOException if an I/O error occurs
    324      */
    325     private int writeVertexNormalLine(final String content) {
    326         writeKeywordLine(ObjConstants.VERTEX_NORMAL_KEYWORD, content);
    327         return normalCount++;
    328     }
    329
    330     /** Write a line of content prefixed with the given OBJ keyword.
    331      * @param keyword OBJ keyword
    332      * @param content line content
    333      * @throws java.io.UncheckedIOException if an I/O error occurs
    334      */
    335     private void writeKeywordLine(final String keyword, final String content) {
    336         write(keyword);
    337         write(SPACE);
    338         write(content);
    339         writeNewLine();
    340     }
    341
    342     /** Class used to produce OBJ mesh content from sequences of facets. As facets are added to the buffer
    343      * their vertices and normals are converted to OBJ vertex and normal definition strings. Vertices and normals
    344      * that produce equal definition strings are shared among all of the facets in the buffer. This process
    345      * converts the facet sequence into a compact mesh suitable for writing as OBJ file content.
    346      *
    347      * <p>Ideally, no vertices or normals would be duplicated in an OBJ file. However, when working with very large
    348      * geometries it may not be desirable to store values in memory before writing to the output. This
    349      * is where the {@code batchSize} property comes into play. The {@code batchSize} represents the maximum
    350      * number of faces that the buffer will store before automatically flushing its contents to the output and
    351      * resetting its state. This reduces the amount of memory used by the buffer at the cost of increasing the
    352      * likelihood of duplicate vertices and/or normals in the output.</p>
    353      */
    354     public final class MeshBuffer {
    355
    356         /** Maximum number of faces that will be stored in the buffer before automatically flushing. */
    357         private final int batchSize;
    358
    359         /** Map of vertex definition strings to their local index. */
    360         private final Map<String, Integer> vertexMap = new LinkedHashMap<>();
    361
    362         /** Map of vertex normals to their local index. */
    363         private final Map<String, Integer> normalMap = new LinkedHashMap<>();
    364
    365         /** List of local face vertex indices. */
    366         private final List<int[]> faceVertices;
    367
    368         /** Map of local face indices to their local normal index. */
    369         private final Map<Integer, Integer> faceToNormalMap = new HashMap<>();
    370
    371         /** Construct a new mesh buffer instance with the given batch size.
    372          * @param batchSize batch size; set to -1 to indicate an unlimited size
    373          */
    374         MeshBuffer(final int batchSize) {
    375             this.batchSize = batchSize;
    376             this.faceVertices = batchSize > -1 ?
    377                     new ArrayList<>(batchSize) :
    378                     new ArrayList<>();
    379         }
    380
    381         /** Add a facet to this buffer. If {@code batchSize} is greater than {@code -1} and the number
    382          * of currently stored faces is greater than or equal to {@code batchSize}, then the buffer
    383          * content is written to the output and the buffer state is reset.
    384          * @param facet facet to add
    385          * @throws java.io.UncheckedIOException if an I/O error occurs
    386          */
    387         public void add(final FacetDefinition facet) {
    388             addFace(facet.getVertices(), facet.getNormal());
    389         }
    390
    391         /** Add a boundary to this buffer. If {@code batchSize} is greater than {@code -1} and the number
    392          * of currently stored faces is greater than or equal to {@code batchSize}, then the buffer
    393          * content is written to the output and the buffer state is reset.
    394          * @param boundary boundary to add
    395          * @throws IllegalArgumentException if the boundary is infinite
    396          * @throws java.io.UncheckedIOException if an I/O error occurs
    397          */
    398         public void add(final PlaneConvexSubset boundary) {
    399             if (boundary.isInfinite()) {
    400                 throw new IllegalArgumentException("OBJ input geometry cannot be infinite: " + boundary);
    401             } else if (!boundary.isEmpty()) {
    402                 addFace(boundary.getVertices(), null);
    403             }
    404         }
    405
    406         /** Add a vertex to the buffer.
    407          * @param vertex vertex to add
    408          * @return the index of the vertex in the buffer
    409          */
    410         public int addVertex(final Vector3D vertex) {
    411             return addToMap(vertex, vertexMap);
    412         }
    413
    414         /** Add a normal to the buffer.
    415          * @param normal normal to add
    416          * @return the index of the normal in the buffer
    417          */
    418         public int addNormal(final Vector3D normal) {
    419             return addToMap(normal, normalMap);
    420         }
    421
    422         /** Flush the buffer content to the output and reset its state.
    423          * @throws java.io.UncheckedIOException if an I/O error occurs
    424          */
    425         public void flush() {
    426             final int vertexOffset = vertexCount;
    427             final int normalOffset = normalCount;
    428
    429             // write vertices
    430             for (final String vertexStr : vertexMap.keySet()) {
    431                 writeVertexLine(vertexStr);
    432             }
    433
    434             // write normals
    435             for (final String normalStr : normalMap.keySet()) {
    436                 writeVertexNormalLine(normalStr);
    437             }
    438
    439             // write faces
    440             Integer normalIndex;
    441             int[] normalIndices;
    442             int faceIndex = 0;
    443             for (final int[] vertexIndices : faceVertices) {
    444                 normalIndex = faceToNormalMap.get(faceIndex);
    445                 if (normalIndex != null) {
    446                     normalIndices = new int[vertexIndices.length];
    447                     Arrays.fill(normalIndices, normalIndex);
    448                 } else {
    449                     normalIndices = null;
    450                 }
    451
    452                 writeFaceWithOffsets(vertexOffset, vertexIndices, normalOffset, normalIndices);
    453
    454                 ++faceIndex;
    455             }
    456
    457             reset();
    458         }
    459
    460         /** Convert the given vector to on OBJ definition string and add it to the
    461          * map if not yet present. The mapped index of the vector is returned.
    462          * @param vec vector to add
    463          * @param map map to add the vector to
    464          * @return the index the vector entry is mapped to
    465          */
    466         private int addToMap(final Vector3D vec, final Map<String, Integer> map) {
    467             final String str = createVectorString(vec);
    468
    469             return map.computeIfAbsent(str, k -> map.size());
    470         }
    471
    472         /** Add a face to the buffer. If {@code batchSize} is greater than {@code -1} and the number
    473          * of currently stored faces is greater than or equal to {@code batchSize}, then the buffer
    474          * content is written to the output and the buffer state is reset.
    475          * @param vertices face vertices
    476          * @param normal face normal; may be null
    477          * @throws java.io.UncheckedIOException if an I/O error occurs
    478          */
    479         private void addFace(final List<Vector3D> vertices, final Vector3D normal) {
    480             final int faceIndex = faceVertices.size();
    481
    482             final int[] vertexIndices = new int[vertices.size()];
    483
    484             int i = -1;
    485             for (final Vector3D vertex : vertices) {
    486                 vertexIndices[++i] = addVertex(vertex);
    487             }
    488             faceVertices.add(vertexIndices);
    489
    490             if (normal != null) {
    491                 faceToNormalMap.put(faceIndex, addNormal(normal));
    492             }
    493
    494             if (batchSize > -1 && faceVertices.size() >= batchSize) {
    495                 flush();
    496             }
    497         }
    498
    499         /** Reset the buffer state.
    500          */
    501         private void reset() {
    502             vertexMap.clear();
    503             normalMap.clear();
    504             faceVertices.clear();
    505             faceToNormalMap.clear();
    506         }
    507     }
    508 }