Material instantiation and cleanup

Hi,

I understand that when a material property is modified at run-time, a new instance of that material is created and then must be cleaned up explicitly on object destruction. However, it looks to me like anything instantiated from a prefab will already instantiate copies of materials. For example, I have a prefab consisting of two quads, each with the same material applied. The following code executed in Startup():

    foreach (Renderer renderer in GetComponentsInChildren<Renderer>())
    {
      foreach (Material material in renderer.sharedMaterials)
      {
        Debug.Log("sharedMaterial=>" + material.name + " " + material.GetInstanceID());
      }

      foreach (Material material in renderer.materials)
      {
        Debug.Log("material=>" + material.name + " " + material.GetInstanceID());
      }
    }

Produces this output:

sharedMaterial=>Tracer 5234
material=>Tracer (Instance) -298272
sharedMaterial=>Tracer 5234
material=>Tracer (Instance) -298274

Each quad has its own renderer, and you can see the shared material is the same as expected, but I thought that the material instance would also be the same as the shared one in both cases (5234) since nothing has been modified. What am I missing? The object is instantiated using the Instantiate() call.

Elsewhere, for different objects, I modify the renderQueue value, which of course creates a unique instance of the material that I must clean up. To facilitate this more easily, my idea was to have a MaterialManager class that maps all materials at startup and then destroys anything it hasn’t encountered. Here is the code:

public class MaterialManager: MonoBehaviour
{
  private static HashSet<Material> m_unity_materials = null;

  static private void TryDestroy(Material material)
  {
    if (!m_unity_materials.Contains(material))
    {
      Debug.Log("Destroying instantiated material: " + material.name + " (" + material.GetInstanceID() + ")");
      Object.Destroy(material);
    }
    else
    {
      Debug.Log("Cannot destroy material: " + material.name + " (" + material.GetInstanceID() + ")");
    }
  }

  static public void DestroyMaterials(GameObject obj)
  {
    List<int> already_destroyed = new List<int>();
    foreach (Renderer renderer in obj.GetComponentsInChildren<Renderer>())
    {
      foreach (Material material in renderer.materials)
      {
        int id = material.GetInstanceID();
        if (!already_destroyed.Contains(id))
        {
          TryDestroy(material);
          already_destroyed.Add(id);
        }
      }
    }
  }

  void Awake()
    {
    if (null == m_unity_materials)
    {
      m_unity_materials = new HashSet<Material>();
    }
    if (m_unity_materials.Count == 0)
    {
      Material[] materials = Resources.FindObjectsOfTypeAll(typeof(Material)) as Material[];
      foreach (Material material in materials)
      {
        m_unity_materials.Add(material);
      }
    }
    Debug.Log("Found " + m_unity_materials.Count + " materials");
    }
}

I then wanted to attach this script to objects whose materials might be modified in code, for auto-cleanup:

/*
* This class should be used with any objects that have their material modified
* programmatically (and this includes the material's renderQueue). Accessing a
* renderer's material will create a new instance that is not garbage collected
* and must be freed explicitly.
*/

using UnityEngine;
using System.Collections;

public class MaterialCleanup : MonoBehaviour
{
  void OnDestroy()
  {
    MaterialManager.DestroyMaterials(gameObject);
  }
}

Basically, my question is: How can I detect which materials have been instantiated and will not be destroyed with the gameObject, so that I can then manually clean up?

1 Like

Hello,

I will give you a more optimized trick.

You should use MaterialPropertyBlock to change your material properties for your instances: Unity - Scripting API: MaterialPropertyBlock

Also if you want to change the texture for each instance, I suggest you to use an Atlas and an UV offset property in your shader.

Everything is explained on the link.

If you really want to keep your method, I think you can use Resources.UnloadUnusedAssets()
This will destroy every materials generated on runtime by Unity automatically if not used.

4 Likes

Hi,

This sounds very interesting and I can definitely see the utility but unfortunately, renderQueue is not a property that can be modified within property blocks.

I don’t want to use UnloadUnusedAssets() because I don’t destroy all objects at once, but rather periodically and one at a time, so I only want to free materials created by that object.

But any idea why I’m seeing unique material instances despite not having modified the material? I thought that sharedMaterials and materials would point to the same set of objects in the case where no game object has modified its materials?

Bart

Hi,

sharedMaterials return the materials you have created in your Assets/
materials return a new instance of it automatically.
I’m not sure but just by doing a check like (if r.material != null), it may create a new instance.

