blob: 47165f6873f5630432c205a0bed7e0021561d46c [file] [log] [blame]
/*
* Copyright (c) 2009-2010 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene.plugins;
import com.jme3.asset.*;
import com.jme3.material.Material;
import com.jme3.material.MaterialList;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Mesh.Mode;
import com.jme3.scene.*;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.mesh.IndexBuffer;
import com.jme3.scene.mesh.IndexIntBuffer;
import com.jme3.scene.mesh.IndexShortBuffer;
import com.jme3.util.BufferUtils;
import com.jme3.util.IntMap;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.Map.Entry;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Reads OBJ format models.
*/
public final class OBJLoader implements AssetLoader {
private static final Logger logger = Logger.getLogger(OBJLoader.class.getName());
protected final ArrayList<Vector3f> verts = new ArrayList<Vector3f>();
protected final ArrayList<Vector2f> texCoords = new ArrayList<Vector2f>();
protected final ArrayList<Vector3f> norms = new ArrayList<Vector3f>();
protected final ArrayList<Face> faces = new ArrayList<Face>();
protected final HashMap<String, ArrayList<Face>> matFaces = new HashMap<String, ArrayList<Face>>();
protected String currentMatName;
protected String currentObjectName;
protected final HashMap<Vertex, Integer> vertIndexMap = new HashMap<Vertex, Integer>(100);
protected final IntMap<Vertex> indexVertMap = new IntMap<Vertex>(100);
protected int curIndex = 0;
protected int objectIndex = 0;
protected int geomIndex = 0;
protected Scanner scan;
protected ModelKey key;
protected AssetManager assetManager;
protected MaterialList matList;
protected String objName;
protected Node objNode;
protected static class Vertex {
Vector3f v;
Vector2f vt;
Vector3f vn;
int index;
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Vertex other = (Vertex) obj;
if (this.v != other.v && (this.v == null || !this.v.equals(other.v))) {
return false;
}
if (this.vt != other.vt && (this.vt == null || !this.vt.equals(other.vt))) {
return false;
}
if (this.vn != other.vn && (this.vn == null || !this.vn.equals(other.vn))) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 5;
hash = 53 * hash + (this.v != null ? this.v.hashCode() : 0);
hash = 53 * hash + (this.vt != null ? this.vt.hashCode() : 0);
hash = 53 * hash + (this.vn != null ? this.vn.hashCode() : 0);
return hash;
}
}
protected static class Face {
Vertex[] verticies;
}
protected class ObjectGroup {
final String objectName;
public ObjectGroup(String objectName){
this.objectName = objectName;
}
public Spatial createGeometry(){
Node groupNode = new Node(objectName);
// if (matFaces.size() > 0){
// for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){
// ArrayList<Face> materialFaces = entry.getValue();
// if (materialFaces.size() > 0){
// Geometry geom = createGeometry(materialFaces, entry.getKey());
// objNode.attachChild(geom);
// }
// }
// }else if (faces.size() > 0){
// // generate final geometry
// Geometry geom = createGeometry(faces, null);
// objNode.attachChild(geom);
// }
return groupNode;
}
}
public void reset(){
verts.clear();
texCoords.clear();
norms.clear();
faces.clear();
matFaces.clear();
vertIndexMap.clear();
indexVertMap.clear();
currentMatName = null;
matList = null;
curIndex = 0;
geomIndex = 0;
scan = null;
}
protected void findVertexIndex(Vertex vert){
Integer index = vertIndexMap.get(vert);
if (index != null){
vert.index = index.intValue();
}else{
vert.index = curIndex++;
vertIndexMap.put(vert, vert.index);
indexVertMap.put(vert.index, vert);
}
}
protected Face[] quadToTriangle(Face f){
assert f.verticies.length == 4;
Face[] t = new Face[]{ new Face(), new Face() };
t[0].verticies = new Vertex[3];
t[1].verticies = new Vertex[3];
Vertex v0 = f.verticies[0];
Vertex v1 = f.verticies[1];
Vertex v2 = f.verticies[2];
Vertex v3 = f.verticies[3];
// find the pair of verticies that is closest to each over
// v0 and v2
// OR
// v1 and v3
float d1 = v0.v.distanceSquared(v2.v);
float d2 = v1.v.distanceSquared(v3.v);
if (d1 < d2){
// put an edge in v0, v2
t[0].verticies[0] = v0;
t[0].verticies[1] = v1;
t[0].verticies[2] = v3;
t[1].verticies[0] = v1;
t[1].verticies[1] = v2;
t[1].verticies[2] = v3;
}else{
// put an edge in v1, v3
t[0].verticies[0] = v0;
t[0].verticies[1] = v1;
t[0].verticies[2] = v2;
t[1].verticies[0] = v0;
t[1].verticies[1] = v2;
t[1].verticies[2] = v3;
}
return t;
}
private ArrayList<Vertex> vertList = new ArrayList<Vertex>();
protected void readFace(){
Face f = new Face();
vertList.clear();
String line = scan.nextLine().trim();
String[] verticies = line.split("\\s");
for (String vertex : verticies){
int v = 0;
int vt = 0;
int vn = 0;
String[] split = vertex.split("/");
if (split.length == 1){
v = Integer.parseInt(split[0].trim());
}else if (split.length == 2){
v = Integer.parseInt(split[0].trim());
vt = Integer.parseInt(split[1].trim());
}else if (split.length == 3 && !split[1].equals("")){
v = Integer.parseInt(split[0].trim());
vt = Integer.parseInt(split[1].trim());
vn = Integer.parseInt(split[2].trim());
}else if (split.length == 3){
v = Integer.parseInt(split[0].trim());
vn = Integer.parseInt(split[2].trim());
}
Vertex vx = new Vertex();
vx.v = verts.get(v - 1);
if (vt > 0)
vx.vt = texCoords.get(vt - 1);
if (vn > 0)
vx.vn = norms.get(vn - 1);
vertList.add(vx);
}
if (vertList.size() > 4 || vertList.size() <= 2)
logger.warning("Edge or polygon detected in OBJ. Ignored.");
f.verticies = new Vertex[vertList.size()];
for (int i = 0; i < vertList.size(); i++){
f.verticies[i] = vertList.get(i);
}
if (matList != null && matFaces.containsKey(currentMatName)){
matFaces.get(currentMatName).add(f);
}else{
faces.add(f); // faces that belong to the default material
}
}
protected Vector3f readVector3(){
Vector3f v = new Vector3f();
v.set(Float.parseFloat(scan.next()),
Float.parseFloat(scan.next()),
Float.parseFloat(scan.next()));
return v;
}
protected Vector2f readVector2(){
Vector2f v = new Vector2f();
String line = scan.nextLine().trim();
String[] split = line.split("\\s");
v.setX( Float.parseFloat(split[0].trim()) );
v.setY( Float.parseFloat(split[1].trim()) );
// v.setX(scan.nextFloat());
// if (scan.hasNextFloat()){
// v.setY(scan.nextFloat());
// if (scan.hasNextFloat()){
// scan.nextFloat(); // ignore
// }
// }
return v;
}
protected void loadMtlLib(String name) throws IOException{
if (!name.toLowerCase().endsWith(".mtl"))
throw new IOException("Expected .mtl file! Got: " + name);
// NOTE: Cut off any relative/absolute paths
name = new File(name).getName();
AssetKey mtlKey = new AssetKey(key.getFolder() + name);
try {
matList = (MaterialList) assetManager.loadAsset(mtlKey);
} catch (AssetNotFoundException ex){
logger.log(Level.WARNING, "Cannot locate {0} for model {1}", new Object[]{name, key});
}
if (matList != null){
// create face lists for every material
for (String matName : matList.keySet()){
matFaces.put(matName, new ArrayList<Face>());
}
}
}
protected boolean nextStatement(){
try {
scan.skip(".*\r{0,1}\n");
return true;
} catch (NoSuchElementException ex){
// EOF
return false;
}
}
protected boolean readLine() throws IOException{
if (!scan.hasNext()){
return false;
}
String cmd = scan.next();
if (cmd.startsWith("#")){
// skip entire comment until next line
return nextStatement();
}else if (cmd.equals("v")){
// vertex position
verts.add(readVector3());
}else if (cmd.equals("vn")){
// vertex normal
norms.add(readVector3());
}else if (cmd.equals("vt")){
// texture coordinate
texCoords.add(readVector2());
}else if (cmd.equals("f")){
// face, can be triangle, quad, or polygon (unsupported)
readFace();
}else if (cmd.equals("usemtl")){
// use material from MTL lib for the following faces
currentMatName = scan.next();
// if (!matList.containsKey(currentMatName))
// throw new IOException("Cannot locate material " + currentMatName + " in MTL file!");
}else if (cmd.equals("mtllib")){
// specify MTL lib to use for this OBJ file
String mtllib = scan.nextLine().trim();
loadMtlLib(mtllib);
}else if (cmd.equals("s") || cmd.equals("g")){
return nextStatement();
}else{
// skip entire command until next line
logger.log(Level.WARNING, "Unknown statement in OBJ! {0}", cmd);
return nextStatement();
}
return true;
}
protected Geometry createGeometry(ArrayList<Face> faceList, String matName) throws IOException{
if (faceList.isEmpty())
throw new IOException("No geometry data to generate mesh");
// Create mesh from the faces
Mesh mesh = constructMesh(faceList);
Geometry geom = new Geometry(objName + "-geom-" + (geomIndex++), mesh);
Material material = null;
if (matName != null && matList != null){
// Get material from material list
material = matList.get(matName);
}
if (material == null){
// create default material
material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
material.setFloat("Shininess", 64);
}
geom.setMaterial(material);
if (material.isTransparent())
geom.setQueueBucket(Bucket.Transparent);
else
geom.setQueueBucket(Bucket.Opaque);
if (material.getMaterialDef().getName().contains("Lighting")
&& mesh.getFloatBuffer(Type.Normal) == null){
logger.log(Level.WARNING, "OBJ mesh {0} doesn't contain normals! "
+ "It might not display correctly", geom.getName());
}
return geom;
}
protected Mesh constructMesh(ArrayList<Face> faceList){
Mesh m = new Mesh();
m.setMode(Mode.Triangles);
boolean hasTexCoord = false;
boolean hasNormals = false;
ArrayList<Face> newFaces = new ArrayList<Face>(faceList.size());
for (int i = 0; i < faceList.size(); i++){
Face f = faceList.get(i);
for (Vertex v : f.verticies){
findVertexIndex(v);
if (!hasTexCoord && v.vt != null)
hasTexCoord = true;
if (!hasNormals && v.vn != null)
hasNormals = true;
}
if (f.verticies.length == 4){
Face[] t = quadToTriangle(f);
newFaces.add(t[0]);
newFaces.add(t[1]);
}else{
newFaces.add(f);
}
}
FloatBuffer posBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 3);
FloatBuffer normBuf = null;
FloatBuffer tcBuf = null;
if (hasNormals){
normBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 3);
m.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
}
if (hasTexCoord){
tcBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 2);
m.setBuffer(VertexBuffer.Type.TexCoord, 2, tcBuf);
}
IndexBuffer indexBuf = null;
if (vertIndexMap.size() >= 65536){
// too many verticies: use intbuffer instead of shortbuffer
IntBuffer ib = BufferUtils.createIntBuffer(newFaces.size() * 3);
m.setBuffer(VertexBuffer.Type.Index, 3, ib);
indexBuf = new IndexIntBuffer(ib);
}else{
ShortBuffer sb = BufferUtils.createShortBuffer(newFaces.size() * 3);
m.setBuffer(VertexBuffer.Type.Index, 3, sb);
indexBuf = new IndexShortBuffer(sb);
}
int numFaces = newFaces.size();
for (int i = 0; i < numFaces; i++){
Face f = newFaces.get(i);
if (f.verticies.length != 3)
continue;
Vertex v0 = f.verticies[0];
Vertex v1 = f.verticies[1];
Vertex v2 = f.verticies[2];
posBuf.position(v0.index * 3);
posBuf.put(v0.v.x).put(v0.v.y).put(v0.v.z);
posBuf.position(v1.index * 3);
posBuf.put(v1.v.x).put(v1.v.y).put(v1.v.z);
posBuf.position(v2.index * 3);
posBuf.put(v2.v.x).put(v2.v.y).put(v2.v.z);
if (normBuf != null){
if (v0.vn != null){
normBuf.position(v0.index * 3);
normBuf.put(v0.vn.x).put(v0.vn.y).put(v0.vn.z);
normBuf.position(v1.index * 3);
normBuf.put(v1.vn.x).put(v1.vn.y).put(v1.vn.z);
normBuf.position(v2.index * 3);
normBuf.put(v2.vn.x).put(v2.vn.y).put(v2.vn.z);
}
}
if (tcBuf != null){
if (v0.vt != null){
tcBuf.position(v0.index * 2);
tcBuf.put(v0.vt.x).put(v0.vt.y);
tcBuf.position(v1.index * 2);
tcBuf.put(v1.vt.x).put(v1.vt.y);
tcBuf.position(v2.index * 2);
tcBuf.put(v2.vt.x).put(v2.vt.y);
}
}
int index = i * 3; // current face * 3 = current index
indexBuf.put(index, v0.index);
indexBuf.put(index+1, v1.index);
indexBuf.put(index+2, v2.index);
}
m.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
// index buffer and others were set on creation
m.setStatic();
m.updateBound();
m.updateCounts();
//m.setInterleaved();
// clear data generated face statements
// to prepare for next mesh
vertIndexMap.clear();
indexVertMap.clear();
curIndex = 0;
return m;
}
@SuppressWarnings("empty-statement")
public Object load(AssetInfo info) throws IOException{
reset();
key = (ModelKey) info.getKey();
assetManager = info.getManager();
objName = key.getName();
String folderName = key.getFolder();
String ext = key.getExtension();
objName = objName.substring(0, objName.length() - ext.length() - 1);
if (folderName != null && folderName.length() > 0){
objName = objName.substring(folderName.length());
}
objNode = new Node(objName + "-objnode");
if (!(info.getKey() instanceof ModelKey))
throw new IllegalArgumentException("Model assets must be loaded using a ModelKey");
InputStream in = null;
try {
in = info.openStream();
scan = new Scanner(in);
scan.useLocale(Locale.US);
while (readLine());
} finally {
if (in != null){
in.close();
}
}
if (matFaces.size() > 0){
for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){
ArrayList<Face> materialFaces = entry.getValue();
if (materialFaces.size() > 0){
Geometry geom = createGeometry(materialFaces, entry.getKey());
objNode.attachChild(geom);
}
}
}else if (faces.size() > 0){
// generate final geometry
Geometry geom = createGeometry(faces, null);
objNode.attachChild(geom);
}
if (objNode.getQuantity() == 1)
// only 1 geometry, so no need to send node
return objNode.getChild(0);
else
return objNode;
}
}