瀏覽代碼

#9756 WWOBJLoader2 V1.0.6

Improvements since initial external V1.0.0 release:
OBJLoader2:
- Removed need for making Parser public. OBJLoader2 has a build function for web worker code.
- MeshCreator is now private to OBJLoader2

WWOBJLoader2:
- Added checks for Blob and URL.createObjectURL
- Worker code build: Removed need to adjust constructor and some new Object calls
- Allow to properly set CORS to MTLLoader via WWOBJLoader2 and WWOBJLoader2Director
- Now allows to enable/disable mesh streaming

Example webgl_loader_obj
- Added GridHelper
- resources to load are now defined outside example classes

Example webgl_loader_obj2_ww
- Allow to clear all meshes in
- Allows to load user OBJ/MTL files
- Added GridHelper
- resources to load are now defined outside example classes

All Examples:
- Created one page examples and tuned naming
- All examples now use dat.gui
- Removed namespace "THREE.examples"
- Fixed comment typos
- Fixed some code formatting issues
- Fixed tabs in examples

General:
- Headers now carry references to development repository
Kai Salmen 8 年之前
父節點
當前提交
fb79968d5a

+ 3 - 0
examples/files.js

@@ -102,6 +102,9 @@ var files = {
 		"webgl_loader_msgpack",
 		"webgl_loader_obj",
 		"webgl_loader_obj_mtl",
+		"webgl_loader_obj2",
+		"webgl_loader_obj2_ww",
+		"webgl_loader_obj2_ww_parallels",
 		"webgl_loader_nrrd",
 		"webgl_loader_pcd",
 		"webgl_loader_pdb",

+ 1002 - 0
examples/js/loaders/OBJLoader2.js

@@ -0,0 +1,1002 @@
+/**
+  * @author Kai Salmen / https://kaisalmen.de
+  * Development repository: https://github.com/kaisalmen/WWOBJLoader
+  */
+
+'use strict';
+
+if ( THREE.OBJLoader2 === undefined ) { THREE.OBJLoader2 = {} }
+THREE.OBJLoader2.version = '1.0.6';
+
+/**
+ * Use this class to load OBJ data from files or to parse OBJ data from arraybuffer or text
+ * @class
+ *
+ * @param {THREE.DefaultLoadingManager} [manager] Extension of {@link THREE.DefaultLoadingManager}
+ */
+THREE.OBJLoader2 = (function () {
+
+	function OBJLoader2( manager ) {
+		this.manager = ( manager == null ) ? THREE.DefaultLoadingManager : manager;
+
+		this.path = '';
+		this.fileLoader = new THREE.FileLoader( this.manager );
+
+		this.meshCreator = new MeshCreator();
+		this.parser = new Parser( this.meshCreator );
+
+		this.validated = false;
+	}
+
+	/**
+	 * Base path to use
+	 * @memberOf THREE.OBJLoader2
+	 *
+	 * @param {string} path The basepath
+	 */
+	OBJLoader2.prototype.setPath = function ( path ) {
+		this.path = ( path == null ) ? this.path : path;
+	};
+
+	/**
+	 * Set the node where the loaded objects will be attached
+	 * @memberOf THREE.OBJLoader2
+	 *
+	 * @param {THREE.Object3D} sceneGraphBaseNode Scenegraph object where meshes will be attached
+	 */
+	OBJLoader2.prototype.setSceneGraphBaseNode = function ( sceneGraphBaseNode ) {
+		this.meshCreator._setSceneGraphBaseNode( sceneGraphBaseNode );
+	};
+
+	/**
+	 * Set materials loaded by MTLLoader
+	 * @memberOf THREE.OBJLoader2
+	 *
+	 * @param {THREE.MTLLoader.MaterialCreator.materials[]} materials {@link THREE.MTLLoader.MaterialCreator.materials}
+	 */
+	OBJLoader2.prototype.setMaterials = function ( materials ) {
+		this.meshCreator._setMaterials( materials );
+	};
+
+	/**
+	 * Allows to set debug mode for the parser and the meshCreator
+	 * @memberOf THREE.OBJLoader2
+	 *
+	 * @param {boolean} parserDebug {@link Parser} will produce debug output
+	 * @param {boolean} meshCreatorDebug {@link THREE.OBJLoader2.MeshCreator} will produce debug output
+	 */
+	OBJLoader2.prototype.setDebug = function ( parserDebug, meshCreatorDebug ) {
+		this.parser._setDebug( parserDebug );
+		this.meshCreator._setDebug( meshCreatorDebug );
+	};
+
+	/**
+	 * Use this convenient method to load an OBJ file at the given URL. Per default the fileLoader uses an arraybuffer
+	 * @memberOf THREE.OBJLoader2
+	 *
+	 * @param {string} url URL of the file to load
+	 * @param {callback} onLoad Called after loading was successfully completed
+	 * @param {callback} onProgress Called to report progress of loading
+	 * @param {callback} onError Called after an error occurred during loading
+	 * @param {boolean} [useArrayBuffer=true] Set this to false to force string based parsing
+	 */
+	OBJLoader2.prototype.load = function ( url, onLoad, onProgress, onError, useArrayBuffer ) {
+		this._validate();
+		this.fileLoader.setPath( this.path );
+		this.fileLoader.setResponseType( ( useArrayBuffer || useArrayBuffer == null ) ? 'arraybuffer' : 'text' );
+
+		var scope = this;
+		scope.fileLoader.load( url, function ( content ) {
+
+			// only use parseText if useArrayBuffer is explicitly set to false
+			onLoad( ( useArrayBuffer || useArrayBuffer == null ) ? scope.parse( content ) : scope.parseText( content ) );
+
+		}, onProgress, onError );
+	};
+
+	/**
+	 * Default parse function: Parses OBJ file content stored in arrayBuffer and returns the sceneGraphBaseNode
+	 * @memberOf THREE.OBJLoader2
+	 *
+	 * @param {Uint8Array} arrayBuffer OBJ data as Uint8Array
+	 */
+	OBJLoader2.prototype.parse = function ( arrayBuffer ) {
+		// fast-fail on bad type
+		if ( ! ( arrayBuffer instanceof ArrayBuffer || arrayBuffer instanceof Uint8Array ) ) {
+
+			throw 'Provided input is not of type arraybuffer! Aborting...';
+
+		}
+		console.log( 'Parsing arrayBuffer...' );
+		console.time( 'parseArrayBuffer' );
+
+		this._validate();
+		this.parser.parseArrayBuffer( arrayBuffer );
+		var sceneGraphAttach = this._finalize();
+
+		console.timeEnd( 'parseArrayBuffer' );
+
+		return sceneGraphAttach;
+	};
+
+	/**
+	 * Legacy parse function: Parses OBJ file content stored in string and returns the sceneGraphBaseNode
+	 * @memberOf THREE.OBJLoader2
+	 *
+	 * @param {string} text OBJ data as string
+	 */
+	OBJLoader2.prototype.parseText = function ( text ) {
+		// fast-fail on bad type
+		if ( ! ( typeof( text ) === 'string' || text instanceof String ) ) {
+
+			throw 'Provided input is not of type String! Aborting...';
+
+		}
+		console.log( 'Parsing text...' );
+		console.time( 'parseText' );
+
+		this._validate();
+		this.parser.parseText( text );
+		var sceneGraphBaseNode = this._finalize();
+
+		console.timeEnd( 'parseText' );
+
+		return sceneGraphBaseNode;
+	};
+
+	OBJLoader2.prototype._validate = function () {
+		if ( this.validated ) return;
+
+		this.fileLoader = ( this.fileLoader == null ) ? new THREE.FileLoader( this.manager ) : this.fileLoader;
+		this.setPath();
+		this.parser._validate();
+		this.meshCreator._validate();
+
+		this.validated = true;
+	};
+
+	OBJLoader2.prototype._finalize = function () {
+		console.log( 'Global output object count: ' + this.meshCreator.globalObjectCount );
+
+		this.parser._finalize();
+		this.fileLoader = null;
+		var sceneGraphBaseNode = this.meshCreator.sceneGraphBaseNode;
+		this.meshCreator._finalize();
+		this.validated = false;
+
+		return sceneGraphBaseNode;
+	};
+
+	/**
+	 * Constants used by THREE.OBJLoader2
+	 */
+	var Consts = {
+		CODE_LF: 10,
+		CODE_CR: 13,
+		CODE_SPACE: 32,
+		CODE_SLASH: 47,
+		STRING_LF: '\n',
+		STRING_CR: '\r',
+		STRING_SPACE: ' ',
+		STRING_SLASH: '/',
+		LINE_F: 'f',
+		LINE_G: 'g',
+		LINE_L: 'l',
+		LINE_O: 'o',
+		LINE_S: 's',
+		LINE_V: 'v',
+		LINE_VT: 'vt',
+		LINE_VN: 'vn',
+		LINE_MTLLIB: 'mtllib',
+		LINE_USEMTL: 'usemtl',
+		/*
+		 * Build Face/Quad: first element in indexArray is the line identification, therefore offset of one needs to be taken into account
+		 * N-Gons are not supported
+		 * Quad Faces: FaceA: 0, 1, 2  FaceB: 2, 3, 0
+		 *
+		 * 0: "f vertex/uv/normal	vertex/uv/normal	vertex/uv/normal	(vertex/uv/normal)"
+		 * 1: "f vertex/uv		  	vertex/uv		   	vertex/uv		   	(vertex/uv		 )"
+		 * 2: "f vertex//normal	 	vertex//normal	  	vertex//normal	  	(vertex//normal  )"
+		 * 3: "f vertex			 	vertex			  	vertex			  	(vertex		  	 )"
+		 *
+		 * @param indexArray
+		 * @param faceType
+		 */
+		QUAD_INDICES_1: [ 1, 2, 3, 3, 4, 1 ],
+		QUAD_INDICES_2: [ 1, 3, 5, 5, 7, 1 ],
+		QUAD_INDICES_3: [ 1, 4, 7, 7, 10, 1 ]
+	};
+
+	/**
+	 * Parse OBJ data either from ArrayBuffer or string
+	 * @class
+	 */
+	var Parser = (function () {
+
+		function Parser( meshCreator ) {
+			this.meshCreator = meshCreator;
+			this.rawObject = null;
+			this.inputObjectCount = 1;
+			this.debug = false;
+		}
+
+		Parser.prototype._setDebug = function ( debug ) {
+			this.debug = ( debug == null ) ? this.debug : debug;
+		};
+
+		Parser.prototype._validate = function () {
+			this.rawObject = new RawObject();
+			this.inputObjectCount = 1;
+		};
+
+		/**
+		 * Parse the provided arraybuffer
+		 * @memberOf Parser
+		 *
+		 * @param {Uint8Array} arrayBuffer OBJ data as Uint8Array
+		 */
+		Parser.prototype.parseArrayBuffer = function ( arrayBuffer ) {
+			var arrayBufferView = new Uint8Array( arrayBuffer );
+			var length = arrayBufferView.byteLength;
+			var buffer = new Array( 32 );
+			var bufferPointer = 0;
+			var slashes = new Array( 32 );
+			var slashesPointer = 0;
+			var reachedFaces = false;
+			var code;
+			var word = '';
+			for ( var i = 0; i < length; i++ ) {
+
+				code = arrayBufferView[ i ];
+				switch ( code ) {
+					case Consts.CODE_SPACE:
+						if ( word.length > 0 ) buffer[ bufferPointer++ ] = word;
+						word = '';
+						break;
+
+					case Consts.CODE_SLASH:
+						slashes[ slashesPointer++ ] = i;
+						if ( word.length > 0 ) buffer[ bufferPointer++ ] = word;
+						word = '';
+						break;
+
+					case Consts.CODE_LF:
+						if ( word.length > 0 ) buffer[ bufferPointer++ ] = word;
+						word = '';
+						reachedFaces = this._processLine( buffer, bufferPointer, slashes, slashesPointer, reachedFaces );
+						slashesPointer = 0;
+						bufferPointer = 0;
+						break;
+
+					case Consts.CODE_CR:
+						break;
+
+					default:
+						word += String.fromCharCode( code );
+						break;
+				}
+			}
+		};
+
+		/**
+		 * Parse the provided text
+		 * @memberOf Parser
+		 *
+		 * @param {string} text OBJ data as string
+		 */
+		Parser.prototype.parseText = function ( text ) {
+			var length = text.length;
+			var buffer = new Array( 32 );
+			var bufferPointer = 0;
+			var slashes = new Array( 32 );
+			var slashesPointer = 0;
+			var reachedFaces = false;
+			var char;
+			var word = '';
+			for ( var i = 0; i < length; i++ ) {
+
+				char = text[ i ];
+				switch ( char ) {
+					case Consts.STRING_SPACE:
+						if ( word.length > 0 ) buffer[ bufferPointer++ ] = word;
+						word = '';
+						break;
+
+					case Consts.STRING_SLASH:
+						slashes[ slashesPointer++ ] = i;
+						if ( word.length > 0 ) buffer[ bufferPointer++ ] = word;
+						word = '';
+						break;
+
+					case Consts.STRING_LF:
+						if ( word.length > 0 ) buffer[ bufferPointer++ ] = word;
+						word = '';
+						reachedFaces = this._processLine( buffer, bufferPointer, slashes, slashesPointer, reachedFaces );
+						slashesPointer = 0;
+						bufferPointer = 0;
+						break;
+
+					case Consts.STRING_CR:
+						break;
+
+					default:
+						word += char;
+				}
+			}
+		};
+
+		Parser.prototype._processLine = function ( buffer, bufferPointer, slashes, slashesPointer, reachedFaces ) {
+			if ( bufferPointer < 1 ) return reachedFaces;
+
+			var bufferLength = bufferPointer - 1;
+			switch ( buffer[ 0 ] ) {
+				case Consts.LINE_V:
+
+					// object complete instance required if reached faces already (= reached next block of v)
+					if ( reachedFaces ) {
+
+						this._processCompletedObject( null, this.rawObject.groupName );
+						reachedFaces = false;
+
+					}
+					this.rawObject._pushVertex( buffer );
+					break;
+
+				case Consts.LINE_VT:
+					this.rawObject._pushUv( buffer );
+					break;
+
+				case Consts.LINE_VN:
+					this.rawObject._pushNormal( buffer );
+					break;
+
+				case Consts.LINE_F:
+					reachedFaces = true;
+					/*
+					 * 0: "f vertex/uv/normal ..."
+					 * 1: "f vertex/uv ..."
+					 * 2: "f vertex//normal ..."
+					 * 3: "f vertex ..."
+					 */
+					var haveQuad = bufferLength % 4 === 0;
+					if ( slashesPointer > 1 && ( slashes[ 1 ] - slashes[ 0 ] ) === 1 ) {
+
+						if ( haveQuad ) {
+							this.rawObject._buildQuadVVn( buffer );
+						} else {
+							this.rawObject._buildFaceVVn( buffer );
+						}
+
+					} else if ( bufferLength === slashesPointer * 2 ) {
+
+						if ( haveQuad ) {
+							this.rawObject._buildQuadVVt( buffer );
+						} else {
+							this.rawObject._buildFaceVVt( buffer );
+						}
+
+					} else if ( bufferLength * 2 === slashesPointer * 3 ) {
+
+						if ( haveQuad ) {
+							this.rawObject._buildQuadVVtVn( buffer );
+						} else {
+							this.rawObject._buildFaceVVtVn( buffer );
+						}
+
+					} else {
+
+						if ( haveQuad ) {
+							this.rawObject._buildQuadV( buffer );
+						} else {
+							this.rawObject._buildFaceV( buffer );
+						}
+
+					}
+					break;
+
+				case Consts.LINE_L:
+					if ( bufferLength === slashesPointer * 2 ) {
+
+						this.rawObject._buildLineVvt( buffer );
+
+					} else {
+
+						this.rawObject._buildLineV( buffer );
+
+					}
+					break;
+
+				case Consts.LINE_S:
+					this.rawObject._pushSmoothingGroup( buffer[ 1 ] );
+					break;
+
+				case Consts.LINE_G:
+					this._processCompletedGroup( buffer[ 1 ] );
+					break;
+
+				case Consts.LINE_O:
+					if ( this.rawObject.vertices.length > 0 ) {
+
+						this._processCompletedObject( buffer[ 1 ], null );
+						reachedFaces = false;
+
+					} else {
+
+						this.rawObject._pushObject( buffer[ 1 ] );
+
+					}
+					break;
+
+				case Consts.LINE_MTLLIB:
+					this.rawObject._pushMtllib( buffer[ 1 ] );
+					break;
+
+				case Consts.LINE_USEMTL:
+					this.rawObject._pushUsemtl( buffer[ 1 ] );
+					break;
+
+				default:
+					break;
+			}
+			return reachedFaces;
+		};
+
+		Parser.prototype._processCompletedObject = function ( objectName, groupName ) {
+			this.rawObject._finalize( this.meshCreator, this.inputObjectCount, this.debug );
+			this.inputObjectCount++;
+			this.rawObject = this.rawObject._newInstanceFromObject( objectName, groupName );
+		};
+
+		Parser.prototype._processCompletedGroup = function ( groupName ) {
+			var notEmpty = this.rawObject._finalize( this.meshCreator, this.inputObjectCount, this.debug );
+			if ( notEmpty ) {
+
+				this.inputObjectCount ++;
+				this.rawObject = this.rawObject._newInstanceFromGroup( groupName );
+
+			} else {
+
+				// if a group was set that did not lead to object creation in finalize, then the group name has to be updated
+				this.rawObject._pushGroup( groupName );
+
+			}
+		};
+
+		Parser.prototype._finalize = function () {
+			this.rawObject._finalize( this.meshCreator, this.inputObjectCount, this.debug );
+			this.inputObjectCount++;
+		};
+
+		return Parser;
+	})();
+
+	/**
+	 * {@link RawObject} is only used by {@link Parser}.
+	 * The user of OBJLoader2 does not need to care about this class.
+	 * It is defined publicly for inclusion in web worker based OBJ loader ({@link THREE.OBJLoader2.WWOBJLoader2})
+	 */
+	var RawObject = (function () {
+
+		function RawObject( objectName, groupName, mtllibName ) {
+			this.globalVertexOffset = 1;
+			this.globalUvOffset = 1;
+			this.globalNormalOffset = 1;
+
+			this.vertices = [];
+			this.normals = [];
+			this.uvs = [];
+
+			// faces are stored according combined index of group, material and smoothingGroup (0 or not)
+			this.mtllibName = ( mtllibName != null ) ? mtllibName : 'none';
+			this.objectName = ( objectName != null ) ? objectName : 'none';
+			this.groupName = ( groupName != null ) ? groupName : 'none';
+			this.activeMtlName = 'none';
+			this.activeSmoothingGroup = 1;
+
+			this.mtlCount = 0;
+			this.smoothingGroupCount = 0;
+
+			this.rawObjectDescriptions = [];
+			// this default index is required as it is possible to define faces without 'g' or 'usemtl'
+			var index = this._buildIndex( this.activeMtlName, this.activeSmoothingGroup );
+			this.rawObjectDescriptionInUse = new RawObjectDescription( this.objectName, this.groupName, this.activeMtlName, this.activeSmoothingGroup );
+			this.rawObjectDescriptions[ index ] = this.rawObjectDescriptionInUse;
+		}
+
+		RawObject.prototype._buildIndex = function ( materialName, smoothingGroup) {
+			return materialName + '|' + smoothingGroup;
+		};
+
+		RawObject.prototype._newInstanceFromObject = function ( objectName, groupName ) {
+			var newRawObject = new RawObject( objectName, groupName, this.mtllibName );
+
+			// move indices forward
+			newRawObject.globalVertexOffset = this.globalVertexOffset + this.vertices.length / 3;
+			newRawObject.globalUvOffset = this.globalUvOffset + this.uvs.length / 2;
+			newRawObject.globalNormalOffset = this.globalNormalOffset + this.normals.length / 3;
+
+			return newRawObject;
+		};
+
+		RawObject.prototype._newInstanceFromGroup = function ( groupName ) {
+			var newRawObject = new RawObject( this.objectName, groupName, this.mtllibName );
+
+			// keep current buffers and indices forward
+			newRawObject.vertices = this.vertices;
+			newRawObject.uvs = this.uvs;
+			newRawObject.normals = this.normals;
+			newRawObject.globalVertexOffset = this.globalVertexOffset;
+			newRawObject.globalUvOffset = this.globalUvOffset;
+			newRawObject.globalNormalOffset = this.globalNormalOffset;
+
+			return newRawObject;
+		};
+
+		RawObject.prototype._pushVertex = function ( buffer ) {
+			this.vertices.push( parseFloat( buffer[ 1 ] ) );
+			this.vertices.push( parseFloat( buffer[ 2 ] ) );
+			this.vertices.push( parseFloat( buffer[ 3 ] ) );
+		};
+
+		RawObject.prototype._pushUv = function ( buffer ) {
+			this.uvs.push( parseFloat( buffer[ 1 ] ) );
+			this.uvs.push( parseFloat( buffer[ 2 ] ) );
+		};
+
+		RawObject.prototype._pushNormal = function ( buffer ) {
+			this.normals.push( parseFloat( buffer[ 1 ] ) );
+			this.normals.push( parseFloat( buffer[ 2 ] ) );
+			this.normals.push( parseFloat( buffer[ 3 ] ) );
+		};
+
+		RawObject.prototype._pushObject = function ( objectName ) {
+			this.objectName = objectName;
+		};
+
+		RawObject.prototype._pushMtllib = function ( mtllibName ) {
+			this.mtllibName = mtllibName;
+		};
+
+		RawObject.prototype._pushGroup = function ( groupName ) {
+			this.groupName = groupName;
+			this._verifyIndex();
+		};
+
+		RawObject.prototype._pushUsemtl = function ( mtlName ) {
+			if ( this.activeMtlName === mtlName || mtlName == null ) return;
+			this.activeMtlName = mtlName;
+			this.mtlCount++;
+
+			this._verifyIndex();
+		};
+
+		RawObject.prototype._pushSmoothingGroup = function ( activeSmoothingGroup ) {
+			var normalized = activeSmoothingGroup === 'off' ? 0 : activeSmoothingGroup;
+			if ( this.activeSmoothingGroup === normalized ) return;
+			this.activeSmoothingGroup = normalized;
+			this.smoothingGroupCount++;
+
+			this._verifyIndex();
+		};
+
+		RawObject.prototype._verifyIndex = function () {
+			var index = this._buildIndex( this.activeMtlName, ( this.activeSmoothingGroup === 0 ) ? 0 : 1 );
+			if ( this.rawObjectDescriptions[ index ] == null ) {
+
+				this.rawObjectDescriptionInUse = this.rawObjectDescriptions[ index ] =
+					new RawObjectDescription(
+						this.objectName, this.groupName, this.activeMtlName, this.activeSmoothingGroup
+					);
+
+			} else {
+
+				this.rawObjectDescriptionInUse = this.rawObjectDescriptions[ index ];
+
+			}
+		};
+
+		RawObject.prototype._buildQuadVVtVn = function ( indexArray ) {
+			for ( var i = 0; i < 6; i ++ ) {
+				this._attachFaceV_( indexArray[ Consts.QUAD_INDICES_3[ i ] ] );
+				this._attachFaceVt( indexArray[ Consts.QUAD_INDICES_3[ i ] + 1 ] );
+				this._attachFaceVn( indexArray[ Consts.QUAD_INDICES_3[ i ] + 2 ] );
+			}
+		};
+
+		RawObject.prototype._buildQuadVVt = function ( indexArray ) {
+			for ( var i = 0; i < 6; i ++ ) {
+				this._attachFaceV_( indexArray[ Consts.QUAD_INDICES_2[ i ] ] );
+				this._attachFaceVt( indexArray[ Consts.QUAD_INDICES_2[ i ] + 1 ] );
+			}
+		};
+
+		RawObject.prototype._buildQuadVVn = function ( indexArray ) {
+			for ( var i = 0; i < 6; i ++ ) {
+				this._attachFaceV_( indexArray[ Consts.QUAD_INDICES_2[ i ] ] );
+				this._attachFaceVn( indexArray[ Consts.QUAD_INDICES_2[ i ] + 1 ] );
+			}
+		};
+
+		RawObject.prototype._buildQuadV = function ( indexArray ) {
+			for ( var i = 0; i < 6; i ++ ) {
+				this._attachFaceV_( indexArray[ Consts.QUAD_INDICES_1[ i ] ] );
+			}
+		};
+
+		RawObject.prototype._buildFaceVVtVn = function ( indexArray ) {
+			for ( var i = 1; i < 10; i += 3 ) {
+				this._attachFaceV_( indexArray[ i ] );
+				this._attachFaceVt( indexArray[ i + 1 ] );
+				this._attachFaceVn( indexArray[ i + 2 ] );
+			}
+		};
+
+		RawObject.prototype._buildFaceVVt = function ( indexArray ) {
+			for ( var i = 1; i < 7; i += 2 ) {
+				this._attachFaceV_( indexArray[ i ] );
+				this._attachFaceVt( indexArray[ i + 1 ] );
+			}
+		};
+
+		RawObject.prototype._buildFaceVVn = function ( indexArray ) {
+			for ( var i = 1; i < 7; i += 2 ) {
+				this._attachFaceV_( indexArray[ i ] );
+				this._attachFaceVn( indexArray[ i + 1 ] );
+			}
+		};
+
+		RawObject.prototype._buildFaceV = function ( indexArray ) {
+			for ( var i = 1; i < 4; i ++ ) {
+				this._attachFaceV_( indexArray[ i ] );
+			}
+		};
+
+		RawObject.prototype._attachFaceV_ = function ( faceIndex ) {
+			var faceIndexInt =  parseInt( faceIndex );
+			var index = ( faceIndexInt - this.globalVertexOffset ) * 3;
+
+			var rodiu = this.rawObjectDescriptionInUse;
+			rodiu.vertices.push( this.vertices[ index++ ] );
+			rodiu.vertices.push( this.vertices[ index++ ] );
+			rodiu.vertices.push( this.vertices[ index ] );
+		};
+
+		RawObject.prototype._attachFaceVt = function ( faceIndex ) {
+			var faceIndexInt =  parseInt( faceIndex );
+			var index = ( faceIndexInt - this.globalUvOffset ) * 2;
+
+			var rodiu = this.rawObjectDescriptionInUse;
+			rodiu.uvs.push( this.uvs[ index++ ] );
+			rodiu.uvs.push( this.uvs[ index ] );
+		};
+
+		RawObject.prototype._attachFaceVn = function ( faceIndex ) {
+			var faceIndexInt =  parseInt( faceIndex );
+			var index = ( faceIndexInt - this.globalNormalOffset ) * 3;
+
+			var rodiu = this.rawObjectDescriptionInUse;
+			rodiu.normals.push( this.normals[ index++ ] );
+			rodiu.normals.push( this.normals[ index++ ] );
+			rodiu.normals.push( this.normals[ index ] );
+		};
+
+		/*
+		 * Support for lines with or without texture. irst element in indexArray is the line identification
+		 * 0: "f vertex/uv		vertex/uv 		..."
+		 * 1: "f vertex			vertex 			..."
+		 */
+		RawObject.prototype._buildLineVvt = function ( lineArray ) {
+			var length = lineArray.length;
+			for ( var i = 1; i < length; i ++ ) {
+				this.vertices.push( parseInt( lineArray[ i ] ) );
+				this.uvs.push( parseInt( lineArray[ i ] ) );
+			}
+		};
+
+		RawObject.prototype._buildLineV = function ( lineArray ) {
+			var length = lineArray.length;
+			for ( var i = 1; i < length; i++ ) {
+				this.vertices.push( parseInt( lineArray[ i ] ) );
+			}
+		};
+
+		/**
+		 * Clear any empty rawObjectDescription and calculate absolute vertex, normal and uv counts
+		 */
+		RawObject.prototype._finalize = function ( meshCreator, inputObjectCount, debug ) {
+			var temp = this.rawObjectDescriptions;
+			this.rawObjectDescriptions = [];
+			var rawObjectDescription;
+			var index = 0;
+			var absoluteVertexCount = 0;
+			var absoluteNormalCount = 0;
+			var absoluteUvCount = 0;
+
+			for ( var name in temp ) {
+
+				rawObjectDescription = temp[ name ];
+				if ( rawObjectDescription.vertices.length > 0 ) {
+
+					if ( rawObjectDescription.objectName === 'none' ) rawObjectDescription.objectName = rawObjectDescription.groupName;
+					this.rawObjectDescriptions[ index++ ] = rawObjectDescription;
+					absoluteVertexCount += rawObjectDescription.vertices.length;
+					absoluteUvCount += rawObjectDescription.uvs.length;
+					absoluteNormalCount += rawObjectDescription.normals.length;
+
+				}
+			}
+
+			// don not continue if no result
+			var notEmpty = false;
+			if ( index > 0 ) {
+
+				if ( debug ) this._createReport( inputObjectCount, true );
+				meshCreator._buildMesh(
+					this.rawObjectDescriptions,
+					inputObjectCount,
+					absoluteVertexCount,
+					absoluteNormalCount,
+					absoluteUvCount
+				);
+				notEmpty = true;
+
+			}
+			return notEmpty;
+		};
+
+		RawObject.prototype._createReport = function ( inputObjectCount, printDirectly ) {
+			var report = {
+				name: this.objectName ? this.objectName : 'groups',
+				mtllibName: this.mtllibName,
+				vertexCount: this.vertices.length / 3,
+				normalCount: this.normals.length / 3,
+				uvCount: this.uvs.length / 2,
+				smoothingGroupCount: this.smoothingGroupCount,
+				mtlCount: this.mtlCount,
+				rawObjectDescriptions: this.rawObjectDescriptions.length
+			};
+
+			if ( printDirectly ) {
+				console.log( 'Input Object number: ' + inputObjectCount + ' Object name: ' + report.name );
+				console.log( 'Mtllib name: ' + report.mtllibName );
+				console.log( 'Vertex count: ' + report.vertexCount );
+				console.log( 'Normal count: ' + report.normalCount );
+				console.log( 'UV count: ' + report.uvCount );
+				console.log( 'SmoothingGroup count: ' + report.smoothingGroupCount );
+				console.log( 'Material count: ' + report.mtlCount );
+				console.log( 'Real RawObjectDescription count: ' + report.rawObjectDescriptions );
+				console.log( '' );
+			}
+
+			return report;
+		};
+
+		return RawObject;
+	})();
+
+	/**
+	 * Descriptive information and data (vertices, normals, uvs) to passed on to mesh building function.
+	 * @class
+	 *
+	 * @param {string} objectName Name of the mesh
+	 * @param {string} groupName Name of the group
+	 * @param {string} materialName Name of the material
+	 * @param {number} smoothingGroup Normalized smoothingGroup (0: THREE.FlatShading, 1: THREE.SmoothShading)
+	 */
+	var RawObjectDescription = (function () {
+
+		function RawObjectDescription( objectName, groupName, materialName, smoothingGroup ) {
+			this.objectName = objectName;
+			this.groupName = groupName;
+			this.materialName = materialName;
+			this.smoothingGroup = smoothingGroup;
+			this.vertices = [];
+			this.uvs = [];
+			this.normals = [];
+		}
+
+		return RawObjectDescription;
+	})();
+
+	/**
+	 * MeshCreator is used to transform RawObjectDescriptions to THREE.Mesh
+	 *
+	 * @class
+	 */
+	var MeshCreator = (function () {
+
+		function MeshCreator() {
+			this.sceneGraphBaseNode = null;
+			this.materials = null;
+			this.debug = false;
+			this.globalObjectCount = 1;
+
+			this.validated = false;
+		}
+
+		MeshCreator.prototype._setSceneGraphBaseNode = function ( sceneGraphBaseNode ) {
+			this.sceneGraphBaseNode = ( sceneGraphBaseNode == null ) ? ( this.sceneGraphBaseNode == null ? new THREE.Group() : this.sceneGraphBaseNode ) : sceneGraphBaseNode;
+		};
+
+		MeshCreator.prototype._setMaterials = function ( materials ) {
+			this.materials = ( materials == null ) ? ( this.materials == null ? { materials: [] } : this.materials ) : materials;
+		};
+
+		MeshCreator.prototype._setDebug = function ( debug ) {
+			this.debug = ( debug == null ) ? this.debug : debug;
+		};
+
+		MeshCreator.prototype._validate = function () {
+			if ( this.validated ) return;
+
+			this._setSceneGraphBaseNode( null );
+			this._setMaterials( null );
+			this._setDebug( null );
+			this.globalObjectCount = 1;
+		};
+
+		MeshCreator.prototype._finalize = function () {
+			this.sceneGraphBaseNode = null;
+			this.materials = null;
+			this.validated = false;
+		};
+
+		/**
+		 * This is an internal function, but due to its importance to Parser it is documented.
+		 * RawObjectDescriptions are transformed to THREE.Mesh.
+		 * It is ensured that rawObjectDescriptions only contain objects with vertices (no need to check).
+		 * This method shall be overridden by the web worker implementation
+		 *
+		 * @param {RawObjectDescription[]} rawObjectDescriptions Array of descriptive information and data (vertices, normals, uvs) about the parsed object(s)
+		 * @param {number} inputObjectCount Number of objects already retrieved from OBJ
+		 * @param {number} absoluteVertexCount Sum of all vertices of all rawObjectDescriptions
+		 * @param {number} absoluteNormalCount Sum of all normals of all rawObjectDescriptions
+		 * @param {number} absoluteUvCount Sum of all uvs of all rawObjectDescriptions
+		 */
+		MeshCreator.prototype._buildMesh = function ( rawObjectDescriptions, inputObjectCount, absoluteVertexCount, absoluteNormalCount, absoluteUvCount ) {
+
+			if ( this.debug ) console.log( 'MeshCreator.buildRawMeshData:\nInput object no.: ' + inputObjectCount );
+
+			var bufferGeometry = new THREE.BufferGeometry();
+			var vertexBA = new THREE.BufferAttribute( new Float32Array( absoluteVertexCount ), 3 );
+			bufferGeometry.addAttribute( 'position', vertexBA );
+
+			var normalBA;
+			if ( absoluteNormalCount > 0 ) {
+
+				normalBA = new THREE.BufferAttribute( new Float32Array( absoluteNormalCount ), 3 );
+				bufferGeometry.addAttribute( 'normal', normalBA );
+
+			}
+			var uvBA;
+			if ( absoluteUvCount > 0 ) {
+
+				uvBA = new THREE.BufferAttribute( new Float32Array( absoluteUvCount ), 2 );
+				bufferGeometry.addAttribute( 'uv', uvBA );
+
+			}
+
+			if ( this.debug ) console.log( 'Creating Multi-Material for object no.: ' + this.globalObjectCount );
+
+			var rawObjectDescription;
+			var material;
+			var materialName;
+			var createMultiMaterial = rawObjectDescriptions.length > 1;
+			var materials = [];
+			var materialIndex = 0;
+			var materialIndexMapping = [];
+			var selectedMaterialIndex;
+
+			var vertexBAOffset = 0;
+			var vertexGroupOffset = 0;
+			var vertexLength;
+			var normalOffset = 0;
+			var uvOffset = 0;
+
+			for ( var oodIndex in rawObjectDescriptions ) {
+				rawObjectDescription = rawObjectDescriptions[ oodIndex ];
+
+				materialName = rawObjectDescription.materialName;
+				material = this.materials[ materialName ];
+				if ( ! material ) {
+
+					material = this.materials[ 'defaultMaterial' ];
+					if ( ! material ) {
+
+						material = new THREE.MeshStandardMaterial( { color: 0xDCF1FF} );
+						material.name = 'defaultMaterial';
+						this.materials[ 'defaultMaterial' ] = material;
+
+					}
+					console.warn( 'object_group "' + rawObjectDescription.objectName + '_' + rawObjectDescription.groupName + '" was defined without material! Assigning "defaultMaterial".' );
+
+				}
+				// clone material in case flat shading is needed due to smoothingGroup 0
+				if ( rawObjectDescription.smoothingGroup === 0 ) {
+
+					materialName = material.name + '_flat';
+					var materialClone = this.materials[ materialName ];
+					if ( ! materialClone ) {
+
+						materialClone = material.clone();
+						materialClone.name = materialName;
+						materialClone.shading = THREE.FlatShading;
+						this.materials[ materialName ] = name;
+
+					}
+
+				}
+
+				vertexLength = rawObjectDescription.vertices.length;
+				if ( createMultiMaterial ) {
+
+					// re-use material if already used before. Reduces materials array size and eliminates duplicates
+					selectedMaterialIndex = materialIndexMapping[ materialName ];
+					if ( ! selectedMaterialIndex ) {
+
+						selectedMaterialIndex = materialIndex;
+						materialIndexMapping[ materialName ] = materialIndex;
+						materials.push( material );
+						materialIndex++;
+
+					}
+
+					bufferGeometry.addGroup( vertexGroupOffset, vertexLength / 3, selectedMaterialIndex );
+					vertexGroupOffset += vertexLength / 3;
+				}
+
+				vertexBA.set( rawObjectDescription.vertices, vertexBAOffset );
+				vertexBAOffset += vertexLength;
+
+				if ( normalBA ) {
+
+					normalBA.set( rawObjectDescription.normals, normalOffset );
+					normalOffset += rawObjectDescription.normals.length;
+
+				}
+				if ( uvBA ) {
+
+					uvBA.set( rawObjectDescription.uvs, uvOffset );
+					uvOffset += rawObjectDescription.uvs.length;
+
+				}
+				if ( this.debug ) this._printReport( rawObjectDescription, selectedMaterialIndex );
+
+			}
+			if ( ! normalBA ) bufferGeometry.computeVertexNormals();
+
+			if ( createMultiMaterial ) material = new THREE.MultiMaterial( materials );
+			var mesh = new THREE.Mesh( bufferGeometry, material );
+			this.sceneGraphBaseNode.add( mesh );
+
+			this.globalObjectCount++;
+		};
+
+		MeshCreator.prototype._printReport = function ( rawObjectDescription, selectedMaterialIndex ) {
+			console.log(
+				' Output Object no.: ' + this.globalObjectCount +
+				'\n objectName: ' + rawObjectDescription.objectName +
+				'\n groupName: ' + rawObjectDescription.groupName +
+				'\n materialName: ' + rawObjectDescription.materialName +
+				'\n materialIndex: ' + selectedMaterialIndex +
+				'\n smoothingGroup: ' + rawObjectDescription.smoothingGroup +
+				'\n #vertices: ' + rawObjectDescription.vertices.length / 3 +
+				'\n #uvs: ' + rawObjectDescription.uvs.length / 2 +
+				'\n #normals: ' + rawObjectDescription.normals.length / 3
+			);
+		};
+
+		return MeshCreator;
+	})();
+
+	OBJLoader2.prototype._buildWebWorkerCode = function ( funcBuildObject, funcBuildSingelton ) {
+		var workerCode = '';
+		workerCode += funcBuildObject( 'Consts', Consts );
+		workerCode += funcBuildSingelton( 'Parser', 'Parser', Parser );
+		workerCode += funcBuildSingelton( 'RawObject', 'RawObject', RawObject );
+		workerCode += funcBuildSingelton( 'RawObjectDescription', 'RawObjectDescription', RawObjectDescription );
+		return workerCode;
+	};
+
+	return OBJLoader2;
+})();

+ 1171 - 0
examples/js/loaders/WWOBJLoader2.js

@@ -0,0 +1,1171 @@
+/**
+  * @author Kai Salmen / https://kaisalmen.de
+  * Development repository: https://github.com/kaisalmen/WWOBJLoader
+  */
+
+'use strict';
+
+if ( THREE.OBJLoader2 === undefined ) { THREE.OBJLoader2 = {} }
+THREE.OBJLoader2.version = '1.0.6';
+
+/**
+ * OBJ data will be loaded by dynamically created web worker.
+ * First feed instructions with: prepareRun
+ * Then: Execute with: run
+ * @class
+ */
+THREE.OBJLoader2.WWOBJLoader2 = (function () {
+
+	function WWOBJLoader2() {
+		this._init();
+	}
+
+	WWOBJLoader2.prototype._init = function () {
+		// check worker support first
+		if ( window.Worker === undefined ) throw "This browser does not support web workers!";
+		if ( window.Blob === undefined  ) throw "This browser does not support Blob!";
+		if ( ! typeof window.URL.createObjectURL === 'function'  ) throw "This browser does not support Object creation from URL!";
+
+		this.instanceNo = 0;
+		this.worker = null;
+		this.workerCode = null;
+		this.debug = false;
+
+		this.sceneGraphBaseNode = null;
+		this.streamMeshes = true;
+		this.meshStore = null;
+		this.modelName = 'none';
+		this.validated = false;
+		this.running = false;
+		this.requestTerminate = false;
+
+		this.callbacks = {
+			progress: null,
+			completedLoading: null,
+			errorWhileLoading: null,
+			materialsLoaded: null,
+			meshLoaded: null,
+			director: {
+				completedLoading: null,
+				errorWhileLoading: null
+			}
+		};
+
+		this.manager = THREE.DefaultLoadingManager;
+		this.fileLoader = new THREE.FileLoader( this.manager );
+		this.mtlLoader = null;
+		this.crossOrigin = null;
+
+		this.dataAvailable = false;
+		this.objAsArrayBuffer = null;
+		this.fileObj = null;
+		this.pathObj = null;
+
+		this.fileMtl = null;
+		this.mtlAsString = null;
+		this.texturePath = null;
+
+		this.materials = [];
+		this.counter = 0;
+	};
+
+	/**
+	 * Set enable or disable debug logging
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {boolean} enabled
+	 */
+	WWOBJLoader2.prototype.setDebug = function ( enabled ) {
+		this.debug = enabled;
+	};
+
+	/**
+	 * Sets the CORS string to be used.
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {string} crossOrigin CORS value
+	 */
+	WWOBJLoader2.prototype.setCrossOrigin = function ( crossOrigin ) {
+		this.crossOrigin = crossOrigin;
+	};
+
+	/**
+	 * Register callback function that is invoked by internal function "_announceProgress" to print feedback
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {callback} callbackProgress Callback function for described functionality
+	 */
+	WWOBJLoader2.prototype.registerCallbackProgress = function ( callbackProgress ) {
+		if ( callbackProgress != null ) this.callbacks.progress = callbackProgress;
+	};
+
+	/**
+	 * Register callback function that is called once loading of the complete model is completed
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {callback} callbackCompletedLoading Callback function for described functionality
+	 */
+	WWOBJLoader2.prototype.registerCallbackCompletedLoading = function ( callbackCompletedLoading ) {
+		if ( callbackCompletedLoading != null ) this.callbacks.completedLoading = callbackCompletedLoading;
+	};
+
+	/**
+	 * Register callback function that is called once materials have been loaded. It allows to alter and return materials
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {callback} callbackMaterialsLoaded Callback function for described functionality
+	 */
+	WWOBJLoader2.prototype.registerCallbackMaterialsLoaded = function ( callbackMaterialsLoaded ) {
+		if ( callbackMaterialsLoaded != null ) this.callbacks.materialsLoaded = callbackMaterialsLoaded;
+	};
+
+	/**
+	 * Register callback function that is called every time a mesh was loaded
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {callback} callbackMeshLoaded Callback function for described functionality
+	 */
+	WWOBJLoader2.prototype.registerCallbackMeshLoaded = function ( callbackMeshLoaded ) {
+		if ( callbackMeshLoaded != null ) this.callbacks.meshLoaded = callbackMeshLoaded;
+	};
+
+	/**
+	 * Report if an error prevented loading
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {callback} callbackErrorWhileLoading Callback function for described functionality
+	 */
+	WWOBJLoader2.prototype.registerCallbackErrorWhileLoading = function ( callbackErrorWhileLoading ) {
+		if ( callbackErrorWhileLoading != null ) this.callbacks.errorWhileLoading = callbackErrorWhileLoading;
+	};
+
+	/**
+	 * Call requestTerminate to terminate the web worker and free local resource after execution
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {boolean} requestTerminate
+	 */
+	WWOBJLoader2.prototype.setRequestTerminate = function ( requestTerminate ) {
+		this.requestTerminate = ( requestTerminate != null && requestTerminate ) ? true : false;
+	};
+
+	WWOBJLoader2.prototype._validate = function () {
+		if ( this.validated ) return;
+		if ( this.worker == null ) {
+
+			this._buildWebWorkerCode();
+			var blob = new Blob( [ this.workerCode ], { type: 'text/plain' } );
+			this.worker = new Worker( window.URL.createObjectURL( blob ) );
+
+			var scope = this;
+			var scopeFunction = function ( e ) {
+				scope._receiveWorkerMessage( e );
+			};
+			this.worker.addEventListener( 'message', scopeFunction, false );
+
+		}
+
+		this.sceneGraphBaseNode = null;
+		this.streamMeshes = true;
+		this.meshStore = [];
+		this.modelName = 'none';
+		this.validated = true;
+		this.running = true;
+		this.requestTerminate = false;
+
+		this.fileLoader = ( this.fileLoader == null ) ? new THREE.FileLoader( this.manager ) : this.fileLoader;
+		this.mtlLoader = ( this.mtlLoader == null ) ?  new THREE.MTLLoader() : this.mtlLoader;
+		if ( this.crossOrigin != null ) this.mtlLoader.setCrossOrigin( this.crossOrigin );
+
+		this.dataAvailable = false;
+		this.fileObj = null;
+		this.pathObj = null;
+		this.fileMtl = null;
+		this.texturePath = null;
+
+		this.objAsArrayBuffer = null;
+		this.mtlAsString = null;
+
+		this.materials = [];
+		var defaultMaterial = new THREE.MeshStandardMaterial( { color: 0xDCF1FF } );
+		defaultMaterial.name = 'defaultMaterial';
+		this.materials[ defaultMaterial.name ] = defaultMaterial;
+
+		this.counter = 0;
+	};
+
+	/**
+	 * Set all parameters for required for execution of "run".
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 *
+	 * @param {Object} params Either {@link THREE.OBJLoader2.WWOBJLoader2.PrepDataArrayBuffer} or {@link THREE.OBJLoader2.WWOBJLoader2.PrepDataFile}
+	 */
+	WWOBJLoader2.prototype.prepareRun = function ( params ) {
+		this._validate();
+		this.dataAvailable = params.dataAvailable;
+		this.modelName = params.modelName;
+		console.time( 'WWOBJLoader2' );
+		if ( this.dataAvailable ) {
+
+			// fast-fail on bad type
+			if ( ! params.objAsArrayBuffer instanceof Uint8Array ) {
+				throw 'Provided input is not of type arraybuffer! Aborting...';
+			}
+
+			this.worker.postMessage( {
+				cmd: 'init',
+				debug: this.debug
+			} );
+
+			this.objAsArrayBuffer = params.objAsArrayBuffer;
+			this.mtlAsString = params.mtlAsString;
+
+		} else {
+
+			// fast-fail on bad type
+			if ( ! ( typeof( params.fileObj ) === 'string' || params.fileObj instanceof String ) ) {
+				throw 'Provided file is not properly defined! Aborting...';
+			}
+
+			this.worker.postMessage( {
+				cmd: 'init',
+				debug: this.debug
+			} );
+
+			this.fileObj = params.fileObj;
+			this.pathObj = params.pathObj;
+			this.fileMtl = params.fileMtl;
+
+		}
+		this.setRequestTerminate( params.requestTerminate );
+		this.pathTexture = params.pathTexture;
+		this.sceneGraphBaseNode = params.sceneGraphBaseNode;
+		this.streamMeshes = params.streamMeshes;
+		if ( ! this.streamMeshes ) this.meshStore = [];
+	};
+
+	/**
+	 * Run the loader according the preparation instruction provided in "prepareRun".
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2
+	 */
+	WWOBJLoader2.prototype.run = function () {
+		var scope = this;
+		var processLoadedMaterials = function ( materialCreator ) {
+			var materialCreatorMaterials = [];
+			var materialNames = [];
+			if ( materialCreator != null ) {
+
+				materialCreator.preload();
+				materialCreatorMaterials = materialCreator.materials;
+				for ( var materialName in materialCreatorMaterials ) {
+
+					if ( materialCreatorMaterials.hasOwnProperty( materialName ) ) {
+
+						materialNames.push( materialName );
+						scope.materials[ materialName ] = materialCreatorMaterials[ materialName ];
+
+					}
+
+				}
+
+			}
+			scope.worker.postMessage( {
+				cmd: 'setMaterials',
+				materialNames: materialNames
+			} );
+
+			if ( scope.callbacks.materialsLoaded != null ) {
+
+				var materialsCallback = scope.callbacks.materialsLoaded( scope.materials );
+				if ( materialsCallback != null ) scope.materials = materialsCallback;
+
+			}
+
+			if ( scope.dataAvailable && scope.objAsArrayBuffer ) {
+
+				scope.worker.postMessage({
+					cmd: 'run',
+					objAsArrayBuffer: scope.objAsArrayBuffer
+				}, [ scope.objAsArrayBuffer.buffer ] );
+
+			} else {
+
+				var refPercentComplete = 0;
+				var percentComplete = 0;
+				var output;
+				var onLoad = function ( objAsArrayBuffer ) {
+
+					scope._announceProgress( 'Running web worker!' );
+					scope.objAsArrayBuffer = new Uint8Array( objAsArrayBuffer );
+					scope.worker.postMessage( {
+						cmd: 'run',
+						objAsArrayBuffer: scope.objAsArrayBuffer
+					}, [ scope.objAsArrayBuffer.buffer ] );
+
+				};
+
+				var onProgress = function ( event ) {
+					if ( ! event.lengthComputable ) return;
+
+					percentComplete = Math.round( event.loaded / event.total * 100 );
+					if ( percentComplete > refPercentComplete ) {
+
+						refPercentComplete = percentComplete;
+						output = 'Download of "' + scope.fileObj + '": ' + percentComplete + '%';
+						console.log( output );
+						scope._announceProgress( output );
+
+					}
+				};
+
+				var onError = function ( event ) {
+					output = 'Error occurred while downloading "' + scope.fileObj + '"';
+					console.error( output + ': ' + event );
+					scope._announceProgress( output );
+					scope._finalize( 'error' );
+
+				};
+
+				scope.fileLoader.setPath( scope.pathObj );
+				scope.fileLoader.setResponseType( 'arraybuffer' );
+				scope.fileLoader.load( scope.fileObj, onLoad, onProgress, onError );
+			}
+			console.timeEnd( 'Loading MTL textures' );
+		};
+
+
+		this.mtlLoader.setPath( this.pathTexture );
+		if ( this.dataAvailable ) {
+
+			processLoadedMaterials( ( this.mtlAsString != null ) ? this.mtlLoader.parse( this.mtlAsString ) : null );
+
+		} else {
+
+			if ( this.fileMtl == null ) {
+
+				processLoadedMaterials();
+
+			} else {
+
+				this.mtlLoader.load( this.fileMtl, processLoadedMaterials );
+
+			}
+
+		}
+	};
+
+	WWOBJLoader2.prototype._receiveWorkerMessage = function ( event ) {
+		var payload = event.data;
+
+		switch ( payload.cmd ) {
+			case 'objData':
+
+				this.counter ++;
+				var bufferGeometry = new THREE.BufferGeometry();
+
+				bufferGeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( payload.vertices ), 3 ) );
+				if ( payload.normals !== null ) {
+
+					bufferGeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( payload.normals ), 3 ) );
+
+				} else {
+
+					bufferGeometry.computeVertexNormals();
+
+				}
+				if ( payload.uvs !== null ) {
+
+					bufferGeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( payload.uvs ), 2 ) );
+
+				}
+
+				var materialDescriptions = payload.materialDescriptions;
+				var materialDescription;
+				var material;
+				var materialName;
+				var createMultiMaterial = payload.multiMaterial;
+				var multiMaterials = [];
+
+				for ( var key in materialDescriptions ) {
+
+					materialDescription = materialDescriptions[ key ];
+					material = this.materials[ materialDescription.name ];
+
+					if ( materialDescription.default ) {
+
+						material = this.materials[ 'defaultMaterial' ];
+
+					} else if ( materialDescription.clone ) {
+
+						materialName = material.name + '_flat';
+						var materialClone = this.materials[ materialName ];
+						if ( ! materialClone ) {
+
+							materialClone = material.clone();
+							materialClone.name = materialName;
+							materialClone.shading = THREE.FlatShading;
+							this.materials[ materialName ] = name;
+
+						}
+
+					} else if ( ! material ) {
+
+						material = this.materials[ 'defaultMaterial' ];
+
+					}
+					if ( createMultiMaterial ) multiMaterials.push( material );
+
+				}
+
+				if ( createMultiMaterial ) {
+
+					material = new THREE.MultiMaterial( multiMaterials );
+					var materialGroups = payload.materialGroups;
+					var materialGroup;
+					for ( var key in materialGroups ) {
+
+						materialGroup = materialGroups[ key ];
+						bufferGeometry.addGroup( materialGroup.start, materialGroup.count, materialGroup.index );
+
+					}
+
+				}
+				if ( this.callbacks.meshLoaded !== null ) {
+
+					var materialOverride = this.callbacks.meshLoaded( payload.meshName, material );
+					if ( materialOverride != null ) material = materialOverride;
+
+				}
+				var mesh = new THREE.Mesh( bufferGeometry, material );
+				mesh.name = payload.meshName;
+				if ( this.streamMeshes ) {
+
+					this.sceneGraphBaseNode.add( mesh );
+
+				} else {
+
+					this.meshStore.push( mesh );
+
+				}
+				var output = '(' + this.counter + '): ' + payload.meshName;
+				this._announceProgress( 'Adding mesh', output );
+				break;
+
+			case 'complete':
+
+				if ( ! this.streamMeshes ) {
+
+					for ( var key in this.meshStore ) {
+
+						this.sceneGraphBaseNode.add( this.meshStore[ key ] );
+
+					}
+
+				}
+
+				console.timeEnd( 'WWOBJLoader2' );
+				if ( payload.msg != null ) {
+
+					this._announceProgress( payload.msg );
+
+				} else {
+
+					this._announceProgress( '' );
+
+				}
+
+				this._finalize( 'complete' );
+				break;
+
+			case 'report_progress':
+				this._announceProgress( '', payload.output );
+				break;
+
+			default:
+				console.error( 'Received unknown command: ' + payload.cmd );
+				break;
+
+		}
+	};
+
+	WWOBJLoader2.prototype._terminate = function () {
+		if ( this.worker != null ) {
+
+			if ( this.running ) throw 'Unable to gracefully terminate worker as it is currently running!';
+
+			this.worker.terminate();
+			this.worker = null;
+			this.workerCode = null;
+			this._finalize( 'terminate' );
+
+		}
+		this.fileLoader = null;
+		this.mtlLoader = null;
+	};
+
+	WWOBJLoader2.prototype._finalize = function ( reason, requestTerminate ) {
+		this.running = false;
+		if ( reason === 'complete' ) {
+
+			if ( this.callbacks.completedLoading != null ) this.callbacks.completedLoading( this.modelName, this.instanceNo, this.requestTerminate );
+			if ( this.callbacks.director.completedLoading != null ) this.callbacks.director.completedLoading( this.modelName, this.instanceNo, this.requestTerminate );
+
+		} else if ( reason === 'error' ) {
+
+			if ( this.callbacks.errorWhileLoading != null ) this.callbacks.errorWhileLoading( this.modelName, this.instanceNo, this.requestTerminate );
+			if ( this.callbacks.director.errorWhileLoading != null ) this.callbacks.director.errorWhileLoading( this.modelName, this.instanceNo, this.requestTerminate );
+
+		}
+		this.validated = false;
+
+		this.setRequestTerminate( requestTerminate );
+
+		if ( this.requestTerminate ) {
+			this._terminate();
+		}
+	};
+
+	WWOBJLoader2.prototype._announceProgress = function ( baseText, text ) {
+		var output = "";
+		if ( baseText !== null && baseText !== undefined ) {
+
+			output = baseText;
+
+		}
+		if ( text !== null && text !== undefined ) {
+
+			output = output + " " + text;
+
+		}
+		if ( this.callbacks.progress !== null ) {
+
+			this.callbacks.progress( output );
+
+		}
+		if ( this.debug ) {
+
+			console.log( output );
+
+		}
+	};
+
+	WWOBJLoader2.prototype._buildWebWorkerCode = function ( existingWorkerCode ) {
+		if ( existingWorkerCode != null ) this.workerCode = existingWorkerCode;
+		if ( this.workerCode == null ) {
+
+			console.time( 'buildWebWorkerCode' );
+			var wwDef = (function () {
+
+				function OBJLoader() {
+					this.meshCreator = new MeshCreator();
+					this.parser = new Parser( this.meshCreator );
+					this.validated = false;
+					this.cmdState = 'created';
+
+					this.debug = false;
+				}
+
+				/**
+				 * Allows to set debug mode for the parser and the meshCreatorDebug
+				 *
+				 * @param parserDebug
+				 * @param meshCreatorDebug
+				 */
+				OBJLoader.prototype._setDebug = function ( parserDebug, meshCreatorDebug ) {
+					this.parser._setDebug( parserDebug );
+					this.meshCreator._setDebug( meshCreatorDebug );
+				};
+
+				/**
+				 * Validate status, then parse arrayBuffer, finalize and return objGroup
+				 *
+				 * @param arrayBuffer
+				 */
+				OBJLoader.prototype.parse = function ( arrayBuffer ) {
+					console.log( 'Parsing arrayBuffer...' );
+					console.time( 'parseArrayBuffer' );
+
+					this._validate();
+					this.parser.parseArrayBuffer( arrayBuffer );
+					var objGroup = this._finalize();
+
+					console.timeEnd( 'parseArrayBuffer' );
+
+					return objGroup;
+				};
+
+				OBJLoader.prototype._validate = function () {
+					if ( this.validated ) return;
+
+					this.parser._validate();
+					this.meshCreator._validate();
+
+					this.validated = true;
+				};
+
+				OBJLoader.prototype._finalize = function () {
+					console.log( 'Global output object count: ' + this.meshCreator.globalObjectCount );
+					this.parser._finalize();
+					this.meshCreator._finalize();
+					this.validated = false;
+				};
+
+				OBJLoader.prototype.init = function ( payload ) {
+					this.cmdState = 'init';
+					this._setDebug( payload.debug, payload.debug );
+				};
+
+				OBJLoader.prototype.setMaterials = function ( payload ) {
+					this.cmdState = 'setMaterials';
+					this.meshCreator._setMaterials( payload.materialNames );
+				};
+
+				OBJLoader.prototype.run = function ( payload ) {
+					this.cmdState = 'run';
+
+					this.parse( payload.objAsArrayBuffer );
+					console.log( 'OBJ loading complete!' );
+
+					this.cmdState = 'complete';
+					self.postMessage( {
+						cmd: this.cmdState,
+						msg: null
+					} );
+				};
+
+				return OBJLoader;
+			})();
+
+			var wwMeshCreatorDef = (function () {
+
+				function MeshCreator() {
+					this.materials = null;
+					this.debug = false;
+					this.globalObjectCount = 1;
+					this.validated = false;
+				}
+
+				MeshCreator.prototype._setMaterials = function ( materials ) {
+					this.materials = ( materials == null ) ? ( this.materials == null ? { materials: [] } : this.materials ) : materials;
+				};
+
+				MeshCreator.prototype._setDebug = function ( debug ) {
+					this.debug = ( debug == null ) ? this.debug : debug;
+				};
+
+				MeshCreator.prototype._validate = function () {
+					if ( this.validated ) return;
+
+					this._setMaterials( null );
+					this._setDebug( null );
+					this.globalObjectCount = 1;
+				};
+
+				MeshCreator.prototype._finalize = function () {
+					this.materials = null;
+					this.validated = false;
+				};
+
+				/**
+				 * RawObjectDescriptions are transformed to THREE.Mesh.
+				 * It is ensured that rawObjectDescriptions only contain objects with vertices (no need to check).
+				 *
+				 * @param rawObjectDescriptions
+				 * @param inputObjectCount
+				 * @param absoluteVertexCount
+				 * @param absoluteNormalCount
+				 * @param absoluteUvCount
+				 */
+				MeshCreator.prototype._buildMesh = function ( rawObjectDescriptions, inputObjectCount, absoluteVertexCount, absoluteNormalCount, absoluteUvCount ) {
+					if ( this.debug ) console.log( 'OBJLoader.buildMesh:\nInput object no.: ' + inputObjectCount );
+
+					var vertexFa = new Float32Array( absoluteVertexCount );
+					var normalFA = ( absoluteNormalCount > 0 ) ? new Float32Array( absoluteNormalCount ) : null;
+					var uvFA = ( absoluteUvCount > 0 ) ? new Float32Array( absoluteUvCount ) : null;
+
+					var rawObjectDescription;
+					var materialDescription;
+					var materialDescriptions = [];
+
+					var createMultiMaterial = ( rawObjectDescriptions.length > 1 );
+					var materialIndex = 0;
+					var materialIndexMapping = [];
+					var selectedMaterialIndex;
+					var materialGroup;
+					var materialGroups = [];
+
+					var vertexBAOffset = 0;
+					var vertexGroupOffset = 0;
+					var vertexLength;
+					var normalOffset = 0;
+					var uvOffset = 0;
+
+					for ( var oodIndex in rawObjectDescriptions ) {
+						rawObjectDescription = rawObjectDescriptions[ oodIndex ];
+
+						materialDescription = { name: rawObjectDescription.materialName, flat: false, default: false };
+						if ( this.materials[ materialDescription.name ] === null ) {
+
+							materialDescription.default = true;
+							console.warn( 'object_group "' + rawObjectDescription.objectName + '_' + rawObjectDescription.groupName + '" was defined without material! Assigning "defaultMaterial".' );
+
+						}
+						// Attach '_flat' to materialName in case flat shading is needed due to smoothingGroup 0
+						if ( rawObjectDescription.smoothingGroup === 0 ) materialDescription.flat = true;
+
+						vertexLength = rawObjectDescription.vertices.length;
+						if ( createMultiMaterial ) {
+
+							// re-use material if already used before. Reduces materials array size and eliminates duplicates
+
+							selectedMaterialIndex = materialIndexMapping[ materialDescription.name ];
+							if ( ! selectedMaterialIndex ) {
+
+								selectedMaterialIndex = materialIndex;
+								materialIndexMapping[ materialDescription.name ] = materialIndex;
+								materialDescriptions.push( materialDescription );
+								materialIndex++;
+
+							}
+							materialGroup = {
+								start: vertexGroupOffset,
+								count: vertexLength / 3,
+								index: selectedMaterialIndex
+							};
+							materialGroups.push( materialGroup );
+							vertexGroupOffset += vertexLength / 3;
+
+						} else {
+
+							materialDescriptions.push( materialDescription );
+
+						}
+
+						vertexFa.set( rawObjectDescription.vertices, vertexBAOffset );
+						vertexBAOffset += vertexLength;
+
+						if ( normalFA ) {
+
+							normalFA.set( rawObjectDescription.normals, normalOffset );
+							normalOffset += rawObjectDescription.normals.length;
+
+						}
+						if ( uvFA ) {
+
+							uvFA.set( rawObjectDescription.uvs, uvOffset );
+							uvOffset += rawObjectDescription.uvs.length;
+
+						}
+						if ( this.debug ) this.printReport( rawObjectDescription, selectedMaterialIndex );
+
+					}
+
+					self.postMessage( {
+						cmd: 'objData',
+						meshName: rawObjectDescription.objectName,
+						multiMaterial: createMultiMaterial,
+						materialDescriptions: materialDescriptions,
+						materialGroups: materialGroups,
+						vertices: vertexFa,
+						normals: normalFA,
+						uvs: uvFA
+					}, [ vertexFa.buffer ], normalFA !== null ? [ normalFA.buffer ] : null, uvFA !== null ? [ uvFA.buffer ] : null );
+
+					this.globalObjectCount++;
+				};
+
+				return MeshCreator;
+			})();
+
+			var wwLoaderRunnerDef = (function () {
+
+				function OBJLoaderRunner() {
+					self.addEventListener( 'message', this.runner, false );
+				}
+
+				OBJLoaderRunner.prototype.runner = function ( event ) {
+					var payload = event.data;
+
+					console.log( 'Command state before: ' + OBJLoaderRef.cmdState );
+
+					switch ( payload.cmd ) {
+						case 'init':
+
+							OBJLoaderRef.init( payload );
+							break;
+
+						case 'setMaterials':
+
+							OBJLoaderRef.setMaterials( payload );
+							break;
+
+						case 'run':
+
+							OBJLoaderRef.run( payload );
+							break;
+
+						default:
+
+							console.error( 'OBJLoader: Received unknown command: ' + payload.cmd );
+							break;
+
+					}
+
+					console.log( 'Command state after: ' + OBJLoaderRef.cmdState );
+				};
+
+				return OBJLoaderRunner;
+			})();
+
+			var buildObject = function ( fullName, object ) {
+				var objectString = fullName + ' = {\n';
+				var part;
+				for ( var name in object ) {
+
+					part = object[ name ];
+					if ( typeof( part ) === 'string' || part instanceof String ) {
+
+						part = part.replace( '\n', '\\n' );
+						part = part.replace( '\r', '\\r' );
+						objectString += '\t' + name + ': "' + part + '",\n';
+
+					} else if ( part instanceof Array ) {
+
+						objectString += '\t' + name + ': [' + part + '],\n';
+
+					} else if ( Number.isInteger( part ) ) {
+
+						objectString += '\t' + name + ': ' + part + ',\n';
+
+					} else if ( typeof part === 'function' ) {
+
+						objectString += '\t' + name + ': ' + part + ',\n';
+
+					}
+
+				}
+				objectString += '}\n\n';
+
+				return objectString;
+			};
+
+			var buildSingelton = function ( fullName, internalName, object ) {
+				var objectString = fullName + ' = (function () {\n\n';
+				objectString += '\t' + object.prototype.constructor.toString() + '\n\n';
+
+				var funcString;
+				var objectPart;
+				for ( var name in object.prototype ) {
+
+					objectPart = object.prototype[ name ];
+					if ( typeof objectPart === 'function' ) {
+
+						funcString = objectPart.toString();
+						objectString += '\t' + internalName + '.prototype.' + name + ' = ' + funcString + ';\n\n';
+
+					}
+
+				}
+				objectString += '\treturn ' + internalName + ';\n';
+				objectString += '})();\n\n';
+
+				return objectString;
+			};
+
+			this.workerCode = '';
+			this.workerCode += '/**\n';
+			this.workerCode += '  * This code was constructed by WWOBJLoader2._buildWebWorkerCode\n';
+			this.workerCode += '  */\n\n';
+
+			// parser re-construction
+			var objLoaderHelper = new THREE.OBJLoader2();
+			this.workerCode += objLoaderHelper._buildWebWorkerCode( buildObject, buildSingelton );
+
+			// web worker construction
+			this.workerCode += buildSingelton( 'OBJLoader', 'OBJLoader', wwDef );
+			this.workerCode += buildSingelton( 'MeshCreator', 'MeshCreator', wwMeshCreatorDef );
+			this.workerCode += 'OBJLoaderRef = new OBJLoader();\n\n';
+			this.workerCode += buildSingelton( 'OBJLoaderRunner', 'OBJLoaderRunner', wwLoaderRunnerDef );
+			this.workerCode += 'new OBJLoaderRunner();\n\n';
+
+			console.timeEnd( 'buildWebWorkerCode' );
+		}
+
+		return this.workerCode;
+	};
+
+	return WWOBJLoader2;
+
+})();
+
+/**
+ * Instruction to configure {@link THREE.OBJLoader2.WWOBJLoader2}.prepareRun to load OBJ from given ArrayBuffer and MTL from given String
+ *
+ * @param {string} modelName Overall name of the model
+ * @param {Uint8Array} objAsArrayBuffer OBJ file content as ArrayBuffer
+ * @param {string} pathTexture Path to texture files
+ * @param {string} mtlAsString MTL file content as string
+ * @param {THREE.Object3D} sceneGraphBaseNode {@link THREE.Object3D} where meshes will be attached
+ * @param {boolean} streamMeshes=true Singles meshes are directly integrated into scene when loaded or later
+ * @param {boolean} [requestTerminate=false] Request termination of web worker and free local resources after execution
+ *
+ * @returns {{modelName: string, dataAvailable: boolean, objAsArrayBuffer: null, pathTexture: null, mtlAsString: null, sceneGraphBaseNode: null, streamMeshes: boolean, requestTerminate: boolean}}
+ * @constructor
+ */
+THREE.OBJLoader2.WWOBJLoader2.PrepDataArrayBuffer = function ( modelName, objAsArrayBuffer, pathTexture, mtlAsString, sceneGraphBaseNode, streamMeshes, requestTerminate ) {
+
+	var data = {
+		modelName: ( modelName == null ) ? 'none' : modelName,
+		dataAvailable: true,
+		objAsArrayBuffer: ( objAsArrayBuffer == null ) ? null : objAsArrayBuffer,
+		pathTexture: ( pathTexture == null ) ? null : pathTexture,
+		mtlAsString: ( mtlAsString == null ) ? null : mtlAsString,
+		sceneGraphBaseNode: ( sceneGraphBaseNode == null ) ? null : sceneGraphBaseNode,
+		streamMeshes: ( streamMeshes == null ) ? true : streamMeshes,
+		requestTerminate: ( requestTerminate == null ) ? false : requestTerminate
+	};
+
+	return data;
+};
+
+/**
+ * Instruction to configure {@link THREE.OBJLoader2.WWOBJLoader2}.prepareRun to load OBJ and MTL from files
+ *
+ * @param {string} modelName Overall name of the model
+ * @param {string} pathObj Path to OBJ file
+ * @param {string} fileObj OBJ file name
+ * @param {string} pathTexture Path to texture files
+ * @param {string} fileMtl MTL file name
+ * @param {THREE.Object3D} sceneGraphBaseNode {@link THREE.Object3D} where meshes will be attached
+ * @param {boolean} streamMeshes=true Singles meshes are directly integrated into scene when loaded or later
+ * @param {boolean} [requestTerminate=false] Request termination of web worker and free local resources after execution
+ *
+ * @returns {{modelName: string, dataAvailable: boolean, pathObj: null, fileObj: null, pathTexture: null, fileMtl: null, sceneGraphBaseNode: null, streamMeshes: boolean,  requestTerminate: boolean}}
+ * @constructor
+ */
+THREE.OBJLoader2.WWOBJLoader2.PrepDataFile = function ( modelName, pathObj, fileObj, pathTexture, fileMtl, sceneGraphBaseNode, streamMeshes, requestTerminate ) {
+
+	var data = {
+		modelName: ( modelName == null ) ? 'none' : modelName,
+		dataAvailable: false,
+		pathObj: ( pathObj == null ) ? null : pathObj,
+		fileObj: ( fileObj == null ) ? null : fileObj,
+		pathTexture: ( pathTexture == null ) ? null : pathTexture,
+		fileMtl: ( fileMtl == null ) ? null : fileMtl,
+		sceneGraphBaseNode: ( sceneGraphBaseNode == null ) ? null : sceneGraphBaseNode,
+		streamMeshes: ( streamMeshes == null ) ? true : streamMeshes,
+		requestTerminate: ( requestTerminate == null ) ? false : requestTerminate
+	};
+
+	return data;
+};
+/**
+ * Orchestrate loading of multiple OBJ files/data from an instruction queue with a configurable amount of workers (1-16).
+ * Use:
+ *   prepareWorkers
+ *   enqueueForRun
+ *   processQueue
+ *   deregister
+ *
+ * @class
+ */
+THREE.OBJLoader2.WWOBJLoader2Director = (function () {
+
+	var MAX_WEB_WORKER = 16;
+	var MAX_QUEUE_SIZE = 1024;
+
+	function WWOBJLoader2Director() {
+		this.maxQueueSize = MAX_QUEUE_SIZE ;
+		this.maxWebWorkers = MAX_WEB_WORKER;
+		this.crossOrigin = null;
+
+		this.workerDescription = {
+			prototypeDef: THREE.OBJLoader2.WWOBJLoader2.prototype,
+			callbacks: {},
+			webWorkers: [],
+			codeBuffer: null
+		};
+		this.objectsCompleted = 0;
+		this.instructionQueue = [];
+	}
+
+	/**
+	 * Returns the maximum length of the instruction queue.
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2Director
+	 *
+	 * @returns {number}
+	 */
+	WWOBJLoader2Director.prototype.getMaxQueueSize = function () {
+		return this.maxQueueSize;
+	};
+
+	/**
+	 * Returns the maximum number of workers.
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2Director
+	 *
+	 * @returns {number}
+	 */
+	WWOBJLoader2Director.prototype.getMaxWebWorkers = function () {
+		return this.maxWebWorkers;
+	};
+
+	/**
+	 * Sets the CORS string to be used.
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2Director
+	 *
+	 * @param {string} crossOrigin CORS value
+	 */
+	WWOBJLoader2Director.prototype.setCrossOrigin = function ( crossOrigin ) {
+		this.crossOrigin = crossOrigin;
+	};
+
+	/**
+	 * Create or destroy workers according limits. Set the name and register callbacks for dynamically created web workers.
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2Director
+	 *
+	 * @param {callback[]} callbacks Register callbacks for all web workers:
+	 * 		{ progress: null, completedLoading: null, errorWhileLoading: null, materialsLoaded: null, meshLoaded: null }
+	 * @param {number} maxQueueSize Set the maximum size of the instruction queue (1-1024)
+	 * @param {number} maxWebWorkers Set the maximum amount of workers (1-16)
+	 */
+	WWOBJLoader2Director.prototype.prepareWorkers = function ( callbacks, maxQueueSize, maxWebWorkers ) {
+		if ( callbacks != null ) {
+
+			for ( var key in callbacks ) {
+
+				if ( callbacks.hasOwnProperty( key ) ) this.workerDescription.callbacks[ key ] = callbacks[ key ];
+
+			}
+
+		}
+
+		this.maxQueueSize = Math.min( maxQueueSize, MAX_QUEUE_SIZE );
+		this.maxWebWorkers = Math.min( maxWebWorkers, MAX_WEB_WORKER );
+		this.objectsCompleted = 0;
+		this.instructionQueue = [];
+
+		var start = this.workerDescription.webWorkers.length;
+		if ( start < this.maxWebWorkers ) {
+
+			for ( i = start; i < this.maxWebWorkers; i ++ ) {
+
+				webWorker = this._buildWebWorker();
+				this.workerDescription.webWorkers[ i ] = webWorker;
+
+			}
+
+		} else {
+
+			for ( var webWorker, i = start - 1; i >= this.maxWebWorkers; i-- ) {
+
+				webWorker = this.workerDescription.webWorkers[ i ];
+				webWorker.setRequestTerminate( true );
+
+				this.workerDescription.webWorkers.pop();
+
+			}
+
+		}
+	};
+
+	/**
+	 * Store run instructions in internal instructionQueue
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2Director
+	 *
+	 * @param {Object} params Either {@link THREE.OBJLoader2.WWOBJLoader2.PrepDataArrayBuffer} or {@link THREE.OBJLoader2.WWOBJLoader2.PrepDataFile}
+	 */
+	WWOBJLoader2Director.prototype.enqueueForRun = function ( runParams ) {
+		if ( this.instructionQueue.length < this.maxQueueSize ) {
+			this.instructionQueue.push( runParams );
+		}
+	};
+
+	/**
+	 * Process the instructionQueue until it is depleted
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2Director
+	 */
+	WWOBJLoader2Director.prototype.processQueue = function () {
+		if ( this.instructionQueue.length === 0 ) return;
+
+		var webWorker;
+		var runParams;
+		var length = Math.min( this.maxWebWorkers, this.instructionQueue.length );
+		for ( var i = 0; i < length; i++ ) {
+
+			webWorker = this.workerDescription.webWorkers[ i ];
+			runParams = this.instructionQueue[ 0 ];
+			webWorker.prepareRun( runParams );
+			webWorker.run();
+			this.instructionQueue.shift();
+
+		}
+	};
+
+	WWOBJLoader2Director.prototype._buildWebWorker = function () {
+		var webWorker = Object.create( this.workerDescription.prototypeDef );
+		webWorker._init();
+		if ( this.crossOrigin != null )	webWorker.setCrossOrigin( this.crossOrigin );
+
+		// Ensure code string is built once and then it is just passed on to every new instance
+		if ( this.workerDescription.codeBuffer == null ) {
+
+			this.workerDescription.codeBuffer = webWorker._buildWebWorkerCode();
+
+		} else {
+
+			webWorker._buildWebWorkerCode( this.workerDescription.codeBuffer );
+
+		}
+		for ( var key in this.workerDescription.callbacks ) {
+
+			if ( webWorker.callbacks.hasOwnProperty( key ) && this.workerDescription.callbacks.hasOwnProperty( key ) ) {
+
+				webWorker.callbacks[ key ] = this.workerDescription.callbacks[ key ];
+
+			}
+
+		}
+		var scope = this;
+		var managerCompletedLoading = function ( modelName, instanceNo, requestTerminate ) {
+			scope.objectsCompleted++;
+			if ( ! requestTerminate ) {
+
+				var rekick = scope.workerDescription.webWorkers[ instanceNo ];
+				var runParams = scope.instructionQueue[ 0 ];
+				if ( runParams != null ) {
+
+					rekick.prepareRun( runParams );
+					rekick.run();
+					scope.instructionQueue.shift();
+
+				}
+
+			}
+		};
+
+		webWorker.callbacks.director[ 'completedLoading' ] = managerCompletedLoading;
+		webWorker.instanceNo = this.workerDescription.webWorkers.length;
+		this.workerDescription.webWorkers.push( webWorker );
+		return webWorker;
+	};
+
+	/**
+	 * Terminate all workers
+	 * @memberOf THREE.OBJLoader2.WWOBJLoader2Director
+	 */
+	WWOBJLoader2Director.prototype.deregister = function () {
+		console.log( 'WWOBJLoader2Director received the unregister call. Terminating all workers!' );
+		for ( var i = 0, webWorker, length = this.workerDescription.webWorkers.length; i < length; i++ ) {
+
+			webWorker = this.workerDescription.webWorkers[ i ];
+			webWorker.setRequestTerminate( true );
+
+		}
+		this.workerDescription.callbacks = {};
+		this.workerDescription.webWorkers = [];
+		this.workerDescription.codeBuffer = null;
+	};
+
+	return WWOBJLoader2Director;
+
+})();

