LoadTracker #1751

Closed
mrdoob opened this Issue Apr 17, 2012 · 38 comments
@mrdoob
Owner

I was trying to tackle the issue of having images loading async and not having control on when they get loaded so the object displays black.

After looking at PxLoader, I was considering something along the lines of this...

var tracker = new THREE.LoadTracker();
tracker.addEventListener( 'complete', render );

var loader = new THREE.ImageLoader( tracker );
var texture = loader.loadTexture( 'blah.png' );

var loader = new THREE.JSONLoader( tracker );
loader.load( 'bleh.js', function ( geometry ) {
    var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) );
} );

or we can also return an empty geometry (or object) in the .load() call...

var tracker = new THREE.LoadTracker();
tracker.addEventListener( 'complete', render );

var loader = new THREE.ImageLoader( tracker );
var texture = loader.loadTexture( 'blah.png' );

var loader = new THREE.JSONLoader( tracker );
var geometry = loader.load( 'bleh.js' );

var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) );

This would replicate what we usually do now:

var tracker = new THREE.LoadTracker();

var loader = new THREE.ImageLoader( tracker );
var texture = loader.loadTexture( 'blah.png' );

var loader = new THREE.JSONLoader( tracker );
var geometry = loader.load( 'bleh.js' );

tracker.addEventListener( 'complete', function () {
    var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) );
} );

Is there a better name than LoadTracker? I was thinking LoaderManager but is not really managing...

Thoughts?

@hedral

LoadTracker works.

Alternative ideas in case anyone thinks there is a better fit...

LoadMonitor
LoadWatcher
LoadDetector

@mrdoob
Owner

Yup, LoadMonitor was suggested on the IRC channel as well. That sounds the best to me.

LoadMonitor, LoaderMonitor, LoadingMonitor, ...

@mrdoob
Owner
var monitor = new THREE.LoadMonitor();

var loader = new THREE.ImageLoader( monitor );
var texture = loader.loadTexture( 'blah.png' );

var loader = new THREE.JSONLoader( monitor );
var geometry = loader.load( 'bleh.js' );

monitor.addEventListener( 'complete', function () {
    var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) );
} );
@mrdoob
Owner

Maybe TextureLoader instead...

var monitor = new THREE.LoadMonitor();

var loader = new THREE.TextureLoader( monitor );
var texture = loader.load( 'blah.png' );

var loader = new THREE.JSONLoader( monitor );
var geometry = loader.load( 'bleh.js' );

monitor.addEventListener( 'complete', function () {
    var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) );
} );

Which would be complemented with THREE.TextureCubeLoader...

@mrdoob
Owner

And just to make it more clearer...

var monitor = new THREE.LoadMonitor();

var loader = new THREE.TextureLoader( monitor );
var texture1 = loader.load( 'blah.png' );
var texture2 = loader.load( 'blaah.png' );

var loader = new THREE.JSONLoader( monitor );
var geometry1 = loader.load( 'bleh.js' );
var geometry2 = loader.load( 'bleeh.js' );

monitor.addEventListener( 'complete', function () {
    var mesh1 = new THREE.Mesh( geometry1, new THREE.MeshBasicMaterial( { map: texture1 } ) );
    var mesh2 = new THREE.Mesh( geometry2, new THREE.MeshBasicMaterial( { map: texture2 } ) );
} );
@hedral

Yes better, LoaderMonitor is the most consistent (alongside var loader, TextureLoader)

Agree with TextureLoader instead of ImageLoader. "Texture" seems the right language for image files applied to 3D models.

@zz85

Loader classes are *Loader, so how about MonitorLoader ?

@mrdoob
Owner

That would, somehow, imply that MonitorLoader will load Monitors :)

@WestLangley
Collaborator

I sort of like the more generic var notifier = new THREE.Notifier().

@slaskis

What about new THREE.LoadQueue() ? I like LoadMonitor too but I figured it was missing among the options ;)

@carlcalderon

I agree on the more generic choice of Notifier or LoadingMonitor. To me LoadMonitor would indicate on performance as well.

@tdavies

not really what your asking but have you come across the promises pattern. My As3 take on it called Pledges is here http://www.tomseysdavies.com/2012/02/21/fluent-asynchronous-responder-for-network-calls/

It would then look something like this

var pledge1:Pledge = new TextureLoader() .addFile('blah.png').addFile('blaah.png').load()
var pledge2:Pledge = new JSONLoader() .addFile('blah.png').addFile('blaah.png').load()
Pledge.when( pledge1, pledge2).done(everythingLoadedHandler)

