PolygonObjParser.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.io.euclidean.threed.obj;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.IntFunction;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
/** Low-level parser class for reading 3D polygon (face) data in the OBJ file format.
* This class provides access to OBJ data structures but does not retain any of the
* parsed data. For example, it is up to callers to store vertices as they are parsed
* for later reference. This allows callers to determine what values are stored and in
* what format.
*/
public class PolygonObjParser extends AbstractObjParser {
/** Set containing OBJ keywords commonly used with files containing only polygon content. */
private static final Set<String> STANDARD_POLYGON_KEYWORDS =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
ObjConstants.VERTEX_KEYWORD,
ObjConstants.VERTEX_NORMAL_KEYWORD,
ObjConstants.TEXTURE_COORDINATE_KEYWORD,
ObjConstants.FACE_KEYWORD,
ObjConstants.OBJECT_KEYWORD,
ObjConstants.GROUP_KEYWORD,
ObjConstants.SMOOTHING_GROUP_KEYWORD,
ObjConstants.MATERIAL_LIBRARY_KEYWORD,
ObjConstants.USE_MATERIAL_KEYWORD
)));
/** Number of vertex keywords encountered in the file so far. */
private int vertexCount;
/** Number of vertex normal keywords encountered in the file so far. */
private int vertexNormalCount;
/** Number of texture coordinate keywords encountered in the file so far. */
private int textureCoordinateCount;
/** If true, parsing will fail when non-polygon keywords are encountered in the OBJ content. */
private boolean failOnNonPolygonKeywords;
/** Construct a new instance for parsing OBJ content from the given reader.
* @param reader reader to parser content from
*/
public PolygonObjParser(final Reader reader) {
this(new SimpleTextParser(reader));
}
/** Construct a new instance for parsing OBJ content from the given text parser.
* @param parser text parser to read content from
*/
public PolygonObjParser(final SimpleTextParser parser) {
super(parser);
}
/** Get the number of {@link ObjConstants#VERTEX_KEYWORD vertex keywords} parsed
* so far.
* @return the number of vertex keywords parsed so far
*/
public int getVertexCount() {
return vertexCount;
}
/** Get the number of {@link ObjConstants#VERTEX_NORMAL_KEYWORD vertex normal keywords} parsed
* so far.
* @return the number of vertex normal keywords parsed so far
*/
public int getVertexNormalCount() {
return vertexNormalCount;
}
/** Get the number of {@link ObjConstants#TEXTURE_COORDINATE_KEYWORD texture coordinate keywords} parsed
* so far.
* @return the number of texture coordinate keywords parsed so far
*/
public int getTextureCoordinateCount() {
return textureCoordinateCount;
}
/** Return true if the instance is configured to throw an {@link IllegalStateException} when OBJ keywords
* not commonly used with files containing only polygon data are encountered. The default value is {@code false},
* meaning that no keyword validation is performed. When set to true, only the following keywords are
* accepted:
* <ul>
* <li>{@code v}</li>
* <li>{@code vn}</li>
* <li>{@code vt}</li>
* <li>{@code f}</li>
* <li>{@code o}</li>
* <li>{@code g}</li>
* <li>{@code s}</li>
* <li>{@code mtllib}</li>
* <li>{@code usemtl}</li>
* </ul>
* @return true if the instance is configured to fail when a non-polygon keyword is encountered
*/
public boolean isFailOnNonPolygonKeywords() {
return failOnNonPolygonKeywords;
}
/** Set the flag determining if the instance should throw an {@link IllegalStateException} when encountering
* keywords not commonly used with OBJ files containing only polygon data. If true, only the following keywords
* are accepted:
* <ul>
* <li>{@code v}</li>
* <li>{@code vn}</li>
* <li>{@code vt}</li>
* <li>{@code f}</li>
* <li>{@code o}</li>
* <li>{@code g}</li>
* <li>{@code s}</li>
* <li>{@code mtllib}</li>
* <li>{@code usemtl}</li>
* </ul>
* If false, all keywords are accepted.
* @param failOnNonPolygonKeywords new flag value
*/
public void setFailOnNonPolygonKeywords(final boolean failOnNonPolygonKeywords) {
this.failOnNonPolygonKeywords = failOnNonPolygonKeywords;
}
/** {@inheritDoc} */
@Override
protected void handleKeyword(final String keywordValue) {
if (failOnNonPolygonKeywords && !STANDARD_POLYGON_KEYWORDS.contains(keywordValue)) {
final String allowedKeywords = STANDARD_POLYGON_KEYWORDS.stream()
.sorted()
.collect(Collectors.joining(", "));
throw getTextParser().tokenError("expected keyword to be one of [" + allowedKeywords +
"] but was [" + keywordValue + "]");
}
// update counts in order to validate face vertex attributes
switch (keywordValue) {
case ObjConstants.VERTEX_KEYWORD:
++vertexCount;
break;
case ObjConstants.VERTEX_NORMAL_KEYWORD:
++vertexNormalCount;
break;
case ObjConstants.TEXTURE_COORDINATE_KEYWORD:
++textureCoordinateCount;
break;
default:
break;
}
}
/** Read an OBJ face definition from the current line.
* @return OBJ face definition read from the current line
* @throws IllegalStateException if a face definition is not able to be parsed
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public Face readFace() {
final List<VertexAttributes> vertices = new ArrayList<>();
while (nextDataLineContent()) {
vertices.add(readFaceVertex());
}
if (vertices.size() < EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
throw getTextParser().parseError(
"face must contain at least " + EuclideanUtils.TRIANGLE_VERTEX_COUNT +
" vertices but found only " + vertices.size());
}
discardDataLine();
return new Face(vertices);
}
/** Read an OBJ face vertex definition from the current parser position.
* @return OBJ face vertex definition
* @throws IllegalStateException if a vertex definition is not able to be parsed
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
private VertexAttributes readFaceVertex() {
final SimpleTextParser parser = getTextParser();
discardDataLineWhitespace();
final int vertexIndex = readNormalizedVertexAttributeIndex("vertex", vertexCount);
int textureIndex = -1;
if (parser.peekChar() == ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
parser.discard(1);
if (parser.peekChar() != ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
textureIndex = readNormalizedVertexAttributeIndex("texture", textureCoordinateCount);
}
}
int normalIndex = -1;
if (parser.peekChar() == ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
parser.discard(1);
if (SimpleTextParser.isIntegerPart(parser.peekChar())) {
normalIndex = readNormalizedVertexAttributeIndex("normal", vertexNormalCount);
}
}
return new VertexAttributes(vertexIndex, textureIndex, normalIndex);
}
/** Read a vertex attribute index from the current parser position and normalize it to
* be 0-based and positive.
* @param type type of attribute being read; this value is used in error messages
* @param available number of available values of the given type parsed from the content
* so far
* @return 0-based positive attribute index
* @throws IllegalStateException if the integer index cannot be parsed or the index is
* out of range for the number of parsed elements of the given type
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
private int readNormalizedVertexAttributeIndex(final String type, final int available) {
final SimpleTextParser parser = getTextParser();
final int objIndex = parser
.nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR, SimpleTextParser::isIntegerPart)
.getCurrentTokenAsInt();
final int normalizedIndex = objIndex < 0 ?
available + objIndex :
objIndex - 1;
if (normalizedIndex < 0 || normalizedIndex >= available) {
final StringBuilder err = new StringBuilder();
err.append(type)
.append(" index ");
if (available < 1) {
err.append("cannot be used because no values of that type have been defined");
} else {
err.append("must evaluate to be within the range [1, ")
.append(available)
.append("] but was ")
.append(objIndex);
}
throw parser.tokenError(err.toString());
}
return normalizedIndex;
}
/** Class representing an OBJ face definition. Faces are defined with the format
* <p>
* <code>
* 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> ...
* </code>
* </p>
* <p>where the {@code v} elements are indices into the model vertices, the {@code vt}
* elements are indices into the model texture coordinates, and the {@code vn} elements
* are indices into the model normal coordinates. Only the vertex indices are required.</p>
*
* <p>All vertex attribute indices are normalized to be 0-based and positive and all
* faces are assumed to define geometrically valid convex polygons.</p>
*/
public static final class Face {
/** List of vertex attributes for the face. */
private final List<VertexAttributes> vertexAttributes;
/** Construct a new instance with the given vertex attributes.
* @param vertexAttributes face vertex attributes
*/
Face(final List<VertexAttributes> vertexAttributes) {
this.vertexAttributes = Collections.unmodifiableList(vertexAttributes);
}
/** Get the list of vertex attributes for the instance.
* @return list of vertex attribute
*/
public List<VertexAttributes> getVertexAttributes() {
return vertexAttributes;
}
/** Get a composite normal for the face by computing the sum of all defined vertex
* normals and normalizing the result. Null is returned if no vertex normals are
* defined or the defined normals sum to zero.
* @param modelNormalFn function used to access normals parsed earlier in the model;
* callers are responsible for storing these values as they are parsed
* @return composite face normal or null if no composite normal can be determined from the
* normals defined for the face
*/
public Vector3D getDefinedCompositeNormal(final IntFunction<Vector3D> modelNormalFn) {
Vector3D sum = Vector3D.ZERO;
int normalIdx;
for (final VertexAttributes vertex : vertexAttributes) {
normalIdx = vertex.getNormalIndex();
if (normalIdx > -1) {
sum = sum.add(modelNormalFn.apply(normalIdx));
}
}
return sum.normalizeOrNull();
}
/** Compute a normal for the face using its first three vertices. The vertices will wind in a
* counter-clockwise direction when viewed looking down the returned normal. Null is returned
* if the normal could not be determined, which would be the case if the vertices lie in the
* same line or two or more are equal.
* @param modelVertexFn function used to access model vertices parsed earlier in the content;
* callers are responsible for storing these values as they are passed
* @return a face normal computed from the first 3 vertices or null if a normal cannot
* be determined
*/
public Vector3D computeNormalFromVertices(final IntFunction<Vector3D> modelVertexFn) {
final Vector3D p0 = modelVertexFn.apply(vertexAttributes.get(0).getVertexIndex());
final Vector3D p1 = modelVertexFn.apply(vertexAttributes.get(1).getVertexIndex());
final Vector3D p2 = modelVertexFn.apply(vertexAttributes.get(2).getVertexIndex());
return p0.vectorTo(p1).cross(p0.vectorTo(p2)).normalizeOrNull();
}
/** Get the vertex attributes for the face listed in the order that produces a counter-clockwise
* winding of vertices when viewed looking down the given normal direction. If {@code normal}
* is null, the original vertex sequence is used.
* @param normal requested face normal; may be null
* @param modelVertexFn function used to access model vertices parsed earlier in the content;
* callers are responsible for storing these values as they are passed
* @return list of vertex attributes for the face, oriented to correspond with the given
* face normal
*/
public List<VertexAttributes> getVertexAttributesCounterClockwise(final Vector3D normal,
final IntFunction<Vector3D> modelVertexFn) {
List<VertexAttributes> result = vertexAttributes;
if (normal != null) {
final Vector3D computedNormal = computeNormalFromVertices(modelVertexFn);
if (computedNormal != null && normal.dot(computedNormal) < 0) {
// face is oriented the opposite way; reverse the order of the vertices
result = new ArrayList<>(vertexAttributes);
Collections.reverse(result);
}
}
return result;
}
/** Get the face vertices in the order defined in the face definition.
* @param modelVertexFn function used to access model vertices parsed earlier in the content;
* callers are responsible for storing these values as they are passed
* @return face vertices in their defined ordering
*/
public List<Vector3D> getVertices(final IntFunction<Vector3D> modelVertexFn) {
return vertexAttributes.stream()
.map(v -> modelVertexFn.apply(v.getVertexIndex()))
.collect(Collectors.toList());
}
/** Get the face vertices in the order that produces a counter-clockwise winding when viewed
* looking down the given normal.
* @param normal requested face normal
* @param modelVertexFn function used to access model vertices parsed earlier in the content;
* callers are responsible for storing these values as they are passed
* @return face vertices in the order that produces a counter-clockwise winding when viewed
* looking down the given normal
* @see #getVertexAttributesCounterClockwise(Vector3D, IntFunction)
*/
public List<Vector3D> getVerticesCounterClockwise(final Vector3D normal,
final IntFunction<Vector3D> modelVertexFn) {
return getVertexAttributesCounterClockwise(normal, modelVertexFn).stream()
.map(v -> modelVertexFn.apply(v.getVertexIndex()))
.collect(Collectors.toList());
}
/** Get the vertex indices for the face.
* @return vertex indices for the face
*/
public int[] getVertexIndices() {
return getIndices(VertexAttributes::getVertexIndex);
}
/** Get the texture indices for the face. The value {@code -1} is used if a texture index
* is not set.
* @return texture indices
*/
public int[] getTextureIndices() {
return getIndices(VertexAttributes::getTextureIndex);
}
/** Get the normal indices for the face. The value {@code -1} is used if a texture index
* is not set.
* @return normal indices
*/
public int[] getNormalIndices() {
return getIndices(VertexAttributes::getNormalIndex);
}
/** Get indices for the face, using the given function to extract the value from
* the vertex attributes.
* @param fn function used to extract the required value from each vertex attribute
* @return extracted indices
*/
private int[] getIndices(final ToIntFunction<VertexAttributes> fn) {
final int len = vertexAttributes.size();
final int[] indices = new int[len];
for (int i = 0; i < len; ++i) {
indices[i] = fn.applyAsInt(vertexAttributes.get(i));
}
return indices;
}
}
/** Class representing a set of attributes for a face vertex. All index values are 0-based
* and positive, in contrast with OBJ indices which are 1-based and support negative
* values. If an index value is not given in the OBJ content, it is set to {@code -1}.
*/
public static final class VertexAttributes {
/** Vertex index. */
private final int vertexIndex;
/** Texture coordinate index. */
private final int textureIndex;
/** Vertex normal index. */
private final int normalIndex;
/** Construct a new instance with the given vertices.
* @param vertexIndex vertex index
* @param textureIndex texture index
* @param normalIndex vertex normal index
*/
VertexAttributes(final int vertexIndex, final int textureIndex, final int normalIndex) {
this.vertexIndex = vertexIndex;
this.textureIndex = textureIndex;
this.normalIndex = normalIndex;
}
/** Get the vertex position index for this instance. This value is required and is guaranteed to
* be a valid index into the list of vertex positions parsed so far in the OBJ content.
* @return vertex index
*/
public int getVertexIndex() {
return vertexIndex;
}
/** Get the texture index for this instance or {@code -1} if not specified in the
* OBJ content.
* @return texture index or {@code -1} if not specified in the OBJ content.
*/
public int getTextureIndex() {
return textureIndex;
}
/** Get the normal index for this instance or {@code -1} if not specified in the
* OBJ content.
* @return normal index or {@code -1} if not specified in the OBJ content.
*/
public int getNormalIndex() {
return normalIndex;
}
}
}