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 }