-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhancement Proposal: Wrap OBJLoader in web worker #9756
Comments
Parse calls parseText (as before) or parseArrayBuffer (obj content as arraybuffer) according to configuration. Parsing has been split into _prepareParsing, _processLine and _finalizeParsing General restructuring of the parsing flow (extract logical blocks to methods). Added inline mesh processing (setWorkInline): A mesh will be added to the container as soon as it is available. Hook for behavior override by webworker (see below).
WWOBJLoaderFrontEnd.js: Handles mtl loading via MTLLoader. Materials are passed without textures to WWOBJLoader. Webworker (WWOBJLoader.js) re-creates the MaterialCreator. It can be used with both files and already loaded files (arraybuffer/string) and pass the obj data to the webworker (arraybuffer is useful when OBJ file was for example stored in zip). Realizes the communication and workflow with the webworker. WWOBJLoader.js: Is an extension of OBJLoader and wraps it into a web worker. It overrides the '_buildSingleMesh' method. Sending back the mesh content to WWOBJLoaderFrontEnd.js WWCommons.js: Relative import paths for webworker defined outside. Possibility to share other common static information
that's impresive ) it would be nice if all loaders could be made like that |
it would be quite simple to parse geometry and construct it inside a worker. Especially a buffered geometry, as it could be stuffed into a transferrable - which eliminates expensive deep copy. Materials I'm not so sure about. |
We have discussed this many times with @mrdoob in other issues. You should read through #8704 As you can see there, @mrdoob agrees that we could create This would leave users that are happy with the current loader in peace, working code. We could log a deprecation warning once the @mrdoob has also wanted some cleanup, refactoring and simplification for OBJ loader so that it would be easier to approach for new devs. My initial hunch is that this new worker based setup would be a lot more complicated for "noobs" to jump into the development :) At least harder to wrap the whole thing into your head, but still probably as easy to find the code part that needs fixes. I'm all for web workers (as I comment in the issue). I like your ideas and effort. The demos work great, I did not look at the code closer yet (I will). I especially like the streaming of each submesh when its done, great. I think this should also be configurable so you can turn it off, or maybe even default off. Most users probably would not like to render partial meshes to the end user. I need to read you code, if you already have this, but there needs to be a configurable worker pool that distributes parallel loads into many workers. Also its important to keep workers alive for a while after its load is done, creating a new worker and loading three.js into it expensive from my experience, sometimes hundreds of msecs (even if the file is in browser cache). I have implemented such a system and would be able to help here. How about you create a separate repo that does not have the full three.js in it. Just your new loader and its needed bits. You could just pull latest three.js as from npm and make demo/test pages that run it with latest release. I could jump in as a contributor and help you out. Once its fleshed out we could import it as
Yes the no copy transfer/transferable is a must for this kind of loader. I have implemented this kind of worker before, imo its best to submit each attribute separately and construct a new buffer geometry in main thread (just add the received attrib/buffer as is). I would probably tranfer material metadata as an object to the main thread and create materials there. Though you could just serialize the worker created material as JSON and deserialize it back in the receiving end. I believe |
Thank you all for providing feedback. Give me some time to process the information. I wasn't aware about #8704. It seems that I didn't check enough issue history. A thorough answer will follow. |
Workers are not free, if you want them as part of the core library - you need management code for it at some point.
Workers currently don't allow live-code transfer, so you have to either load a separate .js files inside a worker or pass all relevant code with dependencies as a string into the worker and then use "new Function(codeString)". You could also use a URL blob (which is what will be used to create the worker initially, i suspect). Spawning too many workers, or keeping them alive without use is probably not a good idea - there is a penalty depending on browser/device. Just something to think about, more long-term. |
@jonnenauha, yes keeping backward compatibility is important and I agree that it is a good idea to create an OBJLoader2 and also to do it in an independent repository as you suggested: What I already achieved with my modified version of the OBJLoader is that it can be used independent of a wrapping web worker. You are still able to use it without touching any code. The WWOBJLoader is just an extension. I needed to move code into new logical blocks which in the end is a fairly heavy modification to the existing OBJLoader. With OBJLoader2 we are able to implement more fundamental changes to the existing loader. If I am not mistaken the processing of the OBJ file content is fairly robust now as the code has received various bug fixes. So, the core processing code should stay, but we could think about optimizations (data structures, etc.). Objectives for OBJLoader2:
Objectives for web worker wrapper/worker controller:
That's it for now. I have added the objectives to the readme of the bespoke repository. |
The simple example using male02.obj is now ported and available in the new repository. |
It took a while, but I have first results and want to share how far I have come. Feedback is very welcome. 2016-11-06: Status update
Some thoughts on the code:Approach is as object oriented as possible: Parsing, raw object creation/data transformation and mesh creation have been encapsulated in classes. Parsing is done by OBJCodeParser. It processes byte by byte independent of text or arraybuffer input. Chars are transformed to char codes. Differnet line parsers (vertex,normal,uv,face,strings) are responsible for delivering the data retrieved from a single line to the RawObjectBuilder. The RawObjectBuilder stores raw vertex, normal and uv information and builds output arrays on-the-fly depending on the delivered face information. One input geometry may lead to various output geometry as a raw output arrays are stored currently stored by group and material. Once a new object is detected from the input, new meshes are created by the ExtendableMeshCreator and the RawObjectBuilder is reset. This is then just a for-loop over the raw objects stored by group_material index. Memory ConsumptionOnly 60% of the original at peek (150 MB input model) has a peak at approx. 800MB whereas the existing has a peak at approx. 1300MB in Chrome. PerformanceSo far, I only ran desktop tests: Firefox is generally faster than Chrome (~125%). 150MB model is loaded in ~6.4 seconds in Firefox and ~8 seconds in Chrome. Existing OBJLoader loader takes ~5.1s in Firefox and ~5.3s in Chrome. Examples:Larger model not in the prototype repository (27MB zip): |
Sounds really good! Regarding OO approach, one thing to be careful about is garbage collection, this is where performance can take a sharp hit. 800MB memory usage is curious, it's good that it takes less, but would be interesting to know how it scales (e.g. 800MB = X*a + b, what are X,a,b?). |
@Usnul: Hope this answer makes sense. :-) During parsing the max. amount of heap is used when a the biggest object is transformed (2140212 vertices in the benchmark example) from input to raw mesh data. |
With the updated I just pushed to WWOBJLoader Prototyping speed is now similar to the existing implementation: Both Firefox and Chrome parse the 150MB model under 5 seconds on my dev machine (100-200ms difference to existing OBJLoader)! Update 2016-11-11: Existing OBJLoader: 4102.86ms (Firefox 64-bit 49.0.2) Btw, these things improved performance most:
|
Cool stuff, good progress, I'll take a peak at the codebase. At this point I'm mostly interested in the new parser code. The web worker threading is ofc a cool addition. Edit: The multi-material feature is quite a big one. I'll see if I can help out with that. If I make a PR can I push new example assets? I could also make some kind of unit test system with grunt that would run all the files and verify the thing is still working :) Quite useful with bigger changes. |
What is the latest code I should read, The structure of the repo is kind of confusing, some suggestions.
Then implement tests you can run with shell eg.
Finally repo/library also needs a better/catchier name. "Hey dude are you using the new WWOBJLoader?" does not exactly roll off the tongue easily :) ObjLoader2 is kind of boring too though, but conveys the message clearly to three.js end users. The web worker part does not need to be in the name, its just one features, as you have improved the main thread performance as well (at least what I'm reading here). Edit: If you agree on the points I could send a PR to you with at least some of the changes. The repo is still small so it would be easy to do at this point. |
Ok, I took the three.js repo layout as blueprint. |
Yeh. I think what at the end of the day goes to three.js is one or a few new files, if its decided to be merged there. Those should be the non-minified build artifacts from this lib repo. So the structure does not necessarily need to be similar. Why loaders are in examples in three.js is because they are not bundled in the build, but separately provided examples (even if they are the defacto loaders most users use). |
With quick testing I'm not seeing the perf gains. For General observations: You have prepared all the classes to be very configurable. Most of the operational modes can be turned on/off or you can provide you own custom functions. To the point where you are wrapping the default built in function call for fetching a single byte out of the array buffer :) This might be a bit overkill, as its done thousands and thousands of times inside the parser.parse. We need to look at the hot path and make it fast. You are also newing up a lot of "complex" objects in the start of each parse. This is probably fine, as they encapsulate state. In the parsers you are again mapping/calling lots of functions to get back to the main parser. var parsers = {
void: new LineParserBase( 'void' ),
mtllib: new LineParserStringSpace( 'mtllib', 'pushMtllib' ),
vertices: new LineParserVertex( 'v', 'pushVertex' ),
normals: new LineParserVertex( 'vn', 'pushNormal' ),
uvs: new LineParserUv(),
objects: new LineParserStringSpace( 'o', 'pushObject' ),
groups: new LineParserStringSpace( 'g', 'pushGroup' ),
usemtls: new LineParserStringSpace( 'usemtl', 'pushMtl' ),
faces: new LineParserFace(),
lines: new LineParserLine(),
smoothingGroups: new LineParserStringSpace( 's', 'pushSmoothingGroup' ),
current: null
};
....
function LineParserBase( name, robRefFunction ) {
this.name = name;
this.robRefFunction = robRefFunction;
}
function LineParserVertex( name, robRefFunction ) {
LineParserBase.call( this, name, robRefFunction );
this.buffer = new Array( 3 );
this.bufferIndex = 0;
}
... later on results
robRef[ this.robRefFunction ]( this.buffer ); I think at the end of the day, as discussed elsewhere, this boils down to
The mem footprint of arraybuffer should be better as we have more control, but the speed does not seem to be there at least for this low/moderate sized mesh. Though I would guess arraybuffer parsing is faster, its just some other stuff (maybe points above) that is slower. Simplifying the whole chain a bit and not wrapping functions over single line execs might belp. I cant really see clearly where the time is spent with chromes profiling tools at this point. Need more investigation and reading the codebase. Edit: From my point of view, at this moment this codebase is more complex than what it is trying to replace. Not saying its a bad thing though, but it was one of mrdoobs points to make the library more approachable, which the regexp soup certainly is not either :) I would maybe start by splitting each of the classes into separate files, easing developers to first of all read the whole thing, instead of jumping inside a one big file. Could use the same module require that three.js itself is now using and the same tools to produce the final build. |
Open parenthesis (
@jonnenauha What think mrdoob and other "mains" contributor of three.js about keep really useful stuff in example directory ? Due to es6 module and rollup with threeshaking only required stuff will be bundle, so it's (in my view) a non sens to keep this stuff out of the lib... If you have some ref issue about that, i will read them with attention, thank. ) Close parenthesis @kaisalmen Really good job ! I will forked your repo as soon as possible to watch in deeper the added stuff. |
Good evening, sorry, I was not able to answer earlier.
P.s.: Screenshot: I did a page reload when I a captured the timeline with Chrome that's why things seem to appear twice. |
@jonnenauha I have overlooked some of your edits:
Yes, this sounds great and I would appreciate it. |
Ok, the missing features have been implemented, but I face an issue with Some insights on how the obj data is processed. "Female02.obj" defines the first object like this (for now the code treats all 'g' as one object when
will result be internally stored as (with vertices, normals and uvs ordered accordingly):
If |
First preview of |
I have completed the parser rework. The parser code is a lot simpler and easier to understand, I think. Performance is there for small and large models. A Firefox 50.0.1 (64bit) fresh browser instance parses the 150MB model in 3.2 seconds:
Chrome is a little slower (57.0.2936.0 canary (64-bit):
Some random thoughts:
Next:
|
@jonnenauha: I have adjusted the repository structure
Edit 2016-12-02:
Edit 2016-12-04:
Edit 2016-12-11:
|
The fun begins:
Edit 2016-12-16: |
Hi all, Now, you can see the completion status of every web worker used: I think, the code is feature complete and I would like to receive some feedback from you. Any feedback on the code and the demos is very welcome! Edit 2016-12-27: As promised some words providing an overviewIn contrast to the existing
What is the reason for separation?The loader should be easily usable within a web worker. But each web worker has its own scope which means any imported code needs to be re-loaded and some things cannot be accessed (e.g. DOM). The aim is to be able to enwrap the parser with two different cloaks:
As
Directing the symphony
Parser POIsThe parser and mesh creation functions have reached full feature parity with the existing OBJ loader. These are some interesting POIs:
Improvements
Examples:Web Worker OBJ Parallels Demo Larger models not in the prototype repository: |
Demo is very cool, great progress :) @mrdoob should we start considering at some point to copy this new I don't really know how we should go forward with this. Should it be left as its own repo, have its own issues etc. I mean the loader is not part of three.js core, but it would be shipped as external files that would be handy for devs. Also being ObjLoader2 it won't break existing users, people can port to it when they like with quite minimal changes, a bit more changes if they wish to utilize the web-worker. git submodules are a mess and a hassle for devs. I would not take that route myself. |
I would not go for git submodules either. |
Nice work. If you feel its ready, make a proper release. Do the gulp build and make a zip file that has the ./build contents and upload it to the github release. This is faster way of people getting the library than clone > jump to tag > build. I will also give this a go at work a bit later when I get back to the relevant tasks/projects. I would make it 1.0 and be done with it. If @mrdoob wants to slurp the library here, he can always use the latest tag from your repo. Imo its kind of irrelevant if it ends up here, it would be nice, but it can also live in the separate repo. We just need to advertise it to three.js users. Remove the "prototyping" from the repo description too :) I'm not sure what the best channels to let people know that this exists. Maybe @mrdoob can tweet the repo link :) |
@mrdoob, @jonnenauha and @Coburn37 I have completed the work on the examples: gulp will now produce single page versions of all examples. WWOBJLoader has now reached version V1.0.0 I have uploaded a zip of the build folder to the release on github! Please RT if you like! Cheers :-) |
…d three examples
…d three examples
@jonnenauha and @mrdoob: I added a the V1.0.0 bundled code packages together with three examples on the branch WWOBJLoader2. Consider this is a proposal. What do you think is the best way to integrate this code with three.js? What is both elegant and easy to maintain? This is a potential way forward, but eventually not the best. That is the main reason why I have not issued a merged request, yet. |
IMO the minified files should be left out. If someone decides to pull these into their project, they can worry about the minification then. I for example have build step for minifying the current ObjLoader. It is /examples so I think the sources should be human readable :) For better discovery I would add your ObjLoader2 repo URL to all the built files header. You have your own website there, but it should have also the project repo. https://github.com/kaisalmen/WWOBJLoader/blob/master/gulpfile.js#L30 Your build step should also have the version in the top comment or something like |
But yes this was the way I thought it would be pulled in to three.js. You could send out a pull request on each significant release you make. |
…examples: obj2, wwobj2 and wwobj2_parallels. Header now carry references to development repository. Added checks for Blob and URL.createObjectURL
@jonnenauha these were good ideas. Version is now included and the headers carry the reference to the development repository. I increased version to 1.0.1 as I made minor modifications to realize the changes and I added checks for Blob and URL.createObjectURL in additions to Worker in I have squashed and rebased the branch and will now issue the merge request. |
…nction for web worker code.
… some new Object calls. Updated version to 1.0.3.
@kaisalmen I'm looking through your examples. Very interesting. I'm looking to do something similar for the THREE.ObjectLoader, specifically to allow its .parse() method to report progress for large models. If you have any additional tips learned from the process that would be applicable to doing this to the other loaders, they would be much appreciated. |
webgl_loader_obj2_ww allows to load user OBJ/MTL files. All examples now use dat.gui.
@fraguada, this lengthy post already contains some tips, but here comes a condensed list of hopefully helpful tips and POIs (btw, check out Implementation Overview):
The list is not yet complete (time is limited today and I tend to forget things as well 😉) I will edit and extend it... |
…examples: obj2, wwobj2 and wwobj2_parallels. Header now carry references to development repository. Added checks for Blob and URL.createObjectURL
…nction for web worker code.
… some new Object calls. Updated version to 1.0.3.
webgl_loader_obj2_ww allows to load user OBJ/MTL files. All examples now use dat.gui.
Improvements since initial external V1.0.0 release: OBJLoader2: - Removed need for making Parser public. OBJLoader2 has a build function for web worker code. - MeshCreator is now private to OBJLoader2 WWOBJLoader2: - Added checks for Blob and URL.createObjectURL - Worker code build: Removed need to adjust constructor and some new Object calls - Allow to properly set CORS to MTLLoader via WWOBJLoader2 and WWOBJLoader2Director - Now allows to enable/disable mesh streaming Example webgl_loader_obj - Added GridHelper - resources to load are now defined outside example classes Example webgl_loader_obj2_ww - Allow to clear all meshes in - Allows to load user OBJ/MTL files - Added GridHelper - resources to load are now defined outside example classes All Examples: - Created one page examples and tuned naming - All examples now use dat.gui - Removed namespace "THREE.examples" - Fixed comment typos - Fixed some code formatting issues - Fixed tabs in examples General: - Headers now carry references to development repository
Adjusted to removal of MultiMaterial
Improvements since initial external V1.0.0 release: OBJLoader2: - Removed need for making Parser public. OBJLoader2 has a build function for web worker code. - MeshCreator is now private to OBJLoader2 - Removed underscores from functions of private classes and adjusted naming of web worker classes WWOBJLoader2: - Added checks for Blob and URL.createObjectURL - Worker code build: Removed need to adjust constructor and some new Object calls - Allow to properly set CORS to MTLLoader via WWOBJLoader2 and WWOBJLoader2Director - Now allows to enable/disable mesh streaming - Removed underscores from functions of private classes and adjusted naming of web worker classes Example webgl_loader_obj - Added GridHelper - resources to load are now defined outside example classes Example webgl_loader_obj2_ww - Allow to clear all meshes in - Allows to load user OBJ/MTL files - Added GridHelper - resources to load are now defined outside example classes All Examples: - Created one page examples and tuned naming - All examples now use dat.gui - Removed namespace "THREE.examples" - Fixed comment typos - Fixed some code formatting issues - Fixed tabs in examples General: - Headers now carry references to development repository
Adjusted to removal of MultiMaterial
Improvements since initial external V1.0.0 release: OBJLoader2: - Removed need for making Parser public. OBJLoader2 has a build function for web worker code. - MeshCreator is now private to OBJLoader2 WWOBJLoader2: - Added checks for Blob and URL.createObjectURL - Worker code build: Removed need to adjust constructor and some new Object calls - Allow to properly set CORS to MTLLoader via WWOBJLoader2 and WWOBJLoader2Director - Now allows to enable/disable mesh streaming Example webgl_loader_obj - Added GridHelper - resources to load are now defined outside example classes Example webgl_loader_obj2_ww - Allow to clear all meshes in - Allows to load user OBJ/MTL files - Added GridHelper - resources to load are now defined outside example classes All Examples: - Created one page examples and tuned naming - All examples now use dat.gui - Removed namespace "THREE.examples" - Fixed comment typos - Fixed some code formatting issues - Fixed tabs in examples General: - Headers now carry references to development repository
OBJLoader2: - Removed underscores from functions of private classes WWOBJLoader2: - Adjusted naming of web worker classes
Adjusted to removal of MultiMaterial
Description of the enhancement
I like to propose several enhancements to the existing OBJLoader. The aim is to wrap the existing OBJLoader into a new web worker. I have created a fully working prototype based on the latest dev code: See my fork of three.js The goal is to integrate large OBJ files (>100MB, >1 million triangles) into a scene while the user is able to interact with the scene.
Proposed changes to OBJLoader:
Web worker OBJLoader wrapper (WWOBJLoader):
Web worker Front End:
Questions:
Prototype examples:
Regressions?
Further thoughts...
Thanks in advance for providing feedback. This could be my first contribution to three.js. ;-)
Kai
Three.js version
Browser
OS
The text was updated successfully, but these errors were encountered: