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.Reader; 20 import java.util.ArrayList; 21 import java.util.Arrays; 22 import java.util.Collections; 23 import java.util.HashSet; 24 import java.util.List; 25 import java.util.Set; 26 import java.util.function.IntFunction; 27 import java.util.function.ToIntFunction; 28 import java.util.stream.Collectors; 29 30 import org.apache.commons.geometry.euclidean.internal.EuclideanUtils; 31 import org.apache.commons.geometry.euclidean.threed.Vector3D; 32 import org.apache.commons.geometry.io.core.internal.SimpleTextParser; 33 34 /** Low-level parser class for reading 3D polygon (face) data in the OBJ file format. 35 * This class provides access to OBJ data structures but does not retain any of the 36 * parsed data. For example, it is up to callers to store vertices as they are parsed 37 * for later reference. This allows callers to determine what values are stored and in 38 * what format. 39 */ 40 public class PolygonObjParser extends AbstractObjParser { 41 42 /** Set containing OBJ keywords commonly used with files containing only polygon content. */ 43 private static final Set<String> STANDARD_POLYGON_KEYWORDS = 44 Collections.unmodifiableSet(new HashSet<>(Arrays.asList( 45 ObjConstants.VERTEX_KEYWORD, 46 ObjConstants.VERTEX_NORMAL_KEYWORD, 47 ObjConstants.TEXTURE_COORDINATE_KEYWORD, 48 ObjConstants.FACE_KEYWORD, 49 50 ObjConstants.OBJECT_KEYWORD, 51 ObjConstants.GROUP_KEYWORD, 52 ObjConstants.SMOOTHING_GROUP_KEYWORD, 53 54 ObjConstants.MATERIAL_LIBRARY_KEYWORD, 55 ObjConstants.USE_MATERIAL_KEYWORD 56 ))); 57 58 /** Number of vertex keywords encountered in the file so far. */ 59 private int vertexCount; 60 61 /** Number of vertex normal keywords encountered in the file so far. */ 62 private int vertexNormalCount; 63 64 /** Number of texture coordinate keywords encountered in the file so far. */ 65 private int textureCoordinateCount; 66 67 /** If true, parsing will fail when non-polygon keywords are encountered in the OBJ content. */ 68 private boolean failOnNonPolygonKeywords; 69 70 /** Construct a new instance for parsing OBJ content from the given reader. 71 * @param reader reader to parser content from 72 */ 73 public PolygonObjParser(final Reader reader) { 74 this(new SimpleTextParser(reader)); 75 } 76 77 /** Construct a new instance for parsing OBJ content from the given text parser. 78 * @param parser text parser to read content from 79 */ 80 public PolygonObjParser(final SimpleTextParser parser) { 81 super(parser); 82 } 83 84 /** Get the number of {@link ObjConstants#VERTEX_KEYWORD vertex keywords} parsed 85 * so far. 86 * @return the number of vertex keywords parsed so far 87 */ 88 public int getVertexCount() { 89 return vertexCount; 90 } 91 92 /** Get the number of {@link ObjConstants#VERTEX_NORMAL_KEYWORD vertex normal keywords} parsed 93 * so far. 94 * @return the number of vertex normal keywords parsed so far 95 */ 96 public int getVertexNormalCount() { 97 return vertexNormalCount; 98 } 99 100 /** Get the number of {@link ObjConstants#TEXTURE_COORDINATE_KEYWORD texture coordinate keywords} parsed 101 * so far. 102 * @return the number of texture coordinate keywords parsed so far 103 */ 104 public int getTextureCoordinateCount() { 105 return textureCoordinateCount; 106 } 107 108 /** Return true if the instance is configured to throw an {@link IllegalStateException} when OBJ keywords 109 * not commonly used with files containing only polygon data are encountered. The default value is {@code false}, 110 * meaning that no keyword validation is performed. When set to true, only the following keywords are 111 * accepted: 112 * <ul> 113 * <li>{@code v}</li> 114 * <li>{@code vn}</li> 115 * <li>{@code vt}</li> 116 * <li>{@code f}</li> 117 * <li>{@code o}</li> 118 * <li>{@code g}</li> 119 * <li>{@code s}</li> 120 * <li>{@code mtllib}</li> 121 * <li>{@code usemtl}</li> 122 * </ul> 123 * @return true if the instance is configured to fail when a non-polygon keyword is encountered 124 */ 125 public boolean isFailOnNonPolygonKeywords() { 126 return failOnNonPolygonKeywords; 127 } 128 129 /** Set the flag determining if the instance should throw an {@link IllegalStateException} when encountering 130 * keywords not commonly used with OBJ files containing only polygon data. If true, only the following keywords 131 * are accepted: 132 * <ul> 133 * <li>{@code v}</li> 134 * <li>{@code vn}</li> 135 * <li>{@code vt}</li> 136 * <li>{@code f}</li> 137 * <li>{@code o}</li> 138 * <li>{@code g}</li> 139 * <li>{@code s}</li> 140 * <li>{@code mtllib}</li> 141 * <li>{@code usemtl}</li> 142 * </ul> 143 * If false, all keywords are accepted. 144 * @param failOnNonPolygonKeywords new flag value 145 */ 146 public void setFailOnNonPolygonKeywords(final boolean failOnNonPolygonKeywords) { 147 this.failOnNonPolygonKeywords = failOnNonPolygonKeywords; 148 } 149 150 /** {@inheritDoc} */ 151 @Override 152 protected void handleKeyword(final String keywordValue) { 153 if (failOnNonPolygonKeywords && !STANDARD_POLYGON_KEYWORDS.contains(keywordValue)) { 154 final String allowedKeywords = STANDARD_POLYGON_KEYWORDS.stream() 155 .sorted() 156 .collect(Collectors.joining(", ")); 157 158 throw getTextParser().tokenError("expected keyword to be one of [" + allowedKeywords + 159 "] but was [" + keywordValue + "]"); 160 } 161 162 // update counts in order to validate face vertex attributes 163 switch (keywordValue) { 164 case ObjConstants.VERTEX_KEYWORD: 165 ++vertexCount; 166 break; 167 case ObjConstants.VERTEX_NORMAL_KEYWORD: 168 ++vertexNormalCount; 169 break; 170 case ObjConstants.TEXTURE_COORDINATE_KEYWORD: 171 ++textureCoordinateCount; 172 break; 173 default: 174 break; 175 } 176 } 177 178 /** Read an OBJ face definition from the current line. 179 * @return OBJ face definition read from the current line 180 * @throws IllegalStateException if a face definition is not able to be parsed 181 * @throws java.io.UncheckedIOException if an I/O error occurs 182 */ 183 public Face readFace() { 184 final List<VertexAttributes> vertices = new ArrayList<>(); 185 186 while (nextDataLineContent()) { 187 vertices.add(readFaceVertex()); 188 } 189 190 if (vertices.size() < EuclideanUtils.TRIANGLE_VERTEX_COUNT) { 191 throw getTextParser().parseError( 192 "face must contain at least " + EuclideanUtils.TRIANGLE_VERTEX_COUNT + 193 " vertices but found only " + vertices.size()); 194 } 195 196 discardDataLine(); 197 198 return new Face(vertices); 199 } 200 201 /** Read an OBJ face vertex definition from the current parser position. 202 * @return OBJ face vertex definition 203 * @throws IllegalStateException if a vertex definition is not able to be parsed 204 * @throws java.io.UncheckedIOException if an I/O error occurs 205 */ 206 private VertexAttributes readFaceVertex() { 207 final SimpleTextParser parser = getTextParser(); 208 209 discardDataLineWhitespace(); 210 211 final int vertexIndex = readNormalizedVertexAttributeIndex("vertex", vertexCount); 212 213 int textureIndex = -1; 214 if (parser.peekChar() == ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) { 215 parser.discard(1); 216 217 if (parser.peekChar() != ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) { 218 textureIndex = readNormalizedVertexAttributeIndex("texture", textureCoordinateCount); 219 } 220 } 221 222 int normalIndex = -1; 223 if (parser.peekChar() == ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) { 224 parser.discard(1); 225 226 if (SimpleTextParser.isIntegerPart(parser.peekChar())) { 227 normalIndex = readNormalizedVertexAttributeIndex("normal", vertexNormalCount); 228 } 229 } 230 231 return new VertexAttributes(vertexIndex, textureIndex, normalIndex); 232 } 233 234 /** Read a vertex attribute index from the current parser position and normalize it to 235 * be 0-based and positive. 236 * @param type type of attribute being read; this value is used in error messages 237 * @param available number of available values of the given type parsed from the content 238 * so far 239 * @return 0-based positive attribute index 240 * @throws IllegalStateException if the integer index cannot be parsed or the index is 241 * out of range for the number of parsed elements of the given type 242 * @throws java.io.UncheckedIOException if an I/O error occurs 243 */ 244 private int readNormalizedVertexAttributeIndex(final String type, final int available) { 245 final SimpleTextParser parser = getTextParser(); 246 247 final int objIndex = parser 248 .nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR, SimpleTextParser::isIntegerPart) 249 .getCurrentTokenAsInt(); 250 251 final int normalizedIndex = objIndex < 0 ? 252 available + objIndex : 253 objIndex - 1; 254 255 if (normalizedIndex < 0 || normalizedIndex >= available) { 256 final StringBuilder err = new StringBuilder(); 257 err.append(type) 258 .append(" index "); 259 260 if (available < 1) { 261 err.append("cannot be used because no values of that type have been defined"); 262 } else { 263 err.append("must evaluate to be within the range [1, ") 264 .append(available) 265 .append("] but was ") 266 .append(objIndex); 267 } 268 269 throw parser.tokenError(err.toString()); 270 } 271 272 return normalizedIndex; 273 } 274 275 /** Class representing an OBJ face definition. Faces are defined with the format 276 * <p> 277 * <code> 278 * f v<sub>1</sub>/vt<sub>1</sub>/vn<sub>1</sub> v<sub>2</sub>/vt<sub>2</sub>/vn<sub>2</sub> v<sub>3</sub>/vt<sub>3</sub>/vn<sub>3</sub> ... 279 * </code> 280 * </p> 281 * <p>where the {@code v} elements are indices into the model vertices, the {@code vt} 282 * elements are indices into the model texture coordinates, and the {@code vn} elements 283 * are indices into the model normal coordinates. Only the vertex indices are required.</p> 284 * 285 * <p>All vertex attribute indices are normalized to be 0-based and positive and all 286 * faces are assumed to define geometrically valid convex polygons.</p> 287 */ 288 public static final class Face { 289 290 /** List of vertex attributes for the face. */ 291 private final List<VertexAttributes> vertexAttributes; 292 293 /** Construct a new instance with the given vertex attributes. 294 * @param vertexAttributes face vertex attributes 295 */ 296 Face(final List<VertexAttributes> vertexAttributes) { 297 this.vertexAttributes = Collections.unmodifiableList(vertexAttributes); 298 } 299 300 /** Get the list of vertex attributes for the instance. 301 * @return list of vertex attribute 302 */ 303 public List<VertexAttributes> getVertexAttributes() { 304 return vertexAttributes; 305 } 306 307 /** Get a composite normal for the face by computing the sum of all defined vertex 308 * normals and normalizing the result. Null is returned if no vertex normals are 309 * defined or the defined normals sum to zero. 310 * @param modelNormalFn function used to access normals parsed earlier in the model; 311 * callers are responsible for storing these values as they are parsed 312 * @return composite face normal or null if no composite normal can be determined from the 313 * normals defined for the face 314 */ 315 public Vector3D getDefinedCompositeNormal(final IntFunction<Vector3D> modelNormalFn) { 316 Vector3D sum = Vector3D.ZERO; 317 318 int normalIdx; 319 for (final VertexAttributes vertex : vertexAttributes) { 320 normalIdx = vertex.getNormalIndex(); 321 if (normalIdx > -1) { 322 sum = sum.add(modelNormalFn.apply(normalIdx)); 323 } 324 } 325 326 return sum.normalizeOrNull(); 327 } 328 329 /** Compute a normal for the face using its first three vertices. The vertices will wind in a 330 * counter-clockwise direction when viewed looking down the returned normal. Null is returned 331 * if the normal could not be determined, which would be the case if the vertices lie in the 332 * same line or two or more are equal. 333 * @param modelVertexFn function used to access model vertices parsed earlier in the content; 334 * callers are responsible for storing these values as they are passed 335 * @return a face normal computed from the first 3 vertices or null if a normal cannot 336 * be determined 337 */ 338 public Vector3D computeNormalFromVertices(final IntFunction<Vector3D> modelVertexFn) { 339 final Vector3D p0 = modelVertexFn.apply(vertexAttributes.get(0).getVertexIndex()); 340 final Vector3D p1 = modelVertexFn.apply(vertexAttributes.get(1).getVertexIndex()); 341 final Vector3D p2 = modelVertexFn.apply(vertexAttributes.get(2).getVertexIndex()); 342 343 return p0.vectorTo(p1).cross(p0.vectorTo(p2)).normalizeOrNull(); 344 } 345 346 /** Get the vertex attributes for the face listed in the order that produces a counter-clockwise 347 * winding of vertices when viewed looking down the given normal direction. If {@code normal} 348 * is null, the original vertex sequence is used. 349 * @param normal requested face normal; may be null 350 * @param modelVertexFn function used to access model vertices parsed earlier in the content; 351 * callers are responsible for storing these values as they are passed 352 * @return list of vertex attributes for the face, oriented to correspond with the given 353 * face normal 354 */ 355 public List<VertexAttributes> getVertexAttributesCounterClockwise(final Vector3D normal, 356 final IntFunction<Vector3D> modelVertexFn) { 357 List<VertexAttributes> result = vertexAttributes; 358 359 if (normal != null) { 360 final Vector3D computedNormal = computeNormalFromVertices(modelVertexFn); 361 if (computedNormal != null && normal.dot(computedNormal) < 0) { 362 // face is oriented the opposite way; reverse the order of the vertices 363 result = new ArrayList<>(vertexAttributes); 364 Collections.reverse(result); 365 } 366 } 367 368 return result; 369 } 370 371 /** Get the face vertices in the order defined in the face definition. 372 * @param modelVertexFn function used to access model vertices parsed earlier in the content; 373 * callers are responsible for storing these values as they are passed 374 * @return face vertices in their defined ordering 375 */ 376 public List<Vector3D> getVertices(final IntFunction<Vector3D> modelVertexFn) { 377 return vertexAttributes.stream() 378 .map(v -> modelVertexFn.apply(v.getVertexIndex())) 379 .collect(Collectors.toList()); 380 } 381 382 /** Get the face vertices in the order that produces a counter-clockwise winding when viewed 383 * looking down the given normal. 384 * @param normal requested face normal 385 * @param modelVertexFn function used to access model vertices parsed earlier in the content; 386 * callers are responsible for storing these values as they are passed 387 * @return face vertices in the order that produces a counter-clockwise winding when viewed 388 * looking down the given normal 389 * @see #getVertexAttributesCounterClockwise(Vector3D, IntFunction) 390 */ 391 public List<Vector3D> getVerticesCounterClockwise(final Vector3D normal, 392 final IntFunction<Vector3D> modelVertexFn) { 393 return getVertexAttributesCounterClockwise(normal, modelVertexFn).stream() 394 .map(v -> modelVertexFn.apply(v.getVertexIndex())) 395 .collect(Collectors.toList()); 396 } 397 398 /** Get the vertex indices for the face. 399 * @return vertex indices for the face 400 */ 401 public int[] getVertexIndices() { 402 return getIndices(VertexAttributes::getVertexIndex); 403 } 404 405 /** Get the texture indices for the face. The value {@code -1} is used if a texture index 406 * is not set. 407 * @return texture indices 408 */ 409 public int[] getTextureIndices() { 410 return getIndices(VertexAttributes::getTextureIndex); 411 } 412 413 /** Get the normal indices for the face. The value {@code -1} is used if a texture index 414 * is not set. 415 * @return normal indices 416 */ 417 public int[] getNormalIndices() { 418 return getIndices(VertexAttributes::getNormalIndex); 419 } 420 421 /** Get indices for the face, using the given function to extract the value from 422 * the vertex attributes. 423 * @param fn function used to extract the required value from each vertex attribute 424 * @return extracted indices 425 */ 426 private int[] getIndices(final ToIntFunction<VertexAttributes> fn) { 427 final int len = vertexAttributes.size(); 428 final int[] indices = new int[len]; 429 430 for (int i = 0; i < len; ++i) { 431 indices[i] = fn.applyAsInt(vertexAttributes.get(i)); 432 } 433 434 return indices; 435 } 436 } 437 438 /** Class representing a set of attributes for a face vertex. All index values are 0-based 439 * and positive, in contrast with OBJ indices which are 1-based and support negative 440 * values. If an index value is not given in the OBJ content, it is set to {@code -1}. 441 */ 442 public static final class VertexAttributes { 443 444 /** Vertex index. */ 445 private final int vertexIndex; 446 447 /** Texture coordinate index. */ 448 private final int textureIndex; 449 450 /** Vertex normal index. */ 451 private final int normalIndex; 452 453 /** Construct a new instance with the given vertices. 454 * @param vertexIndex vertex index 455 * @param textureIndex texture index 456 * @param normalIndex vertex normal index 457 */ 458 VertexAttributes(final int vertexIndex, final int textureIndex, final int normalIndex) { 459 this.vertexIndex = vertexIndex; 460 this.textureIndex = textureIndex; 461 this.normalIndex = normalIndex; 462 } 463 464 /** Get the vertex position index for this instance. This value is required and is guaranteed to 465 * be a valid index into the list of vertex positions parsed so far in the OBJ content. 466 * @return vertex index 467 */ 468 public int getVertexIndex() { 469 return vertexIndex; 470 } 471 472 /** Get the texture index for this instance or {@code -1} if not specified in the 473 * OBJ content. 474 * @return texture index or {@code -1} if not specified in the OBJ content. 475 */ 476 public int getTextureIndex() { 477 return textureIndex; 478 } 479 480 /** Get the normal index for this instance or {@code -1} if not specified in the 481 * OBJ content. 482 * @return normal index or {@code -1} if not specified in the OBJ content. 483 */ 484 public int getNormalIndex() { 485 return normalIndex; 486 } 487 } 488 }