To me, the best practice is to never call materials/material but sharedMaterial and manually generate an instance (new Material(materialmodel))

In your project, are you able to create manually a pool of materials (and add new to this pool if all are used) ?
When you instantiate a prefab, you manually assign a material from this pool (on shared material) and never change the material by using the renderer either.

It’s quite dark how renderer handle theses materials but as I said, if you NEVER call material or materials for whatsoever even a print, it should never instantiate a material.

And an instantied prefab does not have instantied materials, I think you though that because you called .material of his renderer to check if it’s an instance and it created the instance at this moment.

To check if the materials are indeed instances, you can see it in the editor in the renderer.

1 Like

I thought modifying the sharedMaterial will modify the asset on disk in the Unity editor? Not sure what modifying the sharedMaterials[ ] array does – does it basically just create clones like materials[ ]?

Unfortunately, my requirements are a little more complex but I’m trying to work out a solution. I came up with a helper object that should work but is mysteriously not working.

This is quite frustrating. I wish the Unity developers would consider deprecating materials and sharedMaterials in favor of a more explicit API.

I’ll post another reply next week if I can get my program working. Right now it’s failing in all kinds of bizarre ways.

So I came up with this horrible thing you can attach to each object and it appears to do what I want. It will clone all the shared materials once, but it keeps a global, reference-counted set of them, so that multiple instances of the same object will not clone the shared materials again.

You can then reapply the clones (which has the effect of restoring the shared material). The clones are a different instance from the original shared material but they are still “shared” across objects, so render batching should still apply. There might be some slight bugs and CloneSharedMaterials() must be called before any modifications to materials are made (in fact, I will modify the API to make this function public for situations where materials are being set up in the parent object’s Awake()).

What do you think?

I have a test script as well that demonstrates how this works by counting the number of total Material objects known to the game engine. It appears to be working quite well in my limited testing so far.

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

public class MaterialHandler: MonoBehaviour
{
  private class Clone
  {
    public Material sharedMaterial; // original
    public Material cloneMaterial;  // clone
    public int objects;             // number of objects referencing this material

    public Clone(Material sharedMat)
    {
      sharedMaterial = sharedMat;
      cloneMaterial = new Material(sharedMaterial);
      objects = 0;
    }
  }

  private static Dictionary<Material, Clone> m_clonesBySharedMaterial = null;
  private static Dictionary<Material, Clone> m_clonesByCloneMaterial = null;

  private Dictionary<Renderer, Material[]> m_ourSharedMaterialsByRenderer = null;
  private List<Material> m_ourCloneMaterials = null;

  private void SetInsert<T>(List<T> list, T item)
  {
    if (!list.Contains(item))
    {
      list.Add(item);
    }
  }

  private void LazyInit()
  {
    if (null == m_clonesBySharedMaterial)
    {
      m_clonesBySharedMaterial = new Dictionary<Material, Clone>();
    }
    if (null == m_clonesByCloneMaterial)
    {
      m_clonesByCloneMaterial = new Dictionary<Material, Clone>();
    }
    if (null == m_ourSharedMaterialsByRenderer)
    {
      m_ourSharedMaterialsByRenderer = new Dictionary<Renderer, Material[]>(gameObject.GetComponentsInChildren<Renderer>().Length);
    }
    if (null == m_ourCloneMaterials)
    { 
      int maxClones = 0;
      foreach (Renderer renderer in gameObject.GetComponentsInChildren<Renderer>())
      {
        maxClones += renderer.sharedMaterials.Length;
      }
      m_ourCloneMaterials = new List<Material>(maxClones);
      Debug.Log("Our clones list has capacity " + maxClones);
    }
  }

  private void IncrementReferenceCount(Material cloneMaterial)
  {
    ++m_clonesByCloneMaterial[cloneMaterial].objects;
  }

  private void DecrementReferenceCount(Material cloneMaterial)
  {
    Clone clone = m_clonesByCloneMaterial[cloneMaterial];
    if (0 == --clone.objects)
    {
      // Time to destroy!
      Object.Destroy(cloneMaterial);
      m_clonesBySharedMaterial.Remove(clone.sharedMaterial);
      m_clonesByCloneMaterial.Remove(cloneMaterial);
    }
  }

