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?