- Home /
Renderer.materials leaking materials
I recently found out that Materials are not garbage collected in the regular GC pass. This causes our project to leak materials at a prodigious rate. So I added some code to pool and reuse materials:
//! add rendering components to a gameobject
public override void AddComponents(SWFCRenderable obj)
{
base.AddComponents(obj);
// create mesh filter and mesh renderer
MeshRenderer render = obj.MeshRender;
render.enabled = true;
obj.Filter.mesh = m_mesh;
// duplicate each of the materials so we can use renderQueue
Material[] newMats = Dictionary.AllocateMaterials(m_materials.Length);
for (int i = 0; i < m_materials.Length; i++)
{
newMats[i].CopyPropertiesFromMaterial(m_materials[i]);
newMats[i].shader = m_materials[i].shader;
}
render.materials = newMats;
}
//! remove rendering components from a gameobject
public override void RemoveComponents(SWFCRenderable obj)
{
Dictionary.FreeMaterials(obj.MeshRender.materials);
base.RemoveComponents(obj);
obj.MeshRender = null;
obj.Filter = null;
}
The problem is that I'm still leaking materials. My best guess is that setting render.materials does not properly flag the renderer as having local materials... So the next time that renderer.materials is read, it clones the materials in the array over again.
Does anybody have any insight into what's going on, or how I can get these materials to stop leaking?
This question caused me to want to check it myself. I just stuck this line in one of my Update methods to shoot out the total amount of materials constantly in existence: Debug.Log(GameObject.FindObjectsOfType(typeof($$anonymous$$aterial)).Length);
Then I loaded some objects, then unloaded those and loaded some other ones (calling Destroy on the first ones), then reloaded the initial objects (calling Destroy on the other ones). The number appears to rise each time. That's interesting. Like you, I was expecting this number to fall back down to the initial value. Err... I wonder what's going on. I don't have an answer for you, but I'll keep experimenting and see if I can narrow it down.
If you create your own copy of a material you should use shared$$anonymous$$aterials ins$$anonymous$$d of materials. materials is a property which will always return a local copy of the materials. I'm not sure why they leak. Usually when the object get's destroyed the materials should be gone as well.
Note: Never use .material or .materials at edit-time. They will always leak the materials but the editor should even throw a warning about that.
shared$$anonymous$$aterial /shared$$anonymous$$aterials will always return the true reference to the assigned material(s). material / materials always return a copy.
Answer by MartinBrownlow · Dec 07, 2011 at 06:25 PM
OK here's what is happening. The FIRST time you call the getter for material or materials, it CLONES all the materials on the object. It does this even if you supplied the materials in the first place via the setter. For example:
render.material = new Material(Shader.Find(***)); // assign a material
Material mat = render.material; // clones the new material... LEAK!!
This will leak, since the call to the getter causes the renderer to clone the material, even though you supplied it. The following code is better:
Material mat = render.material; // clones the existing material
mat.shader = Shader.Find(***);
The first line clones the material already assigned. The second line alters the cloned material.
This is even more problematic when using a model with multiple materials, since the time when the materials in the renderer become valid does not seem to be properly defined.
MeshFilter mesh = gameObject.GetComponent<MeshFilter>();
MeshRenderer render = gameObject.GetComponent<MeshRenderer>();
mesh.mesh = someMesh;
Material[] mats = render.materials; // clones the materials on the renderer
Note that the last line clones the materials, BUT there are not the correct number of materials in the array for the mesh... The way I got around this was to store an array of materials with my mesh:
MeshFilter mesh = gameObject.GetComponent<MeshFilter>();
MeshRenderer render = gameObject.GetComponent<MeshRenderer>();
mesh.mesh = someMesh;
render.materials = storedMaterials;
Material[] mats = render.materials; // clones storedMaterials
So I set the renderer to use the stored materials. Then I read from materials getter, which causes them to be cloned (this is what I want since I need to alter them programmatically).
Note that if I clone the materials myself before assigned them to render.materials, then read from render.materials, I lose access to the materials I cloned, causing a leak.
It is important to note that the materials are cloned on the very first read of material/materials ONLY. Subsequent reads will not clone the material. This could turn around and bite you if the very first read is out of your control.
Almost forgot... materials are not garbage collected automatically, since they are technically assets. You need to call Resources.UnloadUnusedAssets to collect unused materials.
However, you can call Object.Destroy on the materials when you are done with the renderer. You should only do this if you know that the materials have been cloned by the renderer.
Thank you for sharing this! I wasn't aware the runtime system considered $$anonymous$$aterials assets in that way. I have added Resources.UnloadUnusedAssets to my code in the place where it destroys the previous objects upon loading new ones, and this caused the amount of materials reported by GameObject.FindObjectsOfType(typeof($$anonymous$$aterial)) to drop back down to the expected value when I load back and forth between the same sets of objects. Again, thank you! This helped me correct a leak I wasn't even aware of myself. :)
Your answer
Follow this Question
Related Questions
Changing the materials in Editor Mode Without Getting the leak Error 3 Answers
Material vs. SharedMaterial - difference in SETTING? 1 Answer
Index array is out of range (following H&S tut) 2 Answers
Swapping out a single material on a SkinnedMeshRenderer at runtime 0 Answers
What is the recommended pattern when using ComputeBuffer with SRP? 0 Answers