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();
}
}
}