Instanced Materials?!

Hi everyone,
So today, I was working on a (hopefully) short project to create a system that automatically finds differences between two GameObjects. When I ran the code on two planes, though, I found these two logs (having to do with the Materials of the plane):

COMPONENT: UnityEngine.MeshRenderer
VAR: material
ORIGINAL: Default-Material (Instance) (UnityEngine.Material)
NEW: Default-Material (Instance) (UnityEngine.Material)

As you can see, this log is telling me that MeshRender’s material is different in Plane 1 & 2, even though they are the exact same. I also got this log:

COMPONENT: UnityEngine.MeshRenderer
VAR: materials
ORIGINAL: UnityEngine.Material[ ]
NEW: UnityEngine.Material[ ]

Then I added a piece of code that prints out the whole list (if it is one) and got this:

COMPONENT: UnityEngine.MeshRenderer
VAR: materials
ORIGINAL: [Default-Material (Instance) (UnityEngine.Material)]
NEW: [Default-Material (Instance) (UnityEngine.Material)]

I’m pretty sure the reason why this is happening is because of the (Instance) type you can see on the materials. Is there any way (other than a really hacky way by going through a string to find if “(Instance)” is inside it) to detect if the value is an Instance?

My Code:

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

public class testsiism : MonoBehaviour
{
    public GameObject original;
    public GameObject changed;

    public class Info
    {
        public Component myComp { set; private get; }
        public PropertyInfo thisVar { set; private get; }

        public string objectName { get { return myComp.name; } private set { } }
        public Type componentName { get { return myComp.GetType(); } private set { } }
        public string varName { get { return thisVar.Name; } private set { } }
        public Type type { get { return thisVar.PropertyType; } private set { } }
        public object value { get {
                try
                {
                    return thisVar.GetValue(myComp, null);
                }
                catch (Exception e)
                {
                    if (!(e is TargetInvocationException))
                    {
                        Debug.LogError(e);
                    }
                    return null;
                }
        } private set { } }

        public bool canWrite { get { return thisVar.CanWrite; } private set { } }
    }

    public List<Info> originalProperties;
    public List<Info> changedProperties;

    void Start()
    {
        originalProperties = GetProperties(original);
        changedProperties = GetProperties(changed);

        // COMPARE TIME!
        foreach (Info currentInfo in originalProperties)
        {
            bool foundComponent = false;
            foreach (Info compareInfo in changedProperties)
            {
                if (currentInfo.componentName == compareInfo.componentName && currentInfo.varName == compareInfo.varName)
                {
                    if (currentInfo.value != null && compareInfo.value != null && currentInfo.canWrite && compareInfo.canWrite && !(currentInfo.value.Equals(compareInfo.value)))
                    {
                        // Print out list so I can see what went wrong
                        try
                        {
                            string toPrint = "COMPONENT: " + currentInfo.componentName + "     VAR: " + currentInfo.varName + "     ORIGINAL: [";
                            foreach (object obj in (Array)currentInfo.value)
                            {
                                if (toPrint.EndsWith("["))
                                {
                                    toPrint += obj.ToString();
                                } else
                                {
                                    toPrint += ", " + obj.ToString();
                                }
                            }

                            toPrint += "]     NEW: [";

                            foreach (object obj in (Array)compareInfo.value)
                            {
                                if (toPrint.EndsWith("["))
                                {
                                    toPrint += obj.ToString();
                                }
                                else
                                {
                                    toPrint += ", " + obj.ToString();
                                }
                            }
                            toPrint += "]";
                            Debug.Log(toPrint);
                        } catch
                        {
                            Debug.Log("COMPONENT: " + currentInfo.componentName + "     VAR: " + currentInfo.varName + "     ORIGINAL: " + currentInfo.value + "     NEW: " + compareInfo.value);
                        }
                    }
                    foundComponent = true;
                    break;
                }
            }
            if (!foundComponent)
            {
                Debug.Log("ADD TO NEW: " + currentInfo.componentName);
            }
        }
    }

    List<Info> GetProperties(GameObject input)
    {
        Component[] myComponents = input.GetComponents(typeof(Component));
        List<Info> result = new List<Info>();
        foreach (Component myComp in myComponents)
        {
            Type myObjectType = myComp.GetType();
            foreach (PropertyInfo thisVar in myComp.GetType().GetProperties())
            {
                try
                {
                    if (thisVar.Name.ToString() != "name") // All components have this - we don't need to compare it.
                    {
                        Info currentInfo = new Info();
                        currentInfo.myComp = myComp;
                        currentInfo.thisVar = thisVar;
                        result.Add(currentInfo);
                    }


                    // For testing purposes - prints out all the data
                    // Debug.Log("GameObject:  " + myComp.name + "        Component Name:  " + myComp.GetType() + "        Var Name:  " + thisVar.Name + "         Type:  " + thisVar.PropertyType + "       Value:  " + thisVar.GetValue(myComp, null) + "\n");
                }
                catch (Exception e)
                {
                    if (!(e is TargetInvocationException))
                    {
                        Debug.LogError(e);
                    }
                }
            }
        }
        return result;
    }
}

(Yes, I know, it’s really messy.)
Thanks! I hope I can fix this soon! :slight_smile:

I’m not sure what you’re trying to do, but I have a sneaking suspicion that this might not be the Best Way™ to do it.

Why don’t you back up and tell us all what exactly you’re trying to accomplish with this that Unity doesn’t already give you with the usual Find() and other comparative-based APIs?

Keep in mind also that in order to compute equality, you actually have to truly understand what you mean when you say “equality” in a particular context.

I’m trying to make a program with compares two GameObjects and finds out what is different between them (as in the components and values of the components).

This approach did seem long-winded to me too, so if you have any ideas on an easier way, please tell me!

You can use SerializedObject and SerializedProperty for that. Also note what assets and scenes are stored as text files and it might be easier to find difference in 2 texts than in two live objects

IIRC, When you call the getter on .materials and .material, Unity automatically makes instances of the materials.

Yup, that’s what’s going on.

You’re iterating though all the properties on the attached components. That includes the .material and .materials properties, that has the side-effect of creating a new instance. This is so that if you set material.color, you don’t change every object with the same material.

This was a really fucking bad idea that Unity had early on, and it causes a ton of headache.

Anyway, for your thing to work, it depends. If you’re doing this at edit time, do what @palex-nx is suggesting and iterate the serialized properties. If you’re doing this at runtime, you’ll need to create a list of properties with side-effects to skip.

Perhaps try comparing the shared material instead.

Sorry I haven’t been on this post this much -

The problem is, I can’t make a list of what types of fields have instances. If someone makes a custom scripts that work with instanced stuff, I can’t check if it is an instance or not.

So, could you give me an example on how to do this?

I just want a confirmation, for some reason I thought it was otherwise; I thought objects could share materials but still have different colors… …if you alter anything on a material, for a specific object, it must be made into a different (instanced)material?

They can still share textures though, can’t they? Or are textures then reproduced/doubled on the graphics card as well?

My problem is everything I have in runtime generated, so I can’t really use prefabs, so even models that would generally share a single material seem to be instancing materials. I am doing this because the players will be customizing the models/materials, so I have a strict no editor workflow. I am thinking for low LODs, just have a table of materials, and having the model grab/share the material. Have not tried implementing that yet, want to make sure its not a waste of time first…

Are you looking for this? https://docs.unity3d.com/ScriptReference/MaterialPropertyBlock.html

1 Like

I think that guided me to where I need to go: SRP batching. Thanks.

EDIT: and it looks like the SRP batcher is automatic/default in HDRP if the shader is compatible, at least the frame debugger said it was batching… sooo… cool.

1 Like

I wasn’t sure what you’re trying to do, that’s why I was asking.