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 }