+ 343 - 0
examples/webgl_loader_obj2.html

@@ -0,0 +1,343 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - OBJLoader2</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+
+		<style>
+			body {
+				font-family: Monospace;
+				background-color: #000;
+				color: #fff;
+				margin: 0 0 0 0;
+				padding: 0 0 0 0;
+				border: none;
+				cursor: default;
+			}
+			#info {
+				color: #fff;
+				position: absolute;
+				top: 10px;
+				width: 100%;
+				text-align: center;
+				z-index: 100;
+				display:block;
+			}
+			#info a {
+				color: #f00;
+				font-weight: bold;
+				text-decoration: underline;
+				cursor: pointer
+			}
+			#glFullscreen {
+				width: 100%;
+				height: 100vh;
+				min-width: 640px;
+				min-height: 360px;
+				position: relative;
+				overflow: hidden;
+				z-index: 0;
+			}
+			#example {
+				width: 100%;
+				height: 100%;
+				top: 0;
+				left: 0;
+				background-color: #000000;
+			}
+			#feedback {
+				color: darkorange;
+			}
+			#dat {
+				user-select: none;
+				position: absolute;
+				left: 0;
+				top: 0;
+				z-Index: 200;
+			}
+		</style>
+
+	</head>
+
+	<body>
+		<div id="glFullscreen">
+			<canvas id="example"></canvas>
+		</div>
+		<div id="dat">
+
+		</div>
+		<div id="info">
+			<a href="http://threejs.org" target="_blank">three.js</a> - OBJLoader2 direct loader test
+			<div id="feedback"></div>
+		</div>
+
+		<script src="js/Detector.js"></script>
+		<script src="../build/three.js"></script>
+		<script src="js/controls/TrackballControls.js"></script>
+		<script src="js/loaders/MTLLoader.js"></script>
+		<script src="js/libs/dat.gui.min.js"></script>
+
+		<script src="js/loaders/OBJLoader2.js"></script>
+		<script>
+
+			'use strict';
+
+			var OBJLoader2Example = (function () {
+
+				function OBJLoader2Example( elementToBindTo ) {
+					this.renderer = null;
+					this.canvas = elementToBindTo;
+					this.aspectRatio = 1;
+					this.recalcAspectRatio();
+
+					this.scene = null;
+					this.cameraDefaults = {
+						posCamera: new THREE.Vector3( 0.0, 175.0, 500.0 ),
+						posCameraTarget: new THREE.Vector3( 0, 0, 0 ),
+						near: 0.1,
+						far: 10000,
+						fov: 45
+					};
+					this.camera = null;
+					this.cameraTarget = this.cameraDefaults.posCameraTarget;
+
+					this.controls = null;
+
+					this.smoothShading = true;
+					this.doubleSide = false;
+
+					this.cube = null;
+					this.pivot = null;
+				}
+
+				OBJLoader2Example.prototype.initGL = function () {
+					this.renderer = new THREE.WebGLRenderer( {
+						canvas: this.canvas,
+						antialias: true,
+						autoClear: true
+					} );
+					this.renderer.setClearColor( 0x050505 );
+
+					this.scene = new THREE.Scene();
+
+					this.camera = new THREE.PerspectiveCamera( this.cameraDefaults.fov, this.aspectRatio, this.cameraDefaults.near, this.cameraDefaults.far );
+					this.resetCamera();
+					this.controls = new THREE.TrackballControls( this.camera, this.renderer.domElement );
+
+					var ambientLight = new THREE.AmbientLight( 0x404040 );
+					var directionalLight1 = new THREE.DirectionalLight( 0xC0C090 );
+					var directionalLight2 = new THREE.DirectionalLight( 0xC0C090 );
+
+					directionalLight1.position.set( -100, -50, 100 );
+					directionalLight2.position.set( 100, 50, -100 );
+
+					this.scene.add( directionalLight1 );
+					this.scene.add( directionalLight2 );
+					this.scene.add( ambientLight );
+
+					var helper = new THREE.GridHelper( 1200, 60, 0xFF4444, 0x404040 );
+					this.scene.add( helper );
+
+					var geometry = new THREE.BoxGeometry( 10, 10, 10 );
+					var material = new THREE.MeshNormalMaterial();
+					this.cube = new THREE.Mesh( geometry, material );
+					this.cube.position.set( 0, 0, 0 );
+					this.scene.add( this.cube );
+
+					this.pivot = new THREE.Object3D();
+					this.pivot.name = 'Pivot';
+					this.scene.add( this.pivot );
+				};
+
+				OBJLoader2Example.prototype.initPostGL = function ( objDef ) {
+					var scope = this;
+
+					var mtlLoader = new THREE.MTLLoader();
+					mtlLoader.setPath( objDef.texturePath );
+					mtlLoader.setCrossOrigin( 'anonymous' );
+					mtlLoader.load( objDef.fileMtl, function( materials ) {
+
+						materials.preload();
+
+						var objLoader = new THREE.OBJLoader2();
+						objLoader.setSceneGraphBaseNode( scope.pivot );
+						objLoader.setMaterials( materials.materials );
+						objLoader.setPath( objDef.path );
+						objLoader.setDebug( false, false );
+
+						var onSuccess = function ( object3d ) {
+							console.log( 'Loading complete. Meshes were attached to: ' + object3d.name );
+						};
+
+						var onProgress = function ( event ) {
+							if ( event.lengthComputable ) {
+
+								var percentComplete = event.loaded / event.total * 100;
+								var output = 'Download of "' + objDef.fileObj + '": ' + Math.round( percentComplete ) + '%';
+								console.log(output);
+
+							}
+						};
+
+						var onError = function ( event ) {
+							console.error( 'Error of type "' + event.type + '" occurred when trying to load: ' + event.src );
+						};
+
+						objLoader.load( objDef.fileObj, onSuccess, onProgress, onError );
+
+					});
+
+					return true;
+				};
+
+				OBJLoader2Example.prototype.resizeDisplayGL = function () {
+					this.controls.handleResize();
+
+					this.recalcAspectRatio();
+					this.renderer.setSize( this.canvas.offsetWidth, this.canvas.offsetHeight, false );
+
+					this.updateCamera();
+				};
+
+				OBJLoader2Example.prototype.recalcAspectRatio = function () {
+					this.aspectRatio = ( this.canvas.offsetHeight === 0 ) ? 1 : this.canvas.offsetWidth / this.canvas.offsetHeight;
+				};
+
+				OBJLoader2Example.prototype.resetCamera = function () {
+					this.camera.position.copy( this.cameraDefaults.posCamera );
+					this.cameraTarget.copy( this.cameraDefaults.posCameraTarget );
+
+					this.updateCamera();
+				};
+
+				OBJLoader2Example.prototype.updateCamera = function () {
+					this.camera.aspect = this.aspectRatio;
+					this.camera.lookAt( this.cameraTarget );
+					this.camera.updateProjectionMatrix();
+				};
+
+				OBJLoader2Example.prototype.render = function () {
+					if ( ! this.renderer.autoClear ) this.renderer.clear();
+
+					this.controls.update();
+
+					this.cube.rotation.x += 0.05;
+					this.cube.rotation.y += 0.05;
+
+					this.renderer.render( this.scene, this.camera );
+				};
+
+				OBJLoader2Example.prototype.alterSmoothShading = function () {
+
+					var scope = this;
+					scope.smoothShading = ! scope.smoothShading;
+					console.log( scope.smoothShading ? 'Enabling SmoothShading' : 'Enabling FlatShading');
+
+					scope.traversalFunction = function ( material ) {
+						material.shading = scope.smoothShading ? THREE.SmoothShading : THREE.FlatShading;
+						material.needsUpdate = true;
+					};
+					var scopeTraverse = function ( object3d ) {
+						scope.traverseScene( object3d );
+					};
+					scope.pivot.traverse( scopeTraverse );
+				};
+
+				OBJLoader2Example.prototype.alterDouble = function () {
+
+					var scope = this;
+					scope.doubleSide = ! scope.doubleSide;
+					console.log( scope.doubleSide ? 'Enabling DoubleSide materials' : 'Enabling FrontSide materials');
+
+					scope.traversalFunction  = function ( material ) {
+						material.side = scope.doubleSide ? THREE.DoubleSide : THREE.FrontSide;
+					};
+
+					var scopeTraverse = function ( object3d ) {
+						scope.traverseScene( object3d );
+					};
+					scope.pivot.traverse( scopeTraverse );
+				};
+
+				OBJLoader2Example.prototype.traverseScene = function ( object3d ) {
+
+					if ( object3d.material instanceof THREE.MultiMaterial ) {
+
+						for ( var matName in object3d.material.materials ) {
+
+							this.traversalFunction( object3d.material.materials[ matName ] );
+
+						}
+
+					} else if ( object3d.material ) {
+
+						this.traversalFunction( object3d.material );
+
+					}
+
+				};
+
+				return OBJLoader2Example;
+
+			})();
+
+			var app = new OBJLoader2Example( document.getElementById( 'example' ) );
+
+			// Init dat.gui and controls
+			var OBJLoader2Control = function() {
+				this.smoothShading = app.smoothShading;
+				this.doubleSide = app.doubleSide;
+			};
+			var objLoader2Control = new OBJLoader2Control();
+
+			var gui = new dat.GUI( {
+				autoPlace: false,
+				width: 320
+			} );
+
+			var menuDiv = document.getElementById( 'dat' );
+			menuDiv.appendChild(gui.domElement);
+			var folderQueue = gui.addFolder( 'OBJLoader2 Options' );
+			var controlSmooth = folderQueue.add( objLoader2Control, 'smoothShading' ).name( 'Smooth Shading' );
+			controlSmooth.onChange( function( value ) {
+				console.log( 'Setting smoothShading to: ' + value );
+				app.alterSmoothShading();
+			});
+
+			var controlDouble = folderQueue.add( objLoader2Control, 'doubleSide' ).name( 'Double Side Materials' );
+			controlDouble.onChange( function( value ) {
+				console.log( 'Setting doubleSide to: ' + value );
+				app.alterDouble();
+			});
+			folderQueue.open();
+
+
+
+			// init three.js example application
+			var resizeWindow = function () {
+				app.resizeDisplayGL();
+			};
+
+			var render = function () {
+				requestAnimationFrame( render );
+				app.render();
+			};
+
+			window.addEventListener( 'resize', resizeWindow, false );
+
+			console.log( 'Starting initialisation phase...' );
+			app.initGL();
+			app.resizeDisplayGL();
+			app.initPostGL( {
+				path: 'obj/female02/',
+				fileObj: 'female02.obj',
+				texturePath: 'obj/female02/',
+				fileMtl: 'female02.mtl'
+			} );
+
+			render();
+
+		</script>
+	</body>
+</html>

