diff --git a/examples/files.json b/examples/files.json index b7cc2fcd104b7c..91d6e56e43cc15 100644 --- a/examples/files.json +++ b/examples/files.json @@ -1,4 +1,4 @@ -{ +{ "webgl": [ "webgl_animation_cloth", "webgl_animation_keyframes", @@ -123,6 +123,7 @@ "webgl_loader_vrm", "webgl_loader_vrml", "webgl_loader_vtk", + "webgl_loader_workertaskmanager", "webgl_loader_xyz", "webgl_lod", "webgl_marchingcubes", diff --git a/examples/jsm/loaders/workerTaskManager/WorkerTaskManager.js b/examples/jsm/loaders/workerTaskManager/WorkerTaskManager.js new file mode 100644 index 00000000000000..5eb45f7a7c1a6c --- /dev/null +++ b/examples/jsm/loaders/workerTaskManager/WorkerTaskManager.js @@ -0,0 +1,727 @@ +/** + * Development repository: https://github.com/kaisalmen/three-wtm + * Initial idea by Don McCurdy / https://www.donmccurdy.com + */ + +import { FileLoader } from "../../../../build/three.module.js"; +import { WorkerTaskManagerDefaultRouting } from "./worker/defaultRouting.js"; + +/** + * Register one to many tasks type to the WorkerTaskManager. Then init and enqueue a worker based execution by passing + * configuration and buffers. The WorkerTaskManager allows to execute a maximum number of executions in parallel. + */ +class WorkerTaskManager { + + /** + * Creates a new WorkerTaskManager instance. + * + * @param {number} [maxParallelExecutions] How many workers are allowed to be executed in parallel. + */ + constructor ( maxParallelExecutions ) { + + /** + * @type {Map} + */ + this.taskTypes = new Map(); + this.verbose = false; + this.maxParallelExecutions = maxParallelExecutions ? maxParallelExecutions : 4; + this.actualExecutionCount = 0; + /** + * @type {StoredExecution[]} + */ + this.storedExecutions = []; + this.teardown = false; + + } + + /** + * Set if logging should be verbose + * + * @param {boolean} verbose + * @return {WorkerTaskManager} + */ + setVerbose ( verbose ) { + + this.verbose = verbose; + return this; + + } + + /** + * Set the maximum number of parallel executions. + * + * @param {number} maxParallelExecutions How many workers are allowed to be executed in parallel. + * @return {WorkerTaskManager} + */ + setMaxParallelExecutions ( maxParallelExecutions ) { + + this.maxParallelExecutions = maxParallelExecutions; + return this; + + } + + /** + * Returns the maximum number of parallel executions. + * @return {number} + */ + getMaxParallelExecutions () { + + return this.maxParallelExecutions; + + } + + /** + * Returns true if support for the given task type is available. + * + * @param {string} taskType The task type as string + * @return boolean + */ + supportsTaskType ( taskType ) { + + return this.taskTypes.has( taskType ); + + } + + /** + * Registers functions and dependencies for a new task type. + * + * @param {string} taskType The name to be used for registration. + * @param {Function} initFunction The function to be called when the worker is initialised + * @param {Function} executeFunction The function to be called when the worker is executed + * @param {Function} comRoutingFunction The function that should handle communication, leave undefined for default behavior + * @param {boolean} fallback Set to true if execution should be performed in main + * @param {Object[]} [dependencyDescriptions] + * @return {boolean} Tells if registration is possible (new=true) or if task was already registered (existing=false) + */ + registerTaskType ( taskType, initFunction, executeFunction, comRoutingFunction, fallback, dependencyDescriptions ) { + + let allowedToRegister = ! this.supportsTaskType( taskType ); + if ( allowedToRegister ) { + + let workerTypeDefinition = new WorkerTypeDefinition( taskType, this.maxParallelExecutions, fallback, this.verbose ); + workerTypeDefinition.setFunctions( initFunction, executeFunction, comRoutingFunction ); + workerTypeDefinition.setDependencyDescriptions( dependencyDescriptions ); + this.taskTypes.set( taskType, workerTypeDefinition ); + + } + return allowedToRegister; + + } + + /** + * Registers functionality for a new task type based on module file. + * + * @param {string} taskType The name to be used for registration. + * @param {string} workerModuleUrl The URL to be used for the Worker. Module must provide logic to handle "init" and "execute" messages. + * @return {boolean} Tells if registration is possible (new=true) or if task was already registered (existing=false) + */ + registerTaskTypeModule ( taskType, workerModuleUrl ) { + + let allowedToRegister = ! this.supportsTaskType( taskType ); + if ( allowedToRegister ) { + + let workerTypeDefinition = new WorkerTypeDefinition( taskType, this.maxParallelExecutions, false, this.verbose ); + workerTypeDefinition.setWorkerModule( workerModuleUrl ); + this.taskTypes.set( taskType, workerTypeDefinition ); + + } + return allowedToRegister; + + } + + /** + * Provides initialization configuration and transferable objects. + * + * @param {string} taskType The name of the registered task type. + * @param {object} config Configuration properties as serializable string. + * @param {Transferable[]} [transferables] Any optional {@link ArrayBuffer} encapsulated in object. + */ + async initTaskType ( taskType, config, transferables ) { + + let workerTypeDefinition = this.taskTypes.get( taskType ); + if ( workerTypeDefinition ) { + + if ( ! workerTypeDefinition.status.initStarted ) { + + workerTypeDefinition.status.initStarted = true; + if ( workerTypeDefinition.isWorkerModule() ) { + + await workerTypeDefinition.createWorkerModules() + .then( () => workerTypeDefinition.initWorkers( config, transferables ) ) + .then( () => workerTypeDefinition.status.initComplete = true ) + .catch( e => console.error( e ) ); + + } else { + + await workerTypeDefinition.loadDependencies() + .then( () => workerTypeDefinition.createWorkers() ) + .then( () => workerTypeDefinition.initWorkers( config, transferables ) ) + .then( () => workerTypeDefinition.status.initComplete = true ) + .catch( e => console.error( e ) ); + + } + + } + else { + + while ( ! workerTypeDefinition.status.initComplete ) { + + await this._wait( 10 ); + + } + + } + + } + + } + + async _wait ( milliseconds ) { + + return new Promise(resolve => { + + setTimeout( resolve, milliseconds ); + + } ); + + } + + /** + * Queues a new task of the given type. Task will not execute until initialization completes. + * + * @param {string} taskType The name of the registered task type. + * @param {object} config Configuration properties as serializable string. + * @param {Function} assetAvailableFunction Invoke this function if an asset become intermediately available + * @param {Transferable[]} [transferables] Any optional {@link ArrayBuffer} encapsulated in object. + * @return {Promise} + */ + async enqueueForExecution ( taskType, config, assetAvailableFunction, transferables ) { + + let localPromise = new Promise( ( resolveUser, rejectUser ) => { + + this.storedExecutions.push( new StoredExecution( taskType, config, assetAvailableFunction, resolveUser, rejectUser, transferables ) ); + this._depleteExecutions(); + + } ); + return localPromise; + + } + + _depleteExecutions () { + + let counter = 0; + while ( this.actualExecutionCount < this.maxParallelExecutions && counter < this.storedExecutions.length ) { + + // TODO: storedExecutions and results from worker seem to get mixed up + let storedExecution = this.storedExecutions[ counter ]; + let workerTypeDefinition = this.taskTypes.get( storedExecution.taskType ); + let taskWorker = workerTypeDefinition.getAvailableTask(); + if ( taskWorker ) { + + this.storedExecutions.splice( counter, 1 ); + this.actualExecutionCount ++; + let promiseWorker = new Promise( ( resolveWorker, rejectWorker ) => { + + taskWorker.onmessage = message => { + + // allow intermediate asset provision before flagging execComplete + if ( message.data.cmd === 'assetAvailable' ) { + + if ( storedExecution.assetAvailableFunction instanceof Function ) { + + storedExecution.assetAvailableFunction( message.data ); + + } + + } else { + + resolveWorker( message ); + + } + + }; + taskWorker.onerror = rejectWorker; + taskWorker.postMessage( { + cmd: "execute", + workerId: taskWorker.getId(), + config: storedExecution.config + }, storedExecution.transferables ); + + } ); + promiseWorker.then( ( message ) => { + + workerTypeDefinition.returnAvailableTask( taskWorker ); + storedExecution.resolve( message.data ); + this.actualExecutionCount --; + this._depleteExecutions(); + + } ).catch( ( e ) => { + + storedExecution.reject( "Execution error: " + e ); + + } ); + + } else { + + counter ++; + + } + + } + + } + + /** + * Destroys all workers and associated resources. + * @return {WorkerTaskManager} + */ + dispose () { + + this.teardown = true; + for ( let workerTypeDefinition of this.taskTypes.values() ) { + + workerTypeDefinition.dispose(); + + } + return this; + + } + +} + +/** + * Defines a worker type: functions, dependencies and runtime information once it was created. + */ +class WorkerTypeDefinition { + + /** + * Creates a new instance of {@link WorkerTypeDefinition}. + * + * @param {string} taskType The name of the registered task type. + * @param {Number} maximumCount Maximum worker count + * @param {boolean} fallback Set to true if execution should be performed in main + * @param {boolean} [verbose] Set if logging should be verbose + */ + constructor ( taskType, maximumCount, fallback, verbose ) { + this.taskType = taskType; + this.fallback = fallback; + this.verbose = verbose === true; + this.initialised = false; + this.functions = { + /** @type {Function} */ + init: null, + /** @type {Function} */ + execute: null, + /** @type {Function} */ + comRouting: null, + dependencies: { + /** @type {Object[]} */ + descriptions: [], + /** @type {string[]} */ + code: [] + }, + /** + * @type {URL} + */ + workerModuleUrl: null + }; + + + this.workers = { + /** @type {string[]} */ + code: [], + /** @type {TaskWorker[]|MockedTaskWorker[]} */ + instances: new Array ( maximumCount ), + /** @type {TaskWorker[]|MockedTaskWorker[]} */ + available: [] + }; + + this.status = { + initStarted: false, + initComplete: false + } + + } + + getTaskType () { + + return this.taskType; + + } + + /** + * Set the three functions. A default comRouting function is used if it is not passed here. + * Then it creates the code fr. + * + * @param {Function} initFunction The function to be called when the worker is initialised + * @param {Function} executeFunction The function to be called when the worker is executed + * @param {Function} [comRoutingFunction] The function that should handle communication, leave undefined for default behavior + */ + setFunctions ( initFunction, executeFunction, comRoutingFunction ) { + + this.functions.init = initFunction; + this.functions.execute = executeFunction; + this.functions.comRouting = comRoutingFunction; + if ( this.functions.comRouting === undefined || this.functions.comRouting === null ) { + + this.functions.comRouting = WorkerTaskManagerDefaultRouting.comRouting; + + } + this._addWorkerCode( 'init', this.functions.init.toString() ); + this._addWorkerCode( 'execute', this.functions.execute.toString() ); + this._addWorkerCode( 'comRouting', this.functions.comRouting.toString() ); + this.workers.code.push( 'self.addEventListener( "message", message => comRouting( self, message, null, init, execute ), false );' ); + + } + + /** + * + * @param {string} functionName Name of the function + * @param {string} functionString A function as string + * @private + */ + _addWorkerCode ( functionName, functionString ) { + if ( functionString.startsWith('function') ) { + + this.workers.code.push( 'const ' + functionName + ' = ' + functionString + ';\n\n' ); + + } else { + + this.workers.code.push( 'function ' + functionString + ';\n\n' ); + + } + + } + + /** + * Set the url of all dependent libraries (only used in non-module case). + * + * @param {Object[]} dependencyDescriptions URLs of code init and execute functions rely on. + */ + setDependencyDescriptions ( dependencyDescriptions ) { + + if ( dependencyDescriptions ) { + + dependencyDescriptions.forEach( description => { this.functions.dependencies.descriptions.push( description ) } ); + + } + + } + + /** + * Set the url of the module worker. + * + * @param {string} workerModuleUrl The URL is created from this string. + */ + setWorkerModule ( workerModuleUrl ) { + + this.functions.workerModuleUrl = new URL( workerModuleUrl, window.location.href ); + + } + + /** + * Is it a module worker? + * + * @return {boolean} True or false + */ + isWorkerModule () { + + return ( this.functions.workerModuleUrl !== null ); + + } + + /** + * Loads all dependencies and stores each as {@link ArrayBuffer} into the array. Returns if all loading is completed. + * + * @return {String[]} + */ + async loadDependencies () { + + let promises = []; + let fileLoader = new FileLoader(); + fileLoader.setResponseType( 'arraybuffer' ); + for ( let description of this.functions.dependencies.descriptions ) { + + if ( description.url ) { + + let url = new URL( description.url, window.location.href ); + promises.push( fileLoader.loadAsync( url.href, report => { if ( this.verbose ) console.log( report ); } ) ); + + } + if ( description.code ) { + + promises.push( new Promise( resolve => resolve( description.code ) ) ); + + } + + } + if ( this.verbose ) console.log( 'Task: ' + this.getTaskType() + ': Waiting for completion of loading of all dependencies.'); + this.functions.dependencies.code = await Promise.all( promises ); + + } + + /** + * Creates workers based on the configured function and dependency strings. + * + */ + async createWorkers () { + + let worker; + if ( !this.fallback ) { + + let workerBlob = new Blob( this.functions.dependencies.code.concat( this.workers.code ), { type: 'application/javascript' } ); + let objectURL = window.URL.createObjectURL( workerBlob ); + for ( let i = 0; i < this.workers.instances.length; i ++ ) { + + worker = new TaskWorker( i, objectURL ); + this.workers.instances[ i ] = worker; + + } + + } + else { + + for ( let i = 0; i < this.workers.instances.length; i ++ ) { + + worker = new MockedTaskWorker( i, this.functions.init, this.functions.execute ); + this.workers.instances[ i ] = worker; + + } + + } + + } + + /** + * Creates module workers. + * + */ + async createWorkerModules () { + + for ( let worker, i = 0; i < this.workers.instances.length; i++ ) { + + worker = new TaskWorker( i, this.functions.workerModuleUrl.href, { type: "module" } ); + this.workers.instances[ i ] = worker; + + } + + } + + /** + * Initialises all workers with common configuration data. + * + * @param {object} config + * @param {Transferable[]} transferables + */ + async initWorkers ( config, transferables ) { + + let promises = []; + for ( let taskWorker of this.workers.instances ) { + + let taskWorkerPromise = new Promise( ( resolveWorker, rejectWorker ) => { + + taskWorker.onmessage = message => { + + if ( this.verbose ) console.log( 'Init Complete: ' + message.data.id ); + resolveWorker( message ); + + } + taskWorker.onerror = rejectWorker; + + // ensure all transferables are copies to all workers on init! + let transferablesToWorker; + if ( transferables ) { + transferablesToWorker = []; + for ( let i = 0; i < transferables.length; i++ ) { + transferablesToWorker.push( transferables[ i ].slice( 0 ) ); + } + } + + taskWorker.postMessage( { + cmd: "init", + workerId: taskWorker.getId(), + config: config + }, transferablesToWorker ); + + } ); + promises.push( taskWorkerPromise ); + + } + + if ( this.verbose ) console.log( 'Task: ' + this.getTaskType() + ': Waiting for completion of initialization of all workers.'); + await Promise.all( promises ); + this.workers.available = this.workers.instances; + + } + + /** + * Returns the first {@link TaskWorker} or {@link MockedTaskWorker} from array of available workers. + * + * @return {TaskWorker|MockedTaskWorker|undefined} + */ + getAvailableTask () { + + let task = undefined; + if ( this.hasTask() ) { + + task = this.workers.available.shift(); + + } + return task; + + } + + /** + * Returns if a task is available or not. + * + * @return {boolean} + */ + hasTask () { + + return this.workers.available.length > 0; + + } + + /** + * + * @param {TaskWorker|MockedTaskWorker} taskWorker + */ + returnAvailableTask ( taskWorker ) { + + this.workers.available.push( taskWorker ); + + } + + /** + * Dispose all worker instances. + */ + dispose () { + + for ( let taskWorker of this.workers.instances ) { + + taskWorker.terminate(); + + } + + } + +} + +/** + * Contains all things required for later executions of Worker. + */ +class StoredExecution { + + /** + * Creates a new instance. + * + * @param {string} taskType + * @param {object} config + * @param {Function} assetAvailableFunction + * @param {Function} resolve + * @param {Function} reject + * @param {Transferable[]} [transferables] + */ + constructor( taskType, config, assetAvailableFunction, resolve, reject, transferables ) { + + this.taskType = taskType; + this.config = config; + this.assetAvailableFunction = assetAvailableFunction; + this.resolve = resolve; + this.reject = reject; + this.transferables = transferables; + + } + +} + +/** + * Extends the {@link Worker} with an id. + */ +class TaskWorker extends Worker { + + /** + * Creates a new instance. + * + * @param {number} id Numerical id of the task. + * @param {string} aURL + * @param {object} [options] + */ + constructor( id, aURL, options ) { + + super( aURL, options ); + this.id = id; + + } + + /** + * Returns the id. + * @return {number} + */ + getId() { + + return this.id; + + } + +} + +/** + * This is a mock of a worker to be used on Main. It defines necessary functions, so it can be handled like + * a regular {@link TaskWorker}. + */ +class MockedTaskWorker { + + /** + * Creates a new instance. + * + * @param {number} id + * @param {Function} initFunction + * @param {Function} executeFunction + */ + constructor( id, initFunction, executeFunction ) { + + this.id = id; + this.functions = { + init: initFunction, + execute: executeFunction + } + + } + + /** + * Returns the id. + * @return {number} + */ + getId() { + + return this.id; + + } + + /** + * Delegates the message to the registered functions + * @param {String} message + * @param {Transferable[]} [transfer] + */ + postMessage( message, transfer ) { + + let scope = this; + let self = { + postMessage: function ( m ) { + scope.onmessage( { data: m } ) + } + } + WorkerTaskManagerDefaultRouting.comRouting( self, { data: message }, null, scope.functions.init, scope.functions.execute ) + + } + + /** + * Mocking termination + */ + terminate () {} + +} + + +export { WorkerTaskManager }; diff --git a/examples/jsm/loaders/workerTaskManager/utils/MaterialStore.js b/examples/jsm/loaders/workerTaskManager/utils/MaterialStore.js new file mode 100644 index 00000000000000..55bdfb928a0e04 --- /dev/null +++ b/examples/jsm/loaders/workerTaskManager/utils/MaterialStore.js @@ -0,0 +1,106 @@ +/** + * Development repository: https://github.com/kaisalmen/three-wtm + */ + +import { + MeshStandardMaterial, + LineBasicMaterial, + PointsMaterial, + VertexColors +} from "../../../../../build/three.module.js"; +import { MaterialUtils } from "./MaterialUtils.js"; + +/** + * Helper class around an object storing materials by name. + * Optionally, create and store default materials. + */ +class MaterialStore { + + /** + * Creates a new {@link MaterialStore}. + * @param {boolean} createDefaultMaterials + */ + constructor( createDefaultMaterials ) { + + this.materials = {}; + if ( createDefaultMaterials ) { + + const defaultMaterial = new MeshStandardMaterial( { color: 0xDCF1FF } ); + defaultMaterial.name = 'defaultMaterial'; + + const defaultVertexColorMaterial = new MeshStandardMaterial( { color: 0xDCF1FF } ); + defaultVertexColorMaterial.name = 'defaultVertexColorMaterial'; + defaultVertexColorMaterial.vertexColors = VertexColors; + + const defaultLineMaterial = new LineBasicMaterial(); + defaultLineMaterial.name = 'defaultLineMaterial'; + + const defaultPointMaterial = new PointsMaterial( { size: 0.1 } ); + defaultPointMaterial.name = 'defaultPointMaterial'; + + this.materials[ defaultMaterial.name ] = defaultMaterial; + this.materials[ defaultVertexColorMaterial.name ] = defaultVertexColorMaterial; + this.materials[ defaultLineMaterial.name ] = defaultLineMaterial; + this.materials[ defaultPointMaterial.name ] = defaultPointMaterial; + + } + + } + + /** + * Set materials loaded by any supplier of an Array of {@link Material}. + * + * @param {object} newMaterials Object with named {@link Material} + * @param {boolean} forceOverrideExisting boolean Override existing material + */ + addMaterials ( newMaterials, forceOverrideExisting ) { + + if ( newMaterials === undefined || newMaterials === null ) newMaterials = {}; + if ( Object.keys( newMaterials ).length > 0 ) { + + let material; + for ( const materialName in newMaterials ) { + + material = newMaterials[ materialName ]; + MaterialUtils.addMaterial( this.materials, material, materialName, forceOverrideExisting === true ); + + } + + } + + } + + /** + * Returns the mapping object of material name and corresponding material. + * + * @returns {Object} Map of {@link Material} + */ + getMaterials () { + + return this.materials; + + } + + /** + * + * @param {String} materialName + * @returns {Material} + */ + getMaterial ( materialName ) { + + return this.materials[ materialName ]; + + } + + /** + * Removes all materials + */ + clearMaterials () { + + this.materials = {}; + + } + +} + +export { MaterialStore } diff --git a/examples/jsm/loaders/workerTaskManager/utils/MaterialUtils.js b/examples/jsm/loaders/workerTaskManager/utils/MaterialUtils.js new file mode 100644 index 00000000000000..a80250e29b3f15 --- /dev/null +++ b/examples/jsm/loaders/workerTaskManager/utils/MaterialUtils.js @@ -0,0 +1,109 @@ +/** + * Development repository: https://github.com/kaisalmen/three-wtm + */ + +/** + * Static functions useful in the context of handling materials. + */ +class MaterialUtils { + + /** + * Adds the provided material to the provided materials object if the material does not exists. + * Use force override existing material. + * + * @param {object.} materialsObject + * @param {Material} material + * @param {string} materialName + * @param {boolean} force + * @param {boolean} [log] Log messages to the console + */ + static addMaterial( materialsObject, material, materialName, force, log ) { + let existingMaterial; + // ensure materialName is set + material.name = materialName; + if ( ! force ) { + + existingMaterial = materialsObject[ materialName ]; + if ( existingMaterial ) { + + if ( existingMaterial.uuid !== existingMaterial.uuid ) { + + if ( log ) console.log( 'Same material name "' + existingMaterial.name + '" different uuid [' + existingMaterial.uuid + '|' + material.uuid + ']' ); + + } + + } else { + + materialsObject[ materialName ] = material; + if ( log ) console.info( 'Material with name "' + materialName + '" was added.' ); + + } + + } else { + + materialsObject[ materialName ] = material; + if ( log ) console.info( 'Material with name "' + materialName + '" was forcefully overridden.' ); + + } + } + + /** + * Transforms the named materials object to an object with named jsonified materials. + * + * @param {object.} + * @returns {Object} Map of Materials in JSON representation + */ + static getMaterialsJSON ( materialsObject ) { + + const materialsJSON = {}; + let material; + for ( const materialName in materialsObject ) { + + material = materialsObject[ materialName ]; + if ( typeof material.toJSON === 'function' ) { + + materialsJSON[ materialName ] = material.toJSON(); + + } + + } + return materialsJSON; + + } + + /** + * Clones a material according the provided instructions. + * + * @param {object.} materials + * @param {object} materialCloneInstruction + * @param {boolean} [log] + */ + static cloneMaterial ( materials, materialCloneInstruction, log ) { + + let material; + if ( materialCloneInstruction ) { + + let materialNameOrg = materialCloneInstruction.materialNameOrg; + materialNameOrg = ( materialNameOrg !== undefined && materialNameOrg !== null ) ? materialNameOrg : ''; + const materialOrg = materials[ materialNameOrg ]; + if ( materialOrg ) { + + material = materialOrg.clone(); + Object.assign( material, materialCloneInstruction.materialProperties ); + MaterialUtils.addMaterial( materials, material, materialCloneInstruction.materialProperties.name, true ); + + } + else { + + if ( log ) console.info( 'Requested material "' + materialNameOrg + '" is not available!' ); + + } + + } + return material; + + } + +} + +export { MaterialUtils } diff --git a/examples/jsm/loaders/workerTaskManager/utils/TransportUtils.js b/examples/jsm/loaders/workerTaskManager/utils/TransportUtils.js new file mode 100644 index 00000000000000..5f0e8e4e21174c --- /dev/null +++ b/examples/jsm/loaders/workerTaskManager/utils/TransportUtils.js @@ -0,0 +1,871 @@ +/** + * Development repository: https://github.com/kaisalmen/three-wtm + */ + +import { + BufferGeometry, + BufferAttribute, + Box3, + Sphere, + Texture, + MaterialLoader +} from "../../../../../build/three.module.js"; +import { MaterialUtils } from './MaterialUtils.js'; + +class DeUglify { + + static buildThreeConst () { + return 'const EventDispatcher = THREE.EventDispatcher;\n' + + 'const BufferGeometry = THREE.BufferGeometry;\n' + + 'const BufferAttribute = THREE.BufferAttribute;\n' + + 'const Box3 = THREE.Box3;\n' + + 'const Sphere = THREE.Sphere;\n' + + 'const Texture = THREE.Texture;\n' + + 'const MaterialLoader = THREE.MaterialLoader;\n'; + } + + static buildUglifiedThreeMapping () { + function _BufferGeometry() { return BufferGeometry; } + function _BufferAttribute () { return BufferAttribute; } + function _Box3 () { return Box3; } + function _Sphere () { return Sphere; } + function _Texture () { return Texture; } + function _MaterialLoader () { return MaterialLoader; } + + return DeUglify.buildUglifiedNameAssignment( _BufferGeometry, 'BufferGeometry', /_BufferGeometry/, false ) + + DeUglify.buildUglifiedNameAssignment( _BufferAttribute, 'BufferAttribute', /_BufferAttribute/, false ) + + DeUglify.buildUglifiedNameAssignment( _Box3, 'Box3', /_Box3/, false ) + + DeUglify.buildUglifiedNameAssignment( _Sphere, 'Sphere', /_Sphere/, false ) + + DeUglify.buildUglifiedNameAssignment( _Texture, 'Texture', /_Texture/, false ) + + DeUglify.buildUglifiedNameAssignment( _MaterialLoader, 'MaterialLoader', /_MaterialLoader/, false ); + } + + static buildUglifiedThreeWtmMapping () { + function _DataTransport () { return DataTransport; } + function _GeometryTransport () { return GeometryTransport; } + function _MeshTransport () { return MeshTransport; } + function _MaterialsTransport () { return MaterialsTransport; } + function _MaterialUtils () { return MaterialUtils; } + + return DeUglify.buildUglifiedNameAssignment( _DataTransport, 'DataTransport', /_DataTransport/, true ) + + DeUglify.buildUglifiedNameAssignment( _GeometryTransport, 'GeometryTransport', /_GeometryTransport/, true ) + + DeUglify.buildUglifiedNameAssignment( _MeshTransport, 'MeshTransport', /_MeshTransport/, true ) + + DeUglify.buildUglifiedNameAssignment( _MaterialsTransport, 'MaterialsTransport', /_MaterialsTransport/, true ) + + DeUglify.buildUglifiedNameAssignment( _MaterialUtils, 'MaterialUtils', /_MaterialUtils/, true ); + } + + static buildUglifiedNameAssignment(func, name, methodPattern, invert) { + let funcStr = func.toString(); + // remove the method name and any line breaks (rollup lib creation, non-uglify case + funcStr = funcStr.replace(methodPattern, "").replace(/[\r\n]+/gm, ""); + // remove return and any semi-colons + funcStr = funcStr.replace(/.*return/, "").replace(/\}/, "").replace(/;/gm, ""); + const retrieveNamed = funcStr.trim() + // return non-empty string in uglified case (name!=retrieveNamed); e.g. "const BufferGeometry = e"; + // return empty string in case of non-uglified lib/src + let output = ""; + if (retrieveNamed !== name) { + const left = invert ? name : retrieveNamed; + const right = invert ? retrieveNamed : name; + output = "const " + left + " = " + right + ";\n"; + } + return output; + } +} + + + +/** + * Define a base structure that is used to ship data in between main and workers. + */ +class DataTransport { + + /** + * Creates a new {@link DataTransport}. + * @param {string} [cmd] + * @param {string} [id] + */ + constructor( cmd, id ) { + + this.main = { + cmd: ( cmd !== undefined ) ? cmd : 'unknown', + id: ( id !== undefined ) ? id : 0, + type: 'DataTransport', + /** @type {number} */ + progress: 0, + buffers: {}, + params: { + } + }; + /** @type {ArrayBuffer[]} */ + this.transferables = []; + + } + + /** + * Populate this object with previously serialized data. + * @param {object} transportObject + * @return {DataTransport} + */ + loadData( transportObject ) { + + this.main.cmd = transportObject.cmd; + this.main.id = transportObject.id; + this.main.type = 'DataTransport'; + this.setProgress( transportObject.progress ); + this.setParams( transportObject.params ); + + if ( transportObject.buffers ) { + + Object.entries( transportObject.buffers ).forEach( ( [name, buffer] ) => { + + this.main.buffers[ name ] = buffer; + + } ); + + } + return this; + + } + + /** + * Returns the value of the command. + * @return {string} + */ + getCmd () { + + return this.main.cmd; + + } + + /** + * Returns the id. + * @return {string} + */ + getId() { + + return this.main.id; + + } + + /** + * Set a parameter object which is a map with string keys and strings or objects as values. + * @param {object.} params + * @return {DataTransport} + */ + setParams( params ) { + + if ( params !== null && params !== undefined ) { + this.main.params = params; + } + return this; + + } + + /** + * Return the parameter object + * @return {object.} + */ + getParams() { + + return this.main.params; + + } + + /** + * Set the current progress (e.g. percentage of progress) + * @param {number} numericalValue + * @return {DataTransport} + */ + setProgress( numericalValue ) { + + this.main.progress = numericalValue; + return this; + + } + + /** + * Add a named {@link ArrayBuffer} + * @param {string} name + * @param {ArrayBuffer} buffer + * @return {DataTransport} + */ + addBuffer ( name, buffer ) { + + this.main.buffers[ name ] = buffer; + return this; + + } + + /** + * Retrieve an {@link ArrayBuffer} by name + * @param {string} name + * @return {ArrayBuffer} + */ + getBuffer( name ) { + + return this.main.buffers[ name ]; + + } + + /** + * Package all data buffers into the transferable array. Clone if data needs to stay in current context. + * @param {boolean} cloneBuffers + * @return {DataTransport} + */ + package( cloneBuffers ) { + + for ( let buffer of Object.values( this.main.buffers ) ) { + + if ( buffer !== null && buffer !== undefined ) { + + const potentialClone = cloneBuffers ? buffer.slice( 0 ) : buffer; + this.transferables.push( potentialClone ); + + } + + } + return this; + + } + + /** + * Return main data object + * @return {object} + */ + getMain() { + + return this.main; + + } + + /** + * Return all transferable in one array. + * @return {ArrayBuffer[]} + */ + getTransferables() { + + return this.transferables; + + } + + /** + * Posts a message by invoking the method on the provided object. + * @param {object} postMessageImpl + * @return {DataTransport} + */ + postMessage( postMessageImpl ) { + + postMessageImpl.postMessage( this.main, this.transferables ); + return this; + + } +} + +/** + * Define a structure that is used to ship materials data between main and workers. + */ +class MaterialsTransport extends DataTransport { + + /** + * Creates a new {@link MeshMessageStructure}. + * @param {string} [cmd] + * @param {string} [id] + */ + constructor( cmd, id ) { + + super( cmd, id ); + this.main.type = 'MaterialsTransport'; + /** {object.} */ + this.main.materials = {}; + /** {object.} */ + this.main.multiMaterialNames = {}; + this.main.cloneInstructions = []; + + } + + /** + * See {@link DataTransport#loadData} + * @param {object} transportObject + * @return {MaterialsTransport} + */ + loadData( transportObject ) { + + super.loadData( transportObject ); + this.main.type = 'MaterialsTransport'; + Object.assign( this.main, transportObject ); + + const materialLoader = new MaterialLoader(); + Object.entries( this.main.materials ).forEach( ( [ name, materialObject ] ) => { + + this.main.materials[ name ] = materialLoader.parse( materialObject ) + + } ); + return this; + + } + + _cleanMaterial ( material ) { + + Object.entries( material ).forEach( ( [key, value] ) => { + + if ( value instanceof Texture || value === null ) { + material[ key ] = undefined; + + } + + } ); + return material; + + } + + /** + * See {@link DataTransport#loadData} + * @param {string} name + * @param {ArrayBuffer} buffer + * @return {MaterialsTransport} + */ + addBuffer( name, buffer ) { + + super.addBuffer( name, buffer ); + return this; + + } + + /** + * See {@link DataTransport#setParams} + * @param {object.} params + * @return {MaterialsTransport} + */ + setParams( params ) { + + super.setParams( params ); + return this; + + } + + /** + * Set an object containing named materials. + * @param {object.} materials + */ + setMaterials ( materials ) { + + if ( materials !== undefined && materials !== null && Object.keys( materials ).length > 0 ) this.main.materials = materials; + return this; + + } + + /** + * Returns all maerials + * @return {object.} + */ + getMaterials () { + + return this.main.materials; + + } + + /** + * Removes all textures and null values from all materials + */ + cleanMaterials () { + + let clonedMaterials = {}; + let clonedMaterial; + for ( let material of Object.values( this.main.materials ) ) { + + if ( typeof material.clone === 'function' ) { + + clonedMaterial = material.clone(); + clonedMaterials[ clonedMaterial.name ] = this._cleanMaterial( clonedMaterial ); + + } + + } + this.setMaterials( clonedMaterials ); + return this; + + } + + /** + * See {@link DataTransport#package} + * @param {boolean} cloneBuffers + * @return {DataTransport} + */ + package ( cloneBuffers) { + + super.package( cloneBuffers ); + this.main.materials = MaterialUtils.getMaterialsJSON( this.main.materials ); + return this; + + } + + /** + * Tell whether a multi-material was defined + * @return {boolean} + */ + hasMultiMaterial () { + + return ( Object.keys( this.main.multiMaterialNames ).length > 0 ); + + } + + /** + * Returns a single material if it is defined or null. + * @return {Material|null} + */ + getSingleMaterial () { + + if ( Object.keys( this.main.materials ).length > 0 ) { + + return Object.entries( this.main.materials )[ 0 ][ 1 ]; + + } else { + + return null; + + } + + } + + /** + * Adds contained material or multi-material the provided materials object or it clones and adds new materials according clone instructions. + * + * @param {Object.} materials + * @param {boolean} log + * + * @return {Material|Material[]} + */ + processMaterialTransport ( materials, log ) { + + for ( let i = 0; i < this.main.cloneInstructions.length; i ++ ) { + + MaterialUtils.cloneMaterial( materials, this.main.cloneInstructions[ i ], log ); + + } + + let outputMaterial; + if ( this.hasMultiMaterial() ) { + + // multi-material + outputMaterial = []; + Object.entries( this.main.multiMaterialNames ).forEach( ( [ materialIndex, materialName ] ) => { + + outputMaterial[ materialIndex ] = materials[ materialName ]; + + } ); + + } + else { + + const singleMaterial = this.getSingleMaterial(); + if (singleMaterial !== null ) { + outputMaterial = materials[ singleMaterial.name ]; + if ( !outputMaterial ) outputMaterial = singleMaterial; + } + + } + return outputMaterial; + + } +} + +/** + * Define a structure that is used to send geometry data between main and workers. + */ +class GeometryTransport extends DataTransport { + + /** + * Creates a new {@link GeometryTransport}. + * @param {string} [cmd] + * @param {string} [id] + */ + constructor( cmd, id ) { + + super( cmd, id ); + this.main.type = 'GeometryTransport'; + // 0: mesh, 1: line, 2: point + /** @type {number} */ + this.main.geometryType = 0; + /** @type {object} */ + this.main.geometry = {}; + /** @type {BufferGeometry} */ + this.main.bufferGeometry = null; + + } + + /** + * See {@link DataTransport#loadData} + * @param {object} transportObject + * @return {GeometryTransport} + */ + loadData( transportObject ) { + + super.loadData( transportObject ); + this.main.type = 'GeometryTransport'; + return this.setGeometry( transportObject.geometry, transportObject.geometryType ); + + } + + /** + * Returns the geometry type [0=Mesh|1=LineSegments|2=Points] + * @return {number} + */ + getGeometryType() { + + return this.main.geometryType; + + } + + /** + * See {@link DataTransport#setParams} + * @param {object} params + * @return {GeometryTransport} + */ + setParams( params ) { + + super.setParams( params ); + return this; + + } + + /** + * Set the {@link BufferGeometry} and geometry type that can be used when a mesh is created. + * + * @param {BufferGeometry} geometry + * @param {number} geometryType [0=Mesh|1=LineSegments|2=Points] + * @return {GeometryTransport} + */ + setGeometry( geometry, geometryType ) { + this.main.geometry = geometry; + this.main.geometryType = geometryType; + if ( geometry instanceof BufferGeometry ) this.main.bufferGeometry = geometry; + + return this; + } + + /** + * Package {@link BufferGeometry} and prepare it for transport. + * + * @param {boolean} cloneBuffers Clone buffers if their content shall stay in the current context. + * @return {GeometryTransport} + */ + package( cloneBuffers ) { + + super.package( cloneBuffers ); + const vertexBA = this.main.geometry.getAttribute( 'position' ); + const normalBA = this.main.geometry.getAttribute( 'normal' ); + const uvBA = this.main.geometry.getAttribute( 'uv' ); + const colorBA = this.main.geometry.getAttribute( 'color' ); + const skinIndexBA = this.main.geometry.getAttribute( 'skinIndex' ); + const skinWeightBA = this.main.geometry.getAttribute( 'skinWeight' ); + const indexBA = this.main.geometry.getIndex(); + + this._addBufferAttributeToTransferable( vertexBA, cloneBuffers ); + this._addBufferAttributeToTransferable( normalBA, cloneBuffers ); + this._addBufferAttributeToTransferable( uvBA, cloneBuffers ); + this._addBufferAttributeToTransferable( colorBA, cloneBuffers ); + this._addBufferAttributeToTransferable( skinIndexBA, cloneBuffers ); + this._addBufferAttributeToTransferable( skinWeightBA, cloneBuffers ); + this._addBufferAttributeToTransferable( indexBA, cloneBuffers ); + return this; + } + + /** + * Reconstructs the {@link BufferGeometry} from the raw buffers. + * @param {boolean} cloneBuffers + * @return {GeometryTransport} + */ + reconstruct( cloneBuffers ) { + + if ( this.main.bufferGeometry instanceof BufferGeometry ) return this; + this.main.bufferGeometry = new BufferGeometry(); + + const transferredGeometry = this.main.geometry; + this._assignAttribute( transferredGeometry.attributes.position, 'position', cloneBuffers ); + this._assignAttribute( transferredGeometry.attributes.normal, 'normal', cloneBuffers ); + this._assignAttribute( transferredGeometry.attributes.uv, 'uv', cloneBuffers ); + this._assignAttribute( transferredGeometry.attributes.color, 'color', cloneBuffers ); + this._assignAttribute( transferredGeometry.attributes.skinIndex, 'skinIndex', cloneBuffers ); + this._assignAttribute( transferredGeometry.attributes.skinWeight, 'skinWeight', cloneBuffers ); + + const index = transferredGeometry.index; + if ( index !== null && index !== undefined ) { + + const indexBuffer = cloneBuffers ? index.array.slice( 0 ) : index.array; + this.main.bufferGeometry.setIndex( new BufferAttribute( indexBuffer, index.itemSize, index.normalized ) ); + + } + const boundingBox = transferredGeometry.boundingBox; + if ( boundingBox !== null ) this.main.bufferGeometry.boundingBox = Object.assign( new Box3(), boundingBox ); + + const boundingSphere = transferredGeometry.boundingSphere; + if ( boundingSphere !== null ) this.main.bufferGeometry.boundingSphere = Object.assign( new Sphere(), boundingSphere ); + + this.main.bufferGeometry.uuid = transferredGeometry.uuid; + this.main.bufferGeometry.name = transferredGeometry.name; + this.main.bufferGeometry.type = transferredGeometry.type; + this.main.bufferGeometry.groups = transferredGeometry.groups; + this.main.bufferGeometry.drawRange = transferredGeometry.drawRange; + this.main.bufferGeometry.userData = transferredGeometry.userData; + return this; + + } + + /** + * Returns the {@link BufferGeometry}. + * @return {BufferGeometry|null} + */ + getBufferGeometry() { + + return this.main.bufferGeometry + + } + + _addBufferAttributeToTransferable( input, cloneBuffer ) { + + if ( input !== null && input !== undefined ) { + + const arrayBuffer = cloneBuffer ? input.array.slice( 0 ) : input.array; + this.transferables.push( arrayBuffer.buffer ); + + } + return this; + + } + + _assignAttribute( attr, attrName, cloneBuffer ) { + + if ( attr ) { + + const arrayBuffer = cloneBuffer ? attr.array.slice( 0 ) : attr.array; + this.main.bufferGeometry.setAttribute( attrName, new BufferAttribute( arrayBuffer, attr.itemSize, attr.normalized ) ); + + } + return this; + + } + +} + + +/** + * Define a structure that is used to send mesh data between main and workers. + */ +class MeshTransport extends GeometryTransport { + + /** + * Creates a new {@link MeshTransport}. + * @param {string} [cmd] + * @param {string} [id] + */ + constructor( cmd, id ) { + + super( cmd, id ); + this.main.type = 'MeshTransport'; + // needs to be added as we cannot inherit from both materials and geometry + this.main.materialsTransport = new MaterialsTransport(); + + } + + /** + * See {@link GeometryTransport#loadData} + * @param {object} transportObject + * @return {MeshTransport} + */ + loadData( transportObject ) { + + super.loadData( transportObject ); + this.main.type = 'MeshTransport'; + this.main.meshName = transportObject.meshName; + this.main.materialsTransport = new MaterialsTransport().loadData( transportObject.materialsTransport.main ); + return this; + + } + + /** + * See {@link GeometryTransport#loadData} + * @param {object} params + * @return {MeshTransport} + */ + setParams( params ) { + + super.setParams( params ); + return this; + + } + + /** + * The {@link MaterialsTransport} wraps all info regarding the material for the mesh. + * @param {MaterialsTransport} materialsTransport + * @return {MeshTransport} + */ + setMaterialsTransport( materialsTransport ) { + + if ( materialsTransport instanceof MaterialsTransport ) this.main.materialsTransport = materialsTransport; + return this; + + } + + /** + * @return {MaterialsTransport} + */ + getMaterialsTransport() { + + return this.main.materialsTransport; + + } + + /** + * Sets the mesh and the geometry type [0=Mesh|1=LineSegments|2=Points] + * @param {Mesh} mesh + * @param {number} geometryType + * @return {MeshTransport} + */ + setMesh( mesh, geometryType ) { + + this.main.meshName = mesh.name; + super.setGeometry( mesh.geometry, geometryType ); + return this; + + } + + /** + * See {@link GeometryTransport#package} + * @param {boolean} cloneBuffers + * @return {MeshTransport} + */ + package( cloneBuffers ) { + + super.package( cloneBuffers ); + if ( this.main.materialsTransport !== null ) this.main.materialsTransport.package( cloneBuffers ); + return this; + } + + /** + * See {@link GeometryTransport#reconstruct} + * @param {boolean} cloneBuffers + * @return {MeshTransport} + */ + reconstruct( cloneBuffers ) { + + super.reconstruct( cloneBuffers ); + // so far nothing needs to be done for material + return this; + + } + +} + + +/** + * Utility for serializing object in memory + */ +class ObjectUtils { + + /** + * Serializes a class with an optional prototype + * @param targetClass + * @param targetPrototype + * @param fullObjectName + * @param processPrototype + * @return {string} + */ + static serializePrototype( targetClass, targetPrototype, fullObjectName, processPrototype ) { + + let prototypeFunctions = []; + let objectString = ''; + let target; + if ( processPrototype ) { + objectString = targetClass.toString() + "\n\n" + target = targetPrototype; + } else { + target = targetClass; + } + for ( let name in target ) { + + let objectPart = target[ name ]; + let code = objectPart.toString(); + + if ( typeof objectPart === 'function' ) { + + prototypeFunctions.push( '\t' + name + ': ' + code + ',\n\n' ); + + } + + } + + let protoString = processPrototype ? '.prototype' : ''; + objectString += fullObjectName + protoString + ' = {\n\n'; + for ( let i = 0; i < prototypeFunctions.length; i ++ ) { + + objectString += prototypeFunctions[ i ]; + + } + objectString += '\n}\n;'; + return objectString; + + } + + /** + * Serializes a class. + * @param {object} targetClass An ES6+ class + * @return {string} + */ + static serializeClass( targetClass ) { + + return targetClass.toString() + "\n\n"; + + } + +} + + +/** + * Object manipulation utilities. + */ +class ObjectManipulator { + + /** + * Applies values from parameter object via set functions or via direct assignment. + * + * @param {Object} objToAlter The objToAlter instance + * @param {Object} params The parameter object + * @param {boolean} forceCreation Force the creation of a property + */ + static applyProperties ( objToAlter, params, forceCreation ) { + + // fast-fail + if ( objToAlter === undefined || objToAlter === null || params === undefined || params === null ) return; + + let property, funcName, values; + for ( property in params ) { + + funcName = 'set' + property.substring( 0, 1 ).toLocaleUpperCase() + property.substring( 1 ); + values = params[ property ]; + + if ( typeof objToAlter[ funcName ] === 'function' ) { + + objToAlter[ funcName ]( values ); + + } else if ( objToAlter.hasOwnProperty( property ) || forceCreation ) { + + objToAlter[ property ] = values; + + } + + } + + } + +} + +export { + DataTransport, + GeometryTransport, + MeshTransport, + MaterialsTransport, + ObjectUtils, + ObjectManipulator, + DeUglify +} diff --git a/examples/jsm/loaders/workerTaskManager/worker/defaultRouting.js b/examples/jsm/loaders/workerTaskManager/worker/defaultRouting.js new file mode 100644 index 00000000000000..0c21fc9c9d0f1a --- /dev/null +++ b/examples/jsm/loaders/workerTaskManager/worker/defaultRouting.js @@ -0,0 +1,40 @@ +/** + * Development repository: https://github.com/kaisalmen/three-wtm + */ + +class WorkerTaskManagerDefaultRouting { + + static comRouting ( context, message, object, initFunction, executeFunction ) { + + let payload = message.data; + if ( payload.cmd === 'init' ) { + + if ( object !== undefined && object !== null ) { + + object[ initFunction ]( context, payload.workerId, payload.config ); + + } else { + + initFunction( context, payload.workerId, payload.config ); + + } + + } else if ( payload.cmd === 'execute' ) { + + if ( object !== undefined && object !== null ) { + + object[ executeFunction ]( context, payload.workerId, payload.config ); + + } else { + + executeFunction( context, payload.workerId, payload.config ); + + } + + } + + } + +} + +export { WorkerTaskManagerDefaultRouting } diff --git a/examples/jsm/loaders/workerTaskManager/worker/tmOBJLoader.js b/examples/jsm/loaders/workerTaskManager/worker/tmOBJLoader.js new file mode 100644 index 00000000000000..ac772d35db7fad --- /dev/null +++ b/examples/jsm/loaders/workerTaskManager/worker/tmOBJLoader.js @@ -0,0 +1,91 @@ +import { OBJLoader } from '../../OBJLoader.js'; +import { + DataTransport, + MaterialsTransport, + GeometryTransport, + MeshTransport, + ObjectUtils, + DeUglify +} from '../utils/TransportUtils.js'; +import { MaterialUtils } from '../utils/MaterialUtils.js';; +import { WorkerTaskManagerDefaultRouting } from "./defaultRouting.js"; + +class OBJLoaderWorker { + + static buildStandardWorkerDependencies ( threeJsLocation, objLoaderLocation ) { + return [ + { url: threeJsLocation }, + { code: '\n\n' }, + { code: DeUglify.buildThreeConst() }, + { code: '\n\n' }, + { code: DeUglify.buildUglifiedThreeMapping() }, + { code: '\n\n' }, + { url: objLoaderLocation }, + { code: '\n\nconst OBJLoader = THREE.OBJLoader;\n\n' }, + { code: '\n\n' }, + { code: ObjectUtils.serializeClass( DataTransport ) }, + { code: ObjectUtils.serializeClass( MaterialsTransport ) }, + { code: ObjectUtils.serializeClass( MaterialUtils ) }, + { code: ObjectUtils.serializeClass( GeometryTransport ) }, + { code: ObjectUtils.serializeClass( MeshTransport ) }, + { code: DeUglify.buildUglifiedThreeWtmMapping() }, + { code: '\n\n' } + ] + } + + static init ( context, id, config ) { + + const materialsTransport = new MaterialsTransport().loadData( config ); + context.objLoader = { + loader: null, + buffer: null, + materials: materialsTransport.getMaterials() + } + + const buffer = materialsTransport.getBuffer( 'modelData' ) + if ( buffer !== undefined && buffer !== null ) context.objLoader.buffer = buffer; + + context.postMessage( { + cmd: "init", + id: id + } ); + } + + static execute ( context, id, config ) { + + context.objLoader.loader = new OBJLoader(); + const dataTransport = new DataTransport().loadData( config ); + + context.objLoader.loader.objectId = dataTransport.getId(); + let materials = context.objLoader.materials; + materials[ 'create' ] = function fakeMat( name ) { return materials[ name ]; }; + context.objLoader.loader.setMaterials( context.objLoader.materials ); + + const enc = new TextDecoder("utf-8"); + let meshes = context.objLoader.loader.parse( enc.decode( context.objLoader.buffer ) ); + for ( let mesh, i = 0; i < meshes.children.length; i ++ ) { + + mesh = meshes.children[ i ]; + mesh.name = mesh.name + dataTransport.getId(); + + const materialsTransport = new MaterialsTransport(); + const material = mesh.material; + MaterialUtils.addMaterial( materialsTransport.main.materials, material, material.name, false, false ); + new MeshTransport( 'assetAvailable', dataTransport.getId() ) + .setMesh( mesh, 0 ) + .setMaterialsTransport( materialsTransport ) + .package( false ) + .postMessage( context ); + + } + + // signal complete + new DataTransport( 'execComplete' ).postMessage( context ); + + } + +} + +self.addEventListener( 'message', message => WorkerTaskManagerDefaultRouting.comRouting( self, message, OBJLoaderWorker, 'init', 'execute' ), false ); + +export { OBJLoaderWorker }; diff --git a/examples/webgl_loader_workertaskmanager.html b/examples/webgl_loader_workertaskmanager.html new file mode 100644 index 00000000000000..f5279f0bad8f57 --- /dev/null +++ b/examples/webgl_loader_workertaskmanager.html @@ -0,0 +1,356 @@ + + + + three.js webgl - WorkerTaskManager prototype + + + + + + + +
+ three.js - WorkerTaskManager prototype +
+
+
+ + + +