Browse Source

Merge pull request #16376 from kaisalmen/OBJLoader2_V300_beta_es6

OBJLoader2 V3.0.0-Beta as jsm
Mr.doob 6 năm trước cách đây
mục cha
commit
d67494984c

+ 383 - 0
examples/jsm/loaders/OBJLoader2.js

@@ -0,0 +1,383 @@
+/**
+ * @author Kai Salmen / https://kaisalmen.de
+ * Development repository: https://github.com/kaisalmen/WWOBJLoader
+ */
+
+import {
+	DefaultLoadingManager,
+	FileLoader,
+	Group
+} from "../../../build/three.module.js";
+
+import { Parser } from "./worker/independent/OBJLoader2Parser.js";
+import { MeshReceiver } from "./shared/MeshReceiver.js";
+import { MaterialHandler } from "./shared/MaterialHandler.js";
+
+/**
+ * Use this class to load OBJ data from files or to parse OBJ data from an arraybuffer
+ * @class
+ *
+ * @param {DefaultLoadingManager} [manager] The loadingManager for the loader to use. Default is {@link DefaultLoadingManager}
+ */
+const OBJLoader2 = function ( manager ) {
+	this.manager = ( manager !== undefined && manager !== null ) ? manager : DefaultLoadingManager;
+	this.logging = {
+		enabled: true,
+		debug: false
+	};
+
+	this.modelName = '';
+	this.instanceNo = 0;
+	this.path = undefined;
+	this.resourcePath = undefined;
+	this.useIndices = false;
+	this.disregardNormals = false;
+	this.materialPerSmoothingGroup = false;
+	this.useOAsMesh = false;
+	this.baseObject3d = new Group();
+
+	this.callbacks = {
+		onParseProgress: undefined,
+		genericErrorHandler: undefined
+	};
+
+	this.materialHandler = new MaterialHandler();
+	this.meshReceiver = new MeshReceiver( this.materialHandler );
+};
+OBJLoader2.OBJLOADER2_VERSION = '3.0.0-beta';
+console.info( 'Using OBJLoader2 version: ' + OBJLoader2.OBJLOADER2_VERSION );
+
+
+OBJLoader2.prototype = {
+
+	constructor: OBJLoader2,
+
+	/**
+	 * Enable or disable logging in general (except warn and error), plus enable or disable debug logging.
+	 *
+	 * @param {boolean} enabled True or false.
+	 * @param {boolean} debug True or false.
+	 */
+	setLogging: function ( enabled, debug ) {
+		this.logging.enabled = enabled === true;
+		this.logging.debug = debug === true;
+		return this;
+	},
+
+	/**
+	 * Set the name of the model.
+	 *
+	 * @param {string} modelName
+	 */
+	setModelName: function ( modelName ) {
+		this.modelName = modelName ? modelName : this.modelName;
+		return this;
+	},
+
+	/**
+	 * The URL of the base path.
+	 *
+	 * @param {string} path URL
+	 */
+	setPath: function ( path ) {
+		this.path = path ? path : this.path;
+		return this;
+	},
+
+
+	/**
+	 * Allow to specify resourcePath for dependencies of specified resource.
+	 * @param {string} resourcePath
+	 */
+	setResourcePath: function ( resourcePath ) {
+		this.resourcePath = resourcePath ? resourcePath : this.resourcePath;
+	},
+
+	/**
+	 * Set the node where the loaded objects will be attached directly.
+	 *
+	 * @param {Object3D} baseObject3d Object already attached to scenegraph where new meshes will be attached to
+	 */
+	setBaseObject3d: function ( baseObject3d ) {
+		this.baseObject3d = ( baseObject3d === undefined || baseObject3d === null ) ? this.baseObject3d : baseObject3d;
+		return this;
+	},
+
+	/**
+	 * Add materials as associated array.
+	 *
+	 * @param materials Object with named {@link Material}
+	 */
+	addMaterials: function ( materials ) {
+		this.materialHandler.addMaterials( materials );
+	},
+
+	/**
+	 * Instructs loaders to create indexed {@link BufferGeometry}.
+	 *
+	 * @param {boolean} useIndices=false
+	 */
+	setUseIndices: function ( useIndices ) {
+		this.useIndices = useIndices === true;
+		return this;
+	},
+
+	/**
+	 * Tells whether normals should be completely disregarded and regenerated.
+	 *
+	 * @param {boolean} disregardNormals=false
+	 */
+	setDisregardNormals: function ( disregardNormals ) {
+		this.disregardNormals = disregardNormals === true;
+		return this;
+	},
+
+	/**
+	 * Tells whether a material shall be created per smoothing group.
+	 *
+	 * @param {boolean} materialPerSmoothingGroup=false
+	 */
+	setMaterialPerSmoothingGroup: function ( materialPerSmoothingGroup ) {
+		this.materialPerSmoothingGroup = materialPerSmoothingGroup === true;
+		return this;
+	},
+
+	/**
+	 * Usually 'o' is meta-information and does not result in creation of new meshes, but mesh creation on occurrence of "o" can be enforced.
+	 *
+	 * @param {boolean} useOAsMesh=false
+	 */
+	setUseOAsMesh: function ( useOAsMesh ) {
+		this.useOAsMesh = useOAsMesh === true;
+		return this;
+	},
+
+	/**
+	 * Register an generic error handler that is called if available instead of throwing an exception
+	 * @param {Function} genericErrorHandler
+	 */
+	setGenericErrorHandler: function ( genericErrorHandler ) {
+		if ( genericErrorHandler !== undefined && genericErrorHandler !== null ) {
+
+			this.callbacks.genericErrorHandler = genericErrorHandler;
+
+		}
+	},
+
+	/**
+	 *
+	 * @private
+	 *
+	 * @param {Function} [onParseProgress]
+	 * @param {Function} [onMeshAlter]
+	 * @param {Function} [onLoadMaterials]
+	 * @private
+	 */
+	_setCallbacks: function ( onParseProgress, onMeshAlter, onLoadMaterials ) {
+		if ( onParseProgress !== undefined && onParseProgress !== null ) {
+
+			this.callbacks.onParseProgress = onParseProgress;
+
+		}
+		this.meshReceiver._setCallbacks( onParseProgress, onMeshAlter );
+		this.materialHandler._setCallbacks( onLoadMaterials );
+	},
+
+	/**
+	 * Announce feedback which is give to the registered callbacks.
+	 * @private
+	 *
+	 * @param {string} type The type of event
+	 * @param {string} text Textual description of the event
+	 * @param {number} numericalValue Numerical value describing the progress
+	 */
+	_onProgress: function ( type, text, numericalValue ) {
+		let message = text ? text : '';
+		let event = {
+			detail: {
+				type: type,
+				modelName: this.modelName,
+				instanceNo: this.instanceNo,
+				text: message,
+				numericalValue: numericalValue
+			}
+		};
+		if ( this.callbacks.onParseProgress ) {
+
+			this.callbacks.onParseProgress( event );
+
+		}
+		if ( this.logging.enabled && this.logging.debug ) {
+
+			console.log( message );
+
+		}
+	},
+
+	/**
+	 * Announce error feedback which is given to the generic error handler to the registered callbacks.
+	 * @private
+	 *
+	 * @param {String} errorMessage The event containing the error
+	 */
+	_onError: function ( errorMessage ) {
+		if ( this.callbacks.genericErrorHandler ) {
+
+			this.callbacks.genericErrorHandler( errorMessage );
+
+		}
+		if ( this.logging.enabled && this.logging.debug ) {
+
+			console.log( errorMessage );
+
+		}
+
+	},
+
+	/**
+	 * Use this convenient method to load a file at the given URL. By default the fileLoader uses an ArrayBuffer.
+	 *
+	 * @param {string}  url A string containing the path/URL of the file to be loaded.
+	 * @param {function} onLoad A function to be called after loading is successfully completed. The function receives loaded Object3D as an argument.
+	 * @param {function} [onFileLoadProgress] A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains total and Integer bytes.
+	 * @param {function} [onError] A function to be called if an error occurs during loading. The function receives the error as an argument.
+	 * @param {function} [onMeshAlter] Called after worker successfully delivered a single mesh
+	 */
+	load: function ( url, onLoad, onFileLoadProgress, onError, onMeshAlter ) {
+		let scope = this;
+		if ( onError === null || onError === undefined ) {
+
+			onError = function ( event ) {
+
+				let errorMessage = event;
+				if ( event.currentTarget && event.currentTarget.statusText !== null ) {
+
+					 errorMessage = 'Error occurred while downloading!\nurl: ' + event.currentTarget.responseURL + '\nstatus: ' + event.currentTarget.statusText;
+
+				}
+				scope._onError( errorMessage );
+
+			};
+
+		}
+		if ( ! url ) {
+
+			onError( 'An invalid url was provided. Unable to continue!' );
+
+		}
+		let urlFull = new URL( url, window.location.href ).href;
+		let filename = urlFull;
+		let urlParts = urlFull.split( '/' );
+		if ( urlParts.length > 2 ) {
+
+			filename = urlParts[ urlParts.length - 1 ];
+			let urlPartsPath = urlParts.slice( 0, urlParts.length - 1 ).join( '/' ) + '/';
+			if ( urlPartsPath !== undefined && urlPartsPath !== null ) this.path = urlPartsPath;
+
+		}
+		if ( onFileLoadProgress === null || onFileLoadProgress === undefined ) {
+
+			let numericalValueRef = 0;
+			let numericalValue = 0;
+			onFileLoadProgress = function ( event ) {
+				if ( ! event.lengthComputable ) return;
+
+				numericalValue = event.loaded / event.total;
+				if ( numericalValue > numericalValueRef ) {
+
+					numericalValueRef = numericalValue;
+					let output = 'Download of "' + url + '": ' + (numericalValue * 100).toFixed( 2 ) + '%';
+					scope._onProgress( 'progressLoad', output, numericalValue );
+
+				}
+			};
+
+		}
+		this._setCallbacks( null, onMeshAlter, null );
+		let fileLoaderOnLoad = function ( content ) {
+			onLoad( scope.parse( content ) );
+		};
+		let fileLoader = new FileLoader( this.manager );
+		fileLoader.setPath( this.path || this.resourcePath );
+		fileLoader.setResponseType( 'arraybuffer' );
+		fileLoader.load( filename, fileLoaderOnLoad, onFileLoadProgress, onError );
+	},
+
+	/**
+	 * Parses OBJ data synchronously from arraybuffer or string.
+	 *
+	 * @param {arraybuffer|string} content OBJ data as Uint8Array or String
+	 */
+	parse: function ( content ) {
+		// fast-fail in case of illegal data
+		if ( content === null || content === undefined ) {
+
+			throw 'Provided content is not a valid ArrayBuffer or String. Unable to continue parsing';
+
+		}
+		if ( this.logging.enabled ) {
+
+			console.time( 'OBJLoader parse: ' + this.modelName );
+
+		}
+		let parser = new Parser();
+		parser.setLogging( this.logging.enabled, this.logging.debug );
+		parser.setMaterialPerSmoothingGroup( this.materialPerSmoothingGroup );
+		parser.setUseOAsMesh( this.useOAsMesh );
+		parser.setUseIndices( this.useIndices );
+		parser.setDisregardNormals( this.disregardNormals );
+		// sync code works directly on the material references
+		parser.setMaterials( this.materialHandler.getMaterials() );
+
+		let scope = this;
+		let onMeshLoaded = function ( payload ) {
+
+			if ( payload.cmd !== 'data' ) return;
+
+			if ( payload.type === 'mesh' ) {
+
+				let meshes = scope.meshReceiver.buildMeshes( payload );
+				for ( let mesh of meshes ) {
+					scope.baseObject3d.add( mesh );
+				}
+
+			} else if ( payload.type === 'material' ) {
+
+				scope.materialHandler.addPayloadMaterials( payload );
+
+			}
+		};
+		let onProgressScoped = function ( text, numericalValue ) {
+			scope._onProgress( 'progressParse', text, numericalValue );
+		};
+		let onErrorScoped = function ( message ) {
+			scope._onError( message );
+		};
+		parser.setCallbackOnAssetAvailable( onMeshLoaded );
+		parser.setCallbackOnProgress( onProgressScoped );
+		parser.setCallbackOnError( onErrorScoped );
+		if ( content instanceof ArrayBuffer || content instanceof Uint8Array ) {
+
+			if ( this.logging.enabled ) console.info( 'Parsing arrayBuffer...' );
+			parser.parse( content );
+
+		} else if ( typeof( content ) === 'string' || content instanceof String ) {
+
+			if ( this.logging.enabled ) console.info( 'Parsing text...' );
+			parser.parseText( content );
+
+		} else {
+
+			scope._onError( 'Provided content was neither of type String nor Uint8Array! Aborting...' );
+
+		}
+		if ( this.logging.enabled ) {
+
+			console.timeEnd( 'OBJLoader parse: ' + this.modelName );
+
+		}
+		return this.baseObject3d;
+	}
+};
+
+export { OBJLoader2 };

