/*
* JaamSim Discrete Event Simulation
* Copyright (C) 2013 Ausenco Engineering Canada Inc.
* Copyright (C) 2015 JaamSim Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jaamsim.MeshFiles;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Pattern;
import com.jaamsim.math.Color4d;
import com.jaamsim.math.Mat4d;
import com.jaamsim.math.Vec2d;
import com.jaamsim.math.Vec3d;
import com.jaamsim.math.Vec4d;
import com.jaamsim.render.RenderException;
import com.jaamsim.ui.LogBox;
public class ObjReader {
public static MeshData parse(URI asset) throws RenderException {
try {
ObjReader reader = new ObjReader(asset.toURL());
reader.processContent();
return reader.getMeshData();
} catch (Exception e) {
LogBox.renderLogException(e);
throw new RenderException(e.getMessage());
}
}
private void parseAssert(boolean b) {
if (!b) {
throw new RenderException(String.format("Failed OBJ parsing assert at line: %d", lineNum));
}
}
private final URL contentURL;
private MeshData data;
private static class FaceVert {
public int v;
public int t;
public int n;
}
private static class Material {
public Color4d ambient;
public Color4d spec;
public Color4d diffuse;
public URI diffuseTex;
public String relDiffuseTex;
public double shininess = 1.0;
public double alpha = 1.0;
}
private Material parsingMat;
private final HashMap<String, Material> materialMap = new HashMap<>();
private final ArrayList<Vec3d> vertices = new ArrayList<>();
private final ArrayList<Vec2d> texCoords = new ArrayList<>();
private final ArrayList<Vec3d> normals = new ArrayList<>();
private final ArrayList<FaceVert> faces = new ArrayList<>();
private String activeMat = null;
private int numLoadedMeshes = 0;
private int lineNum = 0;
private final HashMap<String, Integer> loadedMaterials = new HashMap<>();
public ObjReader(URL asset) {
contentURL = asset;
}
private void processContent() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(contentURL.openStream()));
data = new MeshData(false);
while(true) {
String line = br.readLine();
lineNum++;
if (line == null) break;
if (line.length() == 0 || line.charAt(0) == '#') {
continue;
}
while (line.charAt(line.length()-1) == '\\')
{
String nextLine = br.readLine();
lineNum++;
line = line.substring(0, line.length() - 1) + " " + nextLine;
}
parseLine(line);
}
} catch (IOException ex) {
System.err.println(ex.getLocalizedMessage());
return;
}
finishCurrentMesh();
MeshData.TreeNode root = new MeshData.TreeNode();
root.trans = new MeshData.StaticTrans(new Mat4d());
data.setTree(root);
data.finalizeData();
}
// Load the material in the return data (if necessary) and return the appropriate index
private int getMaterialIndex(String matName) {
Integer loaded = loadedMaterials.get(matName);
if (loaded != null) {
return loaded;
}
// Otherwise load it
int newIndex = loadedMaterials.size();
Material mat = materialMap.get(matName);
parseAssert(mat != null);
int transType = mat.alpha == 1.0 ? MeshData.NO_TRANS : MeshData.A_ONE_TRANS;
data.addMaterial(mat.diffuseTex, mat.relDiffuseTex, mat.diffuse, mat.ambient, mat.spec, mat.shininess, transType, new Color4d(1, 1, 1, mat.alpha));
loadedMaterials.put(matName, newIndex);
return newIndex;
}
private void finishCurrentMesh() {
if (faces.size() == 0) {
return;
}
VertexMap map = new VertexMap();
// Now build up the geometry
int[] vertIndices = new int[faces.size()];
for (int i = 0; i < faces.size(); ++i) {
FaceVert fv = faces.get(i);
Vec3d pos = vertices.get(fv.v - 1);
Vec2d texCoord = null;
if (fv.t != -1)
texCoord = texCoords.get(fv.t - 1);
Vec3d normal = null;
if (fv.n != -1) {
normal = normals.get(fv.n - 1);
} else {
// This face does not have a normal, we'd better generate one from the vertices
int faceInd = i / 3;
Vec3d p0 = vertices.get(faces.get(faceInd*3 + 0).v-1);
Vec3d p1 = vertices.get(faces.get(faceInd*3 + 1).v-1);
Vec3d p2 = vertices.get(faces.get(faceInd*3 + 2).v-1);
Vec3d d0 = new Vec3d();
d0.sub3(p1, p0);
Vec3d d1 = new Vec3d();
d1.sub3(p2, p0);
normal = new Vec3d();
normal.cross3(d0, d1);
normal.normalize3();
}
vertIndices[i] = map.getVertIndex(pos, normal, texCoord);
}
int matIndex = getMaterialIndex(activeMat);
data.addSubMesh(map.getVertList(), vertIndices);
data.addStaticMeshInstance(numLoadedMeshes++, matIndex, new Mat4d());
faces.clear();
}
private void parseLine(String line) {
String[] tokens = line.trim().split("\\s+");
if (tokens.length == 0) {
return;
}
if (tokens[0].equals("v")) {
parseVertex(tokens);
} else if (tokens[0].equals("vt")) {
parseTexCoord(tokens);
} else if (tokens[0].equals("vn")) {
parseNormal(tokens);
} else if (tokens[0].equals("f")) {
parseFaces(tokens);
} else if (tokens[0].equals("mtllib")) {
parseMaterial(tokens);
} else if (tokens[0].equals("usemtl")) {
parseAssert(tokens.length == 2);
finishCurrentMesh();
activeMat = tokens[1];
} else if (tokens[0].equals("g")) {
// Just ignore groups
} else if (tokens[0].equals("o")) {
// Just ignore objects
} else {
System.err.printf("Ignoring line: %s\n", line);
}
}
private MeshData getMeshData() {
return data;
}
private void parseVertex(String[] tokens) {
parseAssert(tokens.length >= 4 && tokens.length <= 5);
Vec4d vert = new Vec4d();
try {
vert.x = Double.parseDouble(tokens[1]);
vert.y = Double.parseDouble(tokens[2]);
vert.z = Double.parseDouble(tokens[3]);
vert.w = 1;
if (tokens.length > 4) {
vert.w = Double.parseDouble(tokens[4]);
}
vertices.add(vert);
} catch (NumberFormatException ex) {
parseAssert(false);
return;
}
}
private void parseTexCoord(String[] tokens) {
parseAssert(tokens.length >= 3 && tokens.length <= 4);
Vec2d texCoord = new Vec2d();
try {
texCoord.x = Double.parseDouble(tokens[1]);
texCoord.y = Double.parseDouble(tokens[2]);
texCoords.add(texCoord);
} catch (NumberFormatException ex) {
parseAssert(false);
return;
}
}
private void parseNormal(String[] tokens) {
parseAssert(tokens.length == 4);
Vec3d normal = new Vec3d();
try {
normal.x = Double.parseDouble(tokens[1]);
normal.y = Double.parseDouble(tokens[2]);
normal.z = Double.parseDouble(tokens[3]);
normal.normalize3();
normals.add(normal);
} catch (NumberFormatException ex) {
parseAssert(false);
return;
}
}
private void parseFaces(String[] tokens) {
parseAssert(tokens.length >= 4);
FaceVert[] fvs = new FaceVert[tokens.length - 1];
for (int i = 0; i < tokens.length - 1; ++i) {
boolean hasNormal = false;
boolean hasTex = false;
fvs[i] = new FaceVert();
String[] indices = tokens[i+1].split("/");
parseAssert(indices.length > 0);
fvs[i].v = Integer.parseInt(indices[0]);
if (indices.length < 2 || indices[1].isEmpty()) {
fvs[i].t = -1;
} else {
fvs[i].t = Integer.parseInt(indices[1]);
hasTex = true;
}
if (indices.length < 3 || indices[2].isEmpty()) {
fvs[i].n = -1;
} else {
fvs[i].n = Integer.parseInt(indices[2]);
hasNormal = true;
}
// Check for relative indexing
if (fvs[i].v < 0) {
fvs[i].v = vertices.size() + 1 - fvs[i].v;
}
if (fvs[i].t < 0 && hasTex) {
fvs[i].t = texCoords.size() + 1 - fvs[i].t;
}
if (fvs[i].n < 0 && hasNormal) {
fvs[i].n = normals.size() + 1 - fvs[i].n;
}
}
for (int i = 2; i < tokens.length-1; ++i) {
faces.add(fvs[0]);
faces.add(fvs[i-1]);
faces.add(fvs[i]);
}
}
private static final Pattern whitespace = Pattern.compile("\\s+");
private void parseMaterial(String[] tokens) {
parseAssert(tokens.length == 2);
String mtlFile = tokens[1];
try {
URL mtlURL = new URL(contentURL, mtlFile);
BufferedReader br = new BufferedReader(new InputStreamReader(mtlURL.openStream()));
while (true) {
String line = br.readLine();
if (line == null) break;
if (line.isEmpty() || line.charAt(0) == '#') continue;
String[] mtlTokens = whitespace.split(line.trim());
parseMTLLine(mtlTokens);
}
} catch (MalformedURLException ex) {
throw new RenderException(String.format("Could not open mtl file: %s", mtlFile));
} catch (IOException ex) {
throw new RenderException(String.format("Could not open mtl file: %s", mtlFile));
}
}
private void parseMTLLine(String[] tokens) throws MalformedURLException {
if (tokens.length == 0) return;
if (tokens[0].equals("newmtl")) {
parseAssert(tokens.length == 2);
parsingMat = new Material();
materialMap.put(tokens[1], parsingMat);
} else if (tokens[0].equals("Kd")) {
if (tokens.length == 1) return; // Ignore empty tags
parseAssert(parsingMat != null);
parsingMat.diffuse = parseColor(tokens);
} else if (tokens[0].equals("Ka")) {
if (tokens.length == 1) return; // Ignore empty tags
parseAssert(parsingMat != null);
parsingMat.ambient = parseColor(tokens);
} else if (tokens[0].equals("Ks")) {
if (tokens.length == 1) return; // Ignore empty tags
parseAssert(parsingMat != null);
parsingMat.spec = parseColor(tokens);
} else if (tokens[0].equals("Ns")) {
if (tokens.length == 1) return; // Ignore empty tags
parseAssert(tokens.length == 2);
parseAssert(parsingMat != null);
parsingMat.shininess = Double.parseDouble(tokens[1]);
} else if (tokens[0].equals("d")) {
if (tokens.length == 1) return; // Ignore empty tags
parseAssert(tokens.length == 2);
parseAssert(parsingMat != null);
parsingMat.alpha = Double.parseDouble(tokens[1]);
} else if (tokens[0].equals("map_Kd")) {
if (tokens.length == 1) return; // Ignore empty tags
parseAssert(tokens.length == 2);
parseAssert(parsingMat != null);
try {
parsingMat.diffuseTex = new URL(contentURL, tokens[1]).toURI();
} catch (URISyntaxException e) {
parseAssert(false);
}
parsingMat.relDiffuseTex = tokens[1];
}
}
private Color4d parseColor(String[] tokens) {
parseAssert(tokens.length == 4);
double r = Double.parseDouble(tokens[1]);
double g = Double.parseDouble(tokens[2]);
double b = Double.parseDouble(tokens[3]);
return new Color4d(r, g, b, 1);
}
}