// Copyright 2021 The MediaPipe Authors. // // Licensed 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. import static java.nio.charset.StandardCharsets.UTF_8; import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** * Class for parsing a single .obj file into openGL-usable pieces. * *

Usage: * *

SimpleObjParser objParser = new SimpleObjParser("animations/cow/cow320.obj", .015f); * *

if (objParser.parse()) { ... } */ public class SimpleObjParser { private static class ShortPair { private final Short first; private final Short second; public ShortPair(Short newFirst, Short newSecond) { first = newFirst; second = newSecond; } public Short getFirst() { return first; } public Short getSecond() { return second; } } private static final String TAG = SimpleObjParser.class.getSimpleName(); private static final boolean DEBUG = false; private static final int INVALID_INDEX = -1; private static final int POSITIONS_COORDS_PER_VERTEX = 3; private static final int TEXTURE_COORDS_PER_VERTEX = 2; private final String fileName; // Since .obj doesn't tie together texture coordinates and vertex // coordinates, but OpenGL does, we need to keep a map of all such pairings that occur in // our face list. private final HashMap vertexTexCoordMap; // Internal (de-coupled) unique vertices and texture coordinates private ArrayList vertices; private ArrayList textureCoords; // Data we expose to openGL for rendering private float[] finalizedVertices; private float[] finalizedTextureCoords; private ArrayList finalizedTriangles; // So we only display warnings about dropped w-coordinates once private boolean vertexCoordIgnoredWarning; private boolean textureCoordIgnoredWarning; private boolean startedProcessingFaces; private int numPrimitiveVertices; private int numPrimitiveTextureCoords; private int numPrimitiveFaces; // For scratchwork, so we don't have to keep reallocating private float[] tempCoords; // We scale all our position coordinates uniformly by this factor private float objectUniformScaleFactor; public SimpleObjParser(String objFile, float scaleFactor) { objectUniformScaleFactor = scaleFactor; fileName = objFile; vertices = new ArrayList(); textureCoords = new ArrayList(); vertexTexCoordMap = new HashMap(); finalizedTriangles = new ArrayList(); tempCoords = new float[Math.max(POSITIONS_COORDS_PER_VERTEX, TEXTURE_COORDS_PER_VERTEX)]; numPrimitiveFaces = 0; vertexCoordIgnoredWarning = false; textureCoordIgnoredWarning = false; startedProcessingFaces = false; } // Simple helper wrapper function private void debugLogString(String message) { if (DEBUG) { System.out.println(message); } } private void parseVertex(String[] linePieces) { // Note: Traditionally xyzw is acceptable as a format, with w defaulting to 1.0, but for now // we only parse xyz. if (linePieces.length < POSITIONS_COORDS_PER_VERTEX + 1 || linePieces.length > POSITIONS_COORDS_PER_VERTEX + 2) { System.out.println("Malformed vertex coordinate specification, assuming xyz format only."); return; } else if (linePieces.length == POSITIONS_COORDS_PER_VERTEX + 2 && !vertexCoordIgnoredWarning) { System.out.println( "Only x, y, and z parsed for vertex coordinates; w coordinates will be ignored."); vertexCoordIgnoredWarning = true; } boolean success = true; try { for (int i = 1; i < POSITIONS_COORDS_PER_VERTEX + 1; i++) { tempCoords[i - 1] = Float.parseFloat(linePieces[i]); } } catch (NumberFormatException e) { success = false; System.out.println("Malformed vertex coordinate error: " + e.toString()); } if (success) { for (int i = 0; i < POSITIONS_COORDS_PER_VERTEX; i++) { vertices.add(Float.valueOf(tempCoords[i] * objectUniformScaleFactor)); } } } private void parseTextureCoordinate(String[] linePieces) { // Similar to vertices, uvw is acceptable as a format, with w defaulting to 0.0, but for now we // only parse uv. if (linePieces.length < TEXTURE_COORDS_PER_VERTEX + 1 || linePieces.length > TEXTURE_COORDS_PER_VERTEX + 2) { System.out.println("Malformed texture coordinate specification, assuming uv format only."); return; } else if (linePieces.length == (TEXTURE_COORDS_PER_VERTEX + 2) && !textureCoordIgnoredWarning) { debugLogString("Only u and v parsed for texture coordinates; w coordinates will be ignored."); textureCoordIgnoredWarning = true; } boolean success = true; try { for (int i = 1; i < TEXTURE_COORDS_PER_VERTEX + 1; i++) { tempCoords[i - 1] = Float.parseFloat(linePieces[i]); } } catch (NumberFormatException e) { success = false; System.out.println("Malformed texture coordinate error: " + e.toString()); } if (success) { // .obj files treat (0,0) as top-left, compared to bottom-left for openGL. So invert "v" // texture coordinate only here. textureCoords.add(Float.valueOf(tempCoords[0])); textureCoords.add(Float.valueOf(1.0f - tempCoords[1])); } } // Will return INVALID_INDEX if error occurs, and otherwise will return finalized (combined) // index, adding and hashing new combinations as it sees them. private short parseAndProcessCombinedVertexCoord(String coordString) { String[] coords = coordString.split("/"); try { // Parse vertex and texture indices; 1-indexed from front if positive and from end of list if // negative. short vertexIndex = Short.parseShort(coords[0]); short textureIndex = Short.parseShort(coords[1]); if (vertexIndex > 0) { vertexIndex--; } else { vertexIndex = (short) (vertexIndex + numPrimitiveVertices); } if (textureIndex > 0) { textureIndex--; } else { textureIndex = (short) (textureIndex + numPrimitiveTextureCoords); } // Combine indices and look up in pair map. ShortPair indexPair = new ShortPair(Short.valueOf(vertexIndex), Short.valueOf(textureIndex)); Short combinedIndex = vertexTexCoordMap.get(indexPair); if (combinedIndex == null) { short numIndexPairs = (short) vertexTexCoordMap.size(); vertexTexCoordMap.put(indexPair, numIndexPairs); return numIndexPairs; } else { return combinedIndex.shortValue(); } } catch (NumberFormatException e) { // Failure to parse coordinates as shorts return INVALID_INDEX; } } // Note: it is assumed that face list occurs AFTER vertex and texture coordinate lists finish in // the obj file format. private void parseFace(String[] linePieces) { if (linePieces.length < 4) { System.out.println("Malformed face index list: there must be at least 3 indices per face"); return; } short[] faceIndices = new short[linePieces.length - 1]; boolean success = true; for (int i = 1; i < linePieces.length; i++) { short faceIndex = parseAndProcessCombinedVertexCoord(linePieces[i]); if (faceIndex < 0) { System.out.println(faceIndex); System.out.println("Malformed face index: " + linePieces[i]); success = false; break; } faceIndices[i - 1] = faceIndex; } if (success) { numPrimitiveFaces++; // Manually triangulate the face under the assumption that the points are coplanar, the poly // is convex, and the points are listed in either clockwise or anti-clockwise orientation. for (int i = 1; i < faceIndices.length - 1; i++) { // We use a triangle fan here, so first point is part of all triangles finalizedTriangles.add(faceIndices[0]); finalizedTriangles.add(faceIndices[i]); finalizedTriangles.add(faceIndices[i + 1]); } } } // Iterate over map and reconstruct proper vertex/texture coordinate pairings. private boolean constructFinalCoordinatesFromMap() { final int numIndexPairs = vertexTexCoordMap.size(); // XYZ vertices and UV texture coordinates finalizedVertices = new float[POSITIONS_COORDS_PER_VERTEX * numIndexPairs]; finalizedTextureCoords = new float[TEXTURE_COORDS_PER_VERTEX * numIndexPairs]; try { for (Map.Entry entry : vertexTexCoordMap.entrySet()) { ShortPair indexPair = entry.getKey(); short rawVertexIndex = indexPair.getFirst().shortValue(); short rawTexCoordIndex = indexPair.getSecond().shortValue(); short finalIndex = entry.getValue().shortValue(); for (int i = 0; i < POSITIONS_COORDS_PER_VERTEX; i++) { finalizedVertices[POSITIONS_COORDS_PER_VERTEX * finalIndex + i] = vertices.get(rawVertexIndex * POSITIONS_COORDS_PER_VERTEX + i); } for (int i = 0; i < TEXTURE_COORDS_PER_VERTEX; i++) { finalizedTextureCoords[TEXTURE_COORDS_PER_VERTEX * finalIndex + i] = textureCoords.get(rawTexCoordIndex * TEXTURE_COORDS_PER_VERTEX + i); } } } catch (NumberFormatException e) { System.out.println("Malformed index in vertex/texture coordinate mapping."); return false; } return true; } /** * Returns the vertex position coordinate list (x1, y1, z1, x2, y2, z2, ...) after a successful * call to parse(). */ public float[] getVertices() { return finalizedVertices; } /** * Returns the vertex texture coordinate list (u1, v1, u2, v2, ...) after a successful call to * parse(). */ public float[] getTextureCoords() { return finalizedTextureCoords; } /** * Returns the list of indices (a1, b1, c1, a2, b2, c2, ...) after a successful call to parse(). * Each (a, b, c) triplet specifies a triangle to be rendered, with a, b, and c Short objects used * to index into the coordinates returned by getVertices() and getTextureCoords().

* For example, a Short index representing 5 should be used to index into vertices[15], * vertices[16], and vertices[17], as well as textureCoords[10] and textureCoords[11]. */ public ArrayList getTriangles() { return finalizedTriangles; } /** * Attempts to locate and read the specified .obj file, and parse it accordingly. None of the * getter functions in this class will return valid results until a value of true is returned * from this function. * @return true on success. */ public boolean parse() { boolean success = true; BufferedReader reader = null; try { reader = Files.newBufferedReader(Paths.get(fileName), UTF_8); String line; while ((line = reader.readLine()) != null) { // Skip over lines with no characters if (line.length() < 1) { continue; } // Ignore comment lines entirely if (line.charAt(0) == '#') { continue; } // Split into pieces based on whitespace, and process according to first command piece String[] linePieces = line.split(" +"); switch (linePieces[0]) { case "v": // Add vertex if (startedProcessingFaces) { throw new IOException("Vertices must all be declared before faces in obj files."); } parseVertex(linePieces); break; case "vt": // Add texture coordinate if (startedProcessingFaces) { throw new IOException( "Texture coordinates must all be declared before faces in obj files."); } parseTextureCoordinate(linePieces); break; case "f": // Vertex and texture coordinate lists should be locked into place by now if (!startedProcessingFaces) { startedProcessingFaces = true; numPrimitiveVertices = vertices.size() / POSITIONS_COORDS_PER_VERTEX; numPrimitiveTextureCoords = textureCoords.size() / TEXTURE_COORDS_PER_VERTEX; } // Add face parseFace(linePieces); break; default: // Unknown or unused directive: ignoring // Note: We do not yet process vertex normals or curves, so we ignore {vp, vn, s} // Note: We assume only a single object, so we ignore {g, o} // Note: We also assume a single texture, which we process independently, so we ignore // {mtllib, usemtl} break; } } // If we made it all the way through, then we have a vertex-to-tex-coord pair mapping, so // construct our final vertex and texture coordinate lists now. success = constructFinalCoordinatesFromMap(); } catch (IOException e) { success = false; System.out.println("Failure to parse obj file: " + e.toString()); } finally { try { if (reader != null) { reader.close(); } } catch (IOException e) { System.out.println("Couldn't close reader"); } } if (success) { debugLogString("Successfully parsed " + numPrimitiveVertices + " vertices and " + numPrimitiveTextureCoords + " texture coordinates into " + vertexTexCoordMap.size() + " combined vertices and " + numPrimitiveFaces + " faces, represented as a mesh of " + finalizedTriangles.size() / 3 + " triangles."); } return success; } }