--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/js/OBJLoader.js Thu Apr 06 15:03:40 2017 +0200 @@ -0,0 +1,743 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +THREE.OBJLoader = function ( manager ) { + + this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; + + this.materials = null; + + this.regexp = { + // v float float float + vertex_pattern : /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, + // vn float float float + normal_pattern : /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, + // vt float float + uv_pattern : /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, + // f vertex vertex vertex + face_vertex : /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/, + // f vertex/uv vertex/uv vertex/uv + face_vertex_uv : /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/, + // f vertex/uv/normal vertex/uv/normal vertex/uv/normal + face_vertex_uv_normal : /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/, + // f vertex//normal vertex//normal vertex//normal + face_vertex_normal : /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/, + // o object_name | g group_name + object_pattern : /^[og]\s*(.+)?/, + // s boolean + smoothing_pattern : /^s\s+(\d+|on|off)/, + // mtllib file_reference + material_library_pattern : /^mtllib /, + // usemtl material_name + material_use_pattern : /^usemtl / + }; + +}; + +THREE.OBJLoader.prototype = { + + constructor: THREE.OBJLoader, + + load: function ( url, onLoad, onProgress, onError ) { + + var scope = this; + + var loader = new THREE.FileLoader( scope.manager ); + loader.setPath( this.path ); + loader.load( url, function ( text ) { + + onLoad( scope.parse( text ) ); + + }, onProgress, onError ); + + }, + + setPath: function ( value ) { + + this.path = value; + + }, + + setMaterials: function ( materials ) { + + this.materials = materials; + + }, + + _createParserState : function () { + + var state = { + objects : [], + object : {}, + + vertices : [], + normals : [], + uvs : [], + + materialLibraries : [], + + startObject: function ( name, fromDeclaration ) { + + // If the current object (initial from reset) is not from a g/o declaration in the parsed + // file. We need to use it for the first parsed g/o to keep things in sync. + if ( this.object && this.object.fromDeclaration === false ) { + + this.object.name = name; + this.object.fromDeclaration = ( fromDeclaration !== false ); + return; + + } + + var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined ); + + if ( this.object && typeof this.object._finalize === 'function' ) { + + this.object._finalize( true ); + + } + + this.object = { + name : name || '', + fromDeclaration : ( fromDeclaration !== false ), + + geometry : { + vertices : [], + normals : [], + uvs : [] + }, + materials : [], + smooth : true, + + startMaterial : function( name, libraries ) { + + var previous = this._finalize( false ); + + // New usemtl declaration overwrites an inherited material, except if faces were declared + // after the material, then it must be preserved for proper MultiMaterial continuation. + if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) { + + this.materials.splice( previous.index, 1 ); + + } + + var material = { + index : this.materials.length, + name : name || '', + mtllib : ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ), + smooth : ( previous !== undefined ? previous.smooth : this.smooth ), + groupStart : ( previous !== undefined ? previous.groupEnd : 0 ), + groupEnd : -1, + groupCount : -1, + inherited : false, + + clone : function( index ) { + var cloned = { + index : ( typeof index === 'number' ? index : this.index ), + name : this.name, + mtllib : this.mtllib, + smooth : this.smooth, + groupStart : 0, + groupEnd : -1, + groupCount : -1, + inherited : false + }; + cloned.clone = this.clone.bind(cloned); + return cloned; + } + }; + + this.materials.push( material ); + + return material; + + }, + + currentMaterial : function() { + + if ( this.materials.length > 0 ) { + return this.materials[ this.materials.length - 1 ]; + } + + return undefined; + + }, + + _finalize : function( end ) { + + var lastMultiMaterial = this.currentMaterial(); + if ( lastMultiMaterial && lastMultiMaterial.groupEnd === -1 ) { + + lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; + lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; + lastMultiMaterial.inherited = false; + + } + + // Ignore objects tail materials if no face declarations followed them before a new o/g started. + if ( end && this.materials.length > 1 ) { + + for ( var mi = this.materials.length - 1; mi >= 0; mi-- ) { + if ( this.materials[mi].groupCount <= 0 ) { + this.materials.splice( mi, 1 ); + } + } + + } + + // Guarantee at least one empty material, this makes the creation later more straight forward. + if ( end && this.materials.length === 0 ) { + + this.materials.push({ + name : '', + smooth : this.smooth + }); + + } + + return lastMultiMaterial; + + } + }; + + // Inherit previous objects material. + // Spec tells us that a declared material must be set to all objects until a new material is declared. + // If a usemtl declaration is encountered while this new object is being parsed, it will + // overwrite the inherited material. Exception being that there was already face declarations + // to the inherited material, then it will be preserved for proper MultiMaterial continuation. + + if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function" ) { + + var declared = previousMaterial.clone( 0 ); + declared.inherited = true; + this.object.materials.push( declared ); + + } + + this.objects.push( this.object ); + + }, + + finalize : function() { + + if ( this.object && typeof this.object._finalize === 'function' ) { + + this.object._finalize( true ); + + } + + }, + + parseVertexIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; + + }, + + parseNormalIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; + + }, + + parseUVIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; + + }, + + addVertex: function ( a, b, c ) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push( src[ a + 0 ] ); + dst.push( src[ a + 1 ] ); + dst.push( src[ a + 2 ] ); + dst.push( src[ b + 0 ] ); + dst.push( src[ b + 1 ] ); + dst.push( src[ b + 2 ] ); + dst.push( src[ c + 0 ] ); + dst.push( src[ c + 1 ] ); + dst.push( src[ c + 2 ] ); + + }, + + addVertexLine: function ( a ) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push( src[ a + 0 ] ); + dst.push( src[ a + 1 ] ); + dst.push( src[ a + 2 ] ); + + }, + + addNormal : function ( a, b, c ) { + + var src = this.normals; + var dst = this.object.geometry.normals; + + dst.push( src[ a + 0 ] ); + dst.push( src[ a + 1 ] ); + dst.push( src[ a + 2 ] ); + dst.push( src[ b + 0 ] ); + dst.push( src[ b + 1 ] ); + dst.push( src[ b + 2 ] ); + dst.push( src[ c + 0 ] ); + dst.push( src[ c + 1 ] ); + dst.push( src[ c + 2 ] ); + + }, + + addUV: function ( a, b, c ) { + + var src = this.uvs; + var dst = this.object.geometry.uvs; + + dst.push( src[ a + 0 ] ); + dst.push( src[ a + 1 ] ); + dst.push( src[ b + 0 ] ); + dst.push( src[ b + 1 ] ); + dst.push( src[ c + 0 ] ); + dst.push( src[ c + 1 ] ); + + }, + + addUVLine: function ( a ) { + + var src = this.uvs; + var dst = this.object.geometry.uvs; + + dst.push( src[ a + 0 ] ); + dst.push( src[ a + 1 ] ); + + }, + + addFace: function ( a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd ) { + + var vLen = this.vertices.length; + + var ia = this.parseVertexIndex( a, vLen ); + var ib = this.parseVertexIndex( b, vLen ); + var ic = this.parseVertexIndex( c, vLen ); + var id; + + if ( d === undefined ) { + + this.addVertex( ia, ib, ic ); + + } else { + + id = this.parseVertexIndex( d, vLen ); + + this.addVertex( ia, ib, id ); + this.addVertex( ib, ic, id ); + + } + + if ( ua !== undefined ) { + + var uvLen = this.uvs.length; + + ia = this.parseUVIndex( ua, uvLen ); + ib = this.parseUVIndex( ub, uvLen ); + ic = this.parseUVIndex( uc, uvLen ); + + if ( d === undefined ) { + + this.addUV( ia, ib, ic ); + + } else { + + id = this.parseUVIndex( ud, uvLen ); + + this.addUV( ia, ib, id ); + this.addUV( ib, ic, id ); + + } + + } + + if ( na !== undefined ) { + + // Normals are many times the same. If so, skip function call and parseInt. + var nLen = this.normals.length; + ia = this.parseNormalIndex( na, nLen ); + + ib = na === nb ? ia : this.parseNormalIndex( nb, nLen ); + ic = na === nc ? ia : this.parseNormalIndex( nc, nLen ); + + if ( d === undefined ) { + + this.addNormal( ia, ib, ic ); + + } else { + + id = this.parseNormalIndex( nd, nLen ); + + this.addNormal( ia, ib, id ); + this.addNormal( ib, ic, id ); + + } + + } + + }, + + addLineGeometry: function ( vertices, uvs ) { + + this.object.geometry.type = 'Line'; + + var vLen = this.vertices.length; + var uvLen = this.uvs.length; + + for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) { + + this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); + + } + + for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { + + this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); + + } + + } + + }; + + state.startObject( '', false ); + + return state; + + }, + + parse: function ( text ) { + + console.time( 'OBJLoader' ); + + var state = this._createParserState(); + + if ( text.indexOf( '\r\n' ) !== - 1 ) { + + // This is faster than String.split with regex that splits on both + text = text.replace( /\r\n/g, '\n' ); + + } + + if ( text.indexOf( '\\\n' ) !== - 1) { + + // join lines separated by a line continuation character (\) + text = text.replace( /\\\n/g, '' ); + + } + + var lines = text.split( '\n' ); + var line = '', lineFirstChar = '', lineSecondChar = ''; + var lineLength = 0; + var result = []; + + // Faster to just trim left side of the line. Use if available. + var trimLeft = ( typeof ''.trimLeft === 'function' ); + + for ( var i = 0, l = lines.length; i < l; i ++ ) { + + line = lines[ i ]; + + line = trimLeft ? line.trimLeft() : line.trim(); + + lineLength = line.length; + + if ( lineLength === 0 ) continue; + + lineFirstChar = line.charAt( 0 ); + + // @todo invoke passed in handler if any + if ( lineFirstChar === '#' ) continue; + + if ( lineFirstChar === 'v' ) { + + lineSecondChar = line.charAt( 1 ); + + if ( lineSecondChar === ' ' && ( result = this.regexp.vertex_pattern.exec( line ) ) !== null ) { + + // 0 1 2 3 + // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] + + state.vertices.push( + parseFloat( result[ 1 ] ), + parseFloat( result[ 2 ] ), + parseFloat( result[ 3 ] ) + ); + + } else if ( lineSecondChar === 'n' && ( result = this.regexp.normal_pattern.exec( line ) ) !== null ) { + + // 0 1 2 3 + // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] + + state.normals.push( + parseFloat( result[ 1 ] ), + parseFloat( result[ 2 ] ), + parseFloat( result[ 3 ] ) + ); + + } else if ( lineSecondChar === 't' && ( result = this.regexp.uv_pattern.exec( line ) ) !== null ) { + + // 0 1 2 + // ["vt 0.1 0.2", "0.1", "0.2"] + + state.uvs.push( + parseFloat( result[ 1 ] ), + parseFloat( result[ 2 ] ) + ); + + } else { + + throw new Error( "Unexpected vertex/normal/uv line: '" + line + "'" ); + + } + + } else if ( lineFirstChar === "f" ) { + + if ( ( result = this.regexp.face_vertex_uv_normal.exec( line ) ) !== null ) { + + // f vertex/uv/normal vertex/uv/normal vertex/uv/normal + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + // ["f 1/1/1 2/2/2 3/3/3", "1", "1", "1", "2", "2", "2", "3", "3", "3", undefined, undefined, undefined] + + state.addFace( + result[ 1 ], result[ 4 ], result[ 7 ], result[ 10 ], + result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], + result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] + ); + + } else if ( ( result = this.regexp.face_vertex_uv.exec( line ) ) !== null ) { + + // f vertex/uv vertex/uv vertex/uv + // 0 1 2 3 4 5 6 7 8 + // ["f 1/1 2/2 3/3", "1", "1", "2", "2", "3", "3", undefined, undefined] + + state.addFace( + result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], + result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] + ); + + } else if ( ( result = this.regexp.face_vertex_normal.exec( line ) ) !== null ) { + + // f vertex//normal vertex//normal vertex//normal + // 0 1 2 3 4 5 6 7 8 + // ["f 1//1 2//2 3//3", "1", "1", "2", "2", "3", "3", undefined, undefined] + + state.addFace( + result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], + undefined, undefined, undefined, undefined, + result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] + ); + + } else if ( ( result = this.regexp.face_vertex.exec( line ) ) !== null ) { + + // f vertex vertex vertex + // 0 1 2 3 4 + // ["f 1 2 3", "1", "2", "3", undefined] + + state.addFace( + result[ 1 ], result[ 2 ], result[ 3 ], result[ 4 ] + ); + + } else { + + throw new Error( "Unexpected face line: '" + line + "'" ); + + } + + } else if ( lineFirstChar === "l" ) { + + var lineParts = line.substring( 1 ).trim().split( " " ); + var lineVertices = [], lineUVs = []; + + if ( line.indexOf( "/" ) === - 1 ) { + + lineVertices = lineParts; + + } else { + + for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) { + + var parts = lineParts[ li ].split( "/" ); + + if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] ); + if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] ); + + } + + } + state.addLineGeometry( lineVertices, lineUVs ); + + } else if ( ( result = this.regexp.object_pattern.exec( line ) ) !== null ) { + + // o object_name + // or + // g group_name + + // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 + // var name = result[ 0 ].substr( 1 ).trim(); + var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 ); + + state.startObject( name ); + + } else if ( this.regexp.material_use_pattern.test( line ) ) { + + // material + + state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); + + } else if ( this.regexp.material_library_pattern.test( line ) ) { + + // mtl file + + state.materialLibraries.push( line.substring( 7 ).trim() ); + + } else if ( ( result = this.regexp.smoothing_pattern.exec( line ) ) !== null ) { + + // smooth shading + + // @todo Handle files that have varying smooth values for a set of faces inside one geometry, + // but does not define a usemtl for each face set. + // This should be detected and a dummy material created (later MultiMaterial and geometry groups). + // This requires some care to not create extra material on each smooth value for "normal" obj files. + // where explicit usemtl defines geometry groups. + // Example asset: examples/models/obj/cerberus/Cerberus.obj + + var value = result[ 1 ].trim().toLowerCase(); + state.object.smooth = ( value === '1' || value === 'on' ); + + var material = state.object.currentMaterial(); + if ( material ) { + + material.smooth = state.object.smooth; + + } + + } else { + + // Handle null terminated files without exception + if ( line === '\0' ) continue; + + throw new Error( "Unexpected line: '" + line + "'" ); + + } + + } + + state.finalize(); + + var container = new THREE.Group(); + container.materialLibraries = [].concat( state.materialLibraries ); + + for ( var i = 0, l = state.objects.length; i < l; i ++ ) { + + var object = state.objects[ i ]; + var geometry = object.geometry; + var materials = object.materials; + var isLine = ( geometry.type === 'Line' ); + + // Skip o/g line declarations that did not follow with any faces + if ( geometry.vertices.length === 0 ) continue; + + var buffergeometry = new THREE.BufferGeometry(); + + buffergeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( geometry.vertices ), 3 ) ); + + if ( geometry.normals.length > 0 ) { + + buffergeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( geometry.normals ), 3 ) ); + + } else { + + buffergeometry.computeVertexNormals(); + + } + + if ( geometry.uvs.length > 0 ) { + + buffergeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( geometry.uvs ), 2 ) ); + + } + + // Create materials + + var createdMaterials = []; + + for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { + + var sourceMaterial = materials[mi]; + var material = undefined; + + if ( this.materials !== null ) { + + material = this.materials.create( sourceMaterial.name ); + + // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. + if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) { + + var materialLine = new THREE.LineBasicMaterial(); + materialLine.copy( material ); + material = materialLine; + + } + + } + + if ( ! material ) { + + material = ( ! isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial() ); + material.name = sourceMaterial.name; + + } + + material.shading = sourceMaterial.smooth ? THREE.SmoothShading : THREE.FlatShading; + + createdMaterials.push(material); + + } + + // Create mesh + + var mesh; + + if ( createdMaterials.length > 1 ) { + + for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { + + var sourceMaterial = materials[mi]; + buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); + + } + + var multiMaterial = new THREE.MultiMaterial( createdMaterials ); + mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, multiMaterial ) : new THREE.LineSegments( buffergeometry, multiMaterial ) ); + + } else { + + mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ) : new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ) ); + } + + mesh.name = object.name; + + container.add( mesh ); + + } + + console.timeEnd( 'OBJLoader' ); + + return container; + + } + +};