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 }