+ 41 - 0
examples/jsm/loaders/bridge/MtlObjBridge.js

@@ -0,0 +1,41 @@
+/**
+ * @author Kai Salmen / https://kaisalmen.de
+ * Development repository: https://github.com/kaisalmen/WWOBJLoader
+ */
+
+import { MTLLoader } from "../../../jsm/loaders/MTLLoader.js";
+
+
+const MtlObjBridge = {
+
+	/**
+	 *
+	 * @param processResult
+	 * @param assetLoader
+	 */
+	link: function( processResult, assetLoader ) {
+		if ( typeof assetLoader.addMaterials === 'function' ) {
+
+			assetLoader.addMaterials( this.addMaterialsFromMtlLoader( processResult ) );
+
+		}
+	},
+
+	/**
+	 * Returns the array instance of {@link MTLLoader.MaterialCreator}.
+	 *
+	 * @param Instance of {@link MTLLoader.MaterialCreator}
+	 */
+	addMaterialsFromMtlLoader: function ( materialCreator ) {
+		let newMaterials = {};
+		if ( materialCreator instanceof MTLLoader.MaterialCreator ) {
+
+			materialCreator.preload();
+			newMaterials = materialCreator.materials;
+
+		}
+		return newMaterials;
+	}
+};
+
+export { MtlObjBridge }

+ 207 - 0
examples/jsm/loaders/shared/MaterialHandler.js

@@ -0,0 +1,207 @@
+/**
+ * @author Kai Salmen / https://kaisalmen.de
+ * Development repository: https://github.com/kaisalmen/WWOBJLoader
+ */
+
+import {
+	LineBasicMaterial,
+	MaterialLoader,
+	MeshStandardMaterial,
+	PointsMaterial,
+	VertexColors
+} from "../../../../build/three.module.js";
+
+
+const MaterialHandler = function () {
+	this.logging = {
+		enabled: true,
+		debug: false
+	};
+
+	this.callbacks = {
+		onLoadMaterials: null
+	};
+	this.materials = {};
+	this._createDefaultMaterials();
+};
+
+MaterialHandler.prototype = {
+
+	constructor: MaterialHandler,
+
+	/**
+	 * Enable or disable logging in general (except warn and error), plus enable or disable debug logging.
+	 *
+	 * @param {boolean} enabled True or false.
+	 * @param {boolean} debug True or false.
+	 */
+	setLogging:	function ( enabled, debug ) {
+		this.logging.enabled = enabled === true;
+		this.logging.debug = debug === true;
+	},
+
+	_setCallbacks: function ( onLoadMaterials ) {
+		if ( onLoadMaterials !== undefined && onLoadMaterials !== null ) {
+
+			this.callbacks.onLoadMaterials = onLoadMaterials;
+
+		}
+	},
+
+	_createDefaultMaterials: function () {
+		let defaultMaterial = new MeshStandardMaterial( { color: 0xDCF1FF } );
+		defaultMaterial.name = 'defaultMaterial';
+
+		let defaultVertexColorMaterial = new MeshStandardMaterial( { color: 0xDCF1FF } );
+		defaultVertexColorMaterial.name = 'defaultVertexColorMaterial';
+		defaultVertexColorMaterial.vertexColors = VertexColors;
+
+		let defaultLineMaterial = new LineBasicMaterial();
+		defaultLineMaterial.name = 'defaultLineMaterial';
+
+		let defaultPointMaterial = new PointsMaterial( { size: 0.1 } );
+		defaultPointMaterial.name = 'defaultPointMaterial';
+
+		let runtimeMaterials = {};
+		runtimeMaterials[ defaultMaterial.name ] = defaultMaterial;
+		runtimeMaterials[ defaultVertexColorMaterial.name ] = defaultVertexColorMaterial;
+		runtimeMaterials[ defaultLineMaterial.name ] = defaultLineMaterial;
+		runtimeMaterials[ defaultPointMaterial.name ] = defaultPointMaterial;
+
+		this.addMaterials( runtimeMaterials );
+	},
+
+	/**
+	 * Updates the materials with contained material objects (sync) or from alteration instructions (async).
+	 *
+	 * @param {Object} materialPayload Material update instructions
+	 * @returns {Object} Map of {@link Material}
+	 */
+	addPayloadMaterials: function ( materialPayload ) {
+		let material, materialName;
+		let materialCloneInstructions = materialPayload.materials.materialCloneInstructions;
+		let newMaterials = {};
+
+		if ( materialCloneInstructions !== undefined && materialCloneInstructions !== null ) {
+
+			let materialNameOrg = materialCloneInstructions.materialNameOrg;
+			if ( materialNameOrg !== undefined && materialNameOrg !== null ) {
+
+				let materialOrg = this.materials[ materialNameOrg ];
+				material = materialOrg.clone();
+
+				materialName = materialCloneInstructions.materialName;
+				material.name = materialName;
+
+				let materialProperties = materialCloneInstructions.materialProperties;
+				for ( let key in materialProperties ) {
+
+					if ( material.hasOwnProperty( key ) && materialProperties.hasOwnProperty( key ) ) {
+
+						material[ key ] = materialProperties[ key ];
+
+					}
+
+				}
+				this.materials[ materialName ] = material;
+				newMaterials[ materialName ] = material;
+
+			} else {
+
+				console.info( 'Requested material "' + materialNameOrg + '" is not available!' );
+
+			}
+		}
+
+		let materials = materialPayload.materials.serializedMaterials;
+		if ( materials !== undefined && materials !== null && Object.keys( materials ).length > 0 ) {
+
+			let loader = new MaterialLoader();
+			let materialJson;
+			for ( materialName in materials ) {
+
+				materialJson = materials[ materialName ];
+				if ( materialJson !== undefined && materialJson !== null ) {
+
+					material = loader.parse( materialJson );
+					if ( this.logging.enabled ) console.info( 'De-serialized material with name "' + materialName + '" will be added.' );
+					this.materials[ materialName ] = material;
+					newMaterials[ materialName ] = material;
+
+				}
+
+			}
+
+		}
+		materials = materialPayload.materials.runtimeMaterials;
+		newMaterials = this.addMaterials( materials, newMaterials );
+
+		return newMaterials;
+	},
+
+	/**
+	 * Set materials loaded by any supplier of an Array of {@link Material}.
+	 *
+	 * @param materials Object with named {@link Material}
+	 * @param newMaterials [Object] with named {@link Material}
+	 */
+	addMaterials: function ( materials, newMaterials ) {
+		if ( newMaterials === undefined || newMaterials === null ) {
+
+			newMaterials = {};
+
+		}
+		if ( materials !== undefined && materials !== null && Object.keys( materials ).length > 0 ) {
+
+			let material;
+			for ( let materialName in materials ) {
+
+				material = materials[ materialName ];
+				this.materials[ materialName ] = material;
+				newMaterials[ materialName ] = material;
+				if ( this.logging.enabled ) console.info( 'Material with name "' + materialName + '" was added.' );
+
+			}
+
+		}
+		return newMaterials;
+	},
+
+	/**
+	 * Returns the mapping object of material name and corresponding material.
+	 *
+	 * @returns {Object} Map of {@link Material}
+	 */
+	getMaterials: function () {
+		return this.materials;
+	},
+
+	/**
+	 *
+	 * @param {String} materialName
+	 * @returns {Material}
+	 */
+	getMaterial: function ( materialName ) {
+		return this.materials[ materialName ];
+	},
+
+	/**
+	 * Returns the mapping object of material name and corresponding jsonified material.
+	 *
+	 * @returns {Object} Map of Materials in JSON representation
+	 */
+	getMaterialsJSON: function () {
+		let materialsJSON = {};
+		let material;
+		for ( let materialName in this.materials ) {
+
+			material = this.materials[ materialName ];
+			materialsJSON[ materialName ] = material.toJSON();
+		}
+
+		return materialsJSON;
+	}
+
+};
+
+export { MaterialHandler }

