How to additively load scene without a hitch

I want to be able to load potentially pretty large scenes additively, but without causing a hitch in the game by doing so.

I’ve been building off the example project for additive loading, but adding some code where I am trying to load all the assets first, and make sure they are loaded, before actually loading the scene. The code is pretty rough around the edges while I try to see if this can work.

The basic process I have is this:

  1. Trigger loading process with a scene name, which is also a tag. All relevant assets in the asset registry are tagged with the scene name
  2. Find all assets with the tag and load them
  3. Check on update whether all the assets are loaded
  4. When all assets are loaded, add the scene

Here is my example project: https://playcanvas.com/editor/scene/1164703

I have deliberately made the ‘Van’ scene very heavy indeed, with a few really big textures. Nothing is checked to be preloaded.
Here’s how to see the process:

  1. Launch the project
  2. Click ‘Load Playboy Anim’
  3. Click left arrow and then ‘Load Van Model’
    There’s a long hitch happening after the assets are supposed to be loaded, while adding them to the scene.
    But here’s the thing, if you go back and click ‘Load Playbot Anim’ again it’s set up to remove the Van scene. And then if you go back and click ‘Load Van Model’ there’s only a very minor hitch as it brings the scene back in again.
    On that second time around, it seems that the resources really have been loaded into memory. So my question is, how can I (asynchronously) load all these assets into memory such that when they are called to be added into the scene the first time, it is as slick as it is the second time around?

All the code is in scene-manager.js, but here is the code called to start loading the assets:

SceneManager.prototype._loadSelectedScene = function () {
    if (!this._loadingScene) {
        this._loadingScene = true;
        this._loadSceneButton.active = false;
    
        this.sceneName = this.sceneFilenameStrings[this._selectedSceneIndex];
        this.scene = this.app.scenes.find(this.sceneName);
        this.sceneLoaded = false;
        
        this.assets = this.app.assets.findByTag(this.sceneName);
        this.loadingAssets = true;
        
        console.log('loadAssets: ' + this.sceneName);

        var assetLoaded = function() {

        };

        for (var i = 0; i < this.assets.length; i++) {
            this.assets[i].ready(assetLoaded);
            console.log("Loading: " + i / (this.assets.length-1) + " (" + this.assets[i].name + ")");
            this.app.assets.load(this.assets[i]);
        }
    }
};

And here is the code running on update checking whether the assets have actually loaded, and finally loading the scene:

SceneManager.prototype.update = function (dt) {
    var self = this;
    
    if(this.loadingAssets){
        var assetsLoaded = true;
        for(var j = 0; j < this.assets.length; j++){
            if(!this.assets[j].loaded){
                assetsLoaded = false;
                break;
            }
        }
        
        if(assetsLoaded){
            this.loadingAssets = false;
            
            console.log("All assets loaded!");
            
            var numChildren = this.sceneRootEntity.children.length;
            for(var i = 0; i < numChildren; i++){
                var child = this.sceneRootEntity.children[i];
                if(child.tags.has("Van")){
                    child.destroy();
                }
                if(child.tags.has(this.sceneName)){
                    this.sceneLoaded = true;
                }
            }

            if(this.sceneLoaded){
                this._loadingScene = false;
                this._loadSceneButton.active = true;
                return;
            }

            this.app.scenes.loadSceneHierarchy(this.scene.url, function (err, loadedSceneRootEntity) {
                if (err) {
                    console.error(err);
                } else {
                    loadedSceneRootEntity.reparent(self.sceneRootEntity);    
                    self._loadingScene = false;
                    self._loadSceneButton.active = true;
                }
            });
        }
    }
};

Is there a way to do this loading asynchronously?

Most of the hitch is going to be when all the entities are initialised in the heavy scene.

Not sure how you can avoid that without having them be disabled to start and enabling groups of entities over several frames.

Edit: just read that it seems to be more about assets, brb

It looks like you aren’t unloading assets when switching scenes so assets like textures are already uploaded to the GPU, models are already parsed etc.

Ah, OK, so if I’m reading that right it’s the stage where the assets (I guess particularly the large textures) are uploaded to the GPU that causes the hitch?

Is there any way to that asynchronously, or is that always going to cause a hitch?

I might experiment with bringing things in one by one over a series of frames, but I feel like that might just cause a long string of small hitches, in which case one large hitch might be preferable.

EDIT

Additionally, one of my aims is to have a progress bar during this process. It seems like I won’t be able to report progress during the hitch since everything will be frozen while it loads, is that true?

Looks like it, yes. I believe this happens is when the texture is first used to be rendered. So having them all happen in the same frame, causes that spike.

@mvaligursky Any idea what could be done to mitigate this besides doing this gradually over several frames?

You can do progress on the download of assets, just not when it gets uploaded to the GPU unfortunately.

There’s not much you can do, and slow down cannot be really avoided.
You could upload those in stages, one a frame, but that’s probably easier said that done. At the moment the textures get automatically uploaded when they’re visible in the frame.

In the past I’ve done a texture streaming system (different engine) where initially the scene would only have small textures assigned (say 64x64) to download quickly and minimize start up time to the level. Then large textures would be downloaded, and once a frame one of them would get set up on material (and so uploaded to GPU).

1 Like