@mrdoob
Owner

@slaskis It is not really a Queue though. It's just "monitoring".

@carlcalderon True. LoadingMonitor is tarting to grow on me :)

@sexybiggetje

I second @WestLangley his suggestion for a generic notifier. Would even be able to go for LoadNotifier or ProgressNotifier in this schem.

@mrdoob
Owner

I would use different events rather than creating a class per event...

monitor.addEventListener( 'progress', function ( progress ) {
    console.log( progress );
} );
@mrdoob
Owner

Here's another option...

var texture = new THREE.Texture();

var imageloader = new THREE.ImageLoader();
imageloader.addEventListener( 'complete', function ( image ) {
    texture.image = image;
    texture.needsUpdate = true;
} );
imageloader.load( 'blah.jpg' );

var mesh = new THREE.Mesh( null, new THREE.MeshBasicMaterial( { map: texture } ) );

var jsonloader = new THREE.JSONLoader();
jsonloader.addEventListener( 'complete', function ( geometry ) {
    mesh.geometry = geometry;
} );
jsonloader.load( 'bleh.js' );

var monitor = new THREE.LoadingMonitor();
monitor.addEventListener( 'complete', function () {
    console.log( 'All done!' );
} );
monitor.add( imageloader );
monitor.add( jsonloader );

This aligns with the JavaScript API a bit more. This will require that there would be only a *Loader per item to load.

Definitely more bloated, but this allows anything to be tracked. And also anything to be built on top.

@mrdoob
Owner

That would translate to this if we kept our current style:

var texture = new THREE.Texture();

var imageloader = new THREE.ImageLoader();
imageloader.load( 'blah.jpg', function ( image ) {
    texture.image = image;
    texture.needsUpdate = true;
} );

var mesh = new THREE.Mesh( null, new THREE.MeshBasicMaterial( { map: texture } ) );

var jsonloader = new THREE.JSONLoader();
jsonloader.load( 'bleh.js', function ( geometry ) {
    mesh.geometry = geometry;
} );

var monitor = new THREE.LoadingMonitor();
monitor.watch( [ imageloader, jsonloader ], function () {
    console.log( 'All done!' );
} );

Which is not too bad...

@mrdoob
Owner

This wouldn't even require monitor.

var textureloader = new THREE.TextureLoader();
textureloader.load( 'blah.jpg', function ( texture ) {

    var jsonloader = new THREE.JSONLoader();
    jsonloader.load( 'bleh.js', function ( geometry ) {

        var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) );
        console.log( 'All done!' );

    } );

} );

It's not really what we're trying to solve but interesting nonetheless...

@arcanis

Or maybe a magic-powered (parallelized) queue list ?

new THREE.LoadingMonitor()
  .queue(THREE.TextureLoader, 'blah.jpg')
  .queue(THREE.JSONLoader, 'bleh.js')
  .complete(function (texture, geometry) {
    var mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { map: texture } ) );
    console.log('All done!');
  })
;

This kind of stuff will only be used during the initialization stage, so even if it requires to use Function.arguments and Function.apply for the internal magic, it shouldn't be a big deal in the overall performances.

@mrdoob
Owner

magic

I've been bitten by that kind of approach many times before :/

@gero3

#1797 a first try for a notifier

@mrdoob
Owner

I think I'm going to go with this one...

var texture = new THREE.Texture();

var imageloader = new THREE.ImageLoader();
imageloader.addEventListener( 'complete', function ( image ) {
    texture.image = image;
    texture.needsUpdate = true;
} );
imageloader.load( 'blah.jpg' );

var mesh = new THREE.Mesh( null, new THREE.MeshBasicMaterial( { map: texture } ) );

var jsonloader = new THREE.JSONLoader();
jsonloader.addEventListener( 'complete', function ( geometry ) {
    mesh.geometry = geometry;
} );
jsonloader.load( 'bleh.js' );

var monitor = new THREE.LoadingMonitor();
monitor.addEventListener( 'complete', function () {
    console.log( 'All done!' );
} );
monitor.add( imageloader );
monitor.add( jsonloader );

It's going to be a lot of breakage, but it just mirrors JavaScript and it's also easier to build things on top...

Now, I don't know if it should be... THREE.ImageLoader or THREE.TextureLoader and THREE.CubeTextureLoader...

@mrdoob
Owner