+ 287 - 0
examples/jsm/loaders/shared/MeshReceiver.js

@@ -0,0 +1,287 @@
+/**
+ * @author Kai Salmen / https://kaisalmen.de
+ * Development repository: https://github.com/kaisalmen/WWOBJLoader
+ */
+
+import {
+	BufferAttribute,
+	BufferGeometry,
+	LineSegments,
+	Mesh,
+	Points
+} from "../../../../build/three.module.js";
+
+import { MaterialHandler } from "./MaterialHandler.js";
+
+
+/**
+ *
+ * @param {MaterialHandler} materialHandler
+ * @constructor
+ */
+const MeshReceiver = function( materialHandler ) {
+	console.info( 'Using MeshReceiver version: ' + MeshReceiver.MESH_RECEIVER_VERSION );
+
+	this.logging = {
+		enabled: true,
+		debug: false
+	};
+
+	this.callbacks = {
+		onParseProgress: null,
+		onMeshAlter: null
+	};
+	this.materialHandler = materialHandler;
+};
+MeshReceiver.MESH_RECEIVER_VERSION = '2.0.0-preview';
+
+
+MeshReceiver.prototype = {
+
+	constructor: MeshReceiver,
+
+	/**
+	 * Enable or disable logging in general (except warn and error), plus enable or disable debug logging.
+	 *
+	 * @param {boolean} enabled True or false.
+	 * @param {boolean} debug True or false.
+	 */
+	setLogging:	function ( enabled, debug ) {
+		this.logging.enabled = enabled === true;
+		this.logging.debug = debug === true;
+	},
+
+	/**
+	 *
+	 * @param {Function} onParseProgress
+	 * @param {Function} onMeshAlter
+	 * @private
+	 */
+	_setCallbacks: function ( onParseProgress, onMeshAlter ) {
+		if ( onParseProgress !== undefined && onParseProgress !== null ) {
+
+			this.callbacks.onParseProgress = onParseProgress;
+
+		}
+		if ( onMeshAlter !== undefined && onMeshAlter !== null ) {
+
+			this.callbacks.onMeshAlter = onMeshAlter;
+
+		}
+	},
+
+	/**
+	 * Builds one or multiple meshes from the data described in the payload (buffers, params, material info).
+	 *
+	 * @param {Object} meshPayload Raw mesh description (buffers, params, materials) used to build one to many meshes.
+	 * @returns {Mesh[]} mesh Array of {@link Mesh}
+	 */
+	buildMeshes: function ( meshPayload ) {
+		let meshName = meshPayload.params.meshName;
+
+		let bufferGeometry = new BufferGeometry();
+		bufferGeometry.addAttribute( 'position', new BufferAttribute( new Float32Array( meshPayload.buffers.vertices ), 3 ) );
+		if ( meshPayload.buffers.indices !== null ) {
+
+			bufferGeometry.setIndex( new BufferAttribute( new Uint32Array( meshPayload.buffers.indices ), 1 ) );
+
+		}
+		let haveVertexColors = meshPayload.buffers.colors  !== null;
+		if ( haveVertexColors ) {
+
+			bufferGeometry.addAttribute( 'color', new BufferAttribute( new Float32Array( meshPayload.buffers.colors ), 3 ) );
+
+		}
+		if ( meshPayload.buffers.normals !== null ) {
+
+			bufferGeometry.addAttribute( 'normal', new BufferAttribute( new Float32Array( meshPayload.buffers.normals ), 3 ) );
+
+		} else {
+
+			bufferGeometry.computeVertexNormals();
+
+		}
+		if ( meshPayload.buffers.uvs  !== null ) {
+
+			bufferGeometry.addAttribute( 'uv', new BufferAttribute( new Float32Array( meshPayload.buffers.uvs ), 2 ) );
+
+		}
+		if ( meshPayload.buffers.skinIndex !== null ) {
+
+			bufferGeometry.addAttribute( 'skinIndex', new BufferAttribute( new Uint16Array( meshPayload.buffers.skinIndex ), 4 ) );
+
+		}
+		if ( meshPayload.buffers.skinWeight !== null ) {
+
+			bufferGeometry.addAttribute( 'skinWeight', new BufferAttribute( new Float32Array( meshPayload.buffers.skinWeight ), 4 ) );
+
+		}
+
+		let material, materialName, key;
+		let materialNames = meshPayload.materials.materialNames;
+		let createMultiMaterial = meshPayload.materials.multiMaterial;
+		let multiMaterials = [];
+		for ( key in materialNames ) {
+
+			materialName = materialNames[ key ];
+			material = this.materialHandler.getMaterial( materialName );
+			if ( createMultiMaterial ) multiMaterials.push( material );
+
+		}
+		if ( createMultiMaterial ) {
+
+			material = multiMaterials;
+			let materialGroups = meshPayload.materials.materialGroups;
+			let materialGroup;
+			for ( key in materialGroups ) {
+
+				materialGroup = materialGroups[ key ];
+				bufferGeometry.addGroup( materialGroup.start, materialGroup.count, materialGroup.index );
+
+			}
+
+		}
+
+		let meshes = [];
+		let mesh;
+		let callbackOnMeshAlter = this.callbacks.onMeshAlter;
+		let callbackOnMeshAlterResult;
+		let useOrgMesh = true;
+		let geometryType = meshPayload.geometryType === null ? 0 : meshPayload.geometryType;
+
+		if ( callbackOnMeshAlter ) {
+
+			callbackOnMeshAlterResult = callbackOnMeshAlter(
+				{
+					detail: {
+						meshName: meshName,
+						bufferGeometry: bufferGeometry,
+						material: material,
+						geometryType: geometryType
+					}
+				}
+			);
+		}
+
+		// here LoadedMeshUserOverride is required to be provided by the callback used to alter the results
+		if ( callbackOnMeshAlterResult ) {
+
+			if ( callbackOnMeshAlterResult.isDisregardMesh() ) {
+
+				useOrgMesh = false;
+
+			} else if ( callbackOnMeshAlterResult.providesAlteredMeshes() ) {
+
+				for ( let i in callbackOnMeshAlterResult.meshes ) {
+
+					meshes.push( callbackOnMeshAlterResult.meshes[ i ] );
+
+				}
+				useOrgMesh = false;
+
+			}
+
+		}
+		if ( useOrgMesh ) {
+
+			if ( meshPayload.computeBoundingSphere ) bufferGeometry.computeBoundingSphere();
+			if ( geometryType === 0 ) {
+
+				mesh = new Mesh( bufferGeometry, material );
+
+			} else if ( geometryType === 1 ) {
+
+				mesh = new LineSegments( bufferGeometry, material );
+
+			} else {
+
+				mesh = new Points( bufferGeometry, material );
+
+			}
+			mesh.name = meshName;
+			meshes.push( mesh );
+
+		}
+
+		let progressMessage = meshPayload.params.meshName;
+		if ( meshes.length > 0 ) {
+
+			let meshNames = [];
+			for ( let i in meshes ) {
+
+				mesh = meshes[ i ];
+				meshNames[ i ] = mesh.name;
+
+			}
+			progressMessage += ': Adding mesh(es) (' + meshNames.length + ': ' + meshNames + ') from input mesh: ' + meshName;
+			progressMessage += ' (' + ( meshPayload.progress.numericalValue * 100).toFixed( 2 ) + '%)';
+
+		} else {
+
+			progressMessage += ': Not adding mesh: ' + meshName;
+			progressMessage += ' (' + ( meshPayload.progress.numericalValue * 100).toFixed( 2 ) + '%)';
+
+		}
+		let callbackOnParseProgress = this.callbacks.onParseProgress;
+		if ( callbackOnParseProgress ) {
+
+			callbackOnParseProgress( 'progress', progressMessage, meshPayload.progress.numericalValue );
+
+		}
+
+		return meshes;
+	}
+
+};
+
+/**
+ * Object to return by callback onMeshAlter. Used to disregard a certain mesh or to return one to many meshes.
+ * @class
+ *
+ * @param {boolean} disregardMesh=false Tell implementation to completely disregard this mesh
+ * @param {boolean} disregardMesh=false Tell implementation that mesh(es) have been altered or added
+ */
+const LoadedMeshUserOverride = function( disregardMesh, alteredMesh ) {
+	this.disregardMesh = disregardMesh === true;
+	this.alteredMesh = alteredMesh === true;
+	this.meshes = [];
+};
+
+
+LoadedMeshUserOverride.prototype = {
+
+	constructor: LoadedMeshUserOverride,
+
+	/**
+	 * Add a mesh created within callback.
+	 *
+	 * @param {Mesh} mesh
+	 */
+	addMesh: function ( mesh ) {
+		this.meshes.push( mesh );
+		this.alteredMesh = true;
+	},
+
+	/**
+	 * Answers if mesh shall be disregarded completely.
+	 *
+	 * @returns {boolean}
+	 */
+	isDisregardMesh: function () {
+		return this.disregardMesh;
+	},
+
+	/**
+	 * Answers if new mesh(es) were created.
+	 *
+	 * @returns {boolean}
+	 */
+	providesAlteredMeshes: function () {
+		return this.alteredMesh;
+	}
+};
+
+export {
+	MeshReceiver,
+	LoadedMeshUserOverride
+};

