Ogre importer: Materials are not shared

I have an ogre scene that contains two models with the same material but when i import it to jme the material get duplicated.
There is a reason behind this unexpected behaviour?

One issue that people were complaining about before is that they tried to change the color of a material for a model and it changed the color for all other models.
So I guess the question is, why do you need the materials to be shared?

Isn’t this the exact reason why you share materials?

I’d like to use Instancing and if i got it right the requirements are: same mesh and same material.

1 Like

My impression after reading some source is that this is not an importer issue, but it’s caused by the cloning/caching system. Can someone confirm this?

No, because you may want to change the material of one model without affecting all others.

In that case you can clone the model manually via Spatial.clone(false). The parameter is “cloneMaterials” which indicates whether you want the material to be cloned as well or not.

Yes, the Spatial.clone() method which is used by CloneableAssetProcessor will cause the material to be cloned as well instead of shared.

Well… you are free to duplicate or share a material in jmonkey as well as in blender, so which is the point in messing this in the import?
Of course i’m not talking about sharing the material for all the instances of a model, but only inside the scene(/node).

The idea was to be able to compose the scene without code…

Then why the following code doesn’t behave the same?

import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

import com.jme3.app.SimpleApplication;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.material.Material;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;

public class TestCloningIssue extends SimpleApplication{
    public static void main(String[] args) {
        new TestCloningIssue().start();
    }

    @Override
    public void simpleInitApp() {
        Node root=new Node();
        Geometry geometry1=new Geometry("geometry1",new Box(1,1,1));
        Geometry geometry2=new Geometry("geometry2",new Box(1,1,1));
        Material mat=new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md");
        geometry1.setMaterial(mat);
        geometry2.setMaterial(mat);
        root.attachChild(geometry1);
        root.attachChild(geometry2);
        System.out.println("Original node: "+countGeometries(root)+" geometries, "+countMaterials(root)+" materials");

        Node root_clone=(Node)root.clone(true);
        System.out.println("Cloned node: "+countGeometries(root_clone)+" geometries, "+countMaterials(root_clone)+" materials");

        try{
            String dir=Files.createTempDirectory("jme").toString()+File.separator;
            BinaryExporter exporter=BinaryExporter.getInstance();
            File file=new File(dir+"node.j3o");
            exporter.save(root,file);
            
            assetManager.registerLocator(dir,FileLocator.class);
            Node loaded_node=(Node)assetManager.loadModel("node.j3o");
            System.out.println("Loaded node: "+countGeometries(loaded_node)+" geometries, "+countMaterials(loaded_node)+" materials");

        }catch(Exception e){
            
        }
    }
    
    private int countGeometries(Spatial n) {
        AtomicInteger i=new AtomicInteger();
        n.depthFirstTraversal(s -> {
            if(s instanceof Geometry){
                i.incrementAndGet();
            }
        });
        return i.get();
    }


    private int countMaterials(Spatial n) {
        ArrayList<Material> mats=new ArrayList<Material>();
        n.depthFirstTraversal(s -> {
            if(s instanceof Geometry){
                Geometry g=(Geometry)s;
                Material m=g.getMaterial();
                System.out.println("Material "+m.hashCode());
                if(!mats.contains(m)) mats.add(m);
            }
        });
        return mats.size();
    }

}

Output:

Material 319859876
Material 319859876
Original node: 2 geometries, 1 materials
Material 1411558329
Material 1411558329
Cloned node: 2 geometries, 1 materials
Material 1483548439
Material 1483548439
Loaded node: 2 geometries, 1 materials

While when i import a scene that is similar to this one with ogre i get 2 identical materials

Just to see if I’m following along correctly…

You are talking about importing one scene with two objects that share the same material. But when you load this into JME, each object in the one loaded scene no longer shares the material.

Yes?

If so, that’s not anything to do with cloning and everything to do with the importer.

1 Like

exactly

Yeah, it was indeed an importer issue.
This is the fix: Fix material sharing issue · riccardobl/jmonkeyengine@58b953d · GitHub

EDIT:
The previous patch did solve the material sharing issue but broke mesh sharing/caching.
Working patch:

For scenes with a shared material in them (in the same file) I think this is probably a good thing to fix. I’m not familiar enough with the code to review the patch so I’ll let others do that. However, I think it’s worthy enough to submit a pull request with a link to this thread. It will kind of force the review issue.

I think its probably easier to pass a parameter in ModelKey indicating the caller doesn’t want the materials to be cloned. Then SceneLoader would just pass it in along to the ModelKey for the models it loads.

That’s one way…

However, in this case it’s one scene where the original scene had one material for multiple objects in that scene. It sounds to me like the intent was clear… as if they wanted different materials wouldn’t they have created them?

And so in the rare case where a user has created a scene with shared materials and they really don’t want them shared and don’t want to edit the original scene to make separate materials in their one scene with multiple objects sharing the same material… then they can clone their subobjects and clone the materials.

Or is the ogre exporter doing some magic that is confusing original intent?

1 Like

I don’t think so.
If you ask me, the importer was already made to support material sharing, since, when it loads a scene through the scene loader, it stores all the materials inside a materialList that is passed with the ModelKey, making materials actually shared, until the model is returned by the assetManager that clones it.
So why bother doing all this system if you don’t want materials to be shared?

I explained it better in the PR

Ok… I misunderstood what was happening, I guess. So the scene loader itself is reloading the materials or something? I looked at the patch and I guess that’s your fix… to make sure scene loader doesn’t clone materials based on the key = shared.

Well, almost. It’s not the sceneloader that is cloning the stuff.
The root of every evil is assetManager.loadModel called by the SceneLoader.

This is what is happening with the current importer (pseudocode):

1.This is the SceneLoader

scene= loadScene(x.scene)
materials= loadMaterialsFromScene(scene)
for each Object in Scene
   assetManager.loadModel(new AssetKey(Object.modelPath,Object.materialName,materials))

2.assetManager.loadModel is called from the SceneLoader and does:

   if cache contains AssetKey return cache[AssetKey].clone(true) // <<<<<<<<< 
   else return (cache[AssetKey]= MeshLoader(AssetKey)).clone(true) // <<<<<<<<< 
   // Materials aren't shared anymore since the model is clone(true)

3.MeshLoader (that is still part of the Ogre importer) is called from the assetManager and does:

   Spatial s=loadModel(AssetKey.modelPath)
       // pick the material from the list so assure it is shared... 
   s.applyMaterial(AssetKey.materials.get(AssetKey.materialName)
   return s

The patch removes the assetManager.loadModel call inside the SceneLoader and loads the models directly with the MeshLoader, in this way it skips the engine caching system and thus the clone(true) call.
And in any case, there is no reason to cache the single models at assetManager level if you are loading a .scene, since what you really want to cache is the full Node that contains the scene (and this is untouched by the patch).

I hope this’ll explain it more clearly.

Yeah, I think it makes sense to me.

…and this doesn’t affect mesh loading on its own, right? Or is that even a thing? (Can you tell I’ve never looked at the ogre stuff before? :slight_smile: )

Nope, i’ve made a SceneMeshLoader class that extends MeshLoader and it’s used solely by the SceneLoader so the mesh loading is left untouched.

The only thing that changes is that currently if you load a a.scene that contains the mesh sdjifdij.xml and then b.scene that contains sdjifdij.xml again, even if the scene is different, you take advantage of the engine caching for sdjifdij.xml (since it’s loaded and cached as separated model without the patch) but this can happen only in One case: a.scene and b.scene must share the same identical materialList.

So practically it turns out being worst since you are caching a lot of stuff that you won’t probably ever use.

(Maybe OT) IIRC, in xbuf importer material are replicated by default, else I got error with skeleton and hardware skinning. Because skinning store model’s info in the material.
So May be cloning with Orgre importer is due to same cause.

If you used the JME clone method then you wouldn’t have this issue because it is smart enough to know if the models are animated and if hardware skinning is available.

The loader can assign material, skeleton, animation in any order.

EDIT: The xbuf replicator is a layer over clone, so clone keep reference to master material. as is when I edit in RT the material, I can sync every clone’s attribute, except back listed attributes (like Hardware skinning). I agree than my loader could be improved and smarter.