text: var THREE = require('three')
  
  /**
   * Loads a Wavefront .mtl file specifying materials
   *
   * @author angelxuanchang
   */
  
  THREE.MTLLoader = function ( manager ) {
  
  	this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
  
  };
  
  THREE.MTLLoader.prototype = {
  
  	constructor: THREE.MTLLoader,
  
  	/**
  	 * Loads and parses a MTL asset from a URL.
  	 *
  	 * @param {String} url - URL to the MTL file.
  	 * @param {Function} [onLoad] - Callback invoked with the loaded object.
  	 * @param {Function} [onProgress] - Callback for download progress.
  	 * @param {Function} [onError] - Callback for download errors.
  	 *
  	 * @see setPath setTexturePath
  	 *
  	 * @note In order for relative texture references to resolve correctly
  	 * you must call setPath and/or setTexturePath explicitly prior to load.
  	 */
  	load: function ( url, onLoad, onProgress, onError ) {
  
  		var scope = this;
  
  		var loader = new THREE.FileLoader( this.manager );
  		loader.setPath( this.path );
  		loader.load( url, function ( text ) {
  
  			onLoad( scope.parse( text ) );
  
  		}, onProgress, onError );
  
  	},
  
  	/**
  	 * Set base path for resolving references.
  	 * If set this path will be prepended to each loaded and found reference.
  	 *
  	 * @see setTexturePath
  	 * @param {String} path
  	 * @return {THREE.MTLLoader}
  	 *
  	 * @example
  	 *     mtlLoader.setPath( 'assets/obj/' );
  	 *     mtlLoader.load( 'my.mtl', ... );
  	 */
  	setPath: function ( path ) {
  
  		this.path = path;
  		return this;
  
  	},
  
  	/**
  	 * Set base path for resolving texture references.
  	 * If set this path will be prepended found texture reference.
  	 * If not set and setPath is, it will be used as texture base path.
  	 *
  	 * @see setPath
  	 * @param {String} path
  	 * @return {THREE.MTLLoader}
  	 *
  	 * @example
  	 *     mtlLoader.setPath( 'assets/obj/' );
  	 *     mtlLoader.setTexturePath( 'assets/textures/' );
  	 *     mtlLoader.load( 'my.mtl', ... );
  	 */
  	setTexturePath: function ( path ) {
  
  		this.texturePath = path;
  		return this;
  
  	},
  
  	setBaseUrl: function ( path ) {
  
  		console.warn( 'THREE.MTLLoader: .setBaseUrl() is deprecated. Use .setTexturePath( path ) for texture path or .setPath( path ) for general base path instead.' );
  
  		return this.setTexturePath( path );
  
  	},
  
  	setCrossOrigin: function ( value ) {
  
  		this.crossOrigin = value;
  		return this;
  
  	},
  
  	setMaterialOptions: function ( value ) {
  
  		this.materialOptions = value;
  		return this;
  
  	},
  
  	/**
  	 * Parses a MTL file.
  	 *
  	 * @param {String} text - Content of MTL file
  	 * @return {THREE.MTLLoader.MaterialCreator}
  	 *
  	 * @see setPath setTexturePath
  	 *
  	 * @note In order for relative texture references to resolve correctly
  	 * you must call setPath and/or setTexturePath explicitly prior to parse.
  	 */
  	parse: function ( text ) {
  
  		var lines = text.split( '\n' );
  		var info = {};
  		var delimiter_pattern = /\s+/;
  		var materialsInfo = {};
  
  		for ( var i = 0; i < lines.length; i ++ ) {
  
  			var line = lines[ i ];
  			line = line.trim();
  
  			if ( line.length === 0 || line.charAt( 0 ) === '#' ) {
  
  				// Blank line or comment ignore
  				continue;
  
  			}
  
  			var pos = line.indexOf( ' ' );
  
  			var key = ( pos >= 0 ) ? line.substring( 0, pos ) : line;
  			key = key.toLowerCase();
  
  			var value = ( pos >= 0 ) ? line.substring( pos + 1 ) : '';
  			value = value.trim();
  
  			if ( key === 'newmtl' ) {
  
  				// New material
  
  				info = { name: value };
  				materialsInfo[ value ] = info;
  
  			} else if ( info ) {
  
  				if ( key === 'ka' || key === 'kd' || key === 'ks' ) {
  
  					var ss = value.split( delimiter_pattern, 3 );
  					info[ key ] = [ parseFloat( ss[ 0 ] ), parseFloat( ss[ 1 ] ), parseFloat( ss[ 2 ] ) ];
  
  				} else {
  
  					info[ key ] = value;
  
  				}
  
  			}
  
  		}
  
  		var materialCreator = new THREE.MTLLoader.MaterialCreator( this.texturePath || this.path, this.materialOptions );
  		materialCreator.setCrossOrigin( this.crossOrigin );
  		materialCreator.setManager( this.manager );
  		materialCreator.setMaterials( materialsInfo );
  		return materialCreator;
  
  	}
  
  };
  
  /**
   * Create a new THREE-MTLLoader.MaterialCreator
   * @param baseUrl - Url relative to which textures are loaded
   * @param options - Set of options on how to construct the materials
   *                  side: Which side to apply the material
   *                        THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide
   *                  wrap: What type of wrapping to apply for textures
   *                        THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping
   *                  normalizeRGB: RGBs need to be normalized to 0-1 from 0-255
   *                                Default: false, assumed to be already normalized
   *                  ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's
   *                                  Default: false
   * @constructor
   */
  
  THREE.MTLLoader.MaterialCreator = function ( baseUrl, options ) {
  
  	this.baseUrl = baseUrl || '';
  	this.options = options;
  	this.materialsInfo = {};
  	this.materials = {};
  	this.materialsArray = [];
  	this.nameLookup = {};
  
  	this.side = ( this.options && this.options.side ) ? this.options.side : THREE.FrontSide;
  	this.wrap = ( this.options && this.options.wrap ) ? this.options.wrap : THREE.RepeatWrapping;
  
  };
  
  THREE.MTLLoader.MaterialCreator.prototype = {
  
  	constructor: THREE.MTLLoader.MaterialCreator,
  
  	crossOrigin: 'Anonymous',
  
  	setCrossOrigin: function ( value ) {
  
  		this.crossOrigin = value;
  
  	},
  
  	setManager: function ( value ) {
  
  		this.manager = value;
  
  	},
  
  	setMaterials: function ( materialsInfo ) {
  
  		this.materialsInfo = this.convert( materialsInfo );
  		this.materials = {};
  		this.materialsArray = [];
  		this.nameLookup = {};
  
  	},
  
  	convert: function ( materialsInfo ) {
  
  		if ( ! this.options ) return materialsInfo;
  
  		var converted = {};
  
  		for ( var mn in materialsInfo ) {
  
  			// Convert materials info into normalized form based on options
  
  			var mat = materialsInfo[ mn ];
  
  			var covmat = {};
  
  			converted[ mn ] = covmat;
  
  			for ( var prop in mat ) {
  
  				var save = true;
  				var value = mat[ prop ];
  				var lprop = prop.toLowerCase();
  
  				switch ( lprop ) {
  
  					case 'kd':
  					case 'ka':
  					case 'ks':
  
  						// Diffuse color (color under white light) using RGB values
  
  						if ( this.options && this.options.normalizeRGB ) {
  
  							value = [ value[ 0 ] / 255, value[ 1 ] / 255, value[ 2 ] / 255 ];
  
  						}
  
  						if ( this.options && this.options.ignoreZeroRGBs ) {
  
  							if ( value[ 0 ] === 0 && value[ 1 ] === 0 && value[ 2 ] === 0 ) {
  
  								// ignore
  
  								save = false;
  
  							}
  
  						}
  
  						break;
  
  					default:
  
  						break;
  
  				}
  
  				if ( save ) {
  
  					covmat[ lprop ] = value;
  
  				}
  
  			}
  
  		}
  
  		return converted;
  
  	},
  
  	preload: function () {
  
  		for ( var mn in this.materialsInfo ) {
  
  			this.create( mn );
  
  		}
  
  	},
  
  	getIndex: function ( materialName ) {
  
  		return this.nameLookup[ materialName ];
  
  	},
  
  	getAsArray: function () {
  
  		var index = 0;
  
  		for ( var mn in this.materialsInfo ) {
  
  			this.materialsArray[ index ] = this.create( mn );
  			this.nameLookup[ mn ] = index;
  			index ++;
  
  		}
  
  		return this.materialsArray;
  
  	},
  
  	create: function ( materialName ) {
  
  		if ( this.materials[ materialName ] === undefined ) {
  
  			this.createMaterial_( materialName );
  
  		}
  
  		return this.materials[ materialName ];
  
  	},
  
  	createMaterial_: function ( materialName ) {
  
  		// Create material
  
  		var scope = this;
  		var mat = this.materialsInfo[ materialName ];
  		var params = {
  
  			name: materialName,
  			side: this.side
  
  		};
  
  		function resolveURL( baseUrl, url ) {
  
  			if ( typeof url !== 'string' || url === '' )
  				return '';
  
  			// Absolute URL
  			if ( /^https?:\/\//i.test( url ) ) return url;
  
  			return baseUrl + url;
  
  		}
  
  		function setMapForType( mapType, value ) {
  
  			if ( params[ mapType ] ) return; // Keep the first encountered texture
  
  			var texParams = scope.getTextureParams( value, params );
  			var map = scope.loadTexture( resolveURL( scope.baseUrl, texParams.url ) );
  
  			map.repeat.copy( texParams.scale );
  			map.offset.copy( texParams.offset );
  
  			map.wrapS = scope.wrap;
  			map.wrapT = scope.wrap;
  
  			params[ mapType ] = map;
  
  		}
  
  		for ( var prop in mat ) {
  
  			var value = mat[ prop ];
  			var n;
  
  			if ( value === '' ) continue;
  
  			switch ( prop.toLowerCase() ) {
  
  				// Ns is material specular exponent
  
  				case 'kd':
  
  					// Diffuse color (color under white light) using RGB values
  
  					params.color = new THREE.Color().fromArray( value );
  
  					break;
  
  				case 'ks':
  
  					// Specular color (color when light is reflected from shiny surface) using RGB values
  					params.specular = new THREE.Color().fromArray( value );
  
  					break;
  
  				case 'map_kd':
  
  					// Diffuse texture map
  
  					setMapForType( "map", value );
  
  					break;
  
  				case 'map_ks':
  
  					// Specular map
  
  					setMapForType( "specularMap", value );
  
  					break;
  
  				case 'norm':
  
  					setMapForType( "normalMap", value );
  
  					break;
  
  				case 'map_bump':
  				case 'bump':
  
  					// Bump texture map
  
  					setMapForType( "bumpMap", value );
  
  					break;
  
  				case 'ns':
  
  					// The specular exponent (defines the focus of the specular highlight)
  					// A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000.
  
  					params.shininess = parseFloat( value );
  
  					break;
  
  				case 'd':
  					n = parseFloat( value );
  
  					if ( n < 1 ) {
  
  						params.opacity = n;
  						params.transparent = true;
  
  					}
  
  					break;
  
  				case 'tr':
  					n = parseFloat( value );
  
  					if ( this.options && this.options.invertTrProperty ) n = 1 - n;
  
  					if ( n > 0 ) {
  
  						params.opacity = 1 - n;
  						params.transparent = true;
  
  					}
  
  					break;
  
  				default:
  					break;
  
  			}
  
  		}
  
  		this.materials[ materialName ] = new THREE.MeshPhongMaterial( params );
  		return this.materials[ materialName ];
  
  	},
  
  	getTextureParams: function ( value, matParams ) {
  
  		var texParams = {
  
  			scale: new THREE.Vector2( 1, 1 ),
  			offset: new THREE.Vector2( 0, 0 )
  
  		 };
  
  		var items = value.split( /\s+/ );
  		var pos;
  
  		pos = items.indexOf( '-bm' );
  
  		if ( pos >= 0 ) {
  
  			matParams.bumpScale = parseFloat( items[ pos + 1 ] );
  			items.splice( pos, 2 );
  
  		}
  
  		pos = items.indexOf( '-s' );
  
  		if ( pos >= 0 ) {
  
  			texParams.scale.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) );
  			items.splice( pos, 4 ); // we expect 3 parameters here!
  
  		}
  
  		pos = items.indexOf( '-o' );
  
  		if ( pos >= 0 ) {
  
  			texParams.offset.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) );
  			items.splice( pos, 4 ); // we expect 3 parameters here!
  
  		}
  
  		texParams.url = items.join( ' ' ).trim();
  		return texParams;
  
  	},
  
  	loadTexture: function ( url, mapping, onLoad, onProgress, onError ) {
  
  		var texture;
  		var loader = THREE.Loader.Handlers.get( url );
  		var manager = ( this.manager !== undefined ) ? this.manager : THREE.DefaultLoadingManager;
  
  		if ( loader === null ) {
  
  			loader = new THREE.TextureLoader( manager );
  
  		}
  
  		if ( loader.setCrossOrigin ) loader.setCrossOrigin( this.crossOrigin );
  		texture = loader.load( url, onLoad, onProgress, onError );
  
  		if ( mapping !== undefined ) texture.mapping = mapping;
  
  		return texture;
  
  	}
  
  };
  
  /**
   * @author mrdoob / http://mrdoob.com/
   */
  
  THREE.OBJLoader = ( function () {
  
  	// o object_name | g group_name
  	var object_pattern = /^[og]\s*(.+)?/;
  	// mtllib file_reference
  	var material_library_pattern = /^mtllib /;
  	// usemtl material_name
  	var material_use_pattern = /^usemtl /;
  	// usemap map_name
  	var map_use_pattern = /^usemap /;
  
  	function ParserState() {
  
  		var state = {
  			objects: [],
  			object: {},
  
  			vertices: [],
  			normals: [],
  			colors: [],
  			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: [],
  						colors: [],
  						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 ], src[ a + 1 ], src[ a + 2 ] );
  				dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
  				dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
  
  			},
  
  			addVertexPoint: function ( a ) {
  
  				var src = this.vertices;
  				var dst = this.object.geometry.vertices;
  
  				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
  
  			},
  
  			addVertexLine: function ( a ) {
  
  				var src = this.vertices;
  				var dst = this.object.geometry.vertices;
  
  				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
  
  			},
  
  			addNormal: function ( a, b, c ) {
  
  				var src = this.normals;
  				var dst = this.object.geometry.normals;
  
  				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
  				dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
  				dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
  
  			},
  
  			addColor: function ( a, b, c ) {
  
  				var src = this.colors;
  				var dst = this.object.geometry.colors;
  
  				dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
  				dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
  				dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
  
  			},
  
  			addUV: function ( a, b, c ) {
  
  				var src = this.uvs;
  				var dst = this.object.geometry.uvs;
  
  				dst.push( src[ a + 0 ], src[ a + 1 ] );
  				dst.push( src[ b + 0 ], src[ b + 1 ] );
  				dst.push( src[ c + 0 ], src[ c + 1 ] );
  
  			},
  
  			addUVLine: function ( a ) {
  
  				var src = this.uvs;
  				var dst = this.object.geometry.uvs;
  
  				dst.push( src[ a + 0 ], src[ a + 1 ] );
  
  			},
  
  			addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) {
  
  				var vLen = this.vertices.length;
  
  				var ia = this.parseVertexIndex( a, vLen );
  				var ib = this.parseVertexIndex( b, vLen );
  				var ic = this.parseVertexIndex( c, vLen );
  
  				this.addVertex( ia, ib, ic );
  
  				if ( this.colors.length > 0 ) {
  
  					this.addColor( ia, ib, ic );
  
  				}
  
  				if ( ua !== undefined && ua !== '' ) {
  
  					var uvLen = this.uvs.length;
  					ia = this.parseUVIndex( ua, uvLen );
  					ib = this.parseUVIndex( ub, uvLen );
  					ic = this.parseUVIndex( uc, uvLen );
  					this.addUV( ia, ib, ic );
  
  				}
  
  				if ( na !== undefined && na !== '' ) {
  
  					// 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 );
  
  					this.addNormal( ia, ib, ic );
  
  				}
  
  			},
  
  			addPointGeometry: function ( vertices ) {
  
  				this.object.geometry.type = 'Points';
  
  				var vLen = this.vertices.length;
  
  				for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) {
  
  					this.addVertexPoint( this.parseVertexIndex( vertices[ vi ], vLen ) );
  
  				}
  
  			},
  
  			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;
  
  	}
  
  	//
  
  	function OBJLoader( manager ) {
  
  		THREE.Loader.call( this, manager );
  
  		this.materials = null;
  
  	}
  
  	OBJLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), {
  
  		constructor: 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 );
  
  		},
  
  		setMaterials: function ( materials ) {
  
  			this.materials = materials;
  
  			return this;
  
  		},
  
  		parse: function ( chunks ) {
  
  			// console.time( 'OBJLoader' );
  
  			var state = new ParserState();
  
        if (typeof chunks === 'string') chunks = [chunks];
  
        for (let text of chunks) {
          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 = '';
          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') {
  
              var data = line.split(/\s+/);
  
              switch (data[0]) {
  
                case 'v':
                  state.vertices.push(
                    parseFloat(data[1]),
                    parseFloat(data[2]),
                    parseFloat(data[3])
                  );
                  if (data.length >= 7) {
  
                    state.colors.push(
                      parseFloat(data[4]),
                      parseFloat(data[5]),
                      parseFloat(data[6])
                    );
  
                  }
                  break;
                case 'vn':
                  state.normals.push(
                    parseFloat(data[1]),
                    parseFloat(data[2]),
                    parseFloat(data[3])
                  );
                  break;
                case 'vt':
                  state.uvs.push(
                    parseFloat(data[1]),
                    parseFloat(data[2])
                  );
                  break;
  
              }
  
            } else if (lineFirstChar === 'f') {
  
              var lineData = line.substr(1).trim();
              var vertexData = lineData.split(/\s+/);
              var faceVertices = [];
  
              // Parse the face vertex data into an easy to work with format
  
              for (var j = 0, jl = vertexData.length; j < jl; j++) {
  
                var vertex = vertexData[j];
  
                if (vertex.length > 0) {
  
                  var vertexParts = vertex.split('/');
                  faceVertices.push(vertexParts);
  
                }
  
              }
  
              // Draw an edge between the first vertex and all subsequent vertices to form an n-gon
  
              var v1 = faceVertices[0];
  
              for (var j = 1, jl = faceVertices.length - 1; j < jl; j++) {
  
                var v2 = faceVertices[j];
                var v3 = faceVertices[j + 1];
  
                state.addFace(
                  v1[0], v2[0], v3[0],
                  v1[1], v2[1], v3[1],
                  v1[2], v2[2], v3[2]
                );
  
              }
  
            } 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 (lineFirstChar === 'p') {
  
              var lineData = line.substr(1).trim();
              var pointData = lineData.split(" ");
  
              state.addPointGeometry(pointData);
  
            } else if ((result = 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 (material_use_pattern.test(line)) {
  
              // material
  
              state.object.startMaterial(line.substring(7).trim(), state.materialLibraries);
  
            } else if (material_library_pattern.test(line)) {
  
              // mtl file
  
              state.materialLibraries.push(line.substring(7).trim());
  
            } else if (map_use_pattern.test(line)) {
  
              // the line is parsed but ignored since the loader assumes textures are defined MTL files
              // (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method)
  
              console.warn('THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.');
  
            } else if (lineFirstChar === 's') {
  
              result = line.split(' ');
  
              // 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
  
              /*
               * http://paulbourke.net/dataformats/obj/
               * or
               * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf
               *
               * From chapter "Grouping" Syntax explanation "s group_number":
               * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
               * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
               * surfaces, smoothing groups are either turned on or off; there is no difference between values greater
               * than 0."
               */
              if (result.length > 1) {
  
                var value = result[1].trim().toLowerCase();
                state.object.smooth = (value !== '0' && value !== 'off');
  
              } else {
  
                // ZBrush can produce "s" lines #11707
                state.object.smooth = true;
  
              }
              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('THREE.OBJLoader: 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' );
  				var isPoints = ( geometry.type === 'Points' );
  				var hasVertexColors = false;
  
  				// Skip o/g line declarations that did not follow with any faces
  				if ( geometry.vertices.length === 0 ) continue;
  
  				var buffergeometry = new THREE.BufferGeometry();
  
  				buffergeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( geometry.vertices, 3 ) );
  
  				if ( geometry.normals.length > 0 ) {
  
  					buffergeometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( geometry.normals, 3 ) );
  
  				} else {
  
  					buffergeometry.computeVertexNormals();
  
  				}
  
  				if ( geometry.colors.length > 0 ) {
  
  					hasVertexColors = true;
  					buffergeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( geometry.colors, 3 ) );
  
  				}
  
  				if ( geometry.uvs.length > 0 ) {
  
  					buffergeometry.setAttribute( 'uv', new THREE.Float32BufferAttribute( 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();
  							THREE.Material.prototype.copy.call( materialLine, material );
  							materialLine.color.copy( material.color );
  							material = materialLine;
  
  						} else if ( isPoints && material && ! ( material instanceof THREE.PointsMaterial ) ) {
  
  							var materialPoints = new THREE.PointsMaterial( { size: 10, sizeAttenuation: false } );
  							THREE.Material.prototype.copy.call( materialPoints, material );
  							materialPoints.color.copy( material.color );
  							materialPoints.map = material.map;
  							material = materialPoints;
  
  						}
  
  					}
  
  					if ( ! material ) {
  
  						if ( isLine ) {
  
  							material = new THREE.LineBasicMaterial();
  
  						} else if ( isPoints ) {
  
  							material = new THREE.PointsMaterial( { size: 1, sizeAttenuation: false } );
  
  						} else {
  
  							material = new THREE.MeshPhongMaterial();
  
  						}
  
  						material.name = sourceMaterial.name;
  
  					}
  
  					material.flatShading = sourceMaterial.smooth ? false : true;
  					material.vertexColors = hasVertexColors ? THREE.VertexColors : THREE.NoColors;
  
  					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 );
  
  					}
  
  					if ( isLine ) {
  
  						mesh = new THREE.LineSegments( buffergeometry, createdMaterials );
  
  					} else if ( isPoints ) {
  
  						mesh = new THREE.Points( buffergeometry, createdMaterials );
  
  					} else {
  
  						mesh = new THREE.Mesh( buffergeometry, createdMaterials );
  
  					}
  
  				} else {
  
  					if ( isLine ) {
  
  						mesh = new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] );
  
  					} else if ( isPoints ) {
  
  						mesh = new THREE.Points( buffergeometry, createdMaterials[ 0 ] );
  
  					} else {
  
  						mesh = new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] );
  
  					}
  
  				}
  
  				mesh.name = object.name;
  
  				container.add( mesh );
  
  			}
  
  			// console.timeEnd( 'OBJLoader' );
  
  			return container;
  
  		}
  
  	} );
  
  	return OBJLoader;
  
  } )();
  
  exports.MTLLoader = THREE.MTLLoader;
  exports.OBJLoader = THREE.OBJLoader;
  