Skip to content
This repository

LoadTracker #1751

Closed
mrdoob opened this Issue · 37 comments

16 participants

Mr.doob hedral Joshua Koo WestLangley Robert Sköld Carl Calderon Tom Davies Martijn de Boer Maël Nison gero3 AlteredQualia Matt MacLaurin flaw75hh Eric Myhre Hendrik Polczynski NileshWalkoli
Mr.doob
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

Mr.doob
Owner

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

LoadMonitor, LoaderMonitor, LoadingMonitor, ...

Mr.doob
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 } ) );
} );
Mr.doob
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...

Mr.doob
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.

Joshua Koo
zz85 commented

Loader classes are *Loader, so how about MonitorLoader ?

Mr.doob
Owner

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

WestLangley

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

Robert Sköld

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

Carl Calderon

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

Tom Davies

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)

Mr.doob
Owner

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

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

Martijn de Boer

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

Mr.doob
Owner

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

monitor.addEventListener( 'progress', function ( progress ) {
    console.log( progress );
} );
Mr.doob
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.

Mr.doob
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...

Mr.doob
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...

Maël Nison

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.

Mr.doob
Owner

magic

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

gero3
gero3 commented

#1797 a first try for a notifier

Mr.doob
Owner
mrdoob commented

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...

Mr.doob
Owner
mrdoob commented

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

Joshua Koo
zz85 commented

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();
 });
}
Maël Nison
arcanis commented
Joshua Koo
zz85 commented

@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 :)

Maël Nison
arcanis commented

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.

Joshua Koo
zz85 commented

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.

AlteredQualia

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.

Mr.doob
Owner
mrdoob commented

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.

Matt MacLaurin

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.

Eric Myhre

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.

Mr.doob
Owner
mrdoob commented

Uhm. That's a nice approach :)

Hendrik Polczynski

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();
    });
Mr.doob mrdoob closed this
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 );

    }
Mr.doob
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.