+ 491 - 0
examples/webgl_loader_obj2_ww.html

@@ -0,0 +1,491 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - WWOBJLoader2</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+
+		<style>
+			body {
+				font-family: Monospace;
+				background-color: #000;
+				color: #fff;
+				margin: 0 0 0 0;
+				padding: 0 0 0 0;
+				border: none;
+				cursor: default;
+			}
+			#info {
+				color: #fff;
+				position: absolute;
+				top: 10px;
+				width: 100%;
+				text-align: center;
+				z-index: 100;
+				display:block;
+			}
+			#info a {
+				color: #f00;
+				font-weight: bold;
+				text-decoration: underline;
+				cursor: pointer
+			}
+			#glFullscreen {
+				width: 100%;
+				height: 100vh;
+				min-width: 640px;
+				min-height: 360px;
+				position: relative;
+				overflow: hidden;
+				z-index: 0;
+			}
+			#example {
+				width: 100%;
+				height: 100%;
+				top: 0;
+				left: 0;
+				background-color: #000000;
+			}
+			#feedback {
+				color: darkorange;
+			}
+			#dat {
+				user-select: none;
+				position: absolute;
+				left: 0;
+				top: 0;
+				z-Index: 200;
+			}
+			#fileUploadInput {
+				display: none;
+			}
+		</style>
+
+	</head>
+
+	<body>
+		<div id="glFullscreen">
+			<canvas id="example"></canvas>
+		</div>
+		<div id="dat">
+
+		</div>
+		<div id="info">
+			<a href="http://threejs.org" target="_blank">three.js</a> - OBJLoader2 direct loader test
+			<div id="feedback"></div>
+		</div>
+		<input id="fileUploadInput" type="file" name="files[]" multiple accept=".obj,.mtl" />
+
+		<script src="js/Detector.js"></script>
+		<script src="../build/three.js"></script>
+		<script src="js/controls/TrackballControls.js"></script>
+		<script src="js/loaders/MTLLoader.js"></script>
+		<script src="js/libs/dat.gui.min.js"></script>
+
+		<script src="js/loaders/OBJLoader2.js"></script>
+		<script src="js/loaders/WWOBJLoader2.js"></script>
+		<script>
+
+			'use strict';
+
+			var WWOBJLoader2Example = (function () {
+
+				function WWOBJLoader2Example( elementToBindTo ) {
+					this.renderer = null;
+					this.canvas = elementToBindTo;
+					this.aspectRatio = 1;
+					this.recalcAspectRatio();
+
+					this.scene = null;
+					this.cameraDefaults = {
+						posCamera: new THREE.Vector3( 0.0, 175.0, 500.0 ),
+						posCameraTarget: new THREE.Vector3( 0, 0, 0 ),
+						near: 0.1,
+						far: 10000,
+						fov: 45
+					};
+					this.camera = null;
+					this.cameraTarget = this.cameraDefaults.posCameraTarget;
+
+					this.controls = null;
+
+					this.smoothShading = true;
+					this.doubleSide = false;
+					this.streamMeshes = true;
+
+					this.cube = null;
+					this.pivot = null;
+
+					this.wwObjLoader2 = new THREE.OBJLoader2.WWOBJLoader2();
+					this.wwObjLoader2.setCrossOrigin( 'anonymous' );
+
+					// Check for the various File API support.
+					this.fileApiAvailable = true;
+					if ( window.File && window.FileReader && window.FileList && window.Blob ) {
+
+						console.log( 'File API is supported! Enabling all features.' );
+
+					} else {
+
+						this.fileApiAvailable = false;
+						console.warn( 'File API is not supported! Disabling file loading.' );
+
+					}
+				}
+
+				WWOBJLoader2Example.prototype.initGL = function () {
+					this.renderer = new THREE.WebGLRenderer( {
+						canvas: this.canvas,
+						antialias: true,
+						autoClear: true
+					} );
+					this.renderer.setClearColor( 0x050505 );
+
+					this.scene = new THREE.Scene();
+
+					this.camera = new THREE.PerspectiveCamera( this.cameraDefaults.fov, this.aspectRatio, this.cameraDefaults.near, this.cameraDefaults.far );
+					this.resetCamera();
+					this.controls = new THREE.TrackballControls( this.camera, this.renderer.domElement );
+
+					var ambientLight = new THREE.AmbientLight( 0x404040 );
+					var directionalLight1 = new THREE.DirectionalLight( 0xC0C090 );
+					var directionalLight2 = new THREE.DirectionalLight( 0xC0C090 );
+
+					directionalLight1.position.set( -100, -50, 100 );
+					directionalLight2.position.set( 100, 50, -100 );
+
+					this.scene.add( directionalLight1 );
+					this.scene.add( directionalLight2 );
+					this.scene.add( ambientLight );
+
+					var helper = new THREE.GridHelper( 1200, 60, 0xFF4444, 0x404040 );
+					this.scene.add( helper );
+
+					var geometry = new THREE.BoxGeometry( 10, 10, 10 );
+					var material = new THREE.MeshNormalMaterial();
+					this.cube = new THREE.Mesh( geometry, material );
+					this.cube.position.set( 0, 0, 0 );
+					this.scene.add( this.cube );
+
+					this.createPivot();
+				};
+
+				WWOBJLoader2Example.prototype.createPivot = function () {
+					this.pivot = new THREE.Object3D();
+					this.pivot.name = 'Pivot';
+					this.scene.add( this.pivot );
+				};
+
+				WWOBJLoader2Example.prototype.initPostGL = function () {
+					var reportProgress = function ( content ) {
+						console.log( 'Progress: ' + content );
+					};
+					var materialsLoaded = function ( materials ) {
+						var count = 0;
+						console.log( 'The following materials have been loaded:' );
+						for ( var mat in materials ) {
+							count++;
+						}
+						console.log( 'Loaded #' + count + ' materials.' );
+					};
+					var completedLoading = function () {
+						console.log( 'Loading complete!' );
+					};
+					this.wwObjLoader2.registerCallbackProgress( reportProgress );
+					this.wwObjLoader2.registerCallbackCompletedLoading( completedLoading );
+					this.wwObjLoader2.registerCallbackMaterialsLoaded( materialsLoaded );
+
+					return true;
+				};
+
+				WWOBJLoader2Example.prototype.loadFiles = function ( prepData ) {
+					prepData.sceneGraphBaseNode = this.pivot;
+					prepData.streamMeshes = this.streamMeshes;
+					this.wwObjLoader2.prepareRun( prepData );
+					this.wwObjLoader2.run();
+				};
+
+				WWOBJLoader2Example.prototype._handleFileSelect = function ( event, pathTexture ) {
+					var fileObj = null;
+					var fileMtl = null;
+					var files = event.target.files;
+
+					for ( var i = 0, file; file = files[ i ]; i++) {
+
+						if ( file.name.indexOf( '\.obj' ) > 0 && fileObj === null ) {
+							fileObj = file;
+						}
+
+						if ( file.name.indexOf( '\.mtl' ) > 0 && fileMtl === null ) {
+							fileMtl = file;
+						}
+
+					}
+
+					if ( fileObj == null ) {
+						alert( 'Unable to load OBJ file from given files.' );
+					}
+
+					var fileReader = new FileReader();
+					fileReader.onload = function( fileDataObj ) {
+
+						var uint8Array = new Uint8Array( fileDataObj.target.result );
+						if ( fileMtl === null ) {
+
+							app.loadFilesUser({
+								name: 'userObj',
+								objAsArrayBuffer: uint8Array,
+								pathTexture: pathTexture,
+								mtlAsString: null
+							})
+
+						} else {
+
+							fileReader.onload = function( fileDataMtl ) {
+
+								app.loadFilesUser({
+									name: 'userObj',
+									objAsArrayBuffer: uint8Array,
+									pathTexture: pathTexture,
+									mtlAsString: fileDataMtl.target.result
+								})
+							};
+							fileReader.readAsText( fileMtl );
+
+						}
+
+					};
+					fileReader.readAsArrayBuffer( fileObj );
+
+				};
+
+				WWOBJLoader2Example.prototype.loadFilesUser = function ( objDef ) {
+					var prepData = new THREE.OBJLoader2.WWOBJLoader2.PrepDataArrayBuffer(
+						objDef.name, objDef.objAsArrayBuffer, objDef.pathTexture, objDef.mtlAsString, this.pivot, this.streamMeshes
+					);
+					this.wwObjLoader2.prepareRun( prepData );
+					this.wwObjLoader2.run();
+				};
+
+				WWOBJLoader2Example.prototype.resizeDisplayGL = function () {
+					this.controls.handleResize();
+
+					this.recalcAspectRatio();
+					this.renderer.setSize( this.canvas.offsetWidth, this.canvas.offsetHeight, false );
+
+					this.updateCamera();
+				};
+
+				WWOBJLoader2Example.prototype.recalcAspectRatio = function () {
+					this.aspectRatio = ( this.canvas.offsetHeight === 0 ) ? 1 : this.canvas.offsetWidth / this.canvas.offsetHeight;
+				};
+
+				WWOBJLoader2Example.prototype.resetCamera = function () {
+					this.camera.position.copy( this.cameraDefaults.posCamera );
+					this.cameraTarget.copy( this.cameraDefaults.posCameraTarget );
+
+					this.updateCamera();
+				};
+
+				WWOBJLoader2Example.prototype.updateCamera = function () {
+					this.camera.aspect = this.aspectRatio;
+					this.camera.lookAt( this.cameraTarget );
+					this.camera.updateProjectionMatrix();
+				};
+
+				WWOBJLoader2Example.prototype.render = function () {
+					if ( ! this.renderer.autoClear ) this.renderer.clear();
+
+					this.controls.update();
+
+					this.cube.rotation.x += 0.05;
+					this.cube.rotation.y += 0.05;
+
+					this.renderer.render( this.scene, this.camera );
+				};
+
+				WWOBJLoader2Example.prototype.alterSmoothShading = function () {
+
+					var scope = this;
+					scope.smoothShading = ! scope.smoothShading;
+					console.log( scope.smoothShading ? 'Enabling SmoothShading' : 'Enabling FlatShading');
+
+					scope.traversalFunction = function ( material ) {
+						material.shading = scope.smoothShading ? THREE.SmoothShading : THREE.FlatShading;
+						material.needsUpdate = true;
+					};
+					var scopeTraverse = function ( object3d ) {
+						scope.traverseScene( object3d );
+					};
+					scope.pivot.traverse( scopeTraverse );
+				};
+
+				WWOBJLoader2Example.prototype.alterDouble = function () {
+
+					var scope = this;
+					scope.doubleSide = ! scope.doubleSide;
+					console.log( scope.doubleSide ? 'Enabling DoubleSide materials' : 'Enabling FrontSide materials');
+
+					scope.traversalFunction  = function ( material ) {
+						material.side = scope.doubleSide ? THREE.DoubleSide : THREE.FrontSide;
+					};
+
+					var scopeTraverse = function ( object3d ) {
+						scope.traverseScene( object3d );
+					};
+					scope.pivot.traverse( scopeTraverse );
+				};
+
+				WWOBJLoader2Example.prototype.traverseScene = function ( object3d ) {
+
+					if ( object3d.material instanceof THREE.MultiMaterial ) {
+
+						for ( var matName in object3d.material.materials ) {
+
+							this.traversalFunction( object3d.material.materials[ matName ] );
+
+						}
+
+					} else if ( object3d.material ) {
+
+						this.traversalFunction( object3d.material );
+
+					}
+
+				};
+
+				WWOBJLoader2Example.prototype.clearAllAssests = function () {
+					var scope = this;
+					var remover = function ( object3d ) {
+
+						if ( object3d === scope.pivot ) {
+							return;
+						}
+						console.log( 'Removing: ' + object3d.name );
+						scope.scene.remove( object3d );
+
+						if ( object3d.hasOwnProperty( 'geometry' ) ) {
+							object3d.geometry.dispose();
+						}
+						if ( object3d.hasOwnProperty( 'material' ) ) {
+
+							var mat = object3d.material;
+							if ( mat.hasOwnProperty( 'materials' ) ) {
+
+								for ( var mmat in mat.materials ) {
+									mat.materials[ mmat ].dispose();
+								}
+							}
+						}
+						if ( object3d.hasOwnProperty( 'texture' ) ) {
+							object3d.texture.dispose();
+						}
+					};
+
+					scope.scene.remove( scope.pivot );
+					scope.pivot.traverse( remover );
+					scope.createPivot();
+				};
+
+				return WWOBJLoader2Example;
+
+			})();
+
+			var app = new WWOBJLoader2Example( document.getElementById( 'example' ) );
+
+			// Init dat.gui and controls
+			var elemFileInput = document.getElementById( 'fileUploadInput' );
+			var WWOBJLoader2Control = function() {
+				this.smoothShading = app.smoothShading;
+				this.doubleSide = app.doubleSide;
+				this.streamMeshes = app.streamMeshes;
+			};
+			var wwObjLoader2Control = new WWOBJLoader2Control();
+
+			var gui = new dat.GUI( {
+				autoPlace: false,
+				width: 320
+			} );
+
+			var menuDiv = document.getElementById( 'dat' );
+			menuDiv.appendChild(gui.domElement);
+			var folderOptions = gui.addFolder( 'WWOBJLoader2 Options' );
+			var controlSmooth = folderOptions.add( wwObjLoader2Control, 'smoothShading' ).name( 'Smooth Shading' );
+			controlSmooth.onChange( function( value ) {
+				console.log( 'Setting smoothShading to: ' + value );
+				app.alterSmoothShading();
+			});
+
+			var controlDouble = folderOptions.add( wwObjLoader2Control, 'doubleSide' ).name( 'Double Side Materials' );
+			controlDouble.onChange( function( value ) {
+				console.log( 'Setting doubleSide to: ' + value );
+				app.alterDouble();
+			});
+
+			var controlStreamMeshes = folderOptions.add( wwObjLoader2Control, 'streamMeshes' ).name( 'Stream Meshes' );
+			controlStreamMeshes.onChange( function( value ) {
+				console.log( 'Setting streamMeshes to: ' + value );
+				app.streamMeshes = value;
+			});
+
+			if ( app.fileApiAvailable ) {
+
+				wwObjLoader2Control.pathTexture = 'obj/female02/';
+				var controlPathTexture = folderOptions.add( wwObjLoader2Control, 'pathTexture' ).name( 'Relative path to textures' );
+				controlPathTexture.onChange( function( value ) {
+					console.log( 'Setting pathTexture to: ' + value );
+					app.pathTexture = value + '/';
+				});
+
+				wwObjLoader2Control.loadObjFile = function () {
+					elemFileInput.click();
+				};
+				folderOptions.add( wwObjLoader2Control, 'loadObjFile' ).name( 'Load OBJ/MTL Files' );
+
+				var handleFileSelect = function ( object3d ) {
+					app._handleFileSelect( object3d, wwObjLoader2Control.pathTexture );
+				};
+				elemFileInput.addEventListener( 'change' , handleFileSelect, false );
+
+				wwObjLoader2Control.clearAllAssests = function () {
+					app.clearAllAssests();
+				};
+				folderOptions.add( wwObjLoader2Control, 'clearAllAssests' ).name( 'Clear Scene' );
+
+			}
+			folderOptions.open();
+
+
+
+			// init three.js example application
+			var resizeWindow = function () {
+				app.resizeDisplayGL();
+			};
+
+			var render = function () {
+				requestAnimationFrame( render );
+				app.render();
+			};
+
+			window.addEventListener( 'resize', resizeWindow, false );
+
+			console.log( 'Starting initialisation phase...' );
+			app.initGL();
+			app.resizeDisplayGL();
+			app.initPostGL();
+
+			var prepData = new THREE.OBJLoader2.WWOBJLoader2.PrepDataFile(
+				'male02',
+				'obj/male02/',
+				'male02.obj',
+				'obj/male02/',
+				'male02.mtl'
+			);
+			app.loadFiles( prepData );
+
+			// kick render loop
+			render();
+
+		</script>
+	</body>
+</html>

