Dynamic Resource Loading

Hello Everyone! New here to playcanvas. Love it so far! Trying to make it work!

The Dream:
I have been searching for a way to dynamically load resources into the game since i do not know the resource names nor what resources i even need until runtime.

The Problem:
Essentially i have a large number of characters for a 2d game that have many many sprites. I dont want to do alot of drag and drop operations in the editor only to have the animations changed underneath me then I have to redo all that mess. Additionally, i will never know what characters will need to be loaded at any given time. Instead, I like to make the animations data driven where I store the file names somewhere and retrieve them when necessary followed by loading the most current needed images into memory.

Solution1: (failed)

context.assets.loadFromUrl("…/assets/asset_name.jpeg", “texture”)

.then(function(result){

this.promised_resource = result.resource[0];});

update func()
if(this.promised_result !== null)
this.entity.model.material.diffuseMap = this.promised_resource;

What keeps me up at night:

  • is this “…/assets/asset_name.jpeg” a legal url ?

  • how to debug whether an image is loaded using a resource load technique ? currently using console.debug(returned_object)

  • maybe this is possible ? pc.script.attribute(“letters”, “asset”, GETRESOURCES(args), {type: “model”})

where the GET function suggests i load the resources dynamically and set the values later, but programmatically…

Any advice? I want to port my game to play canvas, but do not want to move over x thousands of images by hand…

Hi,

context.assets.loadFromUrl can be used to load external files. You should use the URL for your existing assets. It sounds like you are talking about hosting your image files your own server? In which case you can simply use the URL for the image file on your server.

Note, you must serve files with Cross-Origin (CORS) permissions otherwise they will not work in WebGL.

var url = "http://example.com/my_image.jpg";
context.assets.loadFromUrl(url, "texture").then(function (result) {
    var image = result.resource[0];
    this.entity.model.material.diffuseMap = image;
    this.entity.model.material.update();
}.bind(this), function (err) {
    console.error(err);
});

This should work, assuming the jpg is served with correct CORS headers.

Debugging is best done using something like Chrome Developer tools. Put a break point inside the return function.

An script attribute of type “asset” is used to expose a variable in the Editor so that you can assign assets from the tools. I’m not sure how this would be useful if you aren’t uploading the images into your project?

Hope that helps!

1 Like

Hey Dave! Thank you for your help and direction! Especially the tip on CORS permissions (haven’t figured out just yet. Kinda confusing)

To clarify, yes I would like to either host the images myself so that i can dynamically call them via api or dynamically call them from the playcanvas resource cache. I think in order for the latter to work i would need to be able to add and remove things to the cache as needed manually since the engine does not load assets if they are not directly involved in the “pack”. if the scene cache grows too much, I would want to remove assets from the cache as well otherwise the application may nom nom all the memory. Was thinking that using the script attributes may be a way to easily interface with the resource cache because i can’t seem to interface with it directly. (im sure im doing something wrong)

I have done some extensive testing this morning to try and simply change a texture from either remote source or local source. Hopefully this helps others with similar needs/wants.

What Kinda Sorta Worked:

  1. Creating a brand new material, configuring it, then loading it from the resource cache worked as expected. (but likely has high overhead)

var tex = context.assets.find(“browserepic”, pc.asset.ASSET_TEXTURE);
var box = new pc.Entity();

context.systems.model.addComponent(box, {type: “box”});
var newmat = new pc.PhongMaterial();
newmat.diffuseMap = tex.resource;

newmat.update();
box.model.model.meshInstances[0].material = newmat;

I have a feeling that creating a new material each animation frame is a bad idea though. Must have some sort of overhead and possible memory leak if i use them once then just leave them laying around in the cache.

Things That Didn’t Work As Expected:

  1. Simple way to update material textures (preferably without swapping materials)
  • this.entity.model.material.diffusiveMap = image; does not do anything observable. even after calling an update() on the material. maybe the “this.entity.stuff” is read only or something.
  1. using the mysterious context.assets.addAsset(result.asset) on the callback but before trying to use the texture. Hoping that it would add external resources to the cache because i just can’t get the blasted image to load.

  2. sometimes trying to set the texture using this.entity.model.model.meshInstances[0].material.diffuseMap = new_texture_var caused silent errors where the script would halt and not execute anymore code. No idea why.

  3. On my webserver (where content is stored). Setting up a quick php index page wasn’t good enough to satisfy the CORS security thing. I don’t know how to test it either? Tried using http://client.cors-api.appspot.com/client, but i have no idea what is success and what is failure nor how playcanvas issues the request ( GET?).

<?php header("Access-Control-Allow-Origin: *"); $test_image = 'fistrunright005.png'; header('Content-Type: image/png'); header("Location:{$test_image}"); exit(0); ?>

Some quick answers to some of the things that didn’t work:

  1. Should be this.entity.model.material.diffuseMap. Make sure you are assigning an object of type pc.Texture to this.

  2. addAsset will just add the asset to the registry. Probably not what you want to do.

  3. You should see any errors reported in the developer console. It can also be useful to enabled “Break on handled exceptions” to catch errors. Or stick a breakpoint on the line. If you are only getting the error sometimes, perhaps you aren’t waiting for the texture to load?

  4. You should be able to test your serve by loading the image in Chrome. The developer tools will show you all the headers present in the response.

Hope that helps.

A couple of extra things while I remember :smile:

