Best way to load/update a set of textures in scene?

Dears,
I had a look at the doc and forum about this and tried to browse through my bookmarks but couldn’t find the information. So I thought, before doing some clumsy pseudo-solution :wink:, I would just ask…

Case is simple: I have an interactive display that shows a set of 60 images/textures per category. And the user can pick from a number of categories. I need to seamlessly download/update this set on category change.

Now, I’m looking for the best way to either load or update URL for these. And then eventually dispose of’em or at least make sure they do not add up and eventually create a memory leak.

I had a look at the assetManager but it looks a bit tedious since (apparently) you need a task per texture (or can you create an array?). By any means, I suppose it would be better somehow if I loaded a script (js or json) in which I would pass all the textures URL but also a description for each which I will also be needing. Then, assign them to my already existing materials and gui tb (for the description).

Does any of you have an example to share which I could use as a base. May be @shaderbytes I sort of remember you did something similar. I stumbled over this post this morning but I sort of remember other posts you did about loading textures. Just can’t find’em anymore.

Thanks in advance for your support and expert advise on this and meanwhile, have a great day :sunglasses:

hey there @mawa :wink:

Yeah the best workflow in the end for this is to have the resources paths in some sort of loadable data , generally json. You could have all categories in one json file but also it could be in separate files , depends how you prefer to structure such things , pro’s and con’s to both ways that even out so , doesnt really matter.

I personally keep a cache of all texture objects I create , using the url as the caching key The Babylon Texture object does have a dispose() method that should release the resources making them available for garbage collection.

I presume with you needing to load 60 textures per category , it would be best to dispose of all the previous ones before creating the new ones. Take note , you wont always see an immediate reduction in the memory footprint everytime you perform this dispose, the browser will do this when it feels the need to do so on its own timeline.

For code as mentioned in the post you referenced , i did resort to waiting for the texture to load before assigning it to the material to prevent that dissapearing issue

my material update function handles just routing the call to 3 types of materials in my scene , not very fancy , just some conditionals :

function updateMaterial(materialData) {
  
  let mat = collectionMaterials[materialData.name];  
  if (!mat) {   
    createUpdateMaterialPending(materialData);
    return;
  }

  if (mat instanceof NodeMaterial) {
    let wrapper = collectionMaterialsWappers[mat.name];
    if (wrapper) {
      if (wrapper instanceof NodeMaterial1) {
        updateMaterialNode1(materialData);
      }
      if (wrapper instanceof NodeMaterial2) {
        updateMaterialNode2(materialData);
      }
    }
  } else {
    updateMaterialPBR(materialData);
  }
}

this is my PBR update function :

function updateMaterialPBR(materialData) {
 
  let mat = collectionMaterials[materialData.name];

  //props
  for (let prop in materialData.props) {
    mat[prop] = materialData.props[prop];
  }
  //colors
  for (let prop in materialData.colors) {
    let colorKey = materialData.colors[prop];
    console.log(colorKey)
    let colorAsset = colorCache[colorKey];
    if (!colorAsset) {
      let colorHex = getColor(colorKey);
      if (colorHex.length > 7) {
        colorAsset = colorCache[colorKey] = Color4.FromHexString(colorHex).toLinearSpace();
      } else {
        colorAsset = colorCache[colorKey] = Color3.FromHexString(colorHex).toLinearSpace();
      }
    }
    mat[prop] = colorAsset;
  }

  //textures
  for (let prop in materialData.textures) {
    let textureData = materialData.textures[prop];
    let textureURL = textureData.url;
    if (!textureURL) {
      textureURL = getTexture(textureData.key);
    }

    //the cache key is using the url now instead of key , since te same texture is listed under several keys for some assets
    //and if I dont use the url , then it will create redundant textures in memory
    let textureAsset = textureCache[textureURL];
    if (!textureAsset) {
      textureAsset = textureCache[textureURL] = new BabylonTextureAsset(textureURL);
      textureAsset.texture = new Texture(window.location_url + textureURL + "?cache_uid=" + buildNumber.value);
      for (let textureProperty in textureData.textureProperties) {
        textureAsset.texture[textureProperty] = textureData.textureProperties[textureProperty];
      }
      textureAsset.texture.onLoadObservable.addOnce((eventData) => {
        var texCacheKey = textureURL;
        var mk = materialData.name;
        var ck = prop;
        collectionMaterials[mk][ck] = eventData;
        textureCache[texCacheKey].loadSuccess();
      });
      textureAsset.load();
    } else {
      if (textureAsset.texture.isReadyOrNotBlocking()) {
        mat[prop] = textureAsset.texture;
      } else {
        //more than one material might want to use this same texture , and the first call to use it would cause a creation and load,
        //since subsequent calls will find it exists they then also need a callback for applying it once loaded
        textureAsset.texture.onLoadObservable.addOnce((eventData) => {
          var mk = materialData.name; // mk is material key
          var ck = prop; // ck is channel key
          collectionMaterials[mk][ck] = eventData;
        });
      }
    }
  }
}

there is a bit happening in there but really:

