package graphics.mesh;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import org.lwjgl.BufferUtils;
import org.lwjgl.util.vector.Vector2f;
import org.lwjgl.util.vector.Vector3f;
import static org.lwjgl.opengl.GL15.*;
/**
* ObjMesh is a representation of an object in 3d-space.
* (Vertices, texture coordinates, normals)
*
* It is loaded from .obj
*
* Obj-file must have vertices, texture-coordinates, and normals (v/vt/vn)
* If the file has a material library specified, it will be automatically loaded and set to defaultMaterial.
* The mesh is automatically converted to triangles.
*
* @author simokr
*/
public class ObjMesh extends Mesh{
/**
* Loads the mesh from .obj of given path and does all the necessary things to have it renderable.
*
* @param path path to the .obj
* @param saveVBO if set to true, VBO data (in RAM) won't be cleared afterwards.
* @return success
*/
public boolean create(String path, boolean saveVBO){
if(this.isReady())
this.free();
if(path == null || path.isEmpty()){
System.err.println("Empty Mesh path");
return false;
}
path = path.trim();
/* Get file contents */
ArrayList<String> file = readFile(path);
if(file.isEmpty()){
System.err.println("Failed to load " + path);
return false;
}
StringBuffer feedback = new StringBuffer();
/* Parse file and generate VBO contents */
FloatBuffer data = this.parseObj(file, feedback, true);
FloatBuffer finalData;
if(data == null){
System.err.println(path+": "+feedback);
return false;
}
data.rewind();
int indices = data.remaining()/9;
int keyframes = 1;
String filePath = path.substring(0, path.lastIndexOf("."));
String fileExt = path.substring(path.lastIndexOf("."));
ArrayList<FloatBuffer> kfBuffers = new ArrayList<>();
while(!(file = readFile(filePath+"_kf_"+keyframes+fileExt)).isEmpty()){
FloatBuffer kfData = this.parseObj(file, feedback, false);
if(kfData == null){
System.err.println(path+": "+feedback);
break;
}
kfBuffers.add(kfData);
keyframes++;
}
if(keyframes > 1){
float[] temp = new float[3];
int size = indices*3+indices*6*keyframes;
finalData = BufferUtils.createFloatBuffer(size);
// XYZ
data.position(0);
for(int i=0;i<indices;i++){
data.get(temp, 0, 3);
finalData.put(temp);
}
// KF XYZ
for(FloatBuffer kf:kfBuffers){
kf.position(0);
for(int i=0;i<indices;i++){
kf.get(temp, 0, 3);
finalData.put(temp);
}
}
// UV
data.position(indices*3);
for(int i=0;i<indices;i++){
data.get(temp, 0, 2);
finalData.put(temp, 0, 2);
}
// NORMAL
data.position(indices*5);
for(int i=0;i<indices;i++){
data.get(temp, 0, 3);
finalData.put(temp);
}
// KF NORMAL
for(FloatBuffer kf:kfBuffers){
kf.position(indices*5);
for(int i=0;i<indices;i++){
kf.get(temp, 0, 3);
finalData.put(temp);
}
}
// ID
data.position(indices*8);
for(int i=0;i<indices;i++){
data.get(temp, 0, 1);
finalData.put(temp[0]);
}
finalData.flip();
System.out.println("Loaded "+path+" with "+keyframes+" keyframes, size in memory: "+finalData.remaining()*4/1024+"kB");
}
else
finalData = data;
/* Create a renderable Mesh */
if(!super.create(finalData, GL_STATIC_DRAW, true, keyframes, indices)){
System.err.println("Failed to load OBJ: " + path);
}
return true;
}
/**
* Parses the Obj data to drawable format
*
* @param data Obj file split to Vector by newlines
* @param feedback Details of error if return value is false
* @return success
*/
private FloatBuffer parseObj(ArrayList<String> data, StringBuffer feedback, boolean createDefaultMaterial){
String[] str;
String materialLib = new String();
int strCount = 0,lineNum = 0, currentMtl=0;
ArrayList<Vector3f> objVertices = new ArrayList<>();
ArrayList<Vector2f> objTexcoords = new ArrayList<>();
ArrayList<Vector3f> objNormals = new ArrayList<>();
ArrayList<ArrayList<Face>> objFaces = new ArrayList<>();
ArrayList<String> materials = new ArrayList<>();
/* Populate Vectors */
for(String line : data){
lineNum++;
line = line.trim();
if(line.isEmpty()){
continue;
}
str = line.split("\\s+");
strCount = str.length - 1;
if(line.startsWith("v ")){
/* Vertex */
if(strCount < 3){
feedback.append("Too few floats in vertex. ").append(strCount).append("(3) line: ").append(lineNum);
return null;
}
objVertices.add(this.parseToVec3f(str));
}
else if(line.startsWith("vt ")){
/* Texture coordinate */
if(strCount < 2){
feedback.append("Too few floats in texture coordinate. ").append(strCount).append("(2) line: ").append(lineNum);
return null;
}
objTexcoords.add(this.parseToVec2f(str));
}
else if(line.startsWith("vn ")){
/* Normal */
if(strCount < 3){
feedback.append("Too few floats in normal. ").append(strCount).append("(3) line: ").append(lineNum);
return null;
}
objNormals.add(this.parseToVec3f(str));
}
else if(line.startsWith("f ")){
/* Face */
if(strCount < 3){
feedback.append("Too few indice in face. ").append(strCount).append("(>=3) line: ").append(lineNum);
return null;
}
ArrayList<Face> faceVec = new ArrayList<>();
for (int i = 1; i < strCount+1; i++) {
String index[] = str[i].split("/");
if(index.length < 3){
feedback.append("Face is missing data. Needs v/vt/vn. ").append(index.length).append("(3) line: ").append(lineNum);
return null;
}
float material = currentMtl;
Face tempFace = this.parseToFace(index, material);
if(tempFace != null)
faceVec.add(tempFace);
}
/* Skip face if it has less than 3 vertices */
if(faceVec.size() > 2)
objFaces.add(faceVec);
}
else if(line.startsWith("usemtl ")){
if(!materials.contains(str[1])){
materials.add(str[1]);
}
currentMtl = materials.lastIndexOf(str[1]);
}
else if(line.startsWith("mtllib ")){
if(materialLib.isEmpty())
materialLib = str[1].trim();
}
}
int vertices = objVertices.size();
int textureCoordinates = objTexcoords.size();
int normals = objNormals.size();
int faceCount = 0;
int vertexCount = 0;
/* Check face index validity */
for (ArrayList<Face> vector : objFaces) {
vertexCount += (vector.size()-2)*3;
for (Face face : vector) {
faceCount++;
face.v = this.getCorrectIndex(face.v, vertices);
face.vt = this.getCorrectIndex(face.vt, textureCoordinates);
face.vn = this.getCorrectIndex(face.vn, normals);
if(face.v >= vertices || face.v < 0){
feedback.append("Face ").append(faceCount).append(": vertex index doesn't exist ").append(face.v).append("(>=").append(vertices).append(" || < 0).");
return null;
}
else if(face.vt >= textureCoordinates || face.vt < 0){
feedback.append("Face ").append(faceCount).append(": texture coordinate index doesn't exist ").append(face.vt).append("(>=").append(textureCoordinates).append(" || < 0).");
return null;
}
else if(face.vn >= normals || face.vn < 0){
feedback.append("Face ").append(faceCount).append(": normal index doesn't exist ").append(face.vn).append("(>=").append(normals).append(" || < 0).");
return null;
}
}
}
FloatBuffer buffer = this.generateVBO(objVertices,objTexcoords,objNormals,objFaces,vertexCount);
if(createDefaultMaterial){
this.createDefaultMaterial(materials,materialLib);
}
return buffer;
}
/**
* Generates VBO content from the parsed OBJ
*
* @param objV vertices
* @param objVT texture coordinates
* @param objVN normals
* @param objF faces
* @param vertexCount number of indices in the mesh
* @return true
*/
private FloatBuffer generateVBO(ArrayList<Vector3f> objV, ArrayList<Vector2f> objVT, ArrayList<Vector3f> objVN, ArrayList<ArrayList<Face>> objF, int vertexCount){
/* v(3)+vt(2)+vn(3)+texid(1) */
int floatsPerVertex = 3+2+3+1;
FloatBuffer temporaryBuffer = BufferUtils.createFloatBuffer(vertexCount*floatsPerVertex);
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(vertexCount*3);
FloatBuffer texcBuffer = BufferUtils.createFloatBuffer(vertexCount*2);
FloatBuffer norBuffer = BufferUtils.createFloatBuffer(vertexCount*3);
FloatBuffer idBuffer = BufferUtils.createFloatBuffer(vertexCount*1);
for (ArrayList<Face> vector : objF) {
/* Break polygons to triangles */
for (int i = 0; i < vector.size()-2; i++) {
for(int vertex = 0; vertex<3; vertex++) {
int index = (vertex == 0)?0:i+vertex;
Face face = vector.get(index);
objV.get(face.v).store(posBuffer);
objVT.get(face.vt).store(texcBuffer);
objVN.get(face.vn).store(norBuffer);
idBuffer.put(face.m);
}
}
}
posBuffer.flip();
texcBuffer.flip();
norBuffer.flip();
idBuffer.flip();
temporaryBuffer.put(posBuffer);
temporaryBuffer.put(texcBuffer);
temporaryBuffer.put(norBuffer);
temporaryBuffer.put(idBuffer);
temporaryBuffer.flip();
return temporaryBuffer;
}
private void createDefaultMaterial(ArrayList<String> materials, String materialLib){
/* If there are no materials in the .obj or loading the .mtl fails, set the default material to use grey textures */
if(materialLib.isEmpty() || !this.defaultMaterial.loadFromFile(materials,"res/mesh/"+materialLib)){
for(int i=0;i<materials.size();i++){
this.defaultMaterial.setTexture(i, "empty");
}
}
}
private int getCorrectIndex(int index, int indexSize){
return (index > 0)?--index:indexSize+index;
}
private Vector2f parseToVec2f(String str[]){
Vector2f vec = new Vector2f();
vec.set(Float.parseFloat(str[1]), 1.0f-Float.parseFloat(str[2]));
return vec;
}
private Vector3f parseToVec3f(String str[]){
Vector3f vec = new Vector3f();
vec.set(Float.parseFloat(str[1]), Float.parseFloat(str[2]), Float.parseFloat(str[3]));
return vec;
}
private Face parseToFace(String str[], float material){
for (int i = 0; i < 3; i++) {
if(str[i].equals(""))
return null;
}
Face face = new Face();
face.set(Integer.parseInt(str[0]), Integer.parseInt(str[1]), Integer.parseInt(str[2]), material);
return face;
}
public static ArrayList<String> readFile(String path) {
ArrayList<String> file = new ArrayList<>();
try {
BufferedReader reader = new BufferedReader(new FileReader(path));
// Temporary string to store a line from file
String string;
while((string = reader.readLine()) != null) {
file.add(string);
}
reader.close();
} catch(IOException e) {
// TODO error handling
}
return file;
}
private class Face {
public int v,vt,vn;
public float m;
public Face() {
v = 0;
vt = 0;
vn = 0;
m = 0;
}
public Face(int v, int vt, int vn, float m){
this.v = v;
this.vt = vt;
this.vn = vn;
this.m = m;
}
public void set(int v, int vt, int vn, float m){
this.v = v;
this.vt = vt;
this.vn = vn;
this.m = m;
}
}
}