package processing.core;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* This class is not part of the Processing API and should not be used
* directly. Instead, use loadShape() and methods like it, which will make
* use of this class. Using this class directly will cause your code to break
* when combined with future versions of Processing.
*
* OBJ loading implemented using code from Saito's OBJLoader library:
* http://code.google.com/p/saitoobjloader/
* and OBJReader from Ahmet Kizilay
* http://www.openprocessing.org/visuals/?visualID=191
*
*/
public class PShapeOBJ extends PShape {
/**
* Initializes a new OBJ Object with the given filename.
* @param parent
* @param filename
*/
public PShapeOBJ(PApplet parent, String filename) {
this(parent, parent.createReader(filename), getBasePath(parent, filename));
}
/**
*
* @param parent
* @param reader
*/
public PShapeOBJ(PApplet parent, BufferedReader reader) {
this(parent, reader, "");
}
/**
*
* @param parent
* @param reader
* @param basePath
*/
public PShapeOBJ(PApplet parent, BufferedReader reader, String basePath) {
ArrayList faces = new ArrayList<>();
ArrayList materials = new ArrayList<>();
ArrayList coords = new ArrayList<>();
ArrayList normals = new ArrayList<>();
ArrayList texcoords = new ArrayList<>();
parseOBJ(parent, basePath, reader,
faces, materials, coords, normals, texcoords);
// The OBJ geometry is stored with each face in a separate child shape.
parent = null;
family = GROUP;
addChildren(faces, materials, coords, normals, texcoords);
}
/**
*
* @param face
* @param mtl
* @param coords
* @param normals
* @param texcoords
*/
protected PShapeOBJ(OBJFace face, OBJMaterial mtl,
ArrayList coords,
ArrayList normals,
ArrayList texcoords) {
family = GEOMETRY;
switch (face.vertIdx.size()) {
case 3:
kind = TRIANGLES;
break;
case 4:
kind = QUADS;
break;
default:
kind = POLYGON;
break;
}
stroke = false;
fill = true;
// Setting material properties for the new face
fillColor = rgbaValue(mtl.kd);
ambientColor = rgbaValue(mtl.ka);
specularColor = rgbaValue(mtl.ks);
shininess = mtl.ns;
if (mtl.kdMap != null) {
// If current material is textured, then tinting the texture using the
// diffuse color.
tintColor = rgbaValue(mtl.kd, mtl.d);
}
vertexCount = face.vertIdx.size();
vertices = new float[vertexCount][12];
for (int j = 0; j < face.vertIdx.size(); j++){
int vertIdx, normIdx, texIdx;
PVector vert, norms, tex;
vert = norms = tex = null;
vertIdx = face.vertIdx.get(j) - 1;
vert = coords.get(vertIdx);
if (j < face.normIdx.size()) {
normIdx = face.normIdx.get(j) - 1;
if (-1 < normIdx) {
norms = normals.get(normIdx);
}
}
if (j < face.texIdx.size()) {
texIdx = face.texIdx.get(j) - 1;
if (-1 < texIdx) {
tex = texcoords.get(texIdx);
}
}
vertices[j][X] = vert.x;
vertices[j][Y] = vert.y;
vertices[j][Z] = vert.z;
vertices[j][PGraphics.R] = mtl.kd.x;
vertices[j][PGraphics.G] = mtl.kd.y;
vertices[j][PGraphics.B] = mtl.kd.z;
vertices[j][PGraphics.A] = 1;
if (norms != null) {
vertices[j][PGraphics.NX] = norms.x;
vertices[j][PGraphics.NY] = norms.y;
vertices[j][PGraphics.NZ] = norms.z;
}
if (tex != null) {
vertices[j][PGraphics.U] = tex.x;
vertices[j][PGraphics.V] = tex.y;
}
if (mtl != null && mtl.kdMap != null) {
image = mtl.kdMap;
}
}
}
/**
*
* @param faces
* @param materials
* @param coords
* @param normals
* @param texcoords
*/
protected void addChildren(ArrayList faces,
ArrayList materials,
ArrayList coords,
ArrayList normals,
ArrayList texcoords) {
int mtlIdxCur = -1;
OBJMaterial mtl = null;
for (int i = 0; i < faces.size(); i++) {
OBJFace face = faces.get(i);
// Getting current material.
if (mtlIdxCur != face.matIdx || face.matIdx == -1) {
// To make sure that at least we get the default material
mtlIdxCur = PApplet.max(0, face.matIdx);
mtl = materials.get(mtlIdxCur);
}
// Creating child shape for current face.
PShape child = new PShapeOBJ(face, mtl, coords, normals, texcoords);
addChild(child);
}
}
/**
*
* @param parent
* @param path
* @param reader
* @param faces
* @param materials
* @param coords
* @param normals
* @param texcoords
*/
static protected void parseOBJ(PApplet parent, String path,
BufferedReader reader,
ArrayList faces,
ArrayList materials,
ArrayList coords,
ArrayList normals,
ArrayList texcoords) {
Map mtlTable = new HashMap<>();
int mtlIdxCur = -1;
boolean readv, readvn, readvt;
try {
readv = readvn = readvt = false;
String line;
String gname = "object";
while ((line = reader.readLine()) != null) {
// Parse the line.
line = line.trim();
if (line.equals("") || line.indexOf('#') == 0) {
// Empty line of comment, ignore line
continue;
}
// The below patch/hack comes from Carlos Tomas Marti and is a
// fix for single backslashes in Rhino obj files
// BEGINNING OF RHINO OBJ FILES HACK
// Statements can be broken in multiple lines using '\' at the
// end of a line.
// In regular expressions, the backslash is also an escape
// character.
// The regular expression \\ matches a single backslash. This
// regular expression as a Java string, becomes "\\\\".
// That's right: 4 backslashes to match a single one.
while (line.contains("\\")) {
line = line.split("\\\\")[0];
final String s = reader.readLine();
if (s != null)
line += s;
}
// END OF RHINO OBJ FILES HACK
String[] parts = line.split("\\s+");
// if not a blank line, process the line.
if (parts.length > 0) {
switch (parts[0]) {
case "v":
{
// vertex
PVector tempv = new PVector(Float.parseFloat(parts[1]),
Float.parseFloat(parts[2]),
Float.parseFloat(parts[3]));
coords.add(tempv);
readv = true;
break;
}
case "vn":
// normal
PVector tempn = new PVector(Float.parseFloat(parts[1]),
Float.parseFloat(parts[2]),
Float.parseFloat(parts[3]));
normals.add(tempn);
readvn = true;
break;
case "vt":
{
// uv, inverting v to take into account Processing's inverted Y axis
// with respect to OpenGL.
PVector tempv = new PVector(Float.parseFloat(parts[1]),
1 - Float.parseFloat(parts[2]));
texcoords.add(tempv);
readvt = true;
break;
}
// Object name is ignored, for now.
case "o":
break;
case "mtllib":
if (parts[1] != null) {
String fn = parts[1];
if (!fn.contains(File.separator) && !path.equals("")) {
// Relative file name, adding the base path.
fn = path + File.separator + fn;
}
BufferedReader mreader = parent.createReader(fn);
if (mreader != null) {
parseMTL(parent, fn, path, mreader, materials, mtlTable);
mreader.close();
}
} break;
case "g":
gname = 1 < parts.length ? parts[1] : "";
break;
case "usemtl":
// Getting index of current active material (will be applied on
// all subsequent faces).
if (parts[1] != null) {
String mtlname = parts[1];
if (mtlTable.containsKey(mtlname)) {
Integer tempInt = mtlTable.get(mtlname);
mtlIdxCur = tempInt;
} else {
mtlIdxCur = -1;
}
} break;
case "f":
// Face setting
OBJFace face = new OBJFace();
face.matIdx = mtlIdxCur;
face.name = gname;
for (int i = 1; i < parts.length; i++) {
String seg = parts[i];
if (seg.indexOf("/") > 0) {
String[] forder = seg.split("/");
if (forder.length > 2) {
// Getting vertex and texture and normal indexes.
if (forder[0].length() > 0 && readv) {
face.vertIdx.add(Integer.valueOf(forder[0]));
}
if (forder[1].length() > 0 && readvt) {
face.texIdx.add(Integer.valueOf(forder[1]));
}
if (forder[2].length() > 0 && readvn) {
face.normIdx.add(Integer.valueOf(forder[2]));
}
} else if (forder.length > 1) {
// Getting vertex and texture/normal indexes.
if (forder[0].length() > 0 && readv) {
face.vertIdx.add(Integer.valueOf(forder[0]));
}
if (forder[1].length() > 0) {
if (readvt) {
face.texIdx.add(Integer.valueOf(forder[1]));
} else if (readvn) {
face.normIdx.add(Integer.valueOf(forder[1]));
}
}
} else if (forder.length > 0) {
// Getting vertex index only.
if (forder[0].length() > 0 && readv) {
face.vertIdx.add(Integer.valueOf(forder[0]));
}
}
} else {
// Getting vertex index only.
if (seg.length() > 0 && readv) {
face.vertIdx.add(Integer.valueOf(seg));
}
}
} faces.add(face);
break;
default:
break;
}
}
}
if (materials.isEmpty()) {
// No materials definition so far. Adding one default material.
OBJMaterial defMtl = new OBJMaterial();
materials.add(defMtl);
}
} catch (IOException | NumberFormatException e) {
}
}
/**
*
* @param parent
* @param mtlfn
* @param path
* @param reader
* @param materials
* @param materialsHash
*/
static protected void parseMTL(PApplet parent, String mtlfn, String path,
BufferedReader reader,
ArrayList materials,
Map materialsHash) {
try {
String line;
OBJMaterial currentMtl = null;
while ((line = reader.readLine()) != null) {
// Parse the line
line = line.trim();
String parts[] = line.split("\\s+");
if (parts.length > 0) {
// Extract the material data.
if (parts[0].equals("newmtl")) {
// Starting new material.
String mtlname = parts[1];
currentMtl = addMaterial(mtlname, materials, materialsHash);
} else {
if (currentMtl == null) {
currentMtl = addMaterial("material" + materials.size(),
materials, materialsHash);
}
if (parts[0].equals("map_Kd") && parts.length > 1) {
// Loading texture map.
String texname = parts[1];
if (!texname.contains(File.separator) && !path.equals("")) {
// Relative file name, adding the base path.
texname = path + File.separator + texname;
}
File file = new File(parent.dataPath(texname));
if (file.exists()) {
currentMtl.kdMap = parent.loadImage(texname);
} else {
System.err.println("The texture map \"" + texname + "\" " +
"in the materials definition file \"" + mtlfn + "\" " +
"is missing or inaccessible, make sure " +
"the URL is valid or that the file has been " +
"added to your sketch and is readable.");
}
} else if (parts[0].equals("Ka") && parts.length > 3) {
// The ambient color of the material
currentMtl.ka.x = Float.parseFloat(parts[1]);
currentMtl.ka.y = Float.parseFloat(parts[2]);
currentMtl.ka.z = Float.parseFloat(parts[3]);
} else if (parts[0].equals("Kd") && parts.length > 3) {
// The diffuse color of the material
currentMtl.kd.x = Float.parseFloat(parts[1]);
currentMtl.kd.y = Float.parseFloat(parts[2]);
currentMtl.kd.z = Float.parseFloat(parts[3]);
} else if (parts[0].equals("Ks") && parts.length > 3) {
// The specular color weighted by the specular coefficient
currentMtl.ks.x = Float.parseFloat(parts[1]);
currentMtl.ks.y = Float.parseFloat(parts[2]);
currentMtl.ks.z = Float.parseFloat(parts[3]);
} else if ((parts[0].equals("d") ||
parts[0].equals("Tr")) && parts.length > 1) {
// Reading the alpha transparency.
currentMtl.d = Float.parseFloat(parts[1]);
} else if (parts[0].equals("Ns") && parts.length > 1) {
// The specular component of the Phong shading model
currentMtl.ns = Float.parseFloat(parts[1]);
}
}
}
}
} catch (IOException | NumberFormatException e) {
}
}
/**
*
* @param mtlname
* @param materials
* @param materialsHash
* @return
*/
protected static OBJMaterial addMaterial(String mtlname,
ArrayList materials,
Map materialsHash) {
OBJMaterial currentMtl = new OBJMaterial(mtlname);
materialsHash.put(mtlname, materials.size());
materials.add(currentMtl);
return currentMtl;
}
/**
*
* @param color
* @return
*/
protected static int rgbaValue(PVector color) {
return 0xFF000000 | ((int)(color.x * 255) << 16) |
((int)(color.y * 255) << 8) |
(int)(color.z * 255);
}
/**
*
* @param color
* @param alpha
* @return
*/
protected static int rgbaValue(PVector color, float alpha) {
return ((int)(alpha * 255) << 24) |
((int)(color.x * 255) << 16) |
((int)(color.y * 255) << 8) |
(int)(color.z * 255);
}
// Stores a face from an OBJ file
/**
*
*/
static protected class OBJFace {
ArrayList vertIdx;
ArrayList texIdx;
ArrayList normIdx;
int matIdx;
String name;
OBJFace() {
vertIdx = new ArrayList<>();
texIdx = new ArrayList<>();
normIdx = new ArrayList<>();
matIdx = -1;
name = "";
}
}
/**
*
* @param parent
* @param filename
* @return
*/
static protected String getBasePath(PApplet parent, String filename) {
// Obtaining the path
File file = new File(parent.dataPath(filename));
if (!file.exists()) {
file = parent.sketchFile(filename);
}
String absolutePath = file.getAbsolutePath();
return absolutePath.substring(0,
absolutePath.lastIndexOf(File.separator));
}
// Stores a material defined in an MTL file.
/**
*
*/
static protected class OBJMaterial {
String name;
PVector ka;
PVector kd;
PVector ks;
float d;
float ns;
PImage kdMap;
OBJMaterial() {
this("default");
}
OBJMaterial(String name) {
this.name = name;
ka = new PVector(0.5f, 0.5f, 0.5f);
kd = new PVector(0.5f, 0.5f, 0.5f);
ks = new PVector(0.5f, 0.5f, 0.5f);
d = 1.0f;
ns = 0.0f;
kdMap = null;
}
}
}