Source: objects/loader/OBJLoader.js

/**
 * jslint browser: true
 */

/**
 * Creates new loader for a wavefront object (*.obj)
 * @class
 * @param pathString
 * @returns {Ayce.Object3D[]} object
 * @constructor
 */
Ayce.OBJLoader = function (pathString) {
    //Modified version of THREE.js' OBJLoader
    var pathFile = /(.*[\/|\\])(.*)/;
    var path = pathFile.exec(pathString)[1];
    var file = pathFile.exec(pathString)[2];
    var objTxt = Ayce.XMLLoader.getSourceSynch(path+file);
    var mtlName = this.getMtlName(objTxt);
    var mtlTxt = Ayce.XMLLoader.getSourceSynch(path+mtlName);

//    console.log("Processing .obj file");
    var objs = this.readObj(objTxt);
//    console.log("Processing .mtl file");
    var mtl = mtlName ? this.readMtl(mtlTxt) : null;
//    console.log("Creating O3D(s) from .obj");

    var o3Ds = [];
    for(var i=0; i<objs.length; i++){
        var o3d = this.createO3DFromOBJ(objs[i], mtl, path);
        var name = objs[i].name;
//        console.log("Name: " + name);
        o3Ds.push(o3d);
        o3Ds[name] = o3d;
    }
//    console.log("OBJ load finished. Containing " + o3Ds.length + " Object(s).");
    return o3Ds;
};