You definitely do not want to create a material for every animation frame! You should probably combine all your animation frames in to texture atlases (all frames in one image). Then you can modify the UV offset on the material to switch between frames. This should also cut down on the “thousands” of images you need to load.

For serving your files. If you are just serving static image files, running them through PHP is a pretty inefficient. I would recommend something like nginx. It’s easy to configure for static files and straight forward to add the CORS headers.

Finally, looking at the code I’m not sure the default TextureResourceHandler is set up to load CORS images by default… I’ll have to investigate a little further. It’s a hack, but you might have to add:

context.loader._handlers['texture'].crossOrigin = true;

To get it to work.

1 Like

Hey Dave!

I wanted to post back here to tell you thank you!

  1. Yes I was able to get it to work with this.entity.model.material.diffuseMap = some pc.Texture

  2. your right again about addAsset. The loadFromUrl() does this automatically according to the documentation.

  3. I have been hanging out in the console and just debugging out objects to see what they are made of. It has been immensely helpful

  4. I was able to load the images in chrome just fine, and php is definitely not a permanent thing. Just wanted to get something up quick without having to modify my nginx config (funny you mention nginx :slight_smile: I just started using thay instead of apache on all my stuff).

At any rate, I have decided that I will use play canvas on a fresh project that I have had in mind that is smaller and in 3d. This project relies on frame by frame animation because if i set them up into atlases, i would not have enough space on the atlas! i know it sounds crazy, but this games graphics are kinda old school and use hand drawn sprites. i tried to atlas them out and ended up with like 3 2048x2048 images for one character or 6 1024x1024. ahhh. I will build that game in another engine. There can easily be over 100 sprites in size of 128x128 pixels per frame for a basic character.

Anyways, I appreciate the direction Dave. Be seeing you around

Can this be done then?:


  var self = this; self.html3 = null;

    var url = "https://[personal-website]/html3.html";

self.app.assets.loadFromUrl(url, "html").then(function (result) {
    var htmlImage = result.resource[0];
    self.html3 = htmlImage;  console.log("load");
}.bind(self), function (err) {
    console.error(err);
});

What are you expecting as a result/output from the code?

Earlier (within the js-file below) I have been able to load another html-asset by button-click. Now I am aiming at doing the same, but with an external html-asset file - here is the broader context:


 var Ui = pc.createScript('ui');

Ui.attributes.add('css', {type: 'asset', assetType:'css', title: 'CSS Asset'});
Ui.attributes.add('css2', {type: 'asset', assetType:'css', title: 'CSS Asset2'});
Ui.attributes.add('html', {type: 'asset', assetType:'html', title: 'HTML Asset'});
Ui.attributes.add('html2', {type: 'asset', assetType:'html', title: 'HTML Asset2'});

Ui.prototype.initialize = function () {
    // create STYLE element
  

        
       
    
    
    var style = document.createElement('style');

    // append to head
    document.head.appendChild(style);
    style.innerHTML = this.css.resource || '';
    
    // Add the HTML
    this.div = document.createElement('div');
    this.div.classList.add('container');
    this.div.innerHTML = this.html.resource || '';
    
    // append to body
    // can be appended somewhere else
    // it is recommended to have some container element
    // to prevent iOS problems of overfloating elements off the screen
    document.body.appendChild(this.div);
    
    
  this.divDisplay = document.createElement('display');
    
     
    
    this.counter = 0;
    
    this.bindEvents();
    
    
 
};

Ui.prototype.initialize2 = function() {
    var self = this; self.html3 = null;

    var onAssetLoad = function() {
        // Show the entity when the material asset is loaded
       // self.runeCard.enabled = true;
        console.log("load!!");
    };
    
   /* if (self.html2.resource) {
        // The material asset has already been loaded 
        onAssetLoad();
    } else {
        // Start async loading the material asset

        //load(this.html2);
              self.html2.once('load', onAssetLoad);
    }*/
    
    
        var url = "https://[website]/FrdFrm/html3.html";

self.app.assets.loadFromUrl(url, "html").then(function (result) {
    var htmlImage = result.resource[0];
    self.html3 = htmlImage;  console.log("load2!!");
}.bind(self), function (err) {
    console.error(err);
});
        
    
    
    var style = document.createElement('style');

    // append to head
    document.head.appendChild(style);
    style.innerHTML = this.css2.resource || '';
    
    // Add the HTML
    this.div = document.createElement('div');   
    this.div.classList.add('container2');
    
   // this.html2 = 
    
    this.div.innerHTML = this.html3.resource || '';
    
    // append to body
    // can be appended somewhere else
    // it is recommended to have some container element
    // to prevent iOS problems of overfloating elements off the screen
    document.body.appendChild(this.div);
    
     
    
    
    this.divDisplay = document.createElement('display');

    
    this.counter = 0;
    
    this.bindEvents();
};

Ui.prototype.bindEvents = function() {
    var self = this;
    // example
    //
    // get button element by class
    var button = this.div.querySelector('.button');
    var counter = this.div.querySelector('.counter');
    var cont1Vaek = this.div.querySelector('.c2');
    // if found
    if (button) {
        

        
        // add event listener on `click`
        button.addEventListener('click', function() {
             cont1Vaek.style.display = "none";
            self.initialize2();
        }, false);
    }

    if (counter)
        counter.textContent = self.counter;
};

(PS: some of code above; the in-app-ressource-load of ‘another-html-asset’ has been commented out/deleted)