How to avoid duplicate materials with multi asset loading

Hi All

So I have a scene that loads many different assets into it. Many assets have same named materials and it is meant to be this way. So if I change the material texture , it shouldd update all assets using that material.

Of coarse I noticed this was not happening and so logged the materials array of the scene and noticed many instances of the said single material , instead of one.

So my question is how do I load an asset and when it loads it does not create a duplicate material , but rather if it finds that material in the scene already it just maps to that one?

Hello, the materials have to be shared in your DCC tools (or you need to run some code to share them after load)

HI thanks,

Well they were shared in the main file from my DCC , just that individual parts were exported out of the main file as separate .glb.

This is to reduce un-needed bloat in the babylon scene by only doing on demand loading.

Anyway I will look into doing some runtime material management post loading to see to get rid of any duplicates created.

cheers

1 Like

@Deltakosh I know we marked this as solved but I thought to share some code here to help others who might want to look into this.

The reason being , it was not a straight forward thing to do as I discovered some peculiar race conditions that will definitely catch a person of guard when trying to do this.

The race condition is in thinking that the scene material list gets updated exactly once just before each loadSuccess callback while loading several items.

What I found is that when loading several items , it is possible the list can be populated by several items as they are parsed internally before the callbacks. I expected one round of updates to the scene materials per asset just before each callback.

For example if you load say several .glb assets… on the first asset load Success you might see the materials list has the materials of this asset only, all is good.

On the second loadSuccess , you would expect to see only the addition of that assets materials to the list.

This is where you will fault. The scene materials could be populated by several other assets already.

Anyway that is quite a mouthful , sorry :wink: anyway here is a snippet of code I did that managed to get it all working even with this unusual behaviour :

this.Asset.prototype = {
	//other code before here...


	loadSuccess:function(event){			
		this.loaded = true;
		this.loading = false;			
		var m = this.assetController.sceneController.scene.materials;
		var ml = this.assetController.sceneController.scene.materials.length;
		
		//this is to keep a separate collection of only single instance materials with same name , the first loaded is the one used  for all subsequent asset loads
		for(var i=0;i< ml;i++){
				
			if(this.assetController.collectionMaterials[m[i].name] === null || this.assetController.collectionMaterials[m[i].name] === undefined){					
				this.assetController.collectionMaterials[m[i].name] =  m[i];
				//change sorting for alpha textures
				if(m[i].albedoTexture){
					if(m[i].albedoTexture.hasAlpha){				
						m[i].transparencyMode = 1;
					}		
				}
			}			
		}

		//collection to dispose of duplicates
		var disposableMaterials = [];

		//recursive function to handle marking duplicates to be disposed and also setting those assets to use the materials already in the scene
		this.handleDuplicateMaterials = function(nodesArray){		
						
			for(var i=0;i< nodesArray.length;i++){					
				var n = nodesArray[i];									
				if(n.material){						
					var existingMaterial = this.assetController.collectionMaterials[n.material.id];
					if(existingMaterial !== null && existingMaterial !== undefined ){							
						if(n.material.uniqueId !== existingMaterial.uniqueId){								
							disposableMaterials.push(n.material);
							n.material = null;								
							n.material = existingMaterial;
						}
					}
				}
				var nc = n.getChildren();
				if(nc !== null && nc !== undefined ){
					if(nc.length > 0){
						this.handleDuplicateMaterials(nc);
					}
				}
			}
				
		};
		
		//only call the recursive function using the __root__  node , as other items in the event list will all be processed via this recursice loop
		this.handleDuplicateMaterials(event[0].getChildren());
		
		//finnally dispose of the duplicates. 
		for(var i=0;i< disposableMaterials.length;i++){				
			disposableMaterials[i].dispose(true,true);
		}


		disposableMaterials = [];
			
		//othe code follows here ....	
	}	
	
};
6 Likes

ok i just updated the babylon engine for this project to see if it can resolve some issues clients had on older ipads , im sure it is obviously something apple just broke without a care in the world and you guys had to work around , so hoping for the best there

BUT

In doing the engine update , the above code stopped working , I traced it down to the node.material.id now having integers appended to the end when they are duplicates , which circumvented my code trying to look for the already existing material. Anyway I just changed my code to now use node.material .name instead. That works again.

Just thinking should something like this not become a engine feature? surely it is a common thing to have a scene broken into several parts for optimized on demand loading , which will cause material duplication.

Two issues with that , one it probably adds another unnecessary draw call ( not sure about that but based on previous experience with other real time engines , this is the case )

But really it is this next point… when updating a material texture at runtime… obviously if the parts loaded at different times that are supposed to have the same material dont share the same material, then you will only get one material updating and all other duplicates unaffected, which is undesired.

This is basic configurator requirements. I know people voted strongly in the golden path thing for configurators , so really this should be considered to be made a native solution , not a runtime patch.

anyway, just wanted to let everyone know about the code not working after engine version update

1 Like

cc @syntheticmagus

just to point out , i know the issue came about due to me actually using “name” to cache and “id” to reference , I just got away with it because they used to be the same. That said it should not have been assumed that id and name have to always be the same because then having two properties for the same value doesnt make sense anyway. So that was my fault

but anyway the mention of this becoming a native feature is something I really think makes sense :wink:

cheers

Hi shaderbytes,

I do have some golden path Dev Stories coming out in the viewer/configurator space, but they won’t directly deal with this issue as those Dev Stories don’t feature duplicate materials. I actually wouldn’t recommend duplicate materials in a production scenario in general: doubled materials means doubled downloads, and materials tend to be (by far) the largest resources in a scene. More broadly, though, this sounds to me like an asset workflow question — if we don’t want duplicate materials, what do we do instead — and I think that could be the topic of some very interesting discussions/potential features.

