package processing.core; import java.io.BufferedReader; import java.io.File; 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. * <p> * 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. */ public PShapeOBJ(PApplet parent, String filename) { this(parent, parent.createReader(filename), getBasePath(parent, filename)); } public PShapeOBJ(PApplet parent, BufferedReader reader) { this(parent, reader, ""); } public PShapeOBJ(PApplet parent, BufferedReader reader, String basePath) { ArrayList<OBJFace> faces = new ArrayList<OBJFace>(); ArrayList<OBJMaterial> materials = new ArrayList<OBJMaterial>(); ArrayList<PVector> coords = new ArrayList<PVector>(); ArrayList<PVector> normals = new ArrayList<PVector>(); ArrayList<PVector> texcoords = new ArrayList<PVector>(); 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); } protected PShapeOBJ(OBJFace face, OBJMaterial mtl, ArrayList<PVector> coords, ArrayList<PVector> normals, ArrayList<PVector> texcoords) { family = GEOMETRY; if (face.vertIdx.size() == 3) { kind = TRIANGLES; } else if (face.vertIdx.size() == 4) { kind = QUADS; } else { kind = POLYGON; } 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).intValue() - 1; vert = coords.get(vertIdx); if (j < face.normIdx.size()) { normIdx = face.normIdx.get(j).intValue() - 1; if (-1 < normIdx) { norms = normals.get(normIdx); } } if (j < face.texIdx.size()) { texIdx = face.texIdx.get(j).intValue() - 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; } } } protected void addChildren(ArrayList<OBJFace> faces, ArrayList<OBJMaterial> materials, ArrayList<PVector> coords, ArrayList<PVector> normals, ArrayList<PVector> 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); } } static protected void parseOBJ(PApplet parent, String path, BufferedReader reader, ArrayList<OBJFace> faces, ArrayList<OBJMaterial> materials, ArrayList<PVector> coords, ArrayList<PVector> normals, ArrayList<PVector> texcoords) { Map<String, Integer> mtlTable = new HashMap<String, Integer>(); 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) { if (parts[0].equals("v")) { // vertex PVector tempv = new PVector(Float.valueOf(parts[1]).floatValue(), Float.valueOf(parts[2]).floatValue(), Float.valueOf(parts[3]).floatValue()); coords.add(tempv); readv = true; } else if (parts[0].equals("vn")) { // normal PVector tempn = new PVector(Float.valueOf(parts[1]).floatValue(), Float.valueOf(parts[2]).floatValue(), Float.valueOf(parts[3]).floatValue()); normals.add(tempn); readvn = true; } else if (parts[0].equals("vt")) { // uv, inverting v to take into account Processing's inverted Y axis // with respect to OpenGL. PVector tempv = new PVector(Float.valueOf(parts[1]).floatValue(), 1 - Float.valueOf(parts[2]). floatValue()); texcoords.add(tempv); readvt = true; } else if (parts[0].equals("o")) { // Object name is ignored, for now. } else if (parts[0].equals("mtllib")) { if (parts[1] != null) { String fn = parts[1]; if (fn.indexOf(File.separator) == -1 && !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(); } } } else if (parts[0].equals("g")) { gname = 1 < parts.length ? parts[1] : ""; } else if (parts[0].equals("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.intValue(); } else { mtlIdxCur = -1; } } } else if (parts[0].equals("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); } } } if (materials.size() == 0) { // No materials definition so far. Adding one default material. OBJMaterial defMtl = new OBJMaterial(); materials.add(defMtl); } } catch (Exception e) { e.printStackTrace(); } } static protected void parseMTL(PApplet parent, String mtlfn, String path, BufferedReader reader, ArrayList<OBJMaterial> materials, Map<String, Integer> 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.indexOf(File.separator) == -1 && !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.valueOf(parts[1]).floatValue(); currentMtl.ka.y = Float.valueOf(parts[2]).floatValue(); currentMtl.ka.z = Float.valueOf(parts[3]).floatValue(); } else if (parts[0].equals("Kd") && parts.length > 3) { // The diffuse color of the material currentMtl.kd.x = Float.valueOf(parts[1]).floatValue(); currentMtl.kd.y = Float.valueOf(parts[2]).floatValue(); currentMtl.kd.z = Float.valueOf(parts[3]).floatValue(); } else if (parts[0].equals("Ks") && parts.length > 3) { // The specular color weighted by the specular coefficient currentMtl.ks.x = Float.valueOf(parts[1]).floatValue(); currentMtl.ks.y = Float.valueOf(parts[2]).floatValue(); currentMtl.ks.z = Float.valueOf(parts[3]).floatValue(); } else if ((parts[0].equals("d") || parts[0].equals("Tr")) && parts.length > 1) { // Reading the alpha transparency. currentMtl.d = Float.valueOf(parts[1]).floatValue(); } else if (parts[0].equals("Ns") && parts.length > 1) { // The specular component of the Phong shading model currentMtl.ns = Float.valueOf(parts[1]).floatValue(); } } } } } catch (Exception e) { e.printStackTrace(); } } protected static OBJMaterial addMaterial(String mtlname, ArrayList<OBJMaterial> materials, Map<String, Integer> materialsHash) { OBJMaterial currentMtl = new OBJMaterial(mtlname); materialsHash.put(mtlname, Integer.valueOf(materials.size())); materials.add(currentMtl); return currentMtl; } protected static int rgbaValue(PVector color) { return 0xFF000000 | ((int)(color.x * 255) << 16) | ((int)(color.y * 255) << 8) | (int)(color.z * 255); } 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<Integer> vertIdx; ArrayList<Integer> texIdx; ArrayList<Integer> normIdx; int matIdx; String name; OBJFace() { vertIdx = new ArrayList<Integer>(); texIdx = new ArrayList<Integer>(); normIdx = new ArrayList<Integer>(); matIdx = -1; name = ""; } } 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; } } }