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.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 }