Ayce.OBJLoader.prototype = {
    /**
     * TODO: Description
     * @param {String} objTxt
     * @return objCollection
     */
    readObj: function(objTxt){
        var vertex_pattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
        // vn float float float
        var normal_pattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
        // vt float float
        var uv_pattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
        // f vertex vertex vertex ...
        var face_pattern1 = /f( +-?\d+)( +-?\d+)( +-?\d+)( +-?\d+)?/;
        // f vertex/uv vertex/uv vertex/uv ...
        var face_pattern2 = /f( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))?/;
        // f vertex/uv/normal vertex/uv/normal vertex/uv/normal ...
        var face_pattern3 = /f( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))?/;
        // f vertex//normal vertex//normal vertex//normal ...
        var face_pattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))?/;

        var objCollection = [];
        var currentObj = {};
        currentObj.name = "";
        currentObj.vertices = [];
        currentObj.normals = [];
        currentObj.uvs = [];
        currentObj.faces = [];

        var currentMtl;

        var addFace = function(vertexPos, vertexUV, VertexNormal){
            // VertexPos/VertexUV/VertexNormal
            if(vertexPos[3] !== undefined)throw("obj quad rendering not supported");

            var face = [];
            for(var i=0; i<3; i++){
                var pos     = vertexPos[i];
                var uv      = vertexUV      ? vertexUV[i]     : undefined;
                var normal  = VertexNormal  ? VertexNormal[i] : undefined;
                face.push([pos, uv, normal]);
            }
            face.push(currentMtl);
            currentObj.faces.push(face);
        };

        var lines = objTxt.split('\n');
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i];
            line = line.trim();

            var result;

            if (line.length === 0 || line.charAt(0) === '#') {
                continue;
            }
            // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
            else if ((result = vertex_pattern.exec(line)) !== null) {

                currentObj.vertices.push([
                        parseFloat(result[1]),
                        parseFloat(result[2]),
                        parseFloat(result[3])]
                );

            }
            // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
            else if ((result = normal_pattern.exec(line)) !== null) {

                currentObj.normals.push([
                        parseFloat(result[1]),
                        parseFloat(result[2]),
                        parseFloat(result[3])]
                );

            }
            // ["vt 0.1 0.2", "0.1", "0.2"]
            else if ((result = uv_pattern.exec(line)) !== null) {
                currentObj.uvs.push([parseFloat(result[1]), parseFloat(result[2])]);
            }
            // VertexPos
            // ["f 1 2 3", "1", "2", "3", undefined]
            else if ((result = face_pattern1.exec(line)) !== null) {

                addFace(
                    [result[1], result[2], result[3], result[4]]
                );
            }
            // VertexPos/VertexTexture
            // ["f 1/1 2/2 3/3", " 1/1", "1", "1", " 2/2", "2", "2", " 3/3", "3", "3", undefined, undefined, undefined]
            else if ((result = face_pattern2.exec(line)) !== null) {
                addFace(
                    [result[2], result[5], result[8], result[11]],
                    [result[3], result[6], result[9], result[12]]
                );

            }
            // VertexPos/VertexTexture/VertexNormal
            // ["f 1/1/1 2/2/2 3/3/3", " 1/1/1", "1", "1", "1", " 2/2/2", "2", "2", "2", " 3/3/3", "3", "3", "3", undefined, undefined, undefined, undefined]
            else if ((result = face_pattern3.exec(line)) !== null) {
                addFace(
                    [result[2], result[6], result[10], result[14]],
                    [result[3], result[7], result[11], result[15]],
                    [result[4], result[8], result[12], result[16]]
                );

            }
            // VertexPos//VertexNormal
            // ["f 1//1 2//2 3//3", " 1//1", "1", "1", " 2//2", "2", "2", " 3//3", "3", "3", undefined, undefined, undefined]
            else if ((result = face_pattern4.exec(line)) !== null) {
                addFace(
                    [result[2], result[5], result[8], result[11]],
                    undefined,
                    [result[3], result[6], result[9], result[12]]
                );

            } else if (/^o /.test(line) || /^g /.test(line)) {
                if(currentObj.faces.length > 0)currentObj.faces = this.getCorrectIndices(currentObj.faces);
                if(currentObj.faces.length > 0)objCollection.push(currentObj);
                currentObj = {};
                currentObj.name = line.substring(2).trim();
                currentObj.vertices = [];
                currentObj.normals = [];
                currentObj.uvs = [];
                currentObj.faces = [];
            } else if (/^g /.test(line)) {
                //TODO
            } else if (/^mtllib /.test(line)) {
                //ignore
            } else if (/^usemtl /.test(line)) {
                currentMtl = line.substring(7).trim();
            } else if (/^s /.test(line)) {
                //TODO smooth shading
            } else {
//                console.log( "Ayce.OBJLoader: Unhandled line " + line );
            }
        }

        if(objCollection.length > 0)currentObj.faces = this.getCorrectIndices(currentObj.faces);
        objCollection.push(currentObj);
        return objCollection;
    },
    /**
     * Returns material name
     * @param {String} mtlTxt
     * @return {String} name
     */
    getMtlName: function(mtlTxt){
        var lines = mtlTxt.split('\n');

        for (var i = 0; i < lines.length; i++) {
            var line = lines[i];
            line = line.trim();
            if (/^mtllib /.test(line)) {
                return(line.substring(7).trim());
            }
        }
        return null;
    },
    /**
     * Description
     * @param {String} mtlTxt
     * @return mtlContainer
     */
    readMtl: function(mtlTxt){
        var lines = mtlTxt.split('\n');

        var kd_pattern = /Kd ([\d|\.|\+|\-|e|E]+) ([\d|\.|\+|\-|e|E]+) ([\d|\.|\+|\-|e|E]+)/;

        var mtlContainer = {};

        var mtlIndex = -1;
        mtlContainer.mtlName = [];
        mtlContainer.kd = [];
        mtlContainer.mapKd = [];

        for (var i=0; i < lines.length; i++) {
            var line = lines[i];
            var result;

            if (/^newmtl /.test(line)) {
                mtlIndex++;
                mtlContainer.mtlName[mtlIndex] = line.substring(7).trim();
            }
            else if ((result = kd_pattern.exec(line)) !== null) {
                mtlContainer.kd[mtlIndex] = [
                    parseFloat(result[1]),
                    parseFloat(result[2]),
                    parseFloat(result[3]),
                    1.0 //TODO read alpha
                ];
            }
            else if (/^map_Kd /.test(line)) {
                mtlContainer.mapKd[mtlIndex] = line.substring(7).trim();
            }
        }

        return mtlContainer;
    },
    /**
     * TODO: Description
     * @param {} array
     * @return array
     */
    getCorrectIndices: function(array){
        var minPosInx = parseInt(array[0][0][0]);
        var minUvInx  = parseInt(array[0][0][1]);
        var minNorInx = parseInt(array[0][0][2]);

        for(var i=0; i<array.length; i++){
            for(var j=0; j<3; j++){
                if(parseInt(array[i][j][0]) < minPosInx)minPosInx = parseInt(array[i][j][0]);
                if(parseInt(array[i][j][1]) < minUvInx) minUvInx  = parseInt(array[i][j][1]);
                if(parseInt(array[i][j][2]) < minNorInx)minNorInx = parseInt(array[i][j][2]);
            }
        }
        minPosInx -= 1;
        minUvInx -= 1;
        minNorInx -= 1;

        for(var i=0; i<array.length; i++){
            for(var j=0; j<3; j++){
                array[i][j][0] -= minPosInx;
                array[i][j][1] -= minUvInx;
                array[i][j][2] -= minNorInx;
            }
        }
        return array;
    },
    /**
     * Creates Object3D from wavefront (*.obj)
     * @param {} obj
     * @param {} mtl
     * @param {} texturePath
     * @return {Ayce.Object3D} object3D
     */
    createO3DFromOBJ: function(obj, mtl, texturePath){
        var verticesO3D = [];
        var colorO3D    = [];
        var uvO3D       = [];
        var texIndexO3D = [];
        var normalsO3D  = [];
        var indicesO3D  = [];
        var materialsO3D = [];
        var index = 0;
        var useTexture = !mtl || mtl.mapKd.length > 0;
        var multiTexture = !mtl || (useTexture && mtl.mapKd.length > 1);

        for(var i=0; i<obj.faces.length; i++){
            //faces = [[Pos/Tex/Nor][Pos/Tex/Nor][Pos/Tex/Nor][textureName]] [...] [...]
            //face = [Pos/Tex/Nor][Pos/Tex/Nor][Pos/Tex/Nor][textureName]
            var face = obj.faces[i];

            var material;
            var materialIndex = 0;
            var texture = false;
            if(!mtl || (mtl.kd.length === 0 && mtl.mapKd.length === 0)){
                material = [0.5, 0.5, 0.5, 1.0];
            }
            else{
                var mtlIndex = mtl.mtlName.indexOf(face[3]);
                texture = Boolean(mtl.mapKd[mtlIndex]);
                material = texture ? mtl.mapKd[mtlIndex] : mtl.kd[mtlIndex];

                if(texture){
                    materialIndex = materialsO3D.indexOf(material);
                    if(materialIndex < 0){
                        materialsO3D.push(material);
                        materialIndex = materialsO3D.indexOf(material);
                    }
                }
            }

            for(var j=0; j<3; j++){
                var iPos = face[j][0]-1;
                var iTex = face[j][1]-1;
                var iNor = face[j][2]-1;

                var alreadyUsed = false;
                if(false){//TODO 
                    for(var m=0; m<indicesO3D.length; m++){
                        //Existing Values
                        //Position
                        var x = verticesO3D[m*3 + 0];
                        var y = verticesO3D[m*3 + 1];
                        var z = verticesO3D[m*3 + 2];

                        //UV
                        var u = uvO3D[m*2 + 0];
                        var v = uvO3D[m*2 + 1];

                        //color
                        var r = colorO3D[m*4 + 0];
                        var g = colorO3D[m*4 + 1];
                        var b = colorO3D[m*4 + 2];
                        var a = colorO3D[m*4 + 3];

                        //Texture Indice
                        var ti = texIndexO3D[m];

                        //Normal
                        var xN = normalsO3D[m*3 + 0];
                        var yN = normalsO3D[m*3 + 1];
                        var zN = normalsO3D[m*3 + 2];


                        //New Value
                        var vO  = obj.vertices[iPos];
                        var uvO = obj.uvs[iTex];
                        var cO = material;
                        var tiO = materialIndex;
                        var nO  = obj.normals[iNor];

                        var vertexBool  = vO  ? vO[0] === x && vO[1] === y && vO[2] === z : !(x || y || z);
                        var uvBool      = uvO ? uvO[0] === u && uvO[1] === v : !(u || v);
                        var normalBool  = nO  ? nO[0] === xN && nO[1] === yN && nO[2] === zN : !(xN || yN || zN);
                        var tiBool      = tiO === ti;
                        var matBool = Boolean(texture);
                        if(!matBool){
                            tiBool = true;
                            uvBool = true;
                            matBool = cO ? cO[0] === r && cO[1] === g && cO[2] === b && cO[3] === a : !(r || g || b || a);
                        }

                        if(vertexBool && uvBool && normalBool && tiBool && matBool){
                            indicesO3D.push(parseInt(m));
                            alreadyUsed = true;
                            m = indicesO3D.length;
                        }
                    }
                }

                if(!alreadyUsed){
                    Array.prototype.push.apply(verticesO3D, obj.vertices[iPos]);
                    Array.prototype.push.apply(normalsO3D, obj.normals[iNor]);
                    if(texture){
                        Array.prototype.push.apply(uvO3D, obj.uvs[iTex]);
                        Array.prototype.push.apply(colorO3D, [0.5, 0.5, 0.5, 2.0]);
                    }
                    else{
                        Array.prototype.push.apply(colorO3D, material);
                        //TODO
//                        Array.prototype.push.apply(uvO3D, [-1.0, -1.0]);
                        Array.prototype.push.apply(uvO3D, obj.uvs[iTex]);
                    }
                    texIndexO3D.push(materialIndex);
                    indicesO3D.push(index++);
                }
            }
        }

//        console.log("Vertices: " + verticesO3D.length/3);
//        console.log("Colors:   " + colorO3D.length/4);
//        console.log("UVs:      " + uvO3D.length/2);
//        console.log("Normals:  " + normalsO3D.length/3);
//        console.log("TexIndic: " + texIndexO3D.length);
//        console.log("Indices:  " + indicesO3D.length);
//        console.log("Highest Index:  " + index);
//        console.log("---");

        var object3D = new Ayce.Object3D();
        if(verticesO3D.length > 0)  object3D.vertices = verticesO3D;
        if(colorO3D.length > 0)     object3D.colors = colorO3D;
        if(uvO3D.length > 0)        object3D.textureCoords = uvO3D;
        if(multiTexture)            object3D.textureIndices = texIndexO3D;
        if(normalsO3D.length > 0)   object3D.normals = normalsO3D;
        if(indicesO3D.length > 0)   object3D.indices = indicesO3D;

        if(useTexture){
            for(var i=0; i<materialsO3D.length; i++){
                materialsO3D[i] = texturePath+materialsO3D[i].replace(/\\/g, '/');
            }
            if(materialsO3D && materialsO3D.length > 10)throw "OBJ Loader: Can't use more than 10 Textures";
            if(materialsO3D.length > 0)object3D.imageSrc = materialsO3D;
        }

        return object3D;
    },

    /**
     * TODO: description
     * @param originO3D
     * @returns {Ayce.Object3D}
     */
    copyOBJO3D: function(originO3D){
        var newO3D = new Ayce.Object3D();
        if(originO3D.vertices)      newO3D.vertices       = originO3D.vertices.slice();
        if(originO3D.colors)        newO3D.colors         = originO3D.colors.slice();
        if(originO3D.textureCoords) newO3D.textureCoords  = originO3D.textureCoords.slice();
        if(originO3D.textureIndices)newO3D.textureIndices = originO3D.textureIndices.slice();
        if(originO3D.normals)       newO3D.normals        = originO3D.normals.slice();
        if(originO3D.indices)       newO3D.indices        = originO3D.indices.slice();
        
        newO3D.imageSrc = originO3D.imageSrc;
        return newO3D;
    }
};