Loading, rendering then saving the canvas for GLTF models frequently

I am trying to create thumbnail images for my application by rendering component GLTF files then saving off the canvas for later processing, eventually ending up as a small, linked thumbnail. Ultimately there will be a lot of these - many hundreds so I am hoping to find an automated solution.

Note: this will try to save many images to the download folder of your browser although Chrome at least, will ask you first if you want to allow it.

Example code there of the sort of thing I am trying to do - even though I am try to save 50 different images, there are always less than that present in the download folder upon completion.

I imagine it’s because the browser cannot keep up, or perhaps one download starts before the previous one completes, I tried various ways of adding short delay into the loop with some success but it feels like the wrong approach.

Ideally, I would like to observe the download event and wait for it to complete before starting the next but I don’t think that’s possible.

Has anyone worked on something like this before?

Putting an async callback inside a synchronous loop, with each callback sharing access to the scene and canvas, will cause trouble.

My suggestion would be to start by getting this to work with just a solid scene background color (not a model!) rendered each time, and confirm the browser is giving you all the downloads correctly and that the general approach works.

Once that’s working, use something like this to block the loop while waiting for the next model to load:

for (let i = 0; i < count; i++) {
  const gltf = await loader.loadAsync('scene.glb');
  // ...
}

And finally once that’s working, you might consider trying to do all of the models in parallel, or in small parallel batches, depending on the number and their complexity.

Interesting and much appreciated.

I posted here because everyone is helpful but I was worried it was unrelated to Three.js and just a fundamental misunderstanding of JavaScript / browsers on my part. Sounds like that’s not the case.

Lots to try there this evening - thank you!

One part of the problem is a general JavaScript thing. This code …

  const total = 50;
  for (let count = 0; count < total; ++count) {
    setTimeout(function () {
      // ...
    }, 10);
  }

… is just a bit dangerous because anything inside that callback could be called in any order, and if code inside the callback is async, then two callbacks’ execution could overlap. Mostly just easier to reason about async/await if possible.

How well the browser will handle high numbers of downloads I’m not sure, and would be curious how that part works out! Worst case I suppose you might need to use JSZip or something like that to put all the images into a ZIP and trigger one download at the end.

The ZIP approach is a good idea and may be necessary. I changed the code to load everything via an array of loader promises and then, used Promise.all(...). When everything was resolved, I ran the loop to render and save.

Same issue was present sadly and even when the loop did nothing but call render then save, there were lots of missed downloaded.

This worked a couple of years ago on a much older version of Chrome so perhaps some browser internals have changed since then.

I’m not familiar with JSZip but I expect there are plenty of examples out there.

Thank you for your insight.

if it is because it is not in sync with the loading of the models.

createScene(value, execute) {
        var loadingdObjt = 0;        
        function icreateObject(name, iobject) {
            loadingdObjt += 1;                        
            LoadGLBPbject(name, (object) => {                
                iobject(object);
                loadingdObjt -= 1;
                console.log('Wainting for',loadingdObjt);
                if (loadingdObjt <= 0) execute();
            });
        }	
	if(value==1){
	icreateObject('xxx1.glb');
	icreateObject('xxx2.glb');
	icreateObject('xxx3.glb');
	...
	}
}

createScene(1,()=>{
 //All Objects of scene 1Loaded
});

I’m sorry but I don’t understand any of that.

The canvas is saved once the model is loaded so I think they are in sync?

Worst case I suppose you might need to use JSZip or something

I made a version that adds each canvas image to a zip and then saves the zip at the end but the zip is empty. Strangely, it’s empty locally the first time I run it too but if I run it again without reloading the page, the next zip to be created does indeed contain the images.

Probably something silly…

You can use the media API to record the canvas as a video.

I thought about that but I need the files as images - always possible to convert the video to frames I guess - worst case.

I got it working - the canvas.toBlob() call that then pushed the file to the zip wasn’t completing for each until after the zip file was being created and saved.

Adding another array of promises for the canvas.toBlob() operations and then waiting for them all to resolve before adding each file to the zip and saving it seemed to work but is deeply unsatisfying…

(Afterthought: Not least of which because I need to carry along the filename to save in the real application (I use an index in this Fiddle) and I don’t know of a way to compose an array of promises that also contain additional data like filenames etc. with each)

I know this (newer) Promise/resolve paradigm is supposed to be supposed to be more straightforward but I can’t get my little brain around it…

you can use the loadAsync method on the loader to make it look more imperative.

async function generateFrames(){

for(let i=0;i<100;i++){

   let glb = await gltfLoader.loadAsync("mygltf.glb");
   //glb is fully loaded here.. do something with it...
}

}

Promises aren’t more straightforward… it’s just a way to allow/take advantage of multithreading in the browser. While you’re waiting on a promise to resolve, you can do other stuff.

You can also use async / await to explicitly wait for a promisified function to resolve.

  function test( ){
      return new Promise( (resolve,reject)=>{
              setTimeout(()=>{
                     console.log("Waited one second!");
                     resolve()
              }, 1000);
  })

await test();
console.log("Promise finally finished!");



will print:

"waited one second!"
"promise finally finished!"

Here’s a synchronous version of your thing that “awaits” each load before downloading.

1 Like

Thank you for the insight for for me aty least, that fiddle behaves the same way - downloads somewhere between 25-30 images only each time. Perhaps it’s browser specific ? I am using the latest Chrome on macOS.

Hmm ok perhaps I didn’t understand your issue.
I see it reporting linearly:

  • “Saving #”, 0
  • “Saving #”, 1
    through 49. in the console.
    and I see 50 images in my downloads folder.
    I’m using latest chrome on windows. I didn’t explicitly make the download itself synchronous in that fiddle, perhaps that’s the issue you’re talking about?

To make it synchronous you might have to switch to using XMLHTTPRequest per this SO post:

Then you could wrap that in a Promise as well.

Maybe it’s a macOS thing then - I see the output in the console but number of imagers in download folder varies between 20-30 each time I run it.

I was able to make the JSZip approach work well so I think that ends up being a better solution.

Thanks for all the help.

1 Like