+ 911 - 0
examples/jsm/loaders/worker/independent/OBJLoader2Parser.js

@@ -0,0 +1,911 @@
+/**
+ * @author Kai Salmen / https://kaisalmen.de
+ * Development repository: https://github.com/kaisalmen/WWOBJLoader
+ */
+
+/**
+ * Parse OBJ data either from ArrayBuffer or string
+ * @class
+ */
+const Parser = function() {
+	this.callbacks = {
+		onProgress: null,
+		onAssetAvailable: null,
+		onError: null
+	};
+	this.contentRef = null;
+	this.legacyMode = false;
+
+	this.materials = {};
+	this.materialPerSmoothingGroup = false;
+	this.useOAsMesh = false;
+	this.useIndices = false;
+	this.disregardNormals = false;
+
+	this.vertices = [];
+	this.colors = [];
+	this.normals = [];
+	this.uvs = [];
+
+	this.rawMesh = {
+		objectName: '',
+		groupName: '',
+		activeMtlName: '',
+		mtllibName: '',
+
+		// reset with new mesh
+		faceType: - 1,
+		subGroups: [],
+		subGroupInUse: null,
+		smoothingGroup: {
+			splitMaterials: false,
+			normalized: - 1,
+			real: - 1
+		},
+		counts: {
+			doubleIndicesCount: 0,
+			faceCount: 0,
+			mtlCount: 0,
+			smoothingGroupCount: 0
+		}
+	};
+
+	this.inputObjectCount = 1;
+	this.outputObjectCount = 1;
+	this.globalCounts = {
+		vertices: 0,
+		faces: 0,
+		doubleIndicesCount: 0,
+		lineByte: 0,
+		currentByte: 0,
+		totalBytes: 0
+	};
+
+	this.logging = {
+		enabled: true,
+		debug: false
+	};
+};
+
+Parser.prototype = {
+
+	constructor: Parser,
+
+	resetRawMesh: function () {
+		// faces are stored according combined index of group, material and smoothingGroup (0 or not)
+		this.rawMesh.subGroups = [];
+		this.rawMesh.subGroupInUse = null;
+		this.rawMesh.smoothingGroup.normalized = - 1;
+		this.rawMesh.smoothingGroup.real = - 1;
+
+		// this default index is required as it is possible to define faces without 'g' or 'usemtl'
+		this.pushSmoothingGroup( 1 );
+
+		this.rawMesh.counts.doubleIndicesCount = 0;
+		this.rawMesh.counts.faceCount = 0;
+		this.rawMesh.counts.mtlCount = 0;
+		this.rawMesh.counts.smoothingGroupCount = 0;
+	},
+
+	setMaterialPerSmoothingGroup: function ( materialPerSmoothingGroup ) {
+		this.materialPerSmoothingGroup = materialPerSmoothingGroup;
+	},
+
+	setUseOAsMesh: function ( useOAsMesh ) {
+		this.useOAsMesh = useOAsMesh;
+	},
+
+	setUseIndices: function ( useIndices ) {
+		this.useIndices = useIndices;
+	},
+
+	setDisregardNormals: function ( disregardNormals ) {
+		this.disregardNormals = disregardNormals;
+	},
+
+	setMaterials: function ( materials ) {
+		if ( materials === undefined || materials === null ) return;
+
+		for ( let materialName in materials ) {
+			if ( materials.hasOwnProperty( materialName ) ) {
+
+				this.materials[ materialName ] = materials[ materialName ];
+
+			}
+		}
+	},
+
+	setCallbackOnAssetAvailable: function ( onAssetAvailable ) {
+		if ( onAssetAvailable !== null && onAssetAvailable !== undefined ) {
+
+			this.callbacks.onAssetAvailable = onAssetAvailable;
+
+		}
+	},
+
+	setCallbackOnProgress: function ( onProgress ) {
+		if ( onProgress !== null && onProgress !== undefined ) {
+
+			this.callbacks.onProgress = onProgress;
+
+		}
+	},
+
+	setCallbackOnError: function ( onError ) {
+		if ( onError !== null && onError !== undefined ) {
+
+			this.callbacks.onError = onError;
+
+		}
+	},
+
+	setLogging: function ( enabled, debug ) {
+		this.logging.enabled = enabled === true;
+		this.logging.debug = debug === true;
+	},
+
+	configure: function () {
+		if ( this.callbacks.onAssetAvailable === null ) {
+
+			let errorMessage = 'Unable to run as no callback for building meshes is set.';
+			if ( this.callbacks.onError !== null ) {
+
+				this.callbacks.onError( errorMessage );
+
+			} else {
+
+				throw errorMessage;
+			}
+
+		}
+		this.pushSmoothingGroup( 1 );
+		if ( this.logging.enabled ) {
+
+			let matKeys = Object.keys( this.materials );
+			let matNames = (matKeys.length > 0) ? '\n\tmaterialNames:\n\t\t- ' + matKeys.join( '\n\t\t- ' ) : '\n\tmaterialNames: None';
+			let printedConfig = 'OBJLoader.Parser configuration:'
+				+ matNames
+				+ '\n\tmaterialPerSmoothingGroup: ' + this.materialPerSmoothingGroup
+				+ '\n\tuseOAsMesh: ' + this.useOAsMesh
+				+ '\n\tuseIndices: ' + this.useIndices
+				+ '\n\tdisregardNormals: ' + this.disregardNormals;
+			if ( this.callbacks.onProgress !== null ) {
+				printedConfig += '\n\tcallbacks.onProgress: ' + this.callbacks.onProgress.name;
+			}
+			if ( this.callbacks.onAssetAvailable !== null ) {
+				printedConfig += '\n\tcallbacks.onAssetAvailable: ' + this.callbacks.onAssetAvailable.name;
+			}
+			if ( this.callbacks.onError !== null ) {
+				printedConfig += '\n\tcallbacks.onError: ' + this.callbacks.onError.name;
+			}
+			console.info( printedConfig );
+
+		}
+	},
+
+	/**
+	 * Parse the provided arraybuffer
+	 *
+	 * @param {Uint8Array} arrayBuffer OBJ data as Uint8Array
+	 */
+	parse: function ( arrayBuffer ) {
+		if ( this.logging.enabled ) console.time( 'OBJLoader.Parser.parse' );
+		this.configure();
+
+		let arrayBufferView = new Uint8Array( arrayBuffer );
+		this.contentRef = arrayBufferView;
+		let length = arrayBufferView.byteLength;
+		this.globalCounts.totalBytes = length;
+		let buffer = new Array( 128 );
+
+		for ( let code, word = '', bufferPointer = 0, slashesCount = 0, i = 0; i < length; i ++ ) {
+
+			code = arrayBufferView[ i ];
+			switch ( code ) {
+				// space
+				case 32:
+					if ( word.length > 0 ) buffer[ bufferPointer ++ ] = word;
+					word = '';
+					break;
+				// slash
+				case 47:
+					if ( word.length > 0 ) buffer[ bufferPointer ++ ] = word;
+					slashesCount ++;
+					word = '';
+					break;
+
+				// LF
+				case 10:
+					if ( word.length > 0 ) buffer[ bufferPointer ++ ] = word;
+					word = '';
+					this.globalCounts.lineByte = this.globalCounts.currentByte;
+					this.globalCounts.currentByte = i;
+					this.processLine( buffer, bufferPointer, slashesCount );
+					bufferPointer = 0;
+					slashesCount = 0;
+					break;
+
+				// CR
+				case 13:
+					break;
+
+				default:
+					word += String.fromCharCode( code );
+					break;
+			}
+		}
+		this.finalizeParsing();
+		if ( this.logging.enabled ) console.timeEnd( 'OBJLoader.Parser.parse' );
+	},
+
+	/**
+	 * Parse the provided text
+	 *
+	 * @param {string} text OBJ data as string
+	 */
+	parseText: function ( text ) {
+		if ( this.logging.enabled ) console.time( 'OBJLoader.Parser.parseText' );
+		this.configure();
+		this.legacyMode = true;
+		this.contentRef = text;
+		let length = text.length;
+		this.globalCounts.totalBytes = length;
+		let buffer = new Array( 128 );
+
+		for ( let char, word = '', bufferPointer = 0, slashesCount = 0, i = 0; i < length; i ++ ) {
+
+			char = text[ i ];
+			switch ( char ) {
+				case ' ':
+					if ( word.length > 0 ) buffer[ bufferPointer ++ ] = word;
+					word = '';
+					break;
+
+				case '/':
+					if ( word.length > 0 ) buffer[ bufferPointer ++ ] = word;
+					slashesCount ++;
+					word = '';
+					break;
+
+				case '\n':
+					if ( word.length > 0 ) buffer[ bufferPointer ++ ] = word;
+					word = '';
+					this.globalCounts.lineByte = this.globalCounts.currentByte;
+					this.globalCounts.currentByte = i;
+					this.processLine( buffer, bufferPointer, slashesCount );
+					bufferPointer = 0;
+					slashesCount = 0;
+					break;
+
+				case '\r':
+					break;
+
+				default:
+					word += char;
+			}
+		}
+		this.finalizeParsing();
+		if ( this.logging.enabled ) console.timeEnd( 'OBJLoader.Parser.parseText' );
+	},
+
+	processLine: function ( buffer, bufferPointer, slashesCount ) {
+		if ( bufferPointer < 1 ) return;
+
+		let reconstructString = function ( content, legacyMode, start, stop ) {
+			let line = '';
+			if ( stop > start ) {
+
+				let i;
+				if ( legacyMode ) {
+
+					for ( i = start; i < stop; i ++ ) line += content[ i ];
+
+				} else {
+
+
+					for ( i = start; i < stop; i ++ ) line += String.fromCharCode( content[ i ] );
+
+				}
+				line = line.trim();
+
+			}
+			return line;
+		};
+
+		let bufferLength, length, i, lineDesignation;
+		lineDesignation = buffer [ 0 ];
+		switch ( lineDesignation ) {
+			case 'v':
+				this.vertices.push( parseFloat( buffer[ 1 ] ) );
+				this.vertices.push( parseFloat( buffer[ 2 ] ) );
+				this.vertices.push( parseFloat( buffer[ 3 ] ) );
+				if ( bufferPointer > 4 ) {
+
+					this.colors.push( parseFloat( buffer[ 4 ] ) );
+					this.colors.push( parseFloat( buffer[ 5 ] ) );
+					this.colors.push( parseFloat( buffer[ 6 ] ) );
+
+				}
+				break;
+
+			case 'vt':
+				this.uvs.push( parseFloat( buffer[ 1 ] ) );
+				this.uvs.push( parseFloat( buffer[ 2 ] ) );
+				break;
+
+			case 'vn':
+				this.normals.push( parseFloat( buffer[ 1 ] ) );
+				this.normals.push( parseFloat( buffer[ 2 ] ) );
+				this.normals.push( parseFloat( buffer[ 3 ] ) );
+				break;
+
+			case 'f':
+				bufferLength = bufferPointer - 1;
+
+				// "f vertex ..."
+				if ( slashesCount === 0 ) {
+
+					this.checkFaceType( 0 );
+					for ( i = 2, length = bufferLength; i < length; i ++ ) {
+
+						this.buildFace( buffer[ 1 ] );
+						this.buildFace( buffer[ i ] );
+						this.buildFace( buffer[ i + 1 ] );
+
+					}
+
+					// "f vertex/uv ..."
+				} else if ( bufferLength === slashesCount * 2 ) {
+
+					this.checkFaceType( 1 );
+					for ( i = 3, length = bufferLength - 2; i < length; i += 2 ) {
+
+						this.buildFace( buffer[ 1 ], buffer[ 2 ] );
+						this.buildFace( buffer[ i ], buffer[ i + 1 ] );
+						this.buildFace( buffer[ i + 2 ], buffer[ i + 3 ] );
+
+					}
+
+					// "f vertex/uv/normal ..."
+				} else if ( bufferLength * 2 === slashesCount * 3 ) {
+
+					this.checkFaceType( 2 );
+					for ( i = 4, length = bufferLength - 3; i < length; i += 3 ) {
+
+						this.buildFace( buffer[ 1 ], buffer[ 2 ], buffer[ 3 ] );
+						this.buildFace( buffer[ i ], buffer[ i + 1 ], buffer[ i + 2 ] );
+						this.buildFace( buffer[ i + 3 ], buffer[ i + 4 ], buffer[ i + 5 ] );
+
+					}
+
+					// "f vertex//normal ..."
+				} else {
+
+					this.checkFaceType( 3 );
+					for ( i = 3, length = bufferLength - 2; i < length; i += 2 ) {
+
+						this.buildFace( buffer[ 1 ], undefined, buffer[ 2 ] );
+						this.buildFace( buffer[ i ], undefined, buffer[ i + 1 ] );
+						this.buildFace( buffer[ i + 2 ], undefined, buffer[ i + 3 ] );
+
+					}
+
+				}
+				break;
+
+			case 'l':
+			case 'p':
+				bufferLength = bufferPointer - 1;
+				if ( bufferLength === slashesCount * 2 ) {
+
+					this.checkFaceType( 4 );
+					for ( i = 1, length = bufferLength + 1; i < length; i += 2 ) this.buildFace( buffer[ i ], buffer[ i + 1 ] );
+
+				} else {
+
+					this.checkFaceType( (lineDesignation === 'l') ? 5 : 6 );
+					for ( i = 1, length = bufferLength + 1; i < length; i ++ ) this.buildFace( buffer[ i ] );
+
+				}
+				break;
+
+			case 's':
+				this.pushSmoothingGroup( buffer[ 1 ] );
+				break;
+
+			case 'g':
+				// 'g' leads to creation of mesh if valid data (faces declaration was done before), otherwise only groupName gets set
+				this.processCompletedMesh();
+				this.rawMesh.groupName = reconstructString( this.contentRef, this.legacyMode, this.globalCounts.lineByte + 2, this.globalCounts.currentByte );
+				break;
+
+			case 'o':
+				// 'o' is meta-information and usually does not result in creation of new meshes, but can be enforced with "useOAsMesh"
+				if ( this.useOAsMesh ) this.processCompletedMesh();
+				this.rawMesh.objectName = reconstructString( this.contentRef, this.legacyMode, this.globalCounts.lineByte + 2, this.globalCounts.currentByte );
+				break;
+
+			case 'mtllib':
+				this.rawMesh.mtllibName = reconstructString( this.contentRef, this.legacyMode, this.globalCounts.lineByte + 7, this.globalCounts.currentByte );
+				break;
+
+			case 'usemtl':
+				let mtlName = reconstructString( this.contentRef, this.legacyMode, this.globalCounts.lineByte + 7, this.globalCounts.currentByte );
+				if ( mtlName !== '' && this.rawMesh.activeMtlName !== mtlName ) {
+
+					this.rawMesh.activeMtlName = mtlName;
+					this.rawMesh.counts.mtlCount ++;
+					this.checkSubGroup();
+
+				}
+				break;
+
+			default:
+				break;
+		}
+	},
+
+	pushSmoothingGroup: function ( smoothingGroup ) {
+		let smoothingGroupInt = parseInt( smoothingGroup );
+		if ( isNaN( smoothingGroupInt ) ) {
+			smoothingGroupInt = smoothingGroup === "off" ? 0 : 1;
+		}
+
+		let smoothCheck = this.rawMesh.smoothingGroup.normalized;
+		this.rawMesh.smoothingGroup.normalized = this.rawMesh.smoothingGroup.splitMaterials ? smoothingGroupInt : (smoothingGroupInt === 0) ? 0 : 1;
+		this.rawMesh.smoothingGroup.real = smoothingGroupInt;
+
+		if ( smoothCheck !== smoothingGroupInt ) {
+
+			this.rawMesh.counts.smoothingGroupCount ++;
+			this.checkSubGroup();
+
+		}
+	},
+
+	/**
+	 * Expanded faceTypes include all four face types, both line types and the point type
+	 * faceType = 0: "f vertex ..."
+	 * faceType = 1: "f vertex/uv ..."
+	 * faceType = 2: "f vertex/uv/normal ..."
+	 * faceType = 3: "f vertex//normal ..."
+	 * faceType = 4: "l vertex/uv ..." or "l vertex ..."
+	 * faceType = 5: "l vertex ..."
+	 * faceType = 6: "p vertex ..."
+	 */
+	checkFaceType: function ( faceType ) {
+		if ( this.rawMesh.faceType !== faceType ) {
+
+			this.processCompletedMesh();
+			this.rawMesh.faceType = faceType;
+			this.checkSubGroup();
+
+		}
+	},
+
+	checkSubGroup: function () {
+		let index = this.rawMesh.activeMtlName + '|' + this.rawMesh.smoothingGroup.normalized;
+		this.rawMesh.subGroupInUse = this.rawMesh.subGroups[ index ];
+
+		if ( this.rawMesh.subGroupInUse === undefined || this.rawMesh.subGroupInUse === null ) {
+
+			this.rawMesh.subGroupInUse = {
+				index: index,
+				objectName: this.rawMesh.objectName,
+				groupName: this.rawMesh.groupName,
+				materialName: this.rawMesh.activeMtlName,
+				smoothingGroup: this.rawMesh.smoothingGroup.normalized,
+				vertices: [],
+				indexMappingsCount: 0,
+				indexMappings: [],
+				indices: [],
+				colors: [],
+				uvs: [],
+				normals: []
+			};
+			this.rawMesh.subGroups[ index ] = this.rawMesh.subGroupInUse;
+
+		}
+	},
+
+	buildFace: function ( faceIndexV, faceIndexU, faceIndexN ) {
+		let subGroupInUse = this.rawMesh.subGroupInUse;
+		let scope = this;
+		let updateSubGroupInUse = function () {
+
+			let faceIndexVi = parseInt( faceIndexV );
+			let indexPointerV = 3 * (faceIndexVi > 0 ? faceIndexVi - 1 : faceIndexVi + scope.vertices.length / 3);
+			let indexPointerC = scope.colors.length > 0 ? indexPointerV : null;
+
+			let vertices = subGroupInUse.vertices;
+			vertices.push( scope.vertices[ indexPointerV ++ ] );
+			vertices.push( scope.vertices[ indexPointerV ++ ] );
+			vertices.push( scope.vertices[ indexPointerV ] );
+
+			if ( indexPointerC !== null ) {
+
+				let colors = subGroupInUse.colors;
+				colors.push( scope.colors[ indexPointerC ++ ] );
+				colors.push( scope.colors[ indexPointerC ++ ] );
+				colors.push( scope.colors[ indexPointerC ] );
+
+			}
+			if ( faceIndexU ) {
+
+				let faceIndexUi = parseInt( faceIndexU );
+				let indexPointerU = 2 * (faceIndexUi > 0 ? faceIndexUi - 1 : faceIndexUi + scope.uvs.length / 2);
+				let uvs = subGroupInUse.uvs;
+				uvs.push( scope.uvs[ indexPointerU ++ ] );
+				uvs.push( scope.uvs[ indexPointerU ] );
+
+			}
+			if ( faceIndexN && ! scope.disregardNormals ) {
+
+				let faceIndexNi = parseInt( faceIndexN );
+				let indexPointerN = 3 * (faceIndexNi > 0 ? faceIndexNi - 1 : faceIndexNi + scope.normals.length / 3);
+				let normals = subGroupInUse.normals;
+				normals.push( scope.normals[ indexPointerN ++ ] );
+				normals.push( scope.normals[ indexPointerN ++ ] );
+				normals.push( scope.normals[ indexPointerN ] );
+
+			}
+		};
+
+		if ( this.useIndices ) {
+
+			if ( this.disregardNormals ) faceIndexN = undefined;
+			let mappingName = faceIndexV + ( faceIndexU ? '_' + faceIndexU : '_n' ) + ( faceIndexN ? '_' + faceIndexN : '_n' );
+			let indicesPointer = subGroupInUse.indexMappings[ mappingName ];
+			if ( indicesPointer === undefined || indicesPointer === null ) {
+
+				indicesPointer = this.rawMesh.subGroupInUse.vertices.length / 3;
+				updateSubGroupInUse();
+				subGroupInUse.indexMappings[ mappingName ] = indicesPointer;
+				subGroupInUse.indexMappingsCount++;
+
+			} else {
+
+				this.rawMesh.counts.doubleIndicesCount++;
+
+			}
+			subGroupInUse.indices.push( indicesPointer );
+
+		} else {
+
+			updateSubGroupInUse();
+
+		}
+		this.rawMesh.counts.faceCount ++;
+	},
+
+	createRawMeshReport: function ( inputObjectCount ) {
+		return 'Input Object number: ' + inputObjectCount +
+			'\n\tObject name: ' + this.rawMesh.objectName +
+			'\n\tGroup name: ' + this.rawMesh.groupName +
+			'\n\tMtllib name: ' + this.rawMesh.mtllibName +
+			'\n\tVertex count: ' + this.vertices.length / 3 +
+			'\n\tNormal count: ' + this.normals.length / 3 +
+			'\n\tUV count: ' + this.uvs.length / 2 +
+			'\n\tSmoothingGroup count: ' + this.rawMesh.counts.smoothingGroupCount +
+			'\n\tMaterial count: ' + this.rawMesh.counts.mtlCount +
+			'\n\tReal MeshOutputGroup count: ' + this.rawMesh.subGroups.length;
+	},
+
+	/**
+	 * Clear any empty subGroup and calculate absolute vertex, normal and uv counts
+	 */
+	finalizeRawMesh: function () {
+		let meshOutputGroupTemp = [];
+		let meshOutputGroup;
+		let absoluteVertexCount = 0;
+		let absoluteIndexMappingsCount = 0;
+		let absoluteIndexCount = 0;
+		let absoluteColorCount = 0;
+		let absoluteNormalCount = 0;
+		let absoluteUvCount = 0;
+		let indices;
+		for ( let name in this.rawMesh.subGroups ) {
+
+			meshOutputGroup = this.rawMesh.subGroups[ name ];
+			if ( meshOutputGroup.vertices.length > 0 ) {
+
+				indices = meshOutputGroup.indices;
+				if ( indices.length > 0 && absoluteIndexMappingsCount > 0 ) {
+
+					for ( let i = 0; i < indices.length; i++ ) {
+
+						indices[ i ] = indices[ i ] + absoluteIndexMappingsCount;
+
+					}
+
+				}
+				meshOutputGroupTemp.push( meshOutputGroup );
+				absoluteVertexCount += meshOutputGroup.vertices.length;
+				absoluteIndexMappingsCount += meshOutputGroup.indexMappingsCount;
+				absoluteIndexCount += meshOutputGroup.indices.length;
+				absoluteColorCount += meshOutputGroup.colors.length;
+				absoluteUvCount += meshOutputGroup.uvs.length;
+				absoluteNormalCount += meshOutputGroup.normals.length;
+
+			}
+		}
+
+		// do not continue if no result
+		let result = null;
+		if ( meshOutputGroupTemp.length > 0 ) {
+
+			result = {
+				name: this.rawMesh.groupName !== '' ? this.rawMesh.groupName : this.rawMesh.objectName,
+				subGroups: meshOutputGroupTemp,
+				absoluteVertexCount: absoluteVertexCount,
+				absoluteIndexCount: absoluteIndexCount,
+				absoluteColorCount: absoluteColorCount,
+				absoluteNormalCount: absoluteNormalCount,
+				absoluteUvCount: absoluteUvCount,
+				faceCount: this.rawMesh.counts.faceCount,
+				doubleIndicesCount: this.rawMesh.counts.doubleIndicesCount
+			};
+
+		}
+		return result;
+	},
+
+	processCompletedMesh: function () {
+		let result = this.finalizeRawMesh();
+		let haveMesh = result !== null;
+		if ( haveMesh ) {
+
+			if ( this.colors.length > 0 && this.colors.length !== this.vertices.length ) {
+
+				if ( this.callbacks.onError !== null ) {
+
+					this.callbacks.onError( 'Vertex Colors were detected, but vertex count and color count do not match!' );
+
+				}
+
+			}
+			if ( this.logging.enabled && this.logging.debug ) console.debug( this.createRawMeshReport( this.inputObjectCount ) );
+			this.inputObjectCount ++;
+
+			this.buildMesh( result );
+			let progressBytesPercent = this.globalCounts.currentByte / this.globalCounts.totalBytes;
+			if ( this.callbacks.onProgress !== null ) {
+
+				this.callbacks.onProgress( 'Completed [o: ' + this.rawMesh.objectName + ' g:' + this.rawMesh.groupName + '' +
+					'] Total progress: ' + (progressBytesPercent * 100).toFixed( 2 ) + '%', progressBytesPercent );
+
+			}
+			this.resetRawMesh();
+
+		}
+		return haveMesh;
+	},
+
+	/**
+	 * SubGroups are transformed to too intermediate format that is forwarded to the MeshReceiver.
+	 * It is ensured that SubGroups only contain objects with vertices (no need to check).
+	 *
+	 * @param result
+	 */
+	buildMesh: function ( result ) {
+		let meshOutputGroups = result.subGroups;
+
+		let vertexFA = new Float32Array( result.absoluteVertexCount );
+		this.globalCounts.vertices += result.absoluteVertexCount / 3;
+		this.globalCounts.faces += result.faceCount;
+		this.globalCounts.doubleIndicesCount += result.doubleIndicesCount;
+		let indexUA = (result.absoluteIndexCount > 0) ? new Uint32Array( result.absoluteIndexCount ) : null;
+		let colorFA = (result.absoluteColorCount > 0) ? new Float32Array( result.absoluteColorCount ) : null;
+		let normalFA = (result.absoluteNormalCount > 0) ? new Float32Array( result.absoluteNormalCount ) : null;
+		let uvFA = (result.absoluteUvCount > 0) ? new Float32Array( result.absoluteUvCount ) : null;
+		let haveVertexColors = colorFA !== null;
+
+		let meshOutputGroup;
+		let materialNames = [];
+
+		let createMultiMaterial = (meshOutputGroups.length > 1);
+		let materialIndex = 0;
+		let materialIndexMapping = [];
+		let selectedMaterialIndex;
+		let materialGroup;
+		let materialGroups = [];
+
+		let vertexFAOffset = 0;
+		let indexUAOffset = 0;
+		let colorFAOffset = 0;
+		let normalFAOffset = 0;
+		let uvFAOffset = 0;
+		let materialGroupOffset = 0;
+		let materialGroupLength = 0;
+
+		let materialOrg, material, materialName, materialNameOrg;
+		// only one specific face type
+		for ( let oodIndex in meshOutputGroups ) {
+
+			if ( ! meshOutputGroups.hasOwnProperty( oodIndex ) ) continue;
+			meshOutputGroup = meshOutputGroups[ oodIndex ];
+
+			materialNameOrg = meshOutputGroup.materialName;
+			if ( this.rawMesh.faceType < 4 ) {
+
+				materialName = materialNameOrg + (haveVertexColors ? '_vertexColor' : '') + (meshOutputGroup.smoothingGroup === 0 ? '_flat' : '');
+
+
+			} else {
+
+				materialName = this.rawMesh.faceType === 6 ? 'defaultPointMaterial' : 'defaultLineMaterial';
+
+			}
+			materialOrg = this.materials[ materialNameOrg ];
+			material = this.materials[ materialName ];
+
+			// both original and derived names do not lead to an existing material => need to use a default material
+			if ( ( materialOrg === undefined || materialOrg === null ) && ( material === undefined || material === null ) ) {
+
+				materialName = haveVertexColors ? 'defaultVertexColorMaterial' : 'defaultMaterial';
+				material = this.materials[ materialName ];
+				if ( this.logging.enabled ) {
+
+					console.info( 'object_group "' + meshOutputGroup.objectName + '_' +
+						meshOutputGroup.groupName + '" was defined with unresolvable material "' +
+						materialNameOrg + '"! Assigning "' + materialName + '".' );
+
+				}
+
+			}
+			if ( material === undefined || material === null ) {
+
+				let materialCloneInstructions = {
+					materialNameOrg: materialNameOrg,
+					materialName: materialName,
+					materialProperties: {
+						vertexColors: haveVertexColors ? 2 : 0,
+						flatShading: meshOutputGroup.smoothingGroup === 0
+					}
+				};
+				let payload = {
+					cmd: 'data',
+					type: 'material',
+					materials: {
+						materialCloneInstructions: materialCloneInstructions
+					}
+				};
+				this.callbacks.onAssetAvailable( payload );
+
+				// only set materials if they don't exist, yet
+				let matCheck = this.materials[ materialName ];
+				if ( matCheck === undefined || matCheck === null ) {
+
+					this.materials[ materialName ] = materialCloneInstructions;
+
+				}
+
+			}
+
+			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;
+					materialNames.push( materialName );
+					materialIndex ++;
+
+				}
+				materialGroupLength = this.useIndices ? meshOutputGroup.indices.length : meshOutputGroup.vertices.length / 3;
+				materialGroup = {
+					start: materialGroupOffset,
+					count: materialGroupLength,
+					index: selectedMaterialIndex
+				};
+				materialGroups.push( materialGroup );
+				materialGroupOffset += materialGroupLength;
+
+			} else {
+
+				materialNames.push( materialName );
+
+			}
+
+			vertexFA.set( meshOutputGroup.vertices, vertexFAOffset );
+			vertexFAOffset += meshOutputGroup.vertices.length;
+
+			if ( indexUA ) {
+
+				indexUA.set( meshOutputGroup.indices, indexUAOffset );
+				indexUAOffset += meshOutputGroup.indices.length;
+
+			}
+
+			if ( colorFA ) {
+
+				colorFA.set( meshOutputGroup.colors, colorFAOffset );
+				colorFAOffset += meshOutputGroup.colors.length;
+
+			}
+
+			if ( normalFA ) {
+
+				normalFA.set( meshOutputGroup.normals, normalFAOffset );
+				normalFAOffset += meshOutputGroup.normals.length;
+
+			}
+			if ( uvFA ) {
+
+				uvFA.set( meshOutputGroup.uvs, uvFAOffset );
+				uvFAOffset += meshOutputGroup.uvs.length;
+
+			}
+
+			if ( this.logging.enabled && this.logging.debug ) {
+
+				let materialIndexLine = ( selectedMaterialIndex === undefined || selectedMaterialIndex === null ) ? '' : '\n\t\tmaterialIndex: ' + selectedMaterialIndex;
+				let createdReport = '\tOutput Object no.: ' + this.outputObjectCount +
+					'\n\t\tgroupName: ' + meshOutputGroup.groupName +
+					'\n\t\tIndex: ' + meshOutputGroup.index +
+					'\n\t\tfaceType: ' + this.rawMesh.faceType +
+					'\n\t\tmaterialName: ' + meshOutputGroup.materialName +
+					'\n\t\tsmoothingGroup: ' + meshOutputGroup.smoothingGroup +
+					materialIndexLine +
+					'\n\t\tobjectName: ' + meshOutputGroup.objectName +
+					'\n\t\t#vertices: ' + meshOutputGroup.vertices.length / 3 +
+					'\n\t\t#indices: ' + meshOutputGroup.indices.length +
+					'\n\t\t#colors: ' + meshOutputGroup.colors.length / 3 +
+					'\n\t\t#uvs: ' + meshOutputGroup.uvs.length / 2 +
+					'\n\t\t#normals: ' + meshOutputGroup.normals.length / 3;
+				console.debug( createdReport );
+
+			}
+
+		}
+		this.outputObjectCount ++;
+		this.callbacks.onAssetAvailable(
+			{
+				cmd: 'data',
+				type: 'mesh',
+				progress: {
+					numericalValue: this.globalCounts.currentByte / this.globalCounts.totalBytes
+				},
+				params: {
+					meshName: result.name
+				},
+				materials: {
+					multiMaterial: createMultiMaterial,
+					materialNames: materialNames,
+					materialGroups: materialGroups
+				},
+				buffers: {
+					vertices: vertexFA,
+					indices: indexUA,
+					colors: colorFA,
+					normals: normalFA,
+					uvs: uvFA
+				},
+				// 0: mesh, 1: line, 2: point
+				geometryType: this.rawMesh.faceType < 4 ? 0 : (this.rawMesh.faceType === 6) ? 2 : 1
+			},
+			[ vertexFA.buffer ],
+			indexUA !== null ?  [ indexUA.buffer ] : null,
+			colorFA !== null ? [ colorFA.buffer ] : null,
+			normalFA !== null ? [ normalFA.buffer ] : null,
+			uvFA !== null ? [ uvFA.buffer ] : null
+		);
+	},
+
+	finalizeParsing: function () {
+		if ( this.logging.enabled ) console.info( 'Global output object count: ' + this.outputObjectCount );
+		if ( this.processCompletedMesh() && this.logging.enabled ) {
+
+			let parserFinalReport = 'Overall counts: ' +
+				'\n\tVertices: ' + this.globalCounts.vertices +
+				'\n\tFaces: ' + this.globalCounts.faces +
+				'\n\tMultiple definitions: ' + this.globalCounts.doubleIndicesCount;
+			console.info( parserFinalReport );
+
+		}
+	}
+};
+
+export { Parser };

+ 86 - 75
examples/models/obj/verify/verify.html

@@ -71,60 +71,71 @@
 			<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/loaders/OBJLoader.js"></script>
+		<script src="../../../js/WebGL.js"></script>
 		<script src="../../../js/libs/dat.gui.min.js"></script>
 
-		<script src="../../../js/loaders/LoaderSupport.js"></script>
-		<script src="../../../js/loaders/OBJLoader2.js"></script>
-		<script>
+		<script type="module">
 
 			'use strict';
 
-			var OBJLoaderVerify = (function () {
-
-				var Validator = THREE.LoaderSupport.Validator;
+			import {
+				AmbientLight,
+				DirectionalLight,
+				GridHelper,
+				PerspectiveCamera,
+				Scene,
+				Vector3,
+				WebGLRenderer
+			} from "../../../../build/three.module.js";
+
+			import { TrackballControls } from "../../../jsm/controls/TrackballControls.js";
+
+			import { MTLLoader } from "../../../jsm/loaders/MTLLoader.js";
+			import { MtlObjBridge } from "../../../jsm/loaders/bridge/MtlObjBridge.js";
+			import { OBJLoader } from "../../../jsm/loaders/OBJLoader.js";
+			import { OBJLoader2 } from "../../../jsm/loaders/OBJLoader2.js";
+
+			const OBJLoaderVerify = function ( elementToBindTo ) {
+				this.renderer = null;
+				this.canvas = elementToBindTo;
+				this.aspectRatio = 1;
+				this.recalcAspectRatio();
+
+				this.scene = null;
+				this.cameraDefaults = {
+					posCamera: new Vector3( 0.0, 175.0, 500.0 ),
+					posCameraTarget: new Vector3( 0, 0, 0 ),
+					near: 0.1,
+					far: 10000,
+					fov: 45
+				};
+				this.camera = null;
+				this.cameraTarget = this.cameraDefaults.posCameraTarget;
 
-				function OBJLoaderVerify( elementToBindTo ) {
-					this.renderer = null;
-					this.canvas = elementToBindTo;
-					this.aspectRatio = 1;
-					this.recalcAspectRatio();
+				this.controls = null;
+			};
 
-					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;
+				OBJLoaderVerify.prototype = {
 
-					this.controls = null;
-				}
+					constructor: OBJLoaderVerify,
 
-				OBJLoaderVerify.prototype.initGL = function () {
-					this.renderer = new THREE.WebGLRenderer( {
+					initGL: function () {
+					this.renderer = new WebGLRenderer( {
 						canvas: this.canvas,
 						antialias: true,
 						autoClear: true
 					} );
 					this.renderer.setClearColor( 0x050505 );
 
-					this.scene = new THREE.Scene();
+					this.scene = new Scene();
 
-					this.camera = new THREE.PerspectiveCamera( this.cameraDefaults.fov, this.aspectRatio, this.cameraDefaults.near, this.cameraDefaults.far );
+					this.camera = new PerspectiveCamera( this.cameraDefaults.fov, this.aspectRatio, this.cameraDefaults.near, this.cameraDefaults.far );
 					this.resetCamera();
-					this.controls = new THREE.TrackballControls( this.camera, this.renderer.domElement );
+					this.controls = new TrackballControls( this.camera, this.renderer.domElement );
 
-					var ambientLight = new THREE.AmbientLight( 0x404040 );
-					var directionalLight1 = new THREE.DirectionalLight( 0xC0C090 );
-					var directionalLight2 = new THREE.DirectionalLight( 0xC0C090 );
+					let ambientLight = new AmbientLight( 0x404040 );
+					let directionalLight1 = new DirectionalLight( 0xC0C090 );
+					let directionalLight2 = new DirectionalLight( 0xC0C090 );
 
 					directionalLight1.position.set( -100, -50, 100 );
 					directionalLight2.position.set( 100, 50, -100 );
@@ -133,89 +144,89 @@
 					this.scene.add( directionalLight2 );
 					this.scene.add( ambientLight );
 
-					var helper = new THREE.GridHelper( 1200, 60, 0xFF4444, 0x404040 );
+					let helper = new GridHelper( 1200, 60, 0xFF4444, 0x404040 );
 					this.scene.add( helper );
-				};
+				},
 
-				OBJLoaderVerify.prototype.initContent = function () {
-					var modelName = 'verificationCubes';
+				initContent: function () {
+					let modelName = 'verificationCubes';
 					this._reportProgress( { detail: { text: 'Loading: ' + modelName } } );
 
-					var scope = this;
-					var objLoader2 = new THREE.OBJLoader2();
+					let objLoader = new OBJLoader();
+
+					let objLoader2 = new OBJLoader2();
+					objLoader2.setModelName( modelName );
+					objLoader2.setLogging( true, false );
+					objLoader2.setUseOAsMesh( true );
 
-					var callbackOnLoad = function ( event ) {
-						scope.scene.add( event.detail.loaderRootNode );
-						console.log( 'Loading complete: ' + event.detail.modelName );
+					let scope = this;
+					let callbackOnLoad = function ( object3d ) {
+						scope.scene.add( object3d );
+						console.log( 'Loading complete: ' + modelName );
 						scope._reportProgress( { detail: { text: '' } } );
 					};
 
-					var onLoadMtl = function ( materials, materialCreator ) {
-						var objLoader = new THREE.OBJLoader();
-						objLoader.setMaterials( materialCreator );
+					let onLoadMtl = function ( mtlParseResult ) {
+						objLoader.setMaterials( mtlParseResult );
 						objLoader.load( './verify.obj', function ( object ) {
 							object.position.y = -100;
 							scope.scene.add( object );
 						} );
 
-						objLoader2.setModelName( modelName );
-						objLoader2.setMaterials( materials );
-						objLoader2.setLogging( true, false );
-						objLoader2.setUseOAsMesh( true );
-						objLoader2.load( './verify.obj', callbackOnLoad, null, null, null, false );
+						objLoader2.addMaterials( MtlObjBridge.addMaterialsFromMtlLoader( mtlParseResult ) );
+						objLoader2.load( './verify.obj', callbackOnLoad, null, null, null );
 					};
-					objLoader2.loadMtl( './verify.mtl', null, onLoadMtl );
-				};
 
-				OBJLoaderVerify.prototype._reportProgress = function( event ) {
-					var output = Validator.verifyInput( event.detail.text, '' );
+					let mtlLoader = new MTLLoader();
+					mtlLoader.load( './verify.mtl', onLoadMtl );
+				},
+
+				_reportProgress: function( event ) {
+					let output = ( event.detail !== undefined && event.detail !== null && event.detail.text !== undefined && event.detail.text !== null ) ? event.detail.text : '';
 					console.log( 'Progress: ' + output );
 					document.getElementById( 'feedback' ).innerHTML = output;
-				};
+				},
 
-				OBJLoaderVerify.prototype.resizeDisplayGL = function () {
+				resizeDisplayGL: function () {
 					this.controls.handleResize();
 
 					this.recalcAspectRatio();
 					this.renderer.setSize( this.canvas.offsetWidth, this.canvas.offsetHeight, false );
 
 					this.updateCamera();
-				};
+				},
 
-				OBJLoaderVerify.prototype.recalcAspectRatio = function () {
+				recalcAspectRatio: function () {
 					this.aspectRatio = ( this.canvas.offsetHeight === 0 ) ? 1 : this.canvas.offsetWidth / this.canvas.offsetHeight;
-				};
+				},
 
-				OBJLoaderVerify.prototype.resetCamera = function () {
+				resetCamera: function () {
 					this.camera.position.copy( this.cameraDefaults.posCamera );
 					this.cameraTarget.copy( this.cameraDefaults.posCameraTarget );
 
 					this.updateCamera();
-				};
+				},
 
-				OBJLoaderVerify.prototype.updateCamera = function () {
+				updateCamera: function () {
 					this.camera.aspect = this.aspectRatio;
 					this.camera.lookAt( this.cameraTarget );
 					this.camera.updateProjectionMatrix();
-				};
+				},
 
-				OBJLoaderVerify.prototype.render = function () {
+				render: function () {
 					if ( ! this.renderer.autoClear ) this.renderer.clear();
 					this.controls.update();
 					this.renderer.render( this.scene, this.camera );
-				};
-
-				return OBJLoaderVerify;
-
-			})();
+				}
+			};
 
-			var app = new OBJLoaderVerify( document.getElementById( 'example' ) );
+			let app = new OBJLoaderVerify( document.getElementById( 'example' ) );
 
-			var resizeWindow = function () {
+			let resizeWindow = function () {
 				app.resizeDisplayGL();
 			};
 
-			var render = function () {
+			let render = function () {
 				requestAnimationFrame( render );
 				app.render();
 			};