1 /* 2 * Copyright (c) 2009-2010 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.jme3.scene.plugins; 34 35 import com.jme3.asset.*; 36 import com.jme3.material.Material; 37 import com.jme3.material.MaterialList; 38 import com.jme3.math.Vector2f; 39 import com.jme3.math.Vector3f; 40 import com.jme3.renderer.queue.RenderQueue.Bucket; 41 import com.jme3.scene.Mesh.Mode; 42 import com.jme3.scene.*; 43 import com.jme3.scene.VertexBuffer.Type; 44 import com.jme3.scene.mesh.IndexBuffer; 45 import com.jme3.scene.mesh.IndexIntBuffer; 46 import com.jme3.scene.mesh.IndexShortBuffer; 47 import com.jme3.util.BufferUtils; 48 import com.jme3.util.IntMap; 49 import java.io.File; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.nio.FloatBuffer; 53 import java.nio.IntBuffer; 54 import java.nio.ShortBuffer; 55 import java.util.Map.Entry; 56 import java.util.*; 57 import java.util.logging.Level; 58 import java.util.logging.Logger; 59 60 /** 61 * Reads OBJ format models. 62 */ 63 public final class OBJLoader implements AssetLoader { 64 65 private static final Logger logger = Logger.getLogger(OBJLoader.class.getName()); 66 67 protected final ArrayList<Vector3f> verts = new ArrayList<Vector3f>(); 68 protected final ArrayList<Vector2f> texCoords = new ArrayList<Vector2f>(); 69 protected final ArrayList<Vector3f> norms = new ArrayList<Vector3f>(); 70 71 protected final ArrayList<Face> faces = new ArrayList<Face>(); 72 protected final HashMap<String, ArrayList<Face>> matFaces = new HashMap<String, ArrayList<Face>>(); 73 74 protected String currentMatName; 75 protected String currentObjectName; 76 77 protected final HashMap<Vertex, Integer> vertIndexMap = new HashMap<Vertex, Integer>(100); 78 protected final IntMap<Vertex> indexVertMap = new IntMap<Vertex>(100); 79 protected int curIndex = 0; 80 protected int objectIndex = 0; 81 protected int geomIndex = 0; 82 83 protected Scanner scan; 84 protected ModelKey key; 85 protected AssetManager assetManager; 86 protected MaterialList matList; 87 88 protected String objName; 89 protected Node objNode; 90 91 protected static class Vertex { 92 93 Vector3f v; 94 Vector2f vt; 95 Vector3f vn; 96 int index; 97 98 @Override equals(Object obj)99 public boolean equals(Object obj) { 100 if (obj == null) { 101 return false; 102 } 103 if (getClass() != obj.getClass()) { 104 return false; 105 } 106 final Vertex other = (Vertex) obj; 107 if (this.v != other.v && (this.v == null || !this.v.equals(other.v))) { 108 return false; 109 } 110 if (this.vt != other.vt && (this.vt == null || !this.vt.equals(other.vt))) { 111 return false; 112 } 113 if (this.vn != other.vn && (this.vn == null || !this.vn.equals(other.vn))) { 114 return false; 115 } 116 return true; 117 } 118 119 @Override hashCode()120 public int hashCode() { 121 int hash = 5; 122 hash = 53 * hash + (this.v != null ? this.v.hashCode() : 0); 123 hash = 53 * hash + (this.vt != null ? this.vt.hashCode() : 0); 124 hash = 53 * hash + (this.vn != null ? this.vn.hashCode() : 0); 125 return hash; 126 } 127 } 128 129 protected static class Face { 130 Vertex[] verticies; 131 } 132 133 protected class ObjectGroup { 134 135 final String objectName; 136 ObjectGroup(String objectName)137 public ObjectGroup(String objectName){ 138 this.objectName = objectName; 139 } 140 createGeometry()141 public Spatial createGeometry(){ 142 Node groupNode = new Node(objectName); 143 144 // if (matFaces.size() > 0){ 145 // for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){ 146 // ArrayList<Face> materialFaces = entry.getValue(); 147 // if (materialFaces.size() > 0){ 148 // Geometry geom = createGeometry(materialFaces, entry.getKey()); 149 // objNode.attachChild(geom); 150 // } 151 // } 152 // }else if (faces.size() > 0){ 153 // // generate final geometry 154 // Geometry geom = createGeometry(faces, null); 155 // objNode.attachChild(geom); 156 // } 157 158 return groupNode; 159 } 160 } 161 reset()162 public void reset(){ 163 verts.clear(); 164 texCoords.clear(); 165 norms.clear(); 166 faces.clear(); 167 matFaces.clear(); 168 169 vertIndexMap.clear(); 170 indexVertMap.clear(); 171 172 currentMatName = null; 173 matList = null; 174 curIndex = 0; 175 geomIndex = 0; 176 scan = null; 177 } 178 findVertexIndex(Vertex vert)179 protected void findVertexIndex(Vertex vert){ 180 Integer index = vertIndexMap.get(vert); 181 if (index != null){ 182 vert.index = index.intValue(); 183 }else{ 184 vert.index = curIndex++; 185 vertIndexMap.put(vert, vert.index); 186 indexVertMap.put(vert.index, vert); 187 } 188 } 189 quadToTriangle(Face f)190 protected Face[] quadToTriangle(Face f){ 191 assert f.verticies.length == 4; 192 193 Face[] t = new Face[]{ new Face(), new Face() }; 194 t[0].verticies = new Vertex[3]; 195 t[1].verticies = new Vertex[3]; 196 197 Vertex v0 = f.verticies[0]; 198 Vertex v1 = f.verticies[1]; 199 Vertex v2 = f.verticies[2]; 200 Vertex v3 = f.verticies[3]; 201 202 // find the pair of verticies that is closest to each over 203 // v0 and v2 204 // OR 205 // v1 and v3 206 float d1 = v0.v.distanceSquared(v2.v); 207 float d2 = v1.v.distanceSquared(v3.v); 208 if (d1 < d2){ 209 // put an edge in v0, v2 210 t[0].verticies[0] = v0; 211 t[0].verticies[1] = v1; 212 t[0].verticies[2] = v3; 213 214 t[1].verticies[0] = v1; 215 t[1].verticies[1] = v2; 216 t[1].verticies[2] = v3; 217 }else{ 218 // put an edge in v1, v3 219 t[0].verticies[0] = v0; 220 t[0].verticies[1] = v1; 221 t[0].verticies[2] = v2; 222 223 t[1].verticies[0] = v0; 224 t[1].verticies[1] = v2; 225 t[1].verticies[2] = v3; 226 } 227 228 return t; 229 } 230 231 private ArrayList<Vertex> vertList = new ArrayList<Vertex>(); 232 readFace()233 protected void readFace(){ 234 Face f = new Face(); 235 vertList.clear(); 236 237 String line = scan.nextLine().trim(); 238 String[] verticies = line.split("\\s"); 239 for (String vertex : verticies){ 240 int v = 0; 241 int vt = 0; 242 int vn = 0; 243 244 String[] split = vertex.split("/"); 245 if (split.length == 1){ 246 v = Integer.parseInt(split[0].trim()); 247 }else if (split.length == 2){ 248 v = Integer.parseInt(split[0].trim()); 249 vt = Integer.parseInt(split[1].trim()); 250 }else if (split.length == 3 && !split[1].equals("")){ 251 v = Integer.parseInt(split[0].trim()); 252 vt = Integer.parseInt(split[1].trim()); 253 vn = Integer.parseInt(split[2].trim()); 254 }else if (split.length == 3){ 255 v = Integer.parseInt(split[0].trim()); 256 vn = Integer.parseInt(split[2].trim()); 257 } 258 259 Vertex vx = new Vertex(); 260 vx.v = verts.get(v - 1); 261 262 if (vt > 0) 263 vx.vt = texCoords.get(vt - 1); 264 265 if (vn > 0) 266 vx.vn = norms.get(vn - 1); 267 268 vertList.add(vx); 269 } 270 271 if (vertList.size() > 4 || vertList.size() <= 2) 272 logger.warning("Edge or polygon detected in OBJ. Ignored."); 273 274 f.verticies = new Vertex[vertList.size()]; 275 for (int i = 0; i < vertList.size(); i++){ 276 f.verticies[i] = vertList.get(i); 277 } 278 279 if (matList != null && matFaces.containsKey(currentMatName)){ 280 matFaces.get(currentMatName).add(f); 281 }else{ 282 faces.add(f); // faces that belong to the default material 283 } 284 } 285 readVector3()286 protected Vector3f readVector3(){ 287 Vector3f v = new Vector3f(); 288 289 v.set(Float.parseFloat(scan.next()), 290 Float.parseFloat(scan.next()), 291 Float.parseFloat(scan.next())); 292 293 return v; 294 } 295 readVector2()296 protected Vector2f readVector2(){ 297 Vector2f v = new Vector2f(); 298 299 String line = scan.nextLine().trim(); 300 String[] split = line.split("\\s"); 301 v.setX( Float.parseFloat(split[0].trim()) ); 302 v.setY( Float.parseFloat(split[1].trim()) ); 303 304 // v.setX(scan.nextFloat()); 305 // if (scan.hasNextFloat()){ 306 // v.setY(scan.nextFloat()); 307 // if (scan.hasNextFloat()){ 308 // scan.nextFloat(); // ignore 309 // } 310 // } 311 312 return v; 313 } 314 loadMtlLib(String name)315 protected void loadMtlLib(String name) throws IOException{ 316 if (!name.toLowerCase().endsWith(".mtl")) 317 throw new IOException("Expected .mtl file! Got: " + name); 318 319 // NOTE: Cut off any relative/absolute paths 320 name = new File(name).getName(); 321 AssetKey mtlKey = new AssetKey(key.getFolder() + name); 322 try { 323 matList = (MaterialList) assetManager.loadAsset(mtlKey); 324 } catch (AssetNotFoundException ex){ 325 logger.log(Level.WARNING, "Cannot locate {0} for model {1}", new Object[]{name, key}); 326 } 327 328 if (matList != null){ 329 // create face lists for every material 330 for (String matName : matList.keySet()){ 331 matFaces.put(matName, new ArrayList<Face>()); 332 } 333 } 334 } 335 nextStatement()336 protected boolean nextStatement(){ 337 try { 338 scan.skip(".*\r{0,1}\n"); 339 return true; 340 } catch (NoSuchElementException ex){ 341 // EOF 342 return false; 343 } 344 } 345 readLine()346 protected boolean readLine() throws IOException{ 347 if (!scan.hasNext()){ 348 return false; 349 } 350 351 String cmd = scan.next(); 352 if (cmd.startsWith("#")){ 353 // skip entire comment until next line 354 return nextStatement(); 355 }else if (cmd.equals("v")){ 356 // vertex position 357 verts.add(readVector3()); 358 }else if (cmd.equals("vn")){ 359 // vertex normal 360 norms.add(readVector3()); 361 }else if (cmd.equals("vt")){ 362 // texture coordinate 363 texCoords.add(readVector2()); 364 }else if (cmd.equals("f")){ 365 // face, can be triangle, quad, or polygon (unsupported) 366 readFace(); 367 }else if (cmd.equals("usemtl")){ 368 // use material from MTL lib for the following faces 369 currentMatName = scan.next(); 370 // if (!matList.containsKey(currentMatName)) 371 // throw new IOException("Cannot locate material " + currentMatName + " in MTL file!"); 372 373 }else if (cmd.equals("mtllib")){ 374 // specify MTL lib to use for this OBJ file 375 String mtllib = scan.nextLine().trim(); 376 loadMtlLib(mtllib); 377 }else if (cmd.equals("s") || cmd.equals("g")){ 378 return nextStatement(); 379 }else{ 380 // skip entire command until next line 381 logger.log(Level.WARNING, "Unknown statement in OBJ! {0}", cmd); 382 return nextStatement(); 383 } 384 385 return true; 386 } 387 createGeometry(ArrayList<Face> faceList, String matName)388 protected Geometry createGeometry(ArrayList<Face> faceList, String matName) throws IOException{ 389 if (faceList.isEmpty()) 390 throw new IOException("No geometry data to generate mesh"); 391 392 // Create mesh from the faces 393 Mesh mesh = constructMesh(faceList); 394 395 Geometry geom = new Geometry(objName + "-geom-" + (geomIndex++), mesh); 396 397 Material material = null; 398 if (matName != null && matList != null){ 399 // Get material from material list 400 material = matList.get(matName); 401 } 402 if (material == null){ 403 // create default material 404 material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md"); 405 material.setFloat("Shininess", 64); 406 } 407 geom.setMaterial(material); 408 if (material.isTransparent()) 409 geom.setQueueBucket(Bucket.Transparent); 410 else 411 geom.setQueueBucket(Bucket.Opaque); 412 413 if (material.getMaterialDef().getName().contains("Lighting") 414 && mesh.getFloatBuffer(Type.Normal) == null){ 415 logger.log(Level.WARNING, "OBJ mesh {0} doesn't contain normals! " 416 + "It might not display correctly", geom.getName()); 417 } 418 419 return geom; 420 } 421 constructMesh(ArrayList<Face> faceList)422 protected Mesh constructMesh(ArrayList<Face> faceList){ 423 Mesh m = new Mesh(); 424 m.setMode(Mode.Triangles); 425 426 boolean hasTexCoord = false; 427 boolean hasNormals = false; 428 429 ArrayList<Face> newFaces = new ArrayList<Face>(faceList.size()); 430 for (int i = 0; i < faceList.size(); i++){ 431 Face f = faceList.get(i); 432 433 for (Vertex v : f.verticies){ 434 findVertexIndex(v); 435 436 if (!hasTexCoord && v.vt != null) 437 hasTexCoord = true; 438 if (!hasNormals && v.vn != null) 439 hasNormals = true; 440 } 441 442 if (f.verticies.length == 4){ 443 Face[] t = quadToTriangle(f); 444 newFaces.add(t[0]); 445 newFaces.add(t[1]); 446 }else{ 447 newFaces.add(f); 448 } 449 } 450 451 FloatBuffer posBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 3); 452 FloatBuffer normBuf = null; 453 FloatBuffer tcBuf = null; 454 455 if (hasNormals){ 456 normBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 3); 457 m.setBuffer(VertexBuffer.Type.Normal, 3, normBuf); 458 } 459 if (hasTexCoord){ 460 tcBuf = BufferUtils.createFloatBuffer(vertIndexMap.size() * 2); 461 m.setBuffer(VertexBuffer.Type.TexCoord, 2, tcBuf); 462 } 463 464 IndexBuffer indexBuf = null; 465 if (vertIndexMap.size() >= 65536){ 466 // too many verticies: use intbuffer instead of shortbuffer 467 IntBuffer ib = BufferUtils.createIntBuffer(newFaces.size() * 3); 468 m.setBuffer(VertexBuffer.Type.Index, 3, ib); 469 indexBuf = new IndexIntBuffer(ib); 470 }else{ 471 ShortBuffer sb = BufferUtils.createShortBuffer(newFaces.size() * 3); 472 m.setBuffer(VertexBuffer.Type.Index, 3, sb); 473 indexBuf = new IndexShortBuffer(sb); 474 } 475 476 int numFaces = newFaces.size(); 477 for (int i = 0; i < numFaces; i++){ 478 Face f = newFaces.get(i); 479 if (f.verticies.length != 3) 480 continue; 481 482 Vertex v0 = f.verticies[0]; 483 Vertex v1 = f.verticies[1]; 484 Vertex v2 = f.verticies[2]; 485 486 posBuf.position(v0.index * 3); 487 posBuf.put(v0.v.x).put(v0.v.y).put(v0.v.z); 488 posBuf.position(v1.index * 3); 489 posBuf.put(v1.v.x).put(v1.v.y).put(v1.v.z); 490 posBuf.position(v2.index * 3); 491 posBuf.put(v2.v.x).put(v2.v.y).put(v2.v.z); 492 493 if (normBuf != null){ 494 if (v0.vn != null){ 495 normBuf.position(v0.index * 3); 496 normBuf.put(v0.vn.x).put(v0.vn.y).put(v0.vn.z); 497 normBuf.position(v1.index * 3); 498 normBuf.put(v1.vn.x).put(v1.vn.y).put(v1.vn.z); 499 normBuf.position(v2.index * 3); 500 normBuf.put(v2.vn.x).put(v2.vn.y).put(v2.vn.z); 501 } 502 } 503 504 if (tcBuf != null){ 505 if (v0.vt != null){ 506 tcBuf.position(v0.index * 2); 507 tcBuf.put(v0.vt.x).put(v0.vt.y); 508 tcBuf.position(v1.index * 2); 509 tcBuf.put(v1.vt.x).put(v1.vt.y); 510 tcBuf.position(v2.index * 2); 511 tcBuf.put(v2.vt.x).put(v2.vt.y); 512 } 513 } 514 515 int index = i * 3; // current face * 3 = current index 516 indexBuf.put(index, v0.index); 517 indexBuf.put(index+1, v1.index); 518 indexBuf.put(index+2, v2.index); 519 } 520 521 m.setBuffer(VertexBuffer.Type.Position, 3, posBuf); 522 // index buffer and others were set on creation 523 524 m.setStatic(); 525 m.updateBound(); 526 m.updateCounts(); 527 //m.setInterleaved(); 528 529 // clear data generated face statements 530 // to prepare for next mesh 531 vertIndexMap.clear(); 532 indexVertMap.clear(); 533 curIndex = 0; 534 535 return m; 536 } 537 538 @SuppressWarnings("empty-statement") load(AssetInfo info)539 public Object load(AssetInfo info) throws IOException{ 540 reset(); 541 542 key = (ModelKey) info.getKey(); 543 assetManager = info.getManager(); 544 objName = key.getName(); 545 546 String folderName = key.getFolder(); 547 String ext = key.getExtension(); 548 objName = objName.substring(0, objName.length() - ext.length() - 1); 549 if (folderName != null && folderName.length() > 0){ 550 objName = objName.substring(folderName.length()); 551 } 552 553 objNode = new Node(objName + "-objnode"); 554 555 if (!(info.getKey() instanceof ModelKey)) 556 throw new IllegalArgumentException("Model assets must be loaded using a ModelKey"); 557 558 InputStream in = null; 559 try { 560 in = info.openStream(); 561 562 scan = new Scanner(in); 563 scan.useLocale(Locale.US); 564 565 while (readLine()); 566 } finally { 567 if (in != null){ 568 in.close(); 569 } 570 } 571 572 if (matFaces.size() > 0){ 573 for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){ 574 ArrayList<Face> materialFaces = entry.getValue(); 575 if (materialFaces.size() > 0){ 576 Geometry geom = createGeometry(materialFaces, entry.getKey()); 577 objNode.attachChild(geom); 578 } 579 } 580 }else if (faces.size() > 0){ 581 // generate final geometry 582 Geometry geom = createGeometry(faces, null); 583 objNode.attachChild(geom); 584 } 585 586 if (objNode.getQuantity() == 1) 587 // only 1 geometry, so no need to send node 588 return objNode.getChild(0); 589 else 590 return objNode; 591 } 592 593 } 594