package com.logic.engine.gfx.wavefront;
/* ModelOBJ - Represents a 3D mesh, provides methods for loading, rendering, etc.
* NOTE: Supports the wavefront OBJ format only
*
* Written by Nick "SilverLogic" Brabant
* ©2010 - All Rights Reserved.
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import javax.microedition.khronos.opengles.GL10;
import com.logic.engine.gfx.GraphicsManager;
import com.logic.engine.gfx.Object3D;
import com.logic.engine.gfx.TextureManager;
import com.logic.engine.system.Debug;
import com.logic.engine.system.ResourceLoader;
public class ModelOBJ extends Object3D implements Object3D.GfxManager {
// OBJ Groups - Represents individual objects of a model (i.e. the prop of a plane)
private Collection<ModelOBJ> groupCollection = new ArrayList<ModelOBJ>(); // Holds all instances of OBJ groups
// Coordinate Lists - These hold the arrays from each line, and are later referenced by index to build the buffers
private ArrayList<float[]> vertexList = new ArrayList<float[]>();
private ArrayList<float[]> textureList = new ArrayList<float[]>();
private ArrayList<float[]> normalList = new ArrayList<float[]>();
// Indexes - These hold the actual 'faces', each index contains the arrays which correspond to the lists index
private ArrayList<short[]> vertexIndex = new ArrayList<short[]>();
private ArrayList<short[]> textureIndex = new ArrayList<short[]>();
private ArrayList<short[]> normalIndex = new ArrayList<short[]>();
// Buffers - These are the arrays that actually hold all of the faces, and what gets passed to opengl for rendering
private FloatBuffer indiceBuffer;
private FloatBuffer textureBuffer;
private FloatBuffer normalBuffer;
@SuppressWarnings("unused")
private ModelMTL mtlLoader; // Used for loading the model mtl
private Material material = new Material(); // Holds the specific material properties of this model
private String mtlName;
// This is simply used so that we don't try to pass empty buffers to opengl
public boolean ready = false;
// Keeps track of where this file is located (used when the mtlLoader definitions and any textures are loaded)
private String filepath = "";
// Constructor
public ModelOBJ() {}
// Overloaded Constructor - Takes a string to a filename in the assets folder, and loads the model
public ModelOBJ(String fname) {
LoadModel(fname);
}
// LoadModel - This method takes a string to a filename, retrieves a BufferedReader from the resource loader, and parses the file
public void LoadModel(String fname) {
// Load a BufferedReader from the specified asset file so we can read it line by line
ResourceLoader res = new ResourceLoader();
InputStreamReader isr = res.getStreamReader(fname);
BufferedReader br = new BufferedReader(isr);
// Get the local file path of the model (useful when retrieving mtlLoaders and textures)
if(fname.lastIndexOf("/") > 0) { filepath = fname.substring(0, fname.lastIndexOf("/")) + "/"; }
ParseModel(br); // Parse the contents of the file and build the model
try {
br.close(); // Close the input stream and clean everything up
isr.close();
isr = null;
br = null;
res = null;
} catch (IOException e) {
Debug.throwError(e.toString() + "\nError closing out resources");
}
}
// ParseModel - This method parses an obj file line by line, it should be passed a BufferedReader from LoadModel
private void ParseModel(BufferedReader br) {
String line; // Holds the current line that we are working with
String groupName = ""; // Holds the key (name) of the group we are currently in
ModelOBJ tempModel = new ModelOBJ(); // This holds the current group we are working with
try {
while((line = br.readLine()) != null) {
// # - Comment, we can skip these lines
if(line.startsWith("#")) { continue; }
// g - Group, this indicates that we need to create another model instance for these values
if(line.startsWith("g")) {
if(groupName != "") { AttachModel(tempModel, groupName); } // If we were working in a group, add it to the list
line = line.substring(2);
groupName = line; // We are now working in a new group
tempModel = new ModelOBJ(); // Create a new object for this group
continue;
}
// mtllib - Material Library, this specifies attributes about the current group
if(line.startsWith("mtllib")) {
line = line.substring(7);
mtlLoader = new ModelMTL(filepath + line);
continue;
}
// vt - Texture coordinates, we will load these into the textureList
if(line.startsWith("vt")) {
line = line.substring(3);
float[] tempArr = getFloatArray(line, 2);
tempArr[1] = 1.0f - tempArr[1];
tempModel.textureList.add(tempArr);
continue;
}
// vn - Normals, we will load these into the normalList
if(line.startsWith("vn")) {
line = line.substring(3);
tempModel.normalList.add(getFloatArray(line, 3));
continue;
}
// v - Vertices, we will load these into the vertexList
if(line.startsWith("v")) {
line = line.substring(2);
tempModel.vertexList.add(getFloatArray(line, 3));
continue;
}
// f - Face, here we fill the index lists
if(line.startsWith("f")) {
line = line.substring(2);
String[] stemp = line.split(" ", 3);
String[] v1 = stemp[0].split("/", 3);
String[] v2 = stemp[1].split("/", 3);
String[] v3 = stemp[2].split("/", 3);
tempModel.vertexIndex.add(getIndexArray(v1, v2, v3, 0)); // Add vertice indexes to list
tempModel.textureIndex.add(getIndexArray(v1, v2, v3, 1)); // Add texture indexes
tempModel.normalIndex.add(getIndexArray(v1, v2, v3, 2)); // Add normals indexes
continue;
}
// usemtl - lets us know what material group to use for this specific model group
if(line.startsWith("usemtl")) {
line = line.substring(7);
//tempModel.material = mtlLoader.GetMaterial(line);
//mtlName = tempModel.material.name;
mtlName = line;
continue;
}
}
} catch (IOException e) {
Debug.throwError("Unable to parse OBJ File");
}
tempModel.compileBuffers(); // We are finished parsing the file, time to fill the buffers
// Not all models have groups, if a group was never assigned, we can just create the model directly
if(groupName == "") {
this.indiceBuffer = tempModel.indiceBuffer;
this.textureBuffer = tempModel.textureBuffer;
this.normalBuffer = tempModel.normalBuffer;
this.material = tempModel.material;
this.ready = true;
GraphicsManager.removeObject(tempModel); // We don't need this anymore, remove from the graphics manager
} else {
tempModel.setPosition(getPosX(), getPosY(), getPosZ()); // Since this is a child of our current model, render at the same coordinates
tempModel.ready = true;
AttachModel(tempModel, groupName); // If a group was specified, attach the group to this model
}
}
// getFloatArray - Just a method to cleanup the clutter in the ParseModel method, it splits a line into 3 float
// values, and then returns it as a float[3], which can be added to an arraylist
private float[] getFloatArray(String line, int size) {
String[] stemp = line.split(" ", size);
float[] ftemp = new float[size];
for(int i = 0; i < size; i++) {
ftemp[i] = Float.valueOf(stemp[i]).floatValue();
}
return ftemp;
}
// getIndexArray, Another method to cleanup the clutter, this is pretty much the same thing as getFloatArray,
// except you must specify an index depicting which Index you want returned
// Index Key: 0 = vertexIndex, 1 = textureIndex
private short[] getIndexArray(String[] v1, String[] v2, String[] v3, int index) {
// Obtain the array containing the list index values (we must offset by 1, since arrays are zero based, and OBJ format has a base of 1
short[] itemp = { (short)(Integer.valueOf(v1[index]).shortValue() - 1),
(short)(Integer.valueOf(v2[index]).shortValue() - 1),
(short)(Integer.valueOf(v3[index]).shortValue() - 1)};
return itemp;
}
// AttachModel - This method is used both internally for managing object groups, and externally if you wish to
// attach an outside model to this source (i.e. a weapon on a soldier, or a prop on a plane)
public void AttachModel(ModelOBJ obj, String key) {
groupCollection.add(obj); // add the model to this models groupCollection
}
// compileBuffers - This is where the buffers that will be passed to opengl are built, each face in the index list
// is referenced, then based on the index values, the buffer is built using the values at the indexed location
private void compileBuffers() {
short[] index = new short[3]; // holds the index array we are currently working with
// Compile the indiceBuffer
if(vertexIndex.size() > 0) {
indiceBuffer = FloatBuffer.allocate(vertexIndex.size() * 9); // Allocate the buffer
Iterator<short[]> vertexIterator = vertexIndex.iterator();
while(vertexIterator.hasNext()) {
index = vertexIterator.next(); // Grab the next array of index values
// The following lines inserts the 3 vertex's needed to complete the face
indiceBuffer.put(vertexList.get(index[0]));
indiceBuffer.put(vertexList.get(index[1]));
indiceBuffer.put(vertexList.get(index[2]));
}
}
// Compile the textureBuffer
if(textureIndex.size() > 0) {
textureBuffer = FloatBuffer.allocate(textureIndex.size() * 6);
Iterator<short[]> textureIterator = textureIndex.iterator();
while(textureIterator.hasNext()) {
index = textureIterator.next();
textureBuffer.put(textureList.get(index[0]));
textureBuffer.put(textureList.get(index[1]));
textureBuffer.put(textureList.get(index[2]));
}
}
// Compile the normalBuffer
if(normalIndex.size() > 0) {
normalBuffer = FloatBuffer.allocate(normalIndex.size() * 9);
Iterator<short[]> normalIterator = normalIndex.iterator();
while(normalIterator.hasNext()) {
index = normalIterator.next();
normalBuffer.put(normalList.get(index[0]));
normalBuffer.put(normalList.get(index[1]));
normalBuffer.put(normalList.get(index[2]));
}
}
// Just cleanup our un-needed variables, this will help free up a lot of memory
vertexList.clear();
textureList.clear();
normalList.clear();
vertexIndex.clear();
textureIndex.clear();
normalIndex.clear();
}
// We override the rotate method since this object may contain children, we need to apply rotation to all groups
@Override
public void Rotate(float x, float y, float z) {
super.Rotate(x, y, z);
Iterator<ModelOBJ> groupIterator = groupCollection.iterator();
while(groupIterator.hasNext()) {
groupIterator.next().Rotate(x, y, z);
}
}
public void Duplicate(ModelOBJ obj) {
indiceBuffer = obj.indiceBuffer;
normalBuffer = obj.normalBuffer;
textureBuffer = obj.textureBuffer;
mtlName = obj.mtlName;
ready = true;
}
@Override
public void Init(GL10 gl) {
super.Init(gl);
// TODO init
}
// We can call this to destroy all children objects, and cleanup from the graphics manager
@Override
public void Cleanup() {
ModelOBJ tempModel;
Iterator<ModelOBJ> groupIterator = groupCollection.iterator();
while(groupIterator.hasNext()) {
tempModel = groupIterator.next();
GraphicsManager.removeObject(tempModel);
tempModel = null;
}
GraphicsManager.removeObject(this);
}
@Override
public void Render(GL10 gl) {
super.Render(gl);
if(ready) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, TextureManager.GetTextureID(mtlName));
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, indiceBuffer);
gl.glNormalPointer(GL10.GL_FLOAT, 0, normalBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, (indiceBuffer.array().length / 3));
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}
gl.glPopMatrix();
}
}