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 }