@PatrickRyan, any thoughts on this? If I have two different models that I ultimately want to have the same (hefty) material, my thought would be to export them both with ultra-light placeholder materials, then export the hefty material separately and apply it to the models at import time. That’s a little elaborate, but from a code perspective I think it makes some sense. What makes from an art perspective?

@shaderbytes, this is a tricky one. On the one hand if you have complete control over the asset pipeline from end to end knowing you have assets that are designed to share materials, then @syntheticmagus’s approach of loading the material once (even in a glTF that is composed only of materials which then get assigned accordingly) would be the cheapest in terms of loading. This is much the same as a game loading a skinned character model with no animations and a separate file for each animation that has just a skeleton and the animation data and then retargeting the animation. You have the most control over adding/modifying animations without needing to manage and load one giant file.

However, there is a challenge here in that if you don’t have control over the entire asset pipeline, you may be getting glTF files from different sources and they will have materials in them that need to be there for the glTF to render correctly in any engine. There is no way to control any accidental material naming conflicts either if the assets are coming from different sources. So we can’t universally dedupe the materials list automatically. Typically when I am dealing with glTF files and either replacing embedded materials with node materials I will load the glTF file, grab the textures from the loaded material so I have reference to which texture was assigned where and then delete the loaded material from memory before connecting the textures to the node material and assigning back to the asset.

The problem with this workflow is that for every subsequent mesh that is loaded that needs to share a material, we incur the cost of loading the textures and creating the material in scene that also needs to be deleted to keep our resource costs down. There is one way I can think of to handle something like this. Maybe we look at adding a flag to the glTF loader which allows the user to ignore loading textures and creating materials from the load. We have something similar for node materials where we have a flag to ignore loading any embedded textures in the node material. This is useful to speed loading and prevent unnecessary textures from living in memory if you know you have placeholder textures in the node material or maybe the shader came from another project and you no longer need the embedded textures.

This would be an all or nothing solution, however, as we wouldn’t be able to pick and choose from the glTF which materials get loaded or not. So you wouldn’t be able to have shared materials and unique materials in one asset and not incur some extra effort to remove any shared materials from that loaded mesh. We are looking at ideas to chase for version 6.0 so I will note this as a topic for exploration.

SO think a flag in the loader function is a good idea , some thing like “useExistingMaterials” which you could default to false , so then things will naturally behave as they currently do , not breaking others code bases. Then for users that do want the feature, we set it to true.

Concerning the issue mentioned of the duplicates still loading before being trashed , is this not something that can be caught in the gltf/glb parser? Then when the loader flag is found, the parser actually does not proceed to create a duplicate material or even load load the duplicate textures. It just gets immediately mapped to the existing material if found. Material name should be enough to test for duplicates.

Lastly thinking about the mention of situations where people dont have full control of the pipeline and also that this can be a all or nothing approach , it could be possible to define a “ignore list” array

Then for loading assets and setting the flag to use existing materials , you could have an array for exclusions to the rule. So it will perform the above mentioned action in the parser immediately mapping the asset to the existing material UNLESS it finds that name in the exclusion list , at that point it ignores the fag and rather produces the duplicate to avoid conflicts. With this you can actually get the best of all three situations

  1. the flag defaults to false , making engine usage not change for any existing users
  2. the flag allows this functionality and it used right within the parser so that no redundant assets are loaded just to be trashed again
  3. You can use the exclusion list in conjunction with the flag being true to get both scenarios working , so all assets will use preexisting materials if they exist or will create duplicates if excluded.

you could give a unique exclusion argument list to each load , meaning if you were loading several assets and wanted a few to share and only some to ignore … then this is possible.

anyway hope that makes sense , it is a interesting issue to solve

I not against the idea of a material library been loaded as well , and then assets using that , but that still requires some logic to tell the loaded assets to either use those or ignore them and do duplicates , same flag and exclusion list can work here , and maybe it is safer because it not relaying on the first material cache to be provided by the first models loaded with that material name.

So maybe a marriage of all these ideas is best.

This is really error prone, a lot of material named “metal” or “transparent” or “red” have totally different outputs depending on the source.

As well as the 2 flags loading all or nothing, I could see a callback to return a material providing you with the gltf JSON structure so you could return any material fitting your needs ? would that work, the only logic for you is to return scene.getMaterialByName(…) in your case but at least it open a whole word of flexibility ?

yes I agree , abut here is where I mentioned the “exclusion list” so if you find this is a issue on some loaded model for any amount of materials then you still set the flag to try to get the duplicates with same name that are the same , and use the exclusion list to then allow that loaded materials with same names to rather still become duplicates

Here is the proposed update: Gltf materials by sebavan · Pull Request #12149 · BabylonJS/Babylon.js · GitHub basically you could either:

  1. load only materials to reuse at will
  2. load no material to customize at will

[Edit: see following post]
3. rely on a special custom callback to inject your own materials or let the system creates it

1 Like

About 3., actually @bghgary noted it is highly similar to a custom gltf extension so it has been removed to not bloat the code.

1 Like

Here is an example: https://www.babylonjs-playground.com/#20XT9A#4

Read this blog for more information: Extending the glTF Loader in Babylon.js | by Babylon.js | Medium

2 Likes

wow , thanks guys , let me look into what you have done to get a grips on how to use it