Also, while I'm at it, JSONLoader should be probably get renamed to GeometryLoader (as we have the SceneLoader)...

@zz85

just throwing another crazy idea to have the Loaders to initialize all these at once the least lines of code

var imageloader = new THREE.ImageLoader( monitor, complete, progress, error );
var jsonloader = new THREE.JSONLoader( monitor, complete, progress, error );

or even

new THREE.Monitor(
 [
 new THREE.ImageLoader( complete, progress, error ),
 new THREE.JSONLoader( complete, progress, error )
],

complete,
progress,
error
);

but what's interesting is that is, if the loaders completes loading before THREE.Monitor starts to monitor them? Its more hypothetical due to network delays, but assuming it happens, that Monitor might have to initialize the loading doing monitor.loadAll(), which turns monitor kind of a loader/scheduler itself too.

actually, i'm not sure if we are complicating things, because one simple solution to the problem here (if i understood correctly) would be just to have a simple counter like this.

var itemsToLoad = 100;
function itemLoaded() {
 itemsToLoad--;
 if (itemsToLoad==0) {
  alert('completed!');
 }
}

for (i=0;i<100;i++) {
 loader = new Loader();
 loader.addEventListener('complete', function() {
  itemLoaded();
 });
}
@arcanis
@zz85

@arcanis i don't remember a nextTick in browser JS.

var i = new Image();
i.onload = function() { alert('boo'); }'
i.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQIW2NkAAIAAAoAAggA9GkAAAAASUVORK5CYII=';

by the time line 3 is run, onload will be fired off for image.

