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 }