Three.js: Generic WorkerLoader

Created on 22 Mar 2018  ยท  36Comments  ยท  Source: mrdoob/three.js

OBJLoader2 introduced LoaderSupport in R88. This separated the more generic Worker related functions from the OBJ format loader. My idea was that other loaders could use it to allow worker based loading where it makes sense.

I realized during discussion we had (https://github.com/mrdoob/three.js/pull/13263 and #13648) that this is not the right approach. LoaderSupport was over-freighted with functions that have been partially removed in R91, but any loader needs to depend on it.
It seems to be the better idea for to change the direction of the dependency and create a generic WorkerLoader that uses existing loaders and impose as little as possible on how Parsing must be organized.

LoaderSupport will disappear. I already have a WIP version of OBJLoader(2) that no longer depends on LoaderSupport (#13663). It should be replaced by a generic WorkerLoader integrating the useful Worker tools from LoaderSupport.

My overall idea for WorkerLoader looks as follows:
Any Loaders feature these methods where serializeParser is optional:

THREE.AnyLoader.prototype = function () {

    parse: function ( content ) {
    },

    load: function ( url, onLoad, onProgress, onError) {
    },

    serializeParser: function( serializeClassFunction, serializeObjectFunction ) {
    }
}

A Parser should ideally be:

  • independent of any other code (three or other libraries) which makes the Worker code a lot smaller and faster to load and use
  • Should isolate Mesh creation in a function allowing to pass from the Worker back to the Main in Transferables whereever possible

The loader is given to WorkerLoader which either uses the serializeParser from the provided loader, it allows to use an optional Parser which WorkerLoader tries to serialize itself or it loads the given files to the worker, where optionalParser could be used as code wrapper (handling Transferables for example).

THREE.WorkerLoader = function ( loader, optionalParser, extraLibs2Load ) {

    this.workerSupport = new THREE.WorkerLoader.WorkerSupport();
    this.loader = loader;

    var code;
    if ( optionalParser ) {

        code = this.workerSupport.serializeClass( optionalParser );

    } else {

        code = this.loader.serializeParser( this.workerSupport.serializeClass, this.workerSupport.serializeObject ),

    }
    this.workerSupport.validate( code, 'Parser', extraLibs2Load );
};

THREE.WorkerLoader.prototype = function () {

    constructor: THREE.WorkerLoader,

    parse: function( arrayBuffer, onLoad, onMesh ) {
        ...
        this._runWorker( { ... } );
    },

    load: function ( url, onLoad, onProgress, onError, onMesh ) {
        ...
        this._runWorker( { ... } );
    },

    execute: function ( automationInstructions ) {
        ...
        this._runWorker( { ... } );
    },

    _runWorker: function ( buffersAndInstructions ) {
        this.workerSupport.run( buffersAndInstructions );
    }
}

Examples:
Option 1: Parser code is completely supplied by AnyLoader:

var workerLoader = new THREE.WorkerLoader( new AnyLoader() );
workerLoader.parse( arrayBuffer, ... );

Option 2: Parser code and additional libs are suppiled in addition to AnyLoader (provides possibility to supply any code inside Worker):

var workerLoader = new THREE.WorkerLoader( new AnyLoader(), AnyLoaderParserWrapper, [ 'examples/js/loader/AnyLoader.js', 'build/three.min.js' ] );
workerLoader.parse( arrayBuffer, ... );

Not yet fully working, but evolving WIP code is here will become visible in three.js branch soon (WWOBJLoader issue36):
https://github.com/kaisalmen/WWOBJLoader/blob/Issue36/src/loaders/WorkerLoader.js

Let me know what you think. Especially, if it fits your use cases. Thanks.

Three.js version
  • [x] Dev
  • [x] r91
Loaders

Most helpful comment

FYI, I am on holiday with my family. There will be progress again after July 11th.

All 36 comments

I would suggest thinking about a an overarching algorithm and coding that in as a contract of AnyLoader. For example, there are several stages to "loading":

  1. load
  2. decode (convert assets into usable form)
  3. assemble (construct final object graph, meshes, etc.)

Decoding has several parts which are quite well known:

  • Materials
  • Geometries
  • Skins
  • Skeletons
  • Animations
  • Object Hierarchies

the above can be abstracted further into decodeAssets(type, data) where type can be used to add physics, sound and other shenanigans.

We know that assemble stage would take a set of assets of different types and stitch them together, for example - hierarchies would drive what is returned in the end, meshes would bind geometries, material and animations.

The advantage here is - you can make loader a lot more homogeneous, as overarching flow is already fixed. This allows tracking errors more easily and you can essentially take care of any data transfer to/from worker, as the transfer points are known in advance in such an abstraction.

Just some random points. Hope i managed to get my thoughts across :)

@Usnul I like your idea. For example The first step "load" is likely not loader specific at all although every loader does its own thing.
Full automation of the execution based on a set of configuration instructions is something I want to achieve for WorkerLoader as well and this has (after thinking about it again) a strong relation to your suggestion.
If you disregard the worker aspect for a second, a loader should load or retrieve one or more files/contents (ArrayBuffers, Strings, etc), then parse or decode the contents and then integrate them into the scenegraph. Decode and assemble should not be strictly separated. Especially for worker based loading this makes sense as the heavy lifting is done in the worker and the integration is done in main,

Changing every loader to behave as you suggested will almost be impossible, but if you for example design WorkerLoader it could wrap this contract around any loader and emulate the behaviour. Using non-worker based loading is always an easy target, of course (the loader and all its functions can just be used).

Decode and assemble should not be strictly separated. Especially for worker based loading this makes sense as the heavy lifting is done in the worker and the integration is done in main ...

+1. I don't mean to complicate the overall design because I suspect GLTFLoader is an unusual case, but I imagine our ideal stages there would be:

  1. load (main)
  2. decode + assemble
    i. decode CPU-heavy items, e.g. Draco-compressed mesh attributes (1โ€“N workers)
    ii. decode core glTF body and assemble (main)
  3. return assembled nodes (main)

I think assembly is a very useful stage to be separated, and here's why:

  • Assembly is a process which requires every asset, as such it is a process which has very little parallelization that can be exploited.
  • Assembly produces a complex object which is not very each to serialize out of ones that _are_

You can easily offload decoding onto a worker, whereas assembly is most likely going to take place in the main thread, and likely to be fairly trivial in terms of amount of computational work that needs to be done (CPU cycles).

If you define clear decoding pipelines for things like geometry, you can take care of sending decoded data to the assembly stage (Worker -> Main Thread).

The idea in my head is to make the whole threading aspect as much of a non-issue for the implementer of a loader as possible. Give them a frame of thinking by imposing a certain flow to the loading with certain stages which needs to be filled out, and let _our_ code handle things like transfer to/from worker.

@Usnul I agree. One neat feature I would like to keep/already ported from LoaderSupport / async OBJLoader2 is to "stream" meshes (they are integrated into the scene once they are sent over to the Main). As I understand you proposal correctly, you aim to keep all "raw" asset data (buffers, parameters, etc) in memory and assemble everything at the end. For Mesh only scenarios "streaming" may be feasible, for others likely not.

/cc @takahirox Shouldn't we collaborate here? Looking at your latest progress posted on Twitter regarding DRACOLoader this seems logical. Dev WorkerLoader contains the MeshBuilder (moved from LoaderSupport there) takes the raw object description (Buffers/Transferables, parameters plus serialized material description) and creates a Mesh from it. If we have the counterpart "Existing Mesh to raw object description" (does something like it exist already somewhere?) then we could fairly easily put any Loader into a worker as far as they only rely on code executable in a worker environment.
This approach could be expanded beyond Meshes, basically whenever Transferables are available, but not limited to it, of course.

Current status example of WorkerLoader/OBJLoader. New execute functions allows to load list of files and then trigger parse with configuration instructions for WorkerLoader and used loader (OBJLoader in this case):
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_obj2_options.html

Unfortunately, time to continue work is very limited this week. More progress next week, latest.

@kaisalmen
Streaming is an awesome feature. Assembly need not be completely atomic, it can be streaming too. Expose an extra interface, along the lines of onAssetReady vs onAssemblyComplete. There's a fair bit of flxebility that can be achieved/exposed here.

2018-04-25 Status Update:
Ongoing (regular activity here):

  • I am porting WorkerLoaderDirector to use WorkerLoader. This will enable all orchestrated examples to work again (MeshSpray & WWParallels) = status quo.
  • WorkerLoaderDirector will be able to orchestrate differently configured WorkerLoader (use different loaders) at the same time in parallel.

Upcoming:

  • PCDLoader will be ported to supported async loading via WorkerLoader as well (I will then remove open other PR).
  • Find a better spot MeshBuilder and build the opposite functions to be able to serialize arbitrary Meshes for transfer from Worker to Main with Transferables in mind. Then expand both to wider asset scope (beyond sole Mesh handling). @takahirox this is a good starting point for collaboration. I briefly looked at your GLTFLoaderWorker code yesterday and it seems we need to solve similar problems, let's discuss.

Nice Side effects:

  • WorkerLoader is already able to sequentially load a list of files and then pass the content to the parse methodof AnyLoadeer.
  • WorkerLoader can be used as front-end for automated loader usage (configuration objects for loader behaviour adjustment) independent of Workers.

Automation is back via WorkerLoader.Director:
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_obj2_meshspray.html
I am currently porting the automated parallels OBJ example (WWParallels). Afterwards, I will explain the things I put in place to allow instruction based asset loading.

It took a little longer than expected, but finally both multi-worker examples are working:
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_obj2_run_director.html
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_obj2_meshspray.html
Multi-usage option example of OBJLoader has been ported to WorkerLoader where async is used:
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_obj2_options.html

THREE.WorkerLoader can be used with a very simple API:

new THREE.WorkerLoader()
    .setLoader( new THREE.OBJLoader() )
    .setTerminateWorkerOnLoad( false )
    .setBaseObject3d( local )
    .loadAsync( 'models/obj/walt/WaltHead.obj', callbackOnLoad );

or it can be fed with configuration instruction that allow to drive the loading purely by supplement of configuration objects. THREE.WorkerLoader.Director makes heavy use of it:

var rdMtl = new THREE.WorkerLoader.ResourceDescriptor( 'URL', 'Issue14.mtl', '../../resource/obj/14/issue14.mtl' );
var parserInstructionsMtl = {
    payloadType: 'text',
    haveMtl: true,
    texturePath: '../../resource/obj/14/',
    materialOptions: {}
};
rdMtl.setParserInstructions( parserInstructionsMtl )
    .setUseAsync( false );
var rdObj = new THREE.WorkerLoader.ResourceDescriptor( 'URL', 'Issue14.obj', '../../resource/obj/14/issue14.obj' )
    .setUseAsync( false );

var loadingTaskConfig = new THREE.WorkerLoader.LoadingTaskConfig( { instanceNo:  'issue_14', baseObject3d: pivot } )
    .setLoaderConfig( THREE.OBJLoader )
    .addResourceDescriptor( rdMtl )
    .addResourceDescriptor( rdObj );
loadingTaskConfig.setCallbacksPipeline( onComplete )
    .setCallbacksApp( this._reportProgress );
new THREE.WorkerLoader()
    .execute( loadingTaskConfig );

Internally, THREE.WorkerLoader.LoadingTask is used for loading, parsing and for integrating items into the scene. As you can see in the above code WorkerLoader can even use the loader synchronously without worker invocation. A common configuration approach for any loader is available this way.

If you like take a look at the branch mentioned above. More details regarding the code will follow...

I suggest picking a different word other than "instructions", and instruction is a verb, not a noun, configuration, setting, property, attribute maybe.

With respect to termination. I would suggest having default terminating behavior and silent re-start when new requests come in after that. If you want to amortize the downtime of the worker - you could add a small grace period via setTimeout, like say 1 second or so, if no requests come in - it can be decommissioned, when new requests arrive it can be rebuilt internally.
I think most people will be satisfied with such a default behaviour, and for the picky few those options could be exposed via configuration API that you already have.

Also, maybe it would be useful to hide lower level API (ResourceDescriptor and LoadingTaskConfig) behind an abstraction layer via composition?
Like so:

  • WorkerLoader is a LoadingTaskExecutor
    vs

    • WorkerLoader has a LoadingTaskExecutor

WorkerLoader is a LoadingTaskExecutor
vs
WorkerLoader has a LoadingTaskExecutor

Yes, I didn't see that. LoadingTask started as something small, because I wanted to separate the repeatable context from the rest of the WorkerLoader and it evolved already into something very close to what you suggested. WorkerLoader should only offer loadAsync, parseAsync and execute.

I suggest picking a different word other than "instructions", and instruction is a verb, not a noun,

I will use a better name. This is a point where you can identify that one is not a native speaker. :wink:

What do mean by "hiding lower level API (ResourceDescriptor and LoadingTaskConfig)"? Independent of how WorkerLoader relates to LoadingTaskExecutor these things need to be exposed, I think. How should a user otherwise know how to use WorkerLoader.execute?

I will use a better name. This is a point where you can identify that one is not a native speaker. ๐Ÿ˜‰

No worries, didn't mean it as criticism of your lingual abilities :)

What do mean by "hiding lower level API (ResourceDescriptor and LoadingTaskConfig)"? Independent of how WorkerLoader relates to LoadingTaskExecutor these things need to be exposed, I think. How should a user otherwise know how to use WorkerLoader.execute?

that's related to the first point about composition vs inheritance. You could expose that lower level API via a property. Here's an example:

class A{
   execute(...){....},
}

class Inheritor extends A{
   load(...){...}
}

class Composer{
  constuctor(){
       this.a = new A();
       ....
  },
  load(...){...}
}

const x = new Inheritor();
x.load();
x.execute();

const y = new Composer();
y.load();
y.a.execute();

@Usnul I chose to go the composition way. This makes sense. I cleaned-up code logic along.
Check out the examples based on the new code:
webgl_loader_obj2_run_direct
webgl_loader_obj2_meshspray
webgl_loader_obj2_options
Now, there are nicely separated building blocks.
Overall explanation will follow later...

Finally, here comes the explanation (Latest code rebased on current dev has been updated just now and see example links in post above):

WorkerLoader base usage is as simple as possible. For the simplest use-case it offers to the Loader and then perform loadAsync or parseAsync.

The simple use case aims at a one time usage, but already here a user can grab the embedded LoadingTask and set further properties.
LoadingTask can be fully automated by passing a LoadingTaskConfig to its execute function. Of, course all things can be set manually as well.
LoadingTaskConfig is used for the following:

  • Contains config properties for LoadingTask
  • Specifies the loaders and its configuration
  • Defines ResourceDescriptor that should be processed by LoadingTask
  • Is used to specify all callbacks required

Independent of how it was configured LoadingTask does the following things:

  • It loads as many resources specified if they are an URL or it passes readily available content to the next step
  • Parses all resources with the configured loader
  • Reaches the completion stage

Currently these are the things a loader needs to support async parsing:

  • Needs a function buildWorkerCode that takes THREE.WorkerLoader.WorkerSupport.CodeSerializer as input and provides the stringified code and a Parser name as output
  • The Parser needs to be isolated
  • It needs to provide raw mesh data (buffers as ArrayBuffer so they can become Transferables) to a callback function. This is the hook that allows data transfer back to WorkerLoader/WorkerSupport which assemble the mesh with MeshBuilder

As long as there is not a "general scene serialization" tool that supports Transferables (see Useful additions/upcoming, I currently see no way around these requirements.

LoadingTask utilizes these two support functions:

  • WorkerSupport: Is used to build the worker and establish data exchange and a simple communication protocol (Implementation can be completely overridden and replaced with own behaviour).
  • MeshBuilder: Is used to store all loaded and manually added materials and it receives Transferables and other content from worker and build meshes from it. This is currently borrowed from OBJLoader(2).

Additionally, WorkerLoader.Director allows to load utilizes multiple WorkerLoader to load many resources in parallel. It uses LoadingTaskConfig to realizes descriptive loading and re-uses WorkerSupport for caching workers.

Summarizing thoughts:

  • Yes, it is complex, but functionality is separated in block focusing on specific task where extension is possible
  • Description based approach to loading with or without utilizing workers
  • Theoretically, any loader should be usable with this approach if enforceSync is set and loaders have a parse method.

Useful additions/upcoming:

  • 2018-06-16: Ongoing The opposite function of MeshBuilder: A general scene serialization tool that takes the result from a Parser/Loader and stores it in Transferablesque objects.

    • 2018-06-16: Implemented WorkerLoader.Director currently only allows to use one loader type of loader for all WorkerLoader. Multiple loaders with multiple instance should be usable within the same Director

    • 2018-06-16: Implemented WorkerSupport should also support a file-only mode not requiring Blob-creation from code in memory

    • An example showing the descriptive multi-loader usage should be added

I have overhauled the text just now.
@donmccurdy @Usnul @mrdoob @takahirox Feedback or comments welcome! ๐Ÿ˜„

Finally the pieces come together. Unaltered PCDLoader in Worker with mesh disassembler that transforms buffers into Transferables (WIP)...
image

var scope = this;
var callbackOnLoad = function ( event ) {
    scope._reportProgress( { detail: { text: 'Loading complete: ' + event.detail.modelName } } );
};
var buildWorkerCode = function ( codeSerializer ) {
    return {
        code: '',
        parserName: 'THREE.PCDLoader',
        useMeshDisassembler: true,
        libs: {
            locations: [
                'node_modules/three/build/three.min.js',
                'node_modules/three/examples/js/loaders/PCDLoader.js'
            ],
            path: '../../'
        }
    }
};
var rd = new THREE.WorkerLoader.ResourceDescriptor( 'URL', 'Zaghetto.pcd', '../../resource/pcd/binary/Zaghetto.pcd' );

var loadingTaskConfig = new THREE.WorkerLoader.LoadingTaskConfig( { baseObject3d: local } )
    .setLoaderConfig( THREE.PCDLoader )
    .setBuildWorkerCodeFunction( buildWorkerCode )
    .addResourceDescriptor( rd )
    .setCallbacksPipeline( callbackOnLoad );

new THREE.WorkerLoader()
    .getLoadingTask()
    .execute( loadingTaskConfig );

Here come the WIP version. MeshTransmitter did not evolve much, yet, but this version removed all dependencies to OBJLoader2.
@Mugen87 this uses an almost unaltered (allowed default value for undefined url parameter in parse) version of PCDLoader and put it inside a worker with the code mentioned in last post :smile:
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_worker_multi.html

THREE.WorkerLoader.Director now supports separate WorkerLoader pools and webgl_loader_obj2_run_director example now allows to change the worker count of a pool while running:
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_obj2_run_director.html

FYI, I am on holiday with my family. There will be progress again after July 11th.

Finally, my work continues! These are the things I want to achieve next.

Ongoing

  • Isolate Asset loading and asset parsing into objects and let LoadingTask use them. Aim: Allow file loading to already happen in Worker.
    LoadingTask controls the asset processing pipeline: load -> parse -> assemble. First two steps can either happen in Main or Worker Thread.

Next:

  • Make DracoLoader work in Worker just by utilizing WorkerLoader
  • Improve MeshTransmitter so arbitrary mesh content can be supplied from Worker to Main

Question: Is it possible to share data/buffers between to canvases? Was thinking about moving buffers from Offscreen to another canvas, then data already in graphics memory could be made available locally. Don't know whether this is possible also with respect to security. Do you have any experience here or can share resources`? Thanks.

@kaisalmen
regarding access to data on canvas. This is image data, it's a collection of pixels with 4 channels (RGBA), you can sample them as such, that gives you "read" access, you can write data in similar fashion. If you mean FBOs (Frame Buffer Object) - that's a bit different, frame buffers can be read and written to also, but the mechanics are a bit different since you have GL functions available too.

Why do you bring up "canvases" and "data/buffers"? what is your intent here?

My idea is to have an OffscreenCanvas bound to a Worker that is not shown. Here BufferGeometry are created. Then buffers (e.g. vertex arrays) bound to those BufferGeometries are shared with or transferred to a regular Canvas on Main. If data is copied over on the GPU it seems logic re-use it directly Otherwise, data is copied or transferred from Worker to Main and then data is sent over to the GPU when BufferGeometry are created.
I was just thinking whether there is a clever way to put data directly into GPU memory from within the Worker without going over Main thread first.
My gut feeling is that what I have in mind is simply not possible, because it would allow inter-graphics context data exchanges.

You probably want OffscreenCanvas.transferToImageBitmap, but sadly the browser support is pretty scarce. Note that ImageBitmap objects are Transferable and do not require further decoding on GPU upload.

Puh, this latest updated took longer than expected, because I ran into more obstacles than expected. It is now possible to load URL resource from within the Worker only if you do something like this:

var rd = new THREE.WorkerLoader.ResourceDescriptor( 'URL', 'Cerberus.obj', 'models/obj/cerberus/Cerberus.obj' );
rd.configureAsync( true, true );
var loadingTaskConfig = new THREE.WorkerLoader.LoadingTaskConfig( {
        instanceNo: 42,
        baseObject3d: local
    } )
    .setLoaderConfig( THREE.OBJLoader2 )
    .addResourceDescriptor( rd )
    .setCallbacksPipeline( callbackOnLoad )
    .setCallbacksApp( callbackOnProgress );
new THREE.WorkerLoader()
    .executeLoadingTaskConfig( loadingTaskConfig );

The LoadingTask then does everything you requested (load and parse async in the above example), but for now only if parse is performed done in Worker only then load is done async as well.

Overall, I needed to separate the concerns more clearly within the codebase and I isolated for example a FileLoadingExecutor that is fully re-usable in the Worker. I managed to only provide THREE.DefaultLoadingManager, THREE.Cache and THREE.FileLoader if the full three library is not available in Worker (like OBJLoader2 does) and then the foot-print of the Worker is fairly small even if async load is used.

Good thing: External code signature are only slightly adjusted in comparison to the last commit and therefore examples and OBJLoader2 only required minor changes.

I really would like to receive some feedback on what I did, especially if you find it useful and if the code is understandable. Effectively, WorkerLoader has already become a tool for ConfigureAndAutomateAnyLoaderWithAsyncCapability ๐Ÿ˜‰ Code is still ES5. It could benefit from new language features...

Putting DracoLoader in a Worker and upgrading MeshTransmitter is next on my work list. It was a long way already, but now all tools needed should be ready.

@donmccurdy I know have a DracoLoader Wrapper that mimics the Parse.parse interface for WorkerLoader, so it can be executed generically. But I need to override the getDecoderModule function of DracoLoader because it relies on document for loading of scripts which is incompatible with Workers. As WorkerLoader creates the Worker code in the first place this can already be loaded by it and contained in the Worker Blob if I am not in error.

Yes, the assumption was right. It works with an adjusted version of THREE.DRACOLoader where getDecoderModule does not load a script. I also had weird issues with the Promises not working inside the Worker. I used a non-promise callback for now. I already have ideas how to make it work with unmodified loader code, where all adaptation happens in the Wrapper.
image

This is where the magic happens:
https://github.com/kaisalmen/WWOBJLoader/blob/Issue36/test/wldraco/webgl_loader_worker_draco.html#L98-L170
WLDracoWrapper is used in the Worker to adapt to the Parser.parse pattern. WorkerSupport loads the libs specified by buildWorkerCode and makes them available among other things in the Worker Blob.

Ok, I got it working without changing the existing DRACOLoader code directly. I augmented the code in memory and updated the serialization mechanism, so all code adjustments end up in the Worker Blob.
Will put it on three.js branch tonight, then it can be viewed with rawgit.

Here it is:
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_worker_draco.html
Code is augmented in example. This wrapper code should go to a specific file (e.g. WLDraco) and it should ideally become an extension of the DRACOLoader prototype that is used in examples.

Same trick works for PCDLoader (now unmodified on branch again):
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_worker_multi.html

I think this is cool. Anyone else likes this? Some more work is needed to make all rough edges smooth. Whole approach becomes powerful if you use WorkerLoaderDirector to run different loaders running in Workers with multiple instances! ๐Ÿ˜„

Next is GLTFLoader + DRACOLoader (either complete Worker wrapper or only a cloaked DRACOLoader or both) ...

@donmccurdy here comes the first preview of WWDRACOLoader used by GLTFLoader:
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_worker_gltf.html

WWDRACOLoader internally used WorkerLoader to run DRACOLoader in Worker. GLTFLoader runs in main sets WWDRACOLoader as DracoLoader. I also created a small wrapper for GLTFLoader, so its sync execution can be configured and run with WorkerLoader.

image

Other non-obj examples are:
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_worker_draco.html
https://rawgit.com/kaisalmen/three.js/WorkerLoader_OBJLoader/examples/webgl_loader_worker_pcd.html

Thanks @kaisalmen! Great to see this working. I'm not sure I understand the purpose of WWGLTFLoader, though โ€” the wrapper looks pretty light. Could that same configuration be done on WWDRACOLoader instead? If there's a worker and non-worker variant of DRACOLoader, where both work with the existing GLTFLoader, I think that's going to be easier for users to understand.

var loader = new THREE.GLTFLoader();

// Decode in main thread.
loader.setDRACOLoader( new THREE.DracoLoader() );

// Or, decode in Web Worker.
loader.setDRACOLoader( new THREE.WWDracoLoader() );

Expecting that DracoLoader and WWDracoLoader would require different initialization before they can be used, but that seems fine.

WWDRACOLoader does not yet expose all setters of DRACOLoader. All parameters, input and output data need to be send via messages as Strings or Transferable. WorkerLoader provides utility functions to make this easier, but sometimes there must be extra code as in WLDRACOLoader for handling the map arguments in the parse function.

WLGLTFLoader is not required. Sorry, for the confusion. It just applies a common pattern (_parse) to make it commonly configurable and executable for WorkerLoader (it commonly loads resources and kicks a parser, worker based or not).

You can just do it like this, load method would work, too. WWDRACOLoader mimics the DRACOLoader interface and runs WorkerLoader + WLDRACOLoader internally:

var gltfLoader = new THREE.GLTFLoader();
var dracoLoader = new WWDRACOLoader();
// required to find three.min.js and draco_decoder.js when WorkerLoader invokes buildWorkerCode
dracoLoader.setDracoBuilderPath( '../' );
dracoLoader.setDracoLibsPath( 'js/libs/draco/' );
gltfLoader.setDRACOLoader( dracoLoader );

var baseObject3d = new THREE.Group();
var onLoad = function ( gltf ) {
    var meshes = gltf.scene.children;
    var mesh;
    for ( var i in meshes ) {
        mesh = meshes[ i ];
        baseObject3d.add( mesh );
    }
};
gltfLoader.parse( arrayBuffer, '', onLoad );

Looks good, thanks for the explanation. In theory we could write a version of DRACOLoader that doesn't use three.js libraries, and wouldn't need a builder path, right? Something like:

var loader = new THREE.GLTFLoader();
THREE.WWDRACOLoader.setDecoderPath( './draco/' );
loader.setDRACOLoader( new THREE.WWDRACOLoader() );

loader.load( 'foo.glb', ( { scene } ) => {

  mainScene.add( scene );

} );

... which would exactly match the current (non-WW) syntax. Not suggesting we rewrite it at this point, but would be good to know that this fits the WorkerLoader usage pattern too.

Yes, that should work. Potential mitigation (not requiring a re-write) is to move the minified libs and draco_decoder under the same base path. A Parser no longer requiring three like OBJLoader2 removes most init overhead, but requires more effort.
Btw, my first idea was to put the complete GLTFLoader inside a worker, but that is not easily possible due to the usage of non-worker compatible things (e.g. document). buildWorkerCode of WLDRACOloader just puts two altered methods in the Blob that were creating issues, but if you need to replace hundreds lines of codes that will create maintenance hell.

Regarding the comment on OffscreenCanvas and workers here: https://github.com/mrdoob/three.js/issues/13664#issuecomment-410391313

You can almost run a full instance of threejs in a worker. I ran into issues with loading images from within a worker, but that's the only one i hit so far. While it's true you would gain some efficiency by doing individual processes in a worker for assembling large looping or time consuming operations, you can immediately gain benefits to your main thread on the display side by moving the entirety of threejs work to a worker thread with some minor caveats.

Today, without patching threejs, you'd need to patch in some proxy traps for a faux document for any of the requests to Element calls by threejs. In my project I avoided using images all together, but if the image texture loader supported loading without those dependencies, i bet the whole lib would work as is in a worker (given browser support). My project only used Chrome, so I didn't bother coding in considerations for a lack of OffscreenCanvas or ImageBitmap support. The performance benefits were very real, though. It's not a panacea, but it definitely lowered the load on the main thread and threejs just worked when supplied with an OffscreenCanvas that linked to a canvas attached to the DOM. That's in itself was kind of magical.

Pet project is here: https://github.com/jeffreytgilbert/goto-booth

You can almost run a full instance of threejs in a worker.

This example runs a full instance of three.js in a worker:
https://threejs.org/examples/#webgl_worker_offscreencanvas

@mrdoob that's interesting. I ran into a line when loading images to a texture that caused an error. I'll have to revisit my code to check and see why.

I believe the line that it hit was this: var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );

Though, in hindsight, this could have been due to environmental polyfilling that i was doing in the worker to try and fool three.js into believing it was not in a worker, so probably my fault.

Closing in favor of #18234.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zsitro picture zsitro  ยท  3Comments

jack-jun picture jack-jun  ยท  3Comments

clawconduce picture clawconduce  ยท  3Comments

danieljack picture danieljack  ยท  3Comments

alexprut picture alexprut  ยท  3Comments