(p.s. try to resist replying github emails, github doesn't format it well on the web :)

@arcanis

Wouch, I thougth email reply were prettier. Well, I'll try the app next time :)

Actually, it will be called at the end of the script, not at the 3rd line :

var i = new Image();
i.onload = function() { console.log(1); }
i.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQIW2NkAAIAAAoAAggA9GkAAAAASUVORK5CYII=';
console.log(2);

It should print 2 then 1 (on Chrome console it does something else : you will have 2 then undefined then 1. It's because the console will get the result value of the entire script (so undefined) before the onload is triggered).

I don't know if nextTick is the right word for browser js implementations, but I think it's still right in this context : events could not be triggered during the script execution : it would broke many things, and there would be concurrency issues. So browsers buffer all events then call them one at the time when the script is sleeping. That what I call nextTick.

@zz85

ah i see what you mean... i think you're right when executed in a script as opposed to running line by line... thanks for clarifying this.

@alteredq

I think I'm going to go with this one...

Hmmm, I'm not very fond of that one :S.

But we can give it a try, other options didn't look that much better either. This seems like a problem that doesn't lend itself easily to elegant solution :S.

I guess it's better just to start using something and see. If it would feel too grating, we can always change it.

@mrdoob
Owner

Yep. It seems super verbose to me. But I think the benefit of being able to attach many listeners to the same event is a big plus.

Will be doing some changes over the weekend.

@mmaclaurin

One hopes that eventually this can be wrapped to enable:

var myTex = { ground: "ground.png", sign: "sign.png"}
LoadTextures(myTex, callback);
// wherein the file names in myTex are replaced with the textures....?

@flaw75hh

are there any news? is it possible now to check, when all textures are loaded? Because the LoadMonitor gets an error when want to add a JSONLoader. It seem there is no AddListener function inside the Loader. And i dont see any call to a listener in the onLoad function of the texture loader.

@heavenlyhash

I'm working on a project where we're using a Promise library to solve this problem. It's a simple solution, solves this problem very nicely, and the solution works transparently without introducing any additional complexity into three itself, so I'm very pleased with it and thought I would share an example of how it turned out.

(Right now I'm using https://github.com/cujojs/when/, but I understand that any library implementing the Promises spec would do just as well.)

Here's an example of a method I wrapped my loader with:

function load (name) {
    deferred = when.defer();
    new THREE.JSONLoader().load(
        "media/meshes/" + name + ".js",
        function (geometry) {
            deferred.resolve(geometry);
        }
    );
    return deferred.promise;
}

Then my main method fires off the loader, then the scene rendering and other logic follows it with a snippet like this:

    when.all(
        load("model_1")
        load("model_2")
    ).then(function () {
        console.log "loader done"
        // add my models to the scene and render
    });

You can pass results back through a Promise library. I found that to actually be relatively irritating since I'm using the when.all function to have my control flow continue only after all assets are loaded, so I ended up having my load function cache my assets by name, then just got them by name again with later calls that didn't use promises... but you could arrange that however you felt most natural.

@mrdoob
Owner

Uhm. That's a nice approach :)

@hendrikp

If someone is preferring jQuery deferred objects syntax. (could be easily extended to support progress and error)

    var staticData = {};
    that.staticData = staticData;

    function loadJSON(staticData, nameg, namem, url)
    {
        var defer = $.Deferred();
        var loader = new THREE.JSONLoader();

        loader.load(url, function(geometry, materials) {
            staticData[nameg] = geometry;
            staticData[namem] = materials;
            defer.resolve();
        });

        return defer;
    };

    function loadTexture(staticData, name, url)
    {
        var defer = $.Deferred();
        var loader = new THREE.TextureLoader();
        loader.addEventListener( 'load', function ( event ) {
            staticData[name] = event.content;
            defer.resolve();
        });
        loader.load(url);
        return defer;
    }

    function loadCollada(staticData, name, url)
    {
        var defer = $.Deferred();
        var loader = new THREE.ColladaLoader();
        loader.load(url, function(collada) {
            staticData[name] = collada.scene;
            defer.resolve();
        });

        return defer;
    };

    $.when( 
        // Models
        loadJSON(staticData, 'treeGeometry', 'treeMaterial', 'js/widgets/driver/tree_lod2.js'),
        loadCollada(staticData, 'car', 'js/widgets/driver/car.dae'),
        // Skybox
        loadTexture(staticData, 'skyFront', 'images/field_front.jpg'),
        loadTexture(staticData, 'skyBack', 'images/field_back.jpg'),
        loadTexture(staticData, 'skyTop', 'images/field_top.jpg'),
        loadTexture(staticData, 'skyBottom', 'images/field_bot.jpg'),
        loadTexture(staticData, 'skyLeft', 'images/field_left.jpg'),
        loadTexture(staticData, 'skyRight', 'images/field_right.jpg'),
        // Grass
        loadTexture(staticData, 'roadTexture', 'js/widgets/driver/road.jpg'),
        loadTexture(staticData, 'groundTexture', 'js/widgets/driver/grass.jpg')
    ).then(function() {
        // All data loaded
        that._generateScene();
    });
@mrdoob mrdoob closed this May 27, 2013
@NileshWalkoli

HI Mr doob, am not able to render the mesh (having texture geometry) using Canvas Renderer into iPad. Below are my init and render functions. The below code works fine in desktop safari, however when I try to open in in iPad safari. It hangs the iPad and forcefully quits the Safari browser. Can you please let me know why this is happening.
init();
animate();

        function init() {

        camera = new THREE.PerspectiveCamera( 12, window.innerWidth / window.innerHeight, 1, 1000 );
        camera.position.set( 50, 50, 50 );

        controls = new THREE.TrackballControls( camera );

        controls.rotateSpeed = 1.0;
        controls.zoomSpeed = 1.2;
        controls.panSpeed = 0.8;

        controls.noZoom = false;
        controls.noPan = false;

        controls.staticMoving = false;
        controls.dynamicDampingFactor = 0.3;

        controls.keys = [ 65, 83, 68 ];

        scene = new THREE.Scene();
        var light = new THREE.PointLight(0xffffff);
        light.position.set(-100,200,100);
        scene.add(light);

          // Load in the mesh and add it to the scene.
        var loader = new THREE.JSONLoader();
        var loader1 = new THREE.TextureLoader();
        loader.load("cordis2.js", function(geometry){
                geo = geometry;
                loader1.load( 'pouch.jpg', function ( texture ) {
                var material = new THREE.MeshBasicMaterial( { map: texture, overdraw: true } );
                var mesh = new THREE.Mesh( geo, material );
                scene.add( mesh );

                } );
        });

        renderer = new THREE.CanvasRenderer();
        renderer.setSize(900, 600);
        document.body.appendChild( renderer.domElement );
   }

    function animate() {

        requestAnimationFrame( animate );

        controls.update();

        renderer.render( scene, camera );
        //renderer2.render( scene2, camera );

    }
@mrdoob
Owner

As stated in the guidelines, help requests should be directed to stackoverflow. This board is for bugs and feature requests.

@benchung5

Here's an example of a simple loading screen (with HTML5 Loading icon) Here: http://benchung.com/loading-animation-three-js/ using LoadingManager.
Edit Summary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment