[SOLVED] Using materials as dictionary keys

I’m interested in a sane way to use Renderer’s materials as keys in a Dictionary.

Basically, I need to identify the situation when object’s material is the same as the prefabbed materials.
Unfortunately, as far as I can tell, unity engine creates material instances even when I access .sharedMaterials. material instance is not equal to original material.

Ideas?

Are you sure about that?

Yep.Need a test case?

Materials investigated via monodevelop inspector show “instance” even when they’re shared and equality comparison with a prefab material returns false.

Can you show us your code?

I’m testing with the following MonoBehaviour on several meshes.

using UnityEngine;
using System.Collections.Generic;

public class DisplayShaderedMaterial : MonoBehaviour
{
    static Dictionary<Material, int> materials = new Dictionary<Material, int>();
    void Start ()
    {
        foreach(var m in GetComponent<Renderer>().sharedMaterials)
        {
            if (!materials.ContainsKey(m))
                materials[m] = 0;
        }
        Debug.Log( materials.Keys.Count );
    }
}

The number of keys in the dictionary is as expected. So there is no new material instances created.
How do you obtain duplicated materials?

If you’re comparing both sharedMaterials, they should be the same instance. Are you comparing between an object in the scene and a prefab that’s not instantiated? In that case it might be that they’re two different objects. I doubt it, but…

No, I was talking about this kind of usage:

using UnityEngine;
using System.Collections.Generic;

public class DisplayShaderedMaterial : MonoBehaviour
{
    [SerializeField] Material materialPrefab = null;//set it to something
    static Dictionary<Material, int> materials = new Dictionary<Material, int>();
    void Start ()
    {    
        foreach(var m in GetComponent<Renderer>().sharedMaterials)
        {
            materials[m] = 0;
        }
        bool containsKey = materials.ContainsKey(materialPrefab);
        Debug.Log( containsKey );
    }
}

Does this still work for you?

public Material mat; //I assigned this to a material from the project - ie. a "prefab"
public Renderer rend; //I assigned this to a MeshRenderer in the scene with the material from mat assigned

private void Start() {
    Debug.Log(rend.sharedMaterial == mat); //returns true

    Dictionary<Material, int> dict = new Dictionary<Material, int>();
    dict[mat] = 3;

    Debug.Log(dict.ContainsKey(mat)); //returns true
    Debug.Log(dict.ContainsKey(rend.sharedMaterial)); //returns true
    Debug.Log(dict.ContainsKey(rend.sharedMaterials[0])); //returns true
    Debug.Log(dict.ContainsKey(rend.material)); //returns false
}

Seems correct to me.

Just great. Now I cannot reproduce the issue. Had it yesterday, and the code that was causing it has been scrapped since and I was getting “material(Instantiated) when accessing sharedMaterials”.

Now this works.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MaterialTest : MonoBehaviour{
    [SerializeField] Material materialPrefab = null;
    Dictionary<Material, int> matDictionary = new Dictionary<Material, int>();

    void checkMaterials(){
        var components = GetComponentsInChildren<Renderer>();
        foreach(var cur in components){
            Debug.LogFormat("Processing object {0}", cur.name);
            foreach(var curMat in cur.sharedMaterials){
                Debug.LogFormat("Material name: {0}", curMat.name);
                matDictionary[curMat] = 1;
            }
        }

        Debug.LogFormat("Target material: {0}", materialPrefab);
        bool foundKey = matDictionary.ContainsKey(materialPrefab);
        Debug.LogFormat("Material found: {0}", foundKey);
    }

    // Use this for initialization
    void Start (){
        checkMaterials();   
    }
   
}

Oh well. I’ll mark it as freak occurrence.

I’ve found the problem.

Accessing .materials in any way “unshares” .sharedMaterials.

Meaning even if you only access .materials.length, all shared materials will be instantly replaced with instantiated versions instead of original prefabs.

Here’s example;

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MaterialTest : MonoBehaviour{
    [SerializeField] Material materialPrefab = null;
    Dictionary<Material, int> matDictionary = new Dictionary<Material, int>();

    void checkMaterials(){
        var components = GetComponentsInChildren<Renderer>();

        foreach(var cur in components){
            int test = cur.materials.Length;
            Debug.LogFormat("Processing object {0}", cur.name);
            foreach(var curMat in cur.sharedMaterials){
                Debug.LogFormat("Material name: {0}", curMat.name);
                matDictionary[curMat] = 1;
            }
        }

        Debug.LogFormat("Target material: {0}", materialPrefab);
        bool foundKey = matDictionary.ContainsKey(materialPrefab);
        Debug.LogFormat("Material found: {0}", foundKey);
    }

    // Use this for initialization
    void Start (){
        checkMaterials();   
    }
   
}

Interesting and good to know.

I guess that should be made explicit in the documentation:

There is this sentence:

“This function automatically instantiates the materials and makes them unique to this renderer.” (BTW, it’s not a function, it’s a property.)

Though it does not say what happen to the sharedMaterials array.

Edit:
It’s a kind of expected behaviour. Let’s say you have blue material, then you modify this material and turn it red through accessing materials (making it a new instance). I guess you shouldn’t expect to get the blue material again when you access sharedMaterials.

Well, “property” is a syntaxic sugar for getter/setter functions, so accessing the property calls getter. And said getter silently makes a copy of internal material array.

…Sorta makes sense, but it is far from being obvious.

Normally I would expect “unsharing” to occur only during assignment and only affect target material
I.e. “materials[index] = something” or “materials = newMaterialArray”. At the same time, in case of “var tmp = materials”, I would expect materials to remain unchanged.

But then again, C# does not have const methods, so…

Yeah, I thought this was going to be the problem, it’s why I asked to see the code.

It’s really annoying that Unity designed their API like this, but yeah, if you access the ‘.materials’ property it duplicates the underlying material, where as if you access the ‘.sharedMaterials’ it does not.

I guess it was done for the sake of newbs, because if you modified the .material with out first duplicating it, it would effect ALL entities out there that referenced that material.

I honestly wish it was more explicit, like a ‘copyMaterial()’ method or something…

one of those poor design choices on the part of Unity back in early development that sort of hangs around like a giant wart that can’t easily be gotten rid of without breaking compatability.

just mention the possibility outright next time? It could’ve saved me few hours.

I only remembered of hidden copying shenanigans by accident.

Or IMaterial interface with “Renderer.materials” returning MaterialProxy which wraps Material, implements IMaterial, and performs deep copy on assignment, while .sharedMaterials return naked Material class.

Eh, I was out the door to lunch when I posted. Figured I’d check back when I got back… guess I didn’t get around to it.

It’s called I have a job.

That sounds a little to over complicated for no good reason.

Also, why would it have to deep copy on assignment.

Just have a ‘clone’ or ‘copyMaterial’ method on the material… get rid of the ‘sharedMaterial’ property… and be done with it.

renderer.material = renderer.material.Clone();

Because there’s no reason to clone a material, unless an operation is performed that needs to modify it. That’s what “proxy” would be for.

Either way, the problem is solved, so I’m done here. Unsubscribing, thanks for the responses.

It is syntactic sugar. But nonetheless it’s named a property and it is different from a function/method.

Agree. It’s totally unexpected that accessing materials has effective side effects and duplicates the materials. Until you encounter a problem with it and then check the documentation to find out that it does. If it was named differently, for example, instanceMaterials, it could have been some sort of warning, questionning your assumptions and motivating you to check the documentation.
But it’s too late to change the unity API. Maybe that can be a proposal for Unity 6.

That.

A proxy is a substitute for another object; whatever you do with the proxy, it is impacting the targeted object. It’s not the same idea as a duplication.