  private void CloneSharedMaterials()
  {
    LazyInit();
    if (m_ourCloneMaterials.Count > 0)
      return; // already created clones!
    foreach (Renderer renderer in gameObject.GetComponentsInChildren<Renderer>())
    {
      Material[] sharedMaterials = renderer.sharedMaterials;
      for (int i = 0; i < sharedMaterials.Length; i++)
      {
        Material sharedMaterial = sharedMaterials[i];
        Clone clone = null;
        if (!m_clonesBySharedMaterial.TryGetValue(sharedMaterial, out clone))
        {
          // Create new clone
          clone = new Clone(sharedMaterial);
          m_clonesBySharedMaterial.Add(sharedMaterial, clone);
          m_clonesByCloneMaterial.Add(clone.cloneMaterial, clone);
          Debug.Log("Created clone of " + sharedMaterial.GetInstanceID() + ": " + clone.cloneMaterial.GetInstanceID());
        }
        else
        {
          Debug.Log("Retrieved clone of " + sharedMaterial.GetInstanceID() + ": " + clone.cloneMaterial.GetInstanceID());
        }
        m_ourSharedMaterialsByRenderer[renderer] = sharedMaterials; // need to do this because instancing will overwrite sharedMaterials[], too!
        SetInsert(m_ourCloneMaterials, clone.cloneMaterial);
      }
    }
    // Increment object reference counts for each clone we use
    foreach (Material cloneMaterial in m_ourCloneMaterials)
    {
      IncrementReferenceCount(cloneMaterial);
    }
  }

  // Applies clones of the shared materials as the instanced materials. Clones
  // are instantiated only once and are therefore themselves shared between any
  // object instances that clone the same shared materials.
  public void ApplySharedMaterialClones()
  {
    CloneSharedMaterials();
    DestroyInstancedMaterials();
    foreach (Renderer renderer in gameObject.GetComponentsInChildren<Renderer>())
    {
      Material[] sharedMaterials = m_ourSharedMaterialsByRenderer[renderer];
      Material[] cloneMaterials = new Material[sharedMaterials.Length];
      for (int i = 0; i < sharedMaterials.Length; i++)
      {
        cloneMaterials[i] = m_clonesBySharedMaterial[sharedMaterials[i]].cloneMaterial;
      }
      renderer.materials = cloneMaterials;
    }
  }

  private void DestroyInstancedMaterials()
  {
    //TODO: optimization: if sharedMaterials[] == m_ourSharedMaterialsByRenderer[renderer], then we know materials[] hasn't been modified. No need to free!
    foreach (Renderer renderer in gameObject.GetComponentsInChildren<Renderer>())
    {
      Material[] materials = renderer.materials;  // if this is the first read, Unity will deep copy
      foreach (Material material in materials)
      {
        // If this is a cloned shared material, do nothing because we clean
        // them up automatically on destruction. Otherwise, destroy.
        if (null == m_ourCloneMaterials || !m_ourCloneMaterials.Contains(material))
        {
          // Assuming it is safe to call Object.Destroy() multiple times
          Object.Destroy(material);
        }
      }
    }
  }

  private void DestroySharedMaterialClones()
  {
    foreach (Material cloneMaterial in m_ourCloneMaterials)
    {
      DecrementReferenceCount(cloneMaterial);
    }
  }

  private void Awake()
  {
    CloneSharedMaterials();
  }

  private void OnDestroy()
  {
    DestroyInstancedMaterials();
    if (m_ourCloneMaterials.Count > 0)
    {
      DestroySharedMaterialClones();
    }
  }
}

Note that this can be optimized a little bit because whenever clones are applied, a new set of instances is created (that is then immediately destroyed). I discovered that modifying Renderer.materials[ ] will always modify Renderer.sharedMaterials[ ] too, and I think this can be used to detect whether or not materials[ ] has ever been touched without actually touching it :slight_smile: All I have to do is check it against my stored map of the original sharedMaterials[ ].

Well, I don’t know your needs for your project but if it work…

I believe renderer.material may be declared like that in Renderer as well as materials:

public Material material
{
   get
   {
       if(sharedMaterials != null && sharedMaterials.Length > 0)
       {
           if(sharedMaterials[0] is in assets)
           {   
                sharedMaterials[0] = new Material(sharedMaterials[0]);
           }

        return sharedMaterials[0];
       }
       else
           return null;
   }
}

sharedMaterials return the materials used on your renderer without the new Material shit but it will return the instance of material have been called before.

Materials

Shared Materials