it gets a reference to the material and set properties ,

Some updates are primitive values , so they are just handled in a loop at first
materialData.props

Some updates are color values , so they are handled specifically
materialData.colors

Some updates are texture values , so they are handled specifically
materialData.textures

take note a texture can also have properties , these are primitive values again and just handled with a loop again within the block of code handling texture updates ( things like texture level or UV index etc)
textureData.textureProperties

There is some extra complexity in my texture handling because my update system could have more than one material calling for an update that will share a texture , so I need to track this but anyway , hopefully you can see what is happening in the code , the part from the thread you mentioned :
inside the observable ,

textureAsset.texture.onLoadObservable.addOnce...

I assign the texture to the channel on the material :

collectionMaterials[mk][ck] = eventData;

when disposing :

for (let prop in textureCache) {
    let texAsset = textureCache[prop];
    if (texAsset.texture) {
      texAsset.texture.dispose(false, true);
    }

    texAsset.texture = null;
    texAsset = null;
  }
  textureCache = null;
2 Likes

forgot to mention this is just a utility to handle managing the textures , specifically preloading :

import { useAssetPreloader } from "@/stores/AssetPreloader";
export default class BabylonTextureAsset {
  constructor(key) {
   this.key = key;  
   this.texture = null;
   this.loading = false;
   this.loaded = false;
   this.loadPercent = 0;
   this.enabled = false;
   this.assetPreloader = null;
   this.lengthComputable = false;
  }
  dispose(){    
    this.texture.dispose();  

  }
  load() {
    this.assetPreloader = useAssetPreloader();
    this.loading = true;
    this.assetPreloader.load(this);
  }
  loadProgress(event) {
    if (event) {
      this.lengthComputable = event.lengthComputable;
      if (event.lengthComputable) {       
        this.loadPercent = event.loaded / event.total;       
      }
      this.assetPreloader.progress();
    }
  }

  loadSuccess(event) {
    
    this.loading = false;
    this.loaded = true;
    this.assetPreloader.complete(this);
  
  }
 
}
1 Like

First, thanks so much for your time and detailed answer with code sample and everything :hugs:. You rock :metal: I knew you where the one man to ask for this :man_superhero: …yet:

I’m afraid you somewhat overestimated my low skills when it comes to code :face_with_hand_over_mouth: I would still have a couple of questions:

So I need to load it as a module. Where is it to be found (sorry to ask). Also I’m just doing js, so can I import it as module in html?

I’m still a bit confused here. Is collectionMaterials an array of all my materials that require an update?

And then, what exactly should be in my json, knowing that (my case is simpler than yours in that) I only need to update the texture, the texture is not used twice, it’s all just PBR and the same texture is assigned to just two channels, albedo and emissive.

I know I’m asking a lot and you already did more than half of the job for me (thanks again) but if you could just enlighten me on how these last parts would work together, you would really make my day :grin:?!

your json would just be something like

{

category_1:[
{material_name:"target_material_name_in_here",texture_url:"target_texture_path_here"},
{material_name:"target_material_name_in_here",texture_url:"target_texture_path_here"},
],

category_2:[
{material_name:"target_material_name_in_here",texture_url:"target_texture_path_here"},
{material_name:"target_material_name_in_here",texture_url:"target_texture_path_here"},
],

}

an object with key , values. The key is whatever the category name is , its an array with objects that have the material to update and the url. Put as many as you want in that array , 60 i believe you mentioned. then create as many categories as you want.

so then you have some function to recieve events from the UI , that event payload could simply pass the expected key string for the category.

use the key to get the array of objects from the loaded json.

then just loop that array , get reference to material , dispose old textures , create new textures using the url from the data…

some old school javascript , without arrow function array loops , to help you see it clearly :wink:

function updateMyAmazing3DObject(categoryKey) {
  //loadedJSON would be the variable holding the .. drum roll .. drdrdrdrdr .. loaded json
  let categoryData = loadedJSON[categoryKey];
  for (let i = 0; i < categoryData.length; i++) {
    let categoryDataItem = categoryData[i];
    let material = scene.getMaterialByName(categoryDataItem.material_name);
    if (material) {
      let albedoTexture = material.albedoTexture;
      if (albedoTexture) {
        albedoTexture.dispose();
      }
      material.albedoTexture = new Texture(categoryDataItem.texture_url);

      let emissiveTexture = material.emissiveTexture;
      if (emissiveTexture) {
        emissiveTexture.dispose();
      }
      material.emissiveTexture = material.albedoTexture; //you said its the same texture right?
    }
  }
}

Yup :smiley: Thanks a lot :pray:
Obviously, the ‘old school’ version is much more within my reach :older_man:
I had this up and running this morning. Works perfectly.

Looking back at the initial (non-noob version :stuck_out_tongue_winking_eye:) I was just wondering about this caching and the utility. Reading the text once more, I suppose it’s an external tool, is it?
I don’t think I really need it but I just wanted to know… Anyways, thanks again. You saved me a lot of time and troubles with this. I owe you one. :beer: or a couple :beers::wink: