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
33package com.jme3.scene.plugins;
34
35import com.jme3.asset.*;
36import com.jme3.material.Material;
37import com.jme3.material.MaterialList;
38import com.jme3.math.Vector2f;
39import com.jme3.math.Vector3f;
40import com.jme3.renderer.queue.RenderQueue.Bucket;
41import com.jme3.scene.Mesh.Mode;
42import com.jme3.scene.*;
43import com.jme3.scene.VertexBuffer.Type;
44import com.jme3.scene.mesh.IndexBuffer;
45import com.jme3.scene.mesh.IndexIntBuffer;
46import com.jme3.scene.mesh.IndexShortBuffer;
47import com.jme3.util.BufferUtils;
48import com.jme3.util.IntMap;
49import java.io.File;
50import java.io.IOException;
51import java.io.InputStream;
52import java.nio.FloatBuffer;
53import java.nio.IntBuffer;
54import java.nio.ShortBuffer;
55import java.util.Map.Entry;
56import java.util.*;
57import java.util.logging.Level;
58import java.util.logging.Logger;
59
60/**
61 * Reads OBJ format models.
62 */
63public 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
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
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
137        public ObjectGroup(String objectName){
138            this.objectName = objectName;
139        }
140
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
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
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
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
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
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
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
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
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
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
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
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")
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