PolygonObjParser.java

  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. import java.io.Reader;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collections;
  22. import java.util.HashSet;
  23. import java.util.List;
  24. import java.util.Set;
  25. import java.util.function.IntFunction;
  26. import java.util.function.ToIntFunction;
  27. import java.util.stream.Collectors;

  28. import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
  29. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  30. import org.apache.commons.geometry.io.core.internal.SimpleTextParser;

  31. /** Low-level parser class for reading 3D polygon (face) data in the OBJ file format.
  32.  * This class provides access to OBJ data structures but does not retain any of the
  33.  * parsed data. For example, it is up to callers to store vertices as they are parsed
  34.  * for later reference. This allows callers to determine what values are stored and in
  35.  * what format.
  36.  */
  37. public class PolygonObjParser extends AbstractObjParser {

  38.     /** Set containing OBJ keywords commonly used with files containing only polygon content. */
  39.     private static final Set<String> STANDARD_POLYGON_KEYWORDS =
  40.             Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
  41.                         ObjConstants.VERTEX_KEYWORD,
  42.                         ObjConstants.VERTEX_NORMAL_KEYWORD,
  43.                         ObjConstants.TEXTURE_COORDINATE_KEYWORD,
  44.                         ObjConstants.FACE_KEYWORD,

  45.                         ObjConstants.OBJECT_KEYWORD,
  46.                         ObjConstants.GROUP_KEYWORD,
  47.                         ObjConstants.SMOOTHING_GROUP_KEYWORD,

  48.                         ObjConstants.MATERIAL_LIBRARY_KEYWORD,
  49.                         ObjConstants.USE_MATERIAL_KEYWORD
  50.                     )));

  51.     /** Number of vertex keywords encountered in the file so far. */
  52.     private int vertexCount;

  53.     /** Number of vertex normal keywords encountered in the file so far. */
  54.     private int vertexNormalCount;

  55.     /** Number of texture coordinate keywords encountered in the file so far. */
  56.     private int textureCoordinateCount;

  57.     /** If true, parsing will fail when non-polygon keywords are encountered in the OBJ content. */
  58.     private boolean failOnNonPolygonKeywords;

  59.     /** Construct a new instance for parsing OBJ content from the given reader.
  60.      * @param reader reader to parser content from
  61.      */
  62.     public PolygonObjParser(final Reader reader) {
  63.         this(new SimpleTextParser(reader));
  64.     }

  65.     /** Construct a new instance for parsing OBJ content from the given text parser.
  66.      * @param parser text parser to read content from
  67.      */
  68.     public PolygonObjParser(final SimpleTextParser parser) {
  69.         super(parser);
  70.     }

  71.     /** Get the number of {@link ObjConstants#VERTEX_KEYWORD vertex keywords} parsed
  72.      * so far.
  73.      * @return the number of vertex keywords parsed so far
  74.      */
  75.     public int getVertexCount() {
  76.         return vertexCount;
  77.     }

  78.     /** Get the number of {@link ObjConstants#VERTEX_NORMAL_KEYWORD vertex normal keywords} parsed
  79.      * so far.
  80.      * @return the number of vertex normal keywords parsed so far
  81.      */
  82.     public int getVertexNormalCount() {
  83.         return vertexNormalCount;
  84.     }

  85.     /** Get the number of {@link ObjConstants#TEXTURE_COORDINATE_KEYWORD texture coordinate keywords} parsed
  86.      * so far.
  87.      * @return the number of texture coordinate keywords parsed so far
  88.      */
  89.     public int getTextureCoordinateCount() {
  90.         return textureCoordinateCount;
  91.     }

  92.     /** Return true if the instance is configured to throw an {@link IllegalStateException} when OBJ keywords
  93.      * not commonly used with files containing only polygon data are encountered. The default value is {@code false},
  94.      * meaning that no keyword validation is performed. When set to true, only the following keywords are
  95.      * accepted:
  96.      * <ul>
  97.      *  <li>{@code v}</li>
  98.      *  <li>{@code vn}</li>
  99.      *  <li>{@code vt}</li>
  100.      *  <li>{@code f}</li>
  101.      *  <li>{@code o}</li>
  102.      *  <li>{@code g}</li>
  103.      *  <li>{@code s}</li>
  104.      *  <li>{@code mtllib}</li>
  105.      *  <li>{@code usemtl}</li>
  106.      * </ul>
  107.      * @return true if the instance is configured to fail when a non-polygon keyword is encountered
  108.      */
  109.     public boolean isFailOnNonPolygonKeywords() {
  110.         return failOnNonPolygonKeywords;
  111.     }

  112.     /** Set the flag determining if the instance should throw an {@link IllegalStateException} when encountering
  113.      * keywords not commonly used with OBJ files containing only polygon data. If true, only the following keywords
  114.      * are accepted:
  115.      * <ul>
  116.      *  <li>{@code v}</li>
  117.      *  <li>{@code vn}</li>
  118.      *  <li>{@code vt}</li>
  119.      *  <li>{@code f}</li>
  120.      *  <li>{@code o}</li>
  121.      *  <li>{@code g}</li>
  122.      *  <li>{@code s}</li>
  123.      *  <li>{@code mtllib}</li>
  124.      *  <li>{@code usemtl}</li>
  125.      * </ul>
  126.      * If false, all keywords are accepted.
  127.      * @param failOnNonPolygonKeywords new flag value
  128.      */
  129.     public void setFailOnNonPolygonKeywords(final boolean failOnNonPolygonKeywords) {
  130.         this.failOnNonPolygonKeywords = failOnNonPolygonKeywords;
  131.     }

  132.     /** {@inheritDoc} */
  133.     @Override
  134.     protected void handleKeyword(final String keywordValue) {
  135.         if (failOnNonPolygonKeywords && !STANDARD_POLYGON_KEYWORDS.contains(keywordValue)) {
  136.             final String allowedKeywords = STANDARD_POLYGON_KEYWORDS.stream()
  137.                     .sorted()
  138.                     .collect(Collectors.joining(", "));

  139.             throw getTextParser().tokenError("expected keyword to be one of [" + allowedKeywords +
  140.                     "] but was [" + keywordValue + "]");
  141.         }

  142.         // update counts in order to validate face vertex attributes
  143.         switch (keywordValue) {
  144.         case ObjConstants.VERTEX_KEYWORD:
  145.             ++vertexCount;
  146.             break;
  147.         case ObjConstants.VERTEX_NORMAL_KEYWORD:
  148.             ++vertexNormalCount;
  149.             break;
  150.         case ObjConstants.TEXTURE_COORDINATE_KEYWORD:
  151.             ++textureCoordinateCount;
  152.             break;
  153.         default:
  154.             break;
  155.         }
  156.     }

  157.     /** Read an OBJ face definition from the current line.
  158.      * @return OBJ face definition read from the current line
  159.      * @throws IllegalStateException if a face definition is not able to be parsed
  160.      * @throws java.io.UncheckedIOException if an I/O error occurs
  161.      */
  162.     public Face readFace() {
  163.         final List<VertexAttributes> vertices = new ArrayList<>();

  164.         while (nextDataLineContent()) {
  165.             vertices.add(readFaceVertex());
  166.         }

  167.         if (vertices.size() < EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
  168.             throw getTextParser().parseError(
  169.                     "face must contain at least " + EuclideanUtils.TRIANGLE_VERTEX_COUNT +
  170.                     " vertices but found only " + vertices.size());
  171.         }

  172.         discardDataLine();

  173.         return new Face(vertices);
  174.     }

  175.     /** Read an OBJ face vertex definition from the current parser position.
  176.      * @return OBJ face vertex definition
  177.      * @throws IllegalStateException if a vertex definition is not able to be parsed
  178.      * @throws java.io.UncheckedIOException if an I/O error occurs
  179.      */
  180.     private VertexAttributes readFaceVertex() {
  181.         final SimpleTextParser parser = getTextParser();

  182.         discardDataLineWhitespace();

  183.         final int vertexIndex = readNormalizedVertexAttributeIndex("vertex", vertexCount);

  184.         int textureIndex = -1;
  185.         if (parser.peekChar() == ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
  186.             parser.discard(1);

  187.             if (parser.peekChar() != ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
  188.                 textureIndex = readNormalizedVertexAttributeIndex("texture", textureCoordinateCount);
  189.             }
  190.         }

  191.         int normalIndex = -1;
  192.         if (parser.peekChar() == ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
  193.             parser.discard(1);

  194.             if (SimpleTextParser.isIntegerPart(parser.peekChar())) {
  195.                 normalIndex = readNormalizedVertexAttributeIndex("normal", vertexNormalCount);
  196.             }
  197.         }

  198.         return new VertexAttributes(vertexIndex, textureIndex, normalIndex);
  199.     }

  200.     /** Read a vertex attribute index from the current parser position and normalize it to
  201.      * be 0-based and positive.
  202.      * @param type type of attribute being read; this value is used in error messages
  203.      * @param available number of available values of the given type parsed from the content
  204.      *      so far
  205.      * @return 0-based positive attribute index
  206.      * @throws IllegalStateException if the integer index cannot be parsed or the index is
  207.      *      out of range for the number of parsed elements of the given type
  208.      * @throws java.io.UncheckedIOException if an I/O error occurs
  209.      */
  210.     private int readNormalizedVertexAttributeIndex(final String type, final int available) {
  211.         final SimpleTextParser parser = getTextParser();

  212.         final int objIndex = parser
  213.                 .nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR, SimpleTextParser::isIntegerPart)
  214.                 .getCurrentTokenAsInt();

  215.         final int normalizedIndex = objIndex < 0 ?
  216.                 available + objIndex :
  217.                 objIndex - 1;

  218.         if (normalizedIndex < 0 || normalizedIndex >= available) {
  219.             final StringBuilder err = new StringBuilder();
  220.             err.append(type)
  221.                 .append(" index ");

  222.             if (available < 1) {
  223.                 err.append("cannot be used because no values of that type have been defined");
  224.             } else {
  225.                 err.append("must evaluate to be within the range [1, ")
  226.                     .append(available)
  227.                     .append("] but was ")
  228.                     .append(objIndex);
  229.             }

  230.             throw parser.tokenError(err.toString());
  231.         }

  232.         return normalizedIndex;
  233.     }

  234.     /** Class representing an OBJ face definition. Faces are defined with the format
  235.      * <p>
  236.      *  <code>
  237.      *      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> ...
  238.      *  </code>
  239.      * </p>
  240.      * <p>where the {@code v} elements are indices into the model vertices, the {@code vt}
  241.      * elements are indices into the model texture coordinates, and the {@code vn} elements
  242.      * are indices into the model normal coordinates. Only the vertex indices are required.</p>
  243.      *
  244.      * <p>All vertex attribute indices are normalized to be 0-based and positive and all
  245.      * faces are assumed to define geometrically valid convex polygons.</p>
  246.      */
  247.     public static final class Face {

  248.         /** List of vertex attributes for the face. */
  249.         private final List<VertexAttributes> vertexAttributes;

  250.         /** Construct a new instance with the given vertex attributes.
  251.          * @param vertexAttributes face vertex attributes
  252.          */
  253.         Face(final List<VertexAttributes> vertexAttributes) {
  254.             this.vertexAttributes = Collections.unmodifiableList(vertexAttributes);
  255.         }

  256.         /** Get the list of vertex attributes for the instance.
  257.          * @return list of vertex attribute
  258.          */
  259.         public List<VertexAttributes> getVertexAttributes() {
  260.             return vertexAttributes;
  261.         }

  262.         /** Get a composite normal for the face by computing the sum of all defined vertex
  263.          * normals and normalizing the result. Null is returned if no vertex normals are
  264.          * defined or the defined normals sum to zero.
  265.          * @param modelNormalFn function used to access normals parsed earlier in the model;
  266.          *      callers are responsible for storing these values as they are parsed
  267.          * @return composite face normal or null if no composite normal can be determined from the
  268.          *      normals defined for the face
  269.          */
  270.         public Vector3D getDefinedCompositeNormal(final IntFunction<Vector3D> modelNormalFn) {
  271.             Vector3D sum = Vector3D.ZERO;

  272.             int normalIdx;
  273.             for (final VertexAttributes vertex : vertexAttributes) {
  274.                 normalIdx = vertex.getNormalIndex();
  275.                 if (normalIdx > -1) {
  276.                     sum = sum.add(modelNormalFn.apply(normalIdx));
  277.                 }
  278.             }

  279.             return sum.normalizeOrNull();
  280.         }

  281.         /** Compute a normal for the face using its first three vertices. The vertices will wind in a
  282.          * counter-clockwise direction when viewed looking down the returned normal. Null is returned
  283.          * if the normal could not be determined, which would be the case if the vertices lie in the
  284.          * same line or two or more are equal.
  285.          * @param modelVertexFn function used to access model vertices parsed earlier in the content;
  286.          *      callers are responsible for storing these values as they are passed
  287.          * @return a face normal computed from the first 3 vertices or null if a normal cannot
  288.          *      be determined
  289.          */
  290.         public Vector3D computeNormalFromVertices(final IntFunction<Vector3D> modelVertexFn) {
  291.             final Vector3D p0 = modelVertexFn.apply(vertexAttributes.get(0).getVertexIndex());
  292.             final Vector3D p1 = modelVertexFn.apply(vertexAttributes.get(1).getVertexIndex());
  293.             final Vector3D p2 = modelVertexFn.apply(vertexAttributes.get(2).getVertexIndex());

  294.             return p0.vectorTo(p1).cross(p0.vectorTo(p2)).normalizeOrNull();
  295.         }

  296.         /** Get the vertex attributes for the face listed in the order that produces a counter-clockwise
  297.          * winding of vertices when viewed looking down the given normal direction. If {@code normal}
  298.          * is null, the original vertex sequence is used.
  299.          * @param normal requested face normal; may be null
  300.          * @param modelVertexFn function used to access model vertices parsed earlier in the content;
  301.          *      callers are responsible for storing these values as they are passed
  302.          * @return list of vertex attributes for the face, oriented to correspond with the given
  303.          *      face normal
  304.          */
  305.         public List<VertexAttributes> getVertexAttributesCounterClockwise(final Vector3D normal,
  306.                 final IntFunction<Vector3D> modelVertexFn) {
  307.             List<VertexAttributes> result = vertexAttributes;

  308.             if (normal != null) {
  309.                 final Vector3D computedNormal = computeNormalFromVertices(modelVertexFn);
  310.                 if (computedNormal != null && normal.dot(computedNormal) < 0) {
  311.                     // face is oriented the opposite way; reverse the order of the vertices
  312.                     result = new ArrayList<>(vertexAttributes);
  313.                     Collections.reverse(result);
  314.                 }
  315.             }

  316.             return result;
  317.         }

  318.         /** Get the face vertices in the order defined in the face definition.
  319.          * @param modelVertexFn function used to access model vertices parsed earlier in the content;
  320.          *      callers are responsible for storing these values as they are passed
  321.          * @return face vertices in their defined ordering
  322.          */
  323.         public List<Vector3D> getVertices(final IntFunction<Vector3D> modelVertexFn) {
  324.             return vertexAttributes.stream()
  325.                     .map(v -> modelVertexFn.apply(v.getVertexIndex()))
  326.                     .collect(Collectors.toList());
  327.         }

  328.         /** Get the face vertices in the order that produces a counter-clockwise winding when viewed
  329.          * looking down the given normal.
  330.          * @param normal requested face normal
  331.          * @param modelVertexFn function used to access model vertices parsed earlier in the content;
  332.          *      callers are responsible for storing these values as they are passed
  333.          * @return face vertices in the order that produces a counter-clockwise winding when viewed
  334.          *      looking down the given normal
  335.          * @see #getVertexAttributesCounterClockwise(Vector3D, IntFunction)
  336.          */
  337.         public List<Vector3D> getVerticesCounterClockwise(final Vector3D normal,
  338.                 final IntFunction<Vector3D> modelVertexFn) {
  339.             return getVertexAttributesCounterClockwise(normal, modelVertexFn).stream()
  340.                     .map(v -> modelVertexFn.apply(v.getVertexIndex()))
  341.                     .collect(Collectors.toList());
  342.         }

  343.         /** Get the vertex indices for the face.
  344.          * @return vertex indices for the face
  345.          */
  346.         public int[] getVertexIndices() {
  347.             return getIndices(VertexAttributes::getVertexIndex);
  348.         }

  349.         /** Get the texture indices for the face. The value {@code -1} is used if a texture index
  350.          * is not set.
  351.          * @return texture indices
  352.          */
  353.         public int[] getTextureIndices() {
  354.             return getIndices(VertexAttributes::getTextureIndex);
  355.         }

  356.         /** Get the normal indices for the face. The value {@code -1} is used if a texture index
  357.          * is not set.
  358.          * @return normal indices
  359.          */
  360.         public int[] getNormalIndices() {
  361.             return getIndices(VertexAttributes::getNormalIndex);
  362.         }

  363.         /** Get indices for the face, using the given function to extract the value from
  364.          * the vertex attributes.
  365.          * @param fn function used to extract the required value from each vertex attribute
  366.          * @return extracted indices
  367.          */
  368.         private int[] getIndices(final ToIntFunction<VertexAttributes> fn) {
  369.             final int len = vertexAttributes.size();
  370.             final int[] indices = new int[len];

  371.             for (int i = 0; i < len; ++i) {
  372.                 indices[i] = fn.applyAsInt(vertexAttributes.get(i));
  373.             }

  374.             return indices;
  375.         }
  376.     }

  377.     /** Class representing a set of attributes for a face vertex. All index values are 0-based
  378.      * and positive, in contrast with OBJ indices which are 1-based and support negative
  379.      * values. If an index value is not given in the OBJ content, it is set to {@code -1}.
  380.      */
  381.     public static final class VertexAttributes {

  382.         /** Vertex index. */
  383.         private final int vertexIndex;

  384.         /** Texture coordinate index. */
  385.         private final int textureIndex;

  386.         /** Vertex normal index. */
  387.         private final int normalIndex;

  388.         /** Construct a new instance with the given vertices.
  389.          * @param vertexIndex vertex index
  390.          * @param textureIndex texture index
  391.          * @param normalIndex vertex normal index
  392.          */
  393.         VertexAttributes(final int vertexIndex, final int textureIndex, final int normalIndex) {
  394.             this.vertexIndex = vertexIndex;
  395.             this.textureIndex = textureIndex;
  396.             this.normalIndex = normalIndex;
  397.         }

  398.         /** Get the vertex position index for this instance. This value is required and is guaranteed to
  399.          * be a valid index into the list of vertex positions parsed so far in the OBJ content.
  400.          * @return vertex index
  401.          */
  402.         public int getVertexIndex() {
  403.             return vertexIndex;
  404.         }

  405.         /** Get the texture index for this instance or {@code -1} if not specified in the
  406.          * OBJ content.
  407.          * @return texture index or {@code -1} if not specified in the OBJ content.
  408.          */
  409.         public int getTextureIndex() {
  410.             return textureIndex;
  411.         }

  412.         /** Get the normal index for this instance or {@code -1} if not specified in the
  413.          * OBJ content.
  414.          * @return normal index or {@code -1} if not specified in the OBJ content.
  415.          */
  416.         public int getNormalIndex() {
  417.             return normalIndex;
  418.         }
  419.     }
  420. }