/*
* Copyright (c) 2015 NOVA, All rights reserved.
* This library is free software, licensed under GNU Lesser General Public License version 3
*
* This file is part of NOVA.
*
* NOVA is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NOVA is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOVA. If not, see <http://www.gnu.org/licenses/>.
*/
package nova.core.render.model;
import nova.core.render.RenderException;
import nova.core.util.math.Vector3DUtil;
import nova.internal.core.Game;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A OBJ model importer.
* You must load your .obj file and then bind the OBJ texture yourself.
*
* @author Thog
*/
public class WavefrontObjectModelProvider extends ModelProvider {
private static Pattern vertexPattern = Pattern.compile("(v( (\\-)?\\d+\\.\\d+){3,4} *\\n)|(v( (\\-)?\\d+\\.\\d+){3,4} *$)");
private static Pattern vertexNormalPattern = Pattern.compile("(vn( (\\-)?\\d+\\.\\d+){3,4} *\\n)|(vn( (\\-)?\\d+\\.\\d+){3,4} *$)");
private static Pattern textureCoordinatePattern = Pattern.compile("(vt( (\\-)?\\d+\\.\\d+){2,3} *\\n)|(vt( (\\-)?\\d+\\.\\d+){2,3} *$)");
// According to the official Wavefront OBJ specification, a face can have an unlimited amount of vertices
private static Pattern face_V_VT_VN_Pattern = Pattern.compile("(f( \\d+/\\d+/\\d+){3,} *\\n)|(f( \\d+/\\d+/\\d+){3,} *$)");
private static Pattern face_V_VT_Pattern = Pattern.compile("(f( \\d+/\\d+){3,} *\\n)|(f( \\d+/\\d+){3,} *$)");
private static Pattern face_V_VN_Pattern = Pattern.compile("(f( \\d+//\\d+){3,} *\\n)|(f( \\d+//\\d+){3,} *$)");
private static Pattern face_V_Pattern = Pattern.compile("(f( \\d+){3,} *\\n)|(f( \\d+){3,} *$)");
private static Pattern subModelPattern = Pattern.compile("([go]([^\\\\ ]*+)*\\n)|([go]( [^\\\\ ]*+) *$)");
private static Matcher globalMatcher;
//A map of all models generated with their names
private final MeshModel model = new MeshModel();
private MeshModel currentModel = null;
private ArrayList<Vector3D> vertices = new ArrayList<>();
private ArrayList<Vector2D> textureCoordinates = new ArrayList<>();
private ArrayList<Vector3D> vertexNormals = new ArrayList<>();
/**
* Creates new ModelProvider
* @param domain dolain of the assets.
* @param name name of the model.
*/
public WavefrontObjectModelProvider(String domain, String name) {
super(domain, name);
}
@Override
public void load(InputStream stream) {
String currentLine;
int lineCount = 0;
try(BufferedReader reader = new BufferedReader(new InputStreamReader(stream))){
while ((currentLine = reader.readLine()) != null) {
lineCount++;
currentLine = currentLine.replaceAll("\\s+", " ").trim();
if (currentLine.startsWith("#") || currentLine.isEmpty()) {
continue;
} else if (currentLine.startsWith("v ")) {
Vector3D vertex = parseToVertex(currentLine, lineCount);
if (vertex != null) {
vertices.add(vertex);
}
} else if (currentLine.startsWith("vt ")) {
Vector2D textureCoordinate = parseTextureCoordinate(currentLine, lineCount);
if (textureCoordinate != null) {
textureCoordinates.add(textureCoordinate);
}
} else if (currentLine.startsWith("vn ")) {
Vector3D vertexNormal = parseToVertexNormal(currentLine, lineCount);
if (vertexNormal != null) {
vertexNormals.add(vertexNormal);
}
} else if (currentLine.startsWith("vp ")) {
// TODO: Parameter space vertices
Game.logger().warn("Model {} uses parameter space vertices", this.name);
} else if (currentLine.startsWith("f ")) {
if (currentModel == null) {
currentModel = new MeshModel("Default");
}
Face face = parseToFace(currentLine, lineCount);
if (face != null) {
currentModel.faces.add(face);
}
} else if (currentLine.startsWith("g ") | currentLine.startsWith("o ")) {
MeshModel subModel = parseToModel(currentLine, lineCount);
if (subModel != null) {
if (currentModel != null) {
model.children.add(currentModel);
}
}
currentModel = subModel;
}
}
model.children.add(currentModel);
} catch (IOException | UnsupportedOperationException e) {
throw new RenderException("Model " + this.name + " could not be read", e);
} finally {
this.cleanUp();
}
}
private void cleanUp() {
this.currentModel = null;
this.vertices.clear();
this.textureCoordinates.clear();
}
@Override
public MeshModel getModel() {
return model.clone();
}
@Override
public String getType() {
return "obj";
}
private Vector3D parseToVertex(String line, int lineNumber) {
if (isValid(line, vertexPattern)) {
line = line.substring(line.indexOf(' ') + 1);
String[] tokens = line.split(" ");
try {
if (tokens.length == 2) {
return new Vector3D(Float.parseFloat(tokens[0]), Float.parseFloat(tokens[1]), 0);
} else if (tokens.length == 3) {
return new Vector3D(Float.parseFloat(tokens[0]), Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]));
}
} catch (NumberFormatException e) {
throw new RenderException(String.format("Number formatting error at line %d", lineNumber), e);
}
} else {
throw new RenderException("Error parsing entry ('" + line + "'" + ", line " + lineNumber + ") in model '" + this.name + "' - Incorrect format");
}
return null;
}
private Vector2D parseTextureCoordinate(String line, int lineNumber) {
if (isValid(line, textureCoordinatePattern)) {
line = line.substring(line.indexOf(' ') + 1);
String[] tokens = line.split(" ");
try {
if (tokens.length >= 2) {
return new Vector2D(Float.parseFloat(tokens[0]), 1 - Float.parseFloat(tokens[1]));
}
} catch (NumberFormatException e) {
throw new RenderException(String.format("Number formatting error at line %d", lineNumber), e);
}
} else {
throw new RenderException("Error parsing entry ('" + line + "'" + ", line " + lineNumber + ") in model '" + this.name + "' - Incorrect format");
}
return null;
}
private Vector3D parseToVertexNormal(String line, int lineNumber) {
if (isValid(line, vertexNormalPattern)) {
line = line.substring(line.indexOf(' ') + 1);
String[] tokens = line.split(" ");
try {
if (tokens.length == 3) {
// According to the official Wavefront OBJ specification, vertex normals might not be normalized.
return new Vector3D(Float.parseFloat(tokens[0]), Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2])).normalize();
}
} catch (NumberFormatException e) {
throw new RenderException(String.format("Number formatting error at line %d", lineNumber), e);
}
} else {
throw new RenderException("Error parsing entry ('" + line + "'" + ", line " + lineNumber + ") in model '" + this.name + "' - Incorrect format");
}
return null;
}
private Face parseToFace(String line, int lineNumber) {
Face face = null;
if (isValid(line, face_V_VT_VN_Pattern) || isValid(line, face_V_VT_Pattern) || isValid(line, face_V_VN_Pattern) || isValid(line, face_V_Pattern)) {
face = new Face();
String trimmedLine = line.substring(line.indexOf(' ') + 1);
String[] tokens = trimmedLine.split(" ");
String[] subTokens = null;
// f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 ...
if (isValid(line, face_V_VT_VN_Pattern)) {
for (int i = 0; i < tokens.length; ++i) {
subTokens = tokens[i].split("/");
face.drawVertex(new Vertex(getVertex(Integer.parseInt(subTokens[0])), getTexVec(Integer.parseInt(subTokens[1])), getNormal(Integer.parseInt(subTokens[2]))));
}
face.normal = Vector3DUtil.calculateNormal(face);
}
// f v1/vt1 v2/vt2 v3/vt3 ...
else if (isValid(line, face_V_VT_Pattern)) {
for (int i = 0; i < tokens.length; ++i) {
subTokens = tokens[i].split("/");
face.drawVertex(new Vertex(getVertex(Integer.parseInt(subTokens[0])), getTexVec(Integer.parseInt(subTokens[1]))));
}
face.normal = Vector3DUtil.calculateNormal(face);
}
// f v1//vn1 v2//vn2 v3//vn3 ...
else if (isValid(line, face_V_VN_Pattern)) {
for (int i = 0; i < tokens.length; ++i) {
subTokens = tokens[i].split("//");
face.drawVertex(new Vertex(getVertex(Integer.parseInt(subTokens[0])), Vector2D.ZERO, getNormal(Integer.parseInt(subTokens[1]))));
}
face.normal = Vector3DUtil.calculateNormal(face);
}
// f v1 v2 v3 ...
else if (isValid(line, face_V_Pattern)) {
for (int i = 0; i < tokens.length; ++i) {
face.drawVertex(new Vertex(getVertex(Integer.parseInt(tokens[i])), Vector2D.ZERO));
}
face.normal = Vector3DUtil.calculateNormal(face);
} else {
throw new RenderException("Error parsing entry ('" + line + "'" + ", line " + lineNumber + ") in model '" + this.name + "' - Incorrect format");
}
} else {
throw new RenderException("Error parsing entry ('" + line + "'" + ", line " + lineNumber + ") in model '" + this.name + "' - Incorrect format");
}
return face;
}
private MeshModel parseToModel(String line, int lineNumber) {
if (isValid(line, subModelPattern)) {
String trimmedLine = line.substring(line.indexOf(' ') + 1);
if (!trimmedLine.isEmpty()) {
return new MeshModel(trimmedLine);
}
} else {
throw new RenderException("Error parsing entry ('" + line + "'" + ", line " + lineNumber + ") in model '" + this.name + "' - Incorrect format");
}
return null;
}
/**
* Verifies that the given line from the model file is a valid for a given pattern
*
* @param line the line being validated
* @param pattern the validator
* @return true if the line is a valid for a given pattern, false otherwise
*/
private boolean isValid(String line, Pattern pattern) {
if (globalMatcher != null) {
globalMatcher.reset();
}
globalMatcher = pattern.matcher(line);
return globalMatcher.matches();
}
private Vector3D getVertex(int index) {
try {
return vertices.get(index < 0 ? index + textureCoordinates.size() : index - 1);
} catch (IndexOutOfBoundsException e) {
System.err.println("[OBJ]: Can't get vertex " + index + "! Is this model corrupted?");
return Vector3D.ZERO;
}
}
private Vector2D getTexVec(int index) {
try {
return textureCoordinates.get(index < 0 ? index + textureCoordinates.size() : index - 1);
} catch (IndexOutOfBoundsException e) {
System.err.println("[OBJ]: Can't get textureCoordinate " + index + "! Is this model corrupted?");
return Vector2D.ZERO;
}
}
private Vector3D getNormal(int index) {
try {
return vertexNormals.get(index < 0 ? index + textureCoordinates.size() : index - 1);
} catch (IndexOutOfBoundsException e) {
System.err.println("[OBJ]: Can't get vertexNormal " + index + "! Is this model corrupted?");
return Vector3D.ZERO;
}
}
}