+ 409 - 0
examples/webgl_loader_obj2_ww_parallels.html

@@ -0,0 +1,409 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - Web Worker Parallel Demo</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+
+		<style>
+			body {
+				font-family: Monospace;
+				background-color: #000;
+				color: #fff;
+				margin: 0 0 0 0;
+				padding: 0 0 0 0;
+				border: none;
+				cursor: default;
+			}
+			#info {
+				color: #fff;
+				position: absolute;
+				top: 10px;
+				width: 100%;
+				text-align: center;
+				z-index: 100;
+				display:block;
+			}
+			#info a {
+				color: #f00;
+				font-weight: bold;
+				text-decoration: underline;
+				cursor: pointer
+			}
+			#glFullscreen {
+				width: 100%;
+				height: 100vh;
+				min-width: 640px;
+				min-height: 360px;
+				position: relative;
+				overflow: hidden;
+				z-index: 0;
+			}
+			#example {
+				width: 100%;
+				height: 100%;
+				top: 0;
+				left: 0;
+				background-color: #000000;
+			}
+			#feedback {
+				position: absolute;
+				color: darkorange;
+				text-align: left;
+				bottom: 0%;
+				left: 0%;
+				width: auto;
+				padding: 0px 0px 4px 4px;
+			}
+			#dat {
+				user-select: none;
+				position: absolute;
+				left: 0px;
+				top: 0px;
+				z-Index: 2;
+			}
+		</style>
+	</head>
+
+	<body>
+		<div id="glFullscreen">
+			<canvas id="example"></canvas>
+		</div>
+		<div id="dat">
+
+		</div>
+		<div id="info">
+			<a href="http://threejs.org" target="_blank">three.js</a> - WWOBJLoader2Director Parallels Demo
+		</div>
+		<div id="feedback">
+		</div>
+
+		<script src="js/Detector.js"></script>
+		<script src="../build/three.js"></script>
+		<script src="js/controls/TrackballControls.js"></script>
+		<script src="js/loaders/MTLLoader.js"></script>
+		<script src="js/libs/dat.gui.min.js"></script>
+
+		<script src="js/loaders/OBJLoader2.js"></script>
+		<script src="js/loaders/WWOBJLoader2.js"></script>
+		<script>
+
+			'use strict';
+
+			var WWParallels = (function () {
+
+				function WWParallels( elementToBindTo ) {
+					this.renderer = null;
+					this.canvas = elementToBindTo;
+					this.aspectRatio = 1;
+					this.recalcAspectRatio();
+
+					this.scene = null;
+					this.cameraDefaults = {
+						posCamera: new THREE.Vector3( 0.0, 175.0, 500.0 ),
+						posCameraTarget: new THREE.Vector3( 0, 0, 0 ),
+						near: 0.1,
+						far: 10000,
+						fov: 45
+					};
+					this.camera = null;
+					this.cameraTarget = this.cameraDefaults.posCameraTarget;
+
+					this.wwDirector = new THREE.OBJLoader2.WWOBJLoader2Director();
+					this.wwDirector.setCrossOrigin( 'anonymous' );
+
+					this.controls = null;
+					this.cube = null;
+
+					this.allAssets = [];
+					this.feedbackArray = null;
+				}
+
+				WWParallels.prototype.initGL = function () {
+					this.renderer = new THREE.WebGLRenderer( {
+						canvas: this.canvas,
+						antialias: true,
+						autoClear: true
+					} );
+					this.renderer.setClearColor( 0x050505 );
+
+					this.scene = new THREE.Scene();
+
+					this.camera = new THREE.PerspectiveCamera( this.cameraDefaults.fov, this.aspectRatio, this.cameraDefaults.near, this.cameraDefaults.far );
+					this.resetCamera();
+					this.controls = new THREE.TrackballControls( this.camera, this.renderer.domElement );
+
+					var ambientLight = new THREE.AmbientLight( 0x404040 );
+					var directionalLight1 = new THREE.DirectionalLight( 0xC0C090 );
+					var directionalLight2 = new THREE.DirectionalLight( 0xC0C090 );
+
+					directionalLight1.position.set( -100, -50, 100 );
+					directionalLight2.position.set( 100, 50, -100 );
+
+					this.scene.add( directionalLight1 );
+					this.scene.add( directionalLight2 );
+					this.scene.add( ambientLight );
+
+					var geometry = new THREE.BoxGeometry( 10, 10, 10 );
+					var material = new THREE.MeshNormalMaterial();
+					this.cube = new THREE.Mesh( geometry, material );
+					this.cube.position.set( 0, 0, 0 );
+					this.scene.add( this.cube );
+				};
+
+				WWParallels.prototype.resizeDisplayGL = function () {
+					this.controls.handleResize();
+
+					this.recalcAspectRatio();
+					this.renderer.setSize( this.canvas.offsetWidth, this.canvas.offsetHeight, false );
+
+					this.updateCamera();
+				};
+
+				WWParallels.prototype.recalcAspectRatio = function () {
+					this.aspectRatio = ( this.canvas.offsetHeight === 0 ) ? 1 : this.canvas.offsetWidth / this.canvas.offsetHeight;
+				};
+
+				WWParallels.prototype.resetCamera = function () {
+					this.camera.position.copy( this.cameraDefaults.posCamera );
+					this.cameraTarget.copy( this.cameraDefaults.posCameraTarget );
+
+					this.updateCamera();
+				};
+
+				WWParallels.prototype.updateCamera = function () {
+					this.camera.aspect = this.aspectRatio;
+					this.camera.lookAt( this.cameraTarget );
+					this.camera.updateProjectionMatrix();
+				};
+
+				WWParallels.prototype.render = function () {
+					if ( ! this.renderer.autoClear ) this.renderer.clear();
+
+					this.controls.update();
+
+					this.cube.rotation.x += 0.05;
+					this.cube.rotation.y += 0.05;
+
+					this.renderer.render( this.scene, this.camera );
+				};
+				WWParallels.prototype.reportProgress = function( text ) {
+					document.getElementById( 'feedback' ).innerHTML = text;
+				};
+
+				WWParallels.prototype.enqueueAllAssests = function ( maxQueueSize, maxWebWorkers, streamMeshes ) {
+					var scope = this;
+					scope.wwDirector.objectsCompleted = 0;
+					scope.feedbackArray = new Array( maxWebWorkers );
+					for ( var i = 0; i < maxWebWorkers; i++ ) {
+						scope.feedbackArray[ i ] = 'Worker #' + i + ': Awaiting feedback';
+					}
+					scope.reportProgress( scope.feedbackArray.join( '\<br\>' ) );
+
+					var callbackCompletedLoading = function ( modelName, instanceNo ) {
+						var msg = 'Worker #' + instanceNo + ': Completed loading: ' + modelName + ' (#' + scope.wwDirector.objectsCompleted + ')';
+						console.log( msg );
+						scope.feedbackArray[ instanceNo ] = msg;
+						scope.reportProgress( scope.feedbackArray.join( '\<br\>' ) );
+					};
+					var callbackMeshLoaded = function ( meshName, material ) {
+						var replacedMaterial = null;
+
+						if ( material != null && material.name === 'defaultMaterial' || meshName === 'Mesh_Mesh_head_geo.001' ) {
+							replacedMaterial = material;
+							replacedMaterial.color = new THREE.Color( Math.random(), Math.random(), Math.random() );
+						}
+
+						return replacedMaterial;
+					};
+
+					this.wwDirector.prepareWorkers(
+						{
+							completedLoading: callbackCompletedLoading,
+							meshLoaded: callbackMeshLoaded
+						},
+						maxQueueSize,
+						maxWebWorkers
+					);
+					console.log( 'Configuring WWManager with queue size ' + this.wwDirector.getMaxQueueSize() + ' and ' + this.wwDirector.getMaxWebWorkers() + ' workers.' );
+
+					var models = [];
+					models.push( {
+						modelName: 'male02',
+						dataAvailable: false,
+						pathObj: 'obj/male02/',
+						fileObj: 'male02.obj',
+						pathTexture: 'obj/male02/',
+						fileMtl: 'male02.mtl'
+					} );
+
+					models.push( {
+						modelName: 'female02',
+						dataAvailable: false,
+						pathObj: 'obj/female02/',
+						fileObj: 'female02.obj',
+						pathTexture: 'obj/female02/',
+						fileMtl: 'female02.mtl'
+					} );
+
+					models.push( {
+						modelName: 'viveController',
+						dataAvailable: false,
+						pathObj: 'models/obj/vive-controller/',
+						fileObj: 'vr_controller_vive_1_5.obj',
+						scale: 400.0
+					} );
+
+					models.push( {
+						modelName: 'cerberus',
+						dataAvailable: false,
+						pathObj: 'models/obj/cerberus/',
+						fileObj: 'Cerberus.obj',
+						scale: 50.0
+					} );
+					models.push( {
+						modelName:'WaltHead',
+						dataAvailable: false,
+						pathObj: 'obj/walt/',
+						fileObj: 'WaltHead.obj',
+						pathTexture: 'obj/walt/',
+						fileMtl: 'WaltHead.mtl'
+					} );
+
+					var pivot;
+					var distributionBase = -500;
+					var distributionMax = 1000;
+					var modelIndex = 0;
+					var model;
+					var runParams;
+					for ( var i = 0; i < maxQueueSize; i++ ) {
+
+						modelIndex = Math.floor( Math.random() * 5 );
+						model = models[ modelIndex ];
+
+						pivot = new THREE.Object3D();
+						pivot.position.set(
+							distributionBase + distributionMax * Math.random(),
+							distributionBase + distributionMax * Math.random(),
+							distributionBase + distributionMax * Math.random()
+						);
+						if ( model.scale != null ) pivot.scale.set( model.scale, model.scale, model.scale );
+
+						this.scene.add( pivot );
+
+						model.sceneGraphBaseNode = pivot;
+
+						runParams = new THREE.OBJLoader2.WWOBJLoader2.PrepDataFile(
+							model.modelName, model.pathObj, model.fileObj, model.pathTexture, model.fileMtl, model.sceneGraphBaseNode, streamMeshes
+						);
+						this.wwDirector.enqueueForRun( runParams );
+						this.allAssets.push( runParams );
+					}
+
+					this.wwDirector.processQueue();
+				};
+
+				WWParallels.prototype.clearAllAssests = function () {
+					var ref;
+					var scope = this;
+
+					for ( var asset in this.allAssets ) {
+						ref = this.allAssets[asset];
+
+						var remover = function ( object3d ) {
+
+							if ( object3d === ref.sceneGraphBaseNode ) {
+								return;
+							}
+							console.log( 'Removing ' + object3d.name );
+							scope.scene.remove( object3d );
+
+							if ( object3d.hasOwnProperty( 'geometry' ) ) {
+								object3d.geometry.dispose();
+							}
+							if ( object3d.hasOwnProperty( 'material' ) ) {
+
+								var mat = object3d.material;
+								if ( mat.hasOwnProperty( 'materials' ) ) {
+
+									for ( var mmat in mat.materials ) {
+										mat.materials[mmat].dispose();
+									}
+								}
+							}
+							if ( object3d.hasOwnProperty( 'texture' ) ) {
+								object3d.texture.dispose();
+							}
+						};
+						scope.scene.remove( ref.sceneGraphBaseNode );
+						ref.sceneGraphBaseNode.traverse( remover );
+						ref.sceneGraphBaseNode = null;
+					}
+					this.allAssets = [];
+				};
+
+				WWParallels.prototype.terminateManager = function () {
+					this.wwDirector.deregister();
+				};
+
+				return WWParallels;
+
+			})();
+
+			var app = new WWParallels( document.getElementById( 'example' ) );
+
+			var WWParallelsControl = function() {
+				this.queueLength = 128;
+				this.workerCount = 4;
+				this.streamMeshes = true;
+				this.run = function () {
+					app.enqueueAllAssests( this.queueLength, this.workerCount, this.streamMeshes );
+				};
+				this.terminate = function () {
+					app.terminateManager();
+				};
+				this.clearAllAssests = function () {
+					app.terminateManager();
+					app.clearAllAssests();
+				};
+			};
+			var wwParallelsControl = new WWParallelsControl();
+
+			var gui = new dat.GUI( {
+				autoPlace: false,
+				width: 320
+			} );
+
+			var menuDiv = document.getElementById( 'dat' );
+			menuDiv.appendChild(gui.domElement);
+			var folderQueue = gui.addFolder( 'Web Worker Director Queue Control' );
+			folderQueue.add( wwParallelsControl, 'queueLength' ).min( 1 ).max( 1024 ).step( 1 );
+			folderQueue.add( wwParallelsControl, 'workerCount' ).min( 1 ).max( 16 ).step( 1 );
+			folderQueue.add( wwParallelsControl, 'streamMeshes' );
+			folderQueue.add( wwParallelsControl, 'run' ).name( 'Run Queue' );
+			folderQueue.open();
+
+			var folderWWControl = gui.addFolder( 'Resource Management' );
+			folderWWControl.add( wwParallelsControl, 'terminate' ).name( 'Terminate WWManager' );
+			folderWWControl.add( wwParallelsControl, 'clearAllAssests' ).name( 'Clear Scene' );
+
+			var resizeWindow = function () {
+				app.resizeDisplayGL();
+			};
+
+			var render = function () {
+				requestAnimationFrame( render );
+				app.render();
+			};
+
+			window.addEventListener( 'resize', resizeWindow, false );
+
+			console.log( 'Starting initialisation phase...' );
+			app.initGL();
+			app.resizeDisplayGL();
+
+			render();
+
+		</script>
+	</body>
+</html>