S. Langhammer
S. Langhammer

Reputation: 165

Is there a way to wait for THREE.TextureLoader.load() to finish?

I'm working with release R73. My current task is to fill an array with materials. The content of this array is supposed to be used later. That usage depends on all materials to me completely loaded.

By now I loop through an array of JSON information and call this code for every element:

TLoader.load(
        BASE_ODB_URL + jsonMat.pic,
        function (texture) {
            texture.wrapS = THREE.RepeatWrapping;
            texture.wrapT = THREE.RepeatWrapping;
            texture.repeat.set(jsonMat.scaleu, jsonMat.scalev);
            Mat = new THREE.MeshLambertMaterial({
                    map : texture,
                    side : THREE.DoubleSide,
                    name : jsonMat.mname
                });
            THREEMatList.push(Mat);
        },
        function (xhr) {
        }, //onProgress
            function (xhr) {
            Mat = new THREE.MeshLambertMaterial({
                    color : 0xff0000,
                    side : THREE.DoubleSide,
                    name : jsonMat.mname
                });
            THREEMatList.push(Mat);
        } 
    )

TLoader is initialized earlier: var TLoader = new THREE.TextureLoader();

If a material isn't there, when it is needed, I get a fallback material. This was only intended as an error option. Is there a way to wait until .load() finishes?

Upvotes: 7

Views: 11106

Answers (3)

Lodz
Lodz

Reputation: 1000

You can also use this simple helper if you need to load multiple textures before rendering your scene :

    /**
     *
     * @param {Array} texturesSources - List of Strings that represent texture sources
     * @returns {Array} Array containing a Promise for each source 
     */
    function getTextures (texturesSources) {
        const loader = new THREE.TextureLoader()
        return texturesSources.map(textureSource => {
            return new Promise((resolve, reject) => {
                loader.load(
                    textureSource,
                    texture => resolve(texture),
                    undefined, // onProgress callback not supported from r84
                    err => reject(err)
                )
            })
        })
    }

Then wrap your code using Promise.all, allowing to fetch the sources in parallel, and fail properly by catching the error.

Example :

const texturesBasePath = '../assets/textures/'
const texturesSRC = [
    'image1.jpg',
    'image2.jpg',
    'image3.jpg',
].map(texture => texturesBasePath + texture)

Promise.all(getTextures(texturesSRC))
    .then(textures => {
        // create your materials, meshs...
        // render the scene
    })
    .catch(err => console.error(err))

Upvotes: 5

Brandel Zachernuk
Brandel Zachernuk

Reputation: 51

Threejs already provides a callback for all elements loaded - via the use of a LoadingManager. By default, TextureLoader uses the DefaultLoadingManager:

import {TextureLoader, DefaultLoadingManager} from './three.module.js';

const getTextures = ()=> new Promise((resolve, reject)=>{
  const loader = new TextureLoader();
  DefaultLoadingManager.onLoad = ()=>resolve(textures);
  const textures = [
    "image1.jpg",
    "image2.jpg",
    "image3.jpg"
  ].map(filename=>loader.load(filename));
});

getTextures().then(result=>console.log("We received,", result,"!"));

That's going to wait for all assets to load, though. If you want to be listening for a specific subset of assets loaded, you can do that by constructing a custom LoadingManager and passing it into your TextureLoader to manage different asset bundles separately:

import {TextureLoader, LoadingManager} from './three.module.js';

const getTextures = ()=> new Promise((resolve, reject)=>{
  const manager = new LoadingManager(()=>resolve(textures));
  const loader = new TextureLoader(manager);
  const textures = [
    "image1.jpg",
    "image2.jpg",
    "image3.jpg"
  ].map(filename=>loader.load(filename));
});

getTextures().then(result=>console.log("We received,", result,"!"));

Upvotes: 5

Leeft
Leeft

Reputation: 3837

A way to solve this is through Promise's as they are really the way forward and I personally think it's a shame TextureLoader doesn't return a Promise to start with (it would make this a little bit easier). However, do keep in mind that IE11 will need a polyfill as it lacks native support for promises.

Something like this should do it, where textureArray represents your JSON data in an iterable Array:

var allPromises = [];

textureArray.forEach( function( jsonMat ) {

    allPromises.push( new Promise( function( resolve, reject ) {

        TLoader.load(
           BASE_ODB_URL + jsonMat.pic,

           function( texture ) {
               // Success callback of TextureLoader
               texture.wrapS = THREE.RepeatWrapping;
               texture.wrapT = THREE.RepeatWrapping;
               texture.repeat.set( jsonMat.scaleu, jsonMat.scalev );
               var material = new THREE.MeshLambertMaterial({
                   map: texture,
                   side: THREE.DoubleSide,
                   name: jsonMat.mname
               });
               THREEMatList.push( material );

               // We're done, so tell the promise it is complete
               resolve( material );
           },

           function( xhr ) {
               // Progress callback of TextureLoader
               // ...
           },

           function( xhr ) {
               // Failure callback of TextureLoader
               // Reject the promise with the failure
               reject( new Error( 'Could not load ' + jsonMat.pic ) );
           }
        );

    }));

});

Promise.all( allPromises )
    .then( function( arrayOfMaterials ) {
        // All textures are now loaded, and this array
        // contains all the materials that you created
    }, function( error ) {
        console.error( "Could not load all textures:", error );
    });

So what's happening here is that one overarching Promise is used to keep track of the status of the other Promises. Each texture being loaded is wrapped in a Promise, which are added to allPromises. Finally that entire set of promises is tested for success or failure, and at that point you know about the overall success or failure.

One important thing to keep in mind here is that the Promise.all will only succeed if all the textures were loaded, and it will fail as soon as any one fails. If you need finer control than that you'll need a Promise polyfill with more features than the Promise/A+ spec offers, such as RSVP.js. Or instead of reject()ing the promise you could resolve() it and handle the result gracefully.

Upvotes: 2

Related Questions