package glmodel; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.StringTokenizer; import glapp.*; /** * Based on Object3D.java by Jeremy Adams (elias4444) august 2005 * * Read an OBJ file into ArrayLists which can then be imported into a final * mesh class. This class just loads and holds data, and can be used independently * of specific 3D model classes. * * Populates a "faces" Arraylist with all faces in the mesh. Also stores face * groups in the "groups" Arraylist (an Arraylist that contains Group objects). * * jul 2006: add Group class to hold group faces, materialname and groupname * jul 2006: read usemtl command and store material names with groups * */ public class GL_OBJ_Reader { // These three hold the vertex, texture and normal coordinates // There is only one set of verts, textures, normals for entire file public ArrayList vertices = new ArrayList(); // Contains float[3] for each Vertex (XYZ) public ArrayList normals = new ArrayList(); // Contains float[3] for each normal public ArrayList textureCoords = new ArrayList(); // Contains float[3] for each texture map coord (UVW) // Hold all faces in the mesh public ArrayList faces = new ArrayList(); // Holds groups of faces (with name and material for each group) public ArrayList groups = new ArrayList(); // name of material file, or null if no material libe is given public String materialLibeName = null; // materials loaded from .mtl file (or a default material if no mtl file is found) public GLMaterialLib materialLib; // path and name of .obj file public String filepath = ""; public String filename = ""; // mesh min and max points public float leftpoint = 0; // x- public float rightpoint = 0; // x+ public float bottompoint = 0; // y- public float toppoint = 0; // y+ public float farpoint = 0; // z- public float nearpoint = 0; // z+ public GL_OBJ_Reader(String objfilename) { // Construct from file name loadobject(objfilename); } public GL_OBJ_Reader(InputStream in) { // Construct from inputstream loadobject(in); } public void loadobject(String objfilename) { // load from String filename if (objfilename != null && objfilename.length() > 0) { // Separate leading path from filename (we'll load material libe from same folder) String[] pathParts = GLApp.getPathAndFile(objfilename); filepath = pathParts[0]; filename = pathParts[1]; // Load it try { loadobject(GLApp.getInputStream(objfilename)); } catch (Exception e) { System.out.println("GL_OBJ_Reader.loadobject(): Failed to read file: " + objfilename + " " + e); } } } public void loadobject(InputStream in) { // load from inputStream if (in != null) { BufferedReader br = new BufferedReader(new InputStreamReader(in)); loadobject(br); } } /** * OBJ file format in a nutshell: * First part of file lists vertex data: vert coords, texture coords and normals. * These lines start with v, vt and vn respectively. * Second part of file lists faces. These lines start with f. * Each face is defined as a set of verts, usually three, but may be more. * The face definition line contains triplets: three numbers separated by * slashes. Each number is an index into the vert, texture coord, or normal list. * NOTE: these lists are indexed starting with 1, not 0. */ public void loadobject(BufferedReader br) { String line = ""; String materialName = ""; // current material int materialID = -1; // -1 means no material found // make a default group to start (to hold faces not in any group) Group group = new Group("default"); groups.add(group); try { while ((line = br.readLine()) != null) { // remove extra whitespace line = line.trim(); line = line.replaceAll(" ", " "); if (line.length() > 0) { if (line.startsWith("v ")) { // vertex coord line looks like: v 2.628657 -5.257312 8.090169 [optional W value] vertices.add(read3Floats(line)); } else if (line.startsWith("vt")) { // texture coord line looks like: vt 0.187254 0.276553 0.000000 textureCoords.add(read3Floats(line)); } else if (line.startsWith("vn")) { // normal line looks like: vn 0.083837 0.962494 -0.258024 normals.add(read3Floats(line)); } else if (line.startsWith("f ")) { // Face line looks like: f 1/3/1 13/20/13 16/29/16 Face f = readFace(line); // assign material ID to polygon f.materialID = materialID; faces.add(f); // add to complete face list group.faces.add(f); // add to current group group.numTriangles += f.numTriangles(); // track number of triangles in group } else if (line.startsWith("g ")) { // Group line looks like: g someGroupName String groupname = (line.length()>1)? line.substring(2).trim() : ""; // "select" the given group group = findGroup(groupname); // not found: start new group if (group == null) { group = new Group(groupname); group.materialname = materialName; // assign current material to new group group.materialID = materialID; groups.add(group); } } else if (line.startsWith("usemtl")) { // material line: usemtl materialName materialName = line.substring(7).trim(); // lookup material name in libe materialID = (materialLib == null)? -1 : materialLib.findID(materialName); // assign material to current group group.materialname = materialName; group.materialID = materialID; //System.out.println("got usemtl " +group.name + ".materialname now is " + materialName); } else if (line.startsWith("mtllib")) { // material library line: mtllib materialLibeFile.mtl materialLibeName = line.substring(7).trim(); if (materialLibeName.startsWith("./")) { materialLibeName = materialLibeName.substring(2); } // load material library materialLib = new GLMaterialLib(filepath + materialLibeName); } } } } catch (Exception e) { System.out.println("GL_OBJ_Reader.loadObject() failed at line: " + line); } // remove empty groups for (int g=groups.size()-1; g >= 0; g--) { if (getGroupFaces(g).size() <= 0) { //System.out.println("REMOVE EMPTY GROUP " + g); groups.remove(g); } } // find min/max points of mesh calcDimensions(); // debug: System.out.println("GL_OBJ_Reader: read " + numpolygons() + " faces in " + groups.size() + " groups"); // debug: for (int i=0; i < groups.size(); i++) { System.out.println("\tGROUP " + i + " " + ((Group)groups.get(i)).name + " has " + ((Group)groups.get(i)).faces.size() + " faces, material is " + ((Group)groups.get(i)).materialname ); } } /** * Parse three floats from the given input String. Ignore the * first token (the line type identifier, ie. "v", "vn", "vt"). * Return array: float[3]. * @param line contains line from OBJ file * @return array of 3 float values */ private float[] read3Floats(String line) { try { StringTokenizer st = new StringTokenizer(line, " "); st.nextToken(); // throw out line marker (vn, vt, etc.) if (st.countTokens() == 2) { // texture uv may have only 2 values return new float[] {Float.parseFloat(st.nextToken()), Float.parseFloat(st.nextToken()), 0}; } else { return new float[] {Float.parseFloat(st.nextToken()), Float.parseFloat(st.nextToken()), Float.parseFloat(st.nextToken())}; } } catch (Exception e) { System.out.println("GL_OBJ_Reader.read3Floats(): error on line '" + line + "', " + e); return null; } } /** * look for the given groupname in the groups list * @param name * @return the found Group or null if not found */ public Group findGroup(String name) { for (int i=0; i < groups.size(); i++) { if (((Group)groups.get(i)).name.equals(name)) { return (Group)groups.get(i); } } return null; } /** * Read a face definition from line and return a Face object. * Face line looks like: f 1/3/1 13/20/13 16/29/16 * Three or more sets of numbers, each set contains vert/txtr/norm * references. A reference is an index into the vert or txtr * or normal list. * @param line string from OBJ file with face definition * @return Face object */ private Face readFace(String line) { // throw out "f " at start of line, then split String[] triplets = line.substring(2).split(" "); int[] v = new int[triplets.length]; int[] vt = new int[triplets.length]; int[] vn = new int[triplets.length]; for (int i = 0; i < triplets.length; i++) { // triplets look like 13/20/13 and hold // vert/txtr/norm indices. If no texture coord has been // assigned, may be 13//13. Substitute 0 so split works. String[] vertTxtrNorm = triplets[i].replaceAll("//", "/0/").split("/"); if (vertTxtrNorm.length > 0) { v[i] = convertIndex(vertTxtrNorm[0],vertices.size()); } if (vertTxtrNorm.length > 1) { vt[i] = convertIndex(vertTxtrNorm[1],textureCoords.size()); } if (vertTxtrNorm.length > 2) { vn[i] = convertIndex(vertTxtrNorm[2],normals.size()); } } return new Face(v,vt,vn); } /** * Convert a vertex reference number into the correct vertex array index. *
* Face definitions in the OBJ file refer to verts, texture coords and * normals using a reference number. The reference numbers is the position * of the vert in the vertex list, in the order read from the OBJ file. * Reference numbers start at 1, and can be negative (to refer back into * the vert list starting at the bottom, though this seems to be rare). The * same approach applies to texture coords and normals. *
* This function converts reference numbers to an array index starting at 0, * and converts negative reference numbers to 0-N array indexes. It returns * -1 if the token is blank, meaning there was no data given (ie. there * is no texture coord or normal available). *
* @param token a token from the OBJ file containing a numeric value or blank * @return idx will be 0 - N index into vert array, or -1 if token is blank */ public int convertIndex(String token, int numVerts) { int idx = Integer.valueOf(token).intValue(); // OBJ file index starts at 1 idx = (idx < 0) ? (numVerts + idx) : idx-1; // convert index to start at 0 return idx; } /** * Find min/max points of mesh. */ public void calcDimensions() { float[] vertex; // reset min/max points leftpoint = rightpoint = 0; bottompoint = toppoint = 0; farpoint = nearpoint = 0; // loop through all groups for (int g = 0; g < groups.size(); g++) { ArrayList faces = ((Group)groups.get(g)).faces; // loop through all faces in group (ie. triangles) for (int f = 0; f < faces.size(); f++) { Face face = (Face) faces.get(f); int[] vertIDs = face.vertexIDs; // loop through all vertices in face for (int v = 0; v < vertIDs.length; v++) { vertex = (float[]) vertices.get(vertIDs[v]); if (vertex[0] > rightpoint) rightpoint = vertex[0]; if (vertex[0] < leftpoint) leftpoint = vertex[0]; if (vertex[1] > toppoint) toppoint = vertex[1]; if (vertex[1] < bottompoint) bottompoint = vertex[1]; if (vertex[2] > nearpoint) nearpoint = vertex[2]; if (vertex[2] < farpoint) farpoint = vertex[2]; } } } } public float getXWidth() { return rightpoint - leftpoint; } public float getYHeight() { return toppoint - bottompoint; } public float getZDepth() { return nearpoint - farpoint; } public int numpolygons() { int number = 0; for (int i = 0; i < groups.size(); i++) { number += ((Group)groups.get(i)).faces.size(); } return number; } //======================================================================== // These functions get group information without having to expose the // Group class to the outside world. //======================================================================== public int numGroups() { return groups.size(); } public String getGroupName(int g) { return ((Group)groups.get(g)).name; } public ArrayList getGroupFaces(int g) { return ((Group)groups.get(g)).faces; } public String getGroupMaterialName(int g) { return ((Group)groups.get(g)).materialname; } public int getGroupTriangleCount(int g) { return ((Group)groups.get(g)).numTriangles; } //======================================================================== // Group class holds one group of faces with a name and material //======================================================================== class Group { String name; String materialname; int materialID; // index into materials array int numTriangles; ArrayList faces; public void Group_(ArrayList faces, String name, String materialname) { this.name = name; this.materialname = materialname; this.faces = faces; } public Group(String name) { this.name = name; this.materialname = ""; this.faces = new ArrayList(); } } }