- Home /
Changing materials cause memory leaks
Hi everyone,
I have problem of memory leak when using materials. I read some thread about this but I didn't found the answer i was looking for. So I need to change all the materials on a gameobject each second and the materials I need are kept in an array. The problem is that some copies of the materials are kept in memory every time I change the materials.
Here is the code I use:
foreach (GameObject g in Rooms)
{
if (g != null)
{
int index = a number computed;
Material[] mats = g.GetComponent<MeshRenderer>().materials;
for (int j = 0; j < g.GetComponent<MeshRenderer>().materials.Length; j++)
{
mats[j] = StoredMaterial[index];
}
g.GetComponent<MeshRenderer>().materials = mats;
}
}
I tried to found a work around but I found none that resolve all my problem. If i use the following code, I do note have the leaks anymore but only the first material is changed and if the gameobjects have more than one materials it did not work well.
foreach (GameObject g in Rooms)
{
if (g != null)
{
int index = a number previously computed;
g.GetComponent<MeshRenderer>().material = StoredMaterial[index];
}
}
StoredMaterial is an Array of Materials where are stored the different materials that could be used at every iteration of my application to colorize the differents gameobjects with the good color. And the goal is to change the color of some part of a big object at runtime using external data to pick the correct color in the StoredMaterial array. And as I build on a low performance device I must take care of the perf of my application.
Thanks in advance for your help.
Answer by Forc3ment · Jul 09, 2018 at 01:55 PM
It appears that the garbage collector do not collect unused materials and as I do not destroy the materials I replace, it creates a memory leaks. I just had to destroy the Materials in g.GetComponent().materials before assigning them again. In order to change all the materials on one gameobject without creating a leak, I first create a array with the good size, then for each materials inside, I destroy the current one and add the new one in the array. Then replace the old array with the new one.
Material[] mats = new Material[g.GetComponent<MeshRenderer>().materials.Length];
for (int j = 0; j < g.GetComponent<MeshRenderer>().materials.Length; j++)
{
Destroy(g.GetComponent<MeshRenderer>().materials[j]);
mats[j] = StoredMaterial[index];
}
g.GetComponent<MeshRenderer>().materials = mats;
Answer by Bunny83 · Jan 07, 2020 at 08:07 PM
Since all answers are either wrong or provide bad (almost horrible) solutions I'll post an answer.
Many people seem to not understand or are not aware of the difference between the material / materials property compared to the sharedMaterial / sharedMaterials property. Whenever you use the material or materials property (use means either reading or writing it) Unity will automatically instantiate the material for this renderer unless it already has its own instance. This has many severe consequences.
First of all as observed the instantiated materials are not automatically destroyed when you replace the material. This is actually true for all objects that are derived from UnityEngine.Object. So this holds true for Mesh, Material, GameObject, Component, ScriptableObject, .. For GameObjects and its components there's a slight difference. When destroying a gameobject all components and sub gameobjects are destroyed as well. However destroying a gameobject or the renderer component does not destroy an instantiated Material or Mesh. The material is not "owned" by the renderer and could potentially be used by other objects as well. This is not possible with components.
Besides the duty of cleaning up the instantiated Materials when using those properties is that instantiated materials are seperate materials. That means they will always cause to be rendered with its own drawcall, even when all parameters are the same. So looping through 10 objects and assigning the same material to the material / materials property will actually create 10 seperate materials. Such materials will not be batched. So by using those properties you essentially loose any batching support for those objects.
The properties sharedMaterial and sharedMaterials on the other hand do not automatically instantiate the Material. That means if you have 10 objects and you assigned the same material to all 10 sharedMaterial properties, they will all use the same material. Such objects can actually be batched (statically or dynamically). Of course that also means you can not change any material attributes only for one object. So when changing the shared material all objects that are using that material will be affected.
Unfortunately at the moment we can not check / test if a renderer actually has an instantiated material or not because any access to material / materials would immediately instantiate the material(s). So like mentioned in the documentation of the material property you are responsible for keeping track of where you actually created instances of Materials.
About the code snippet in the question another warning when using the materials or sharedMaterials property. They return an array of materials. However each time you read this property a new array will be created. Since the for loop uses .materials.Length in the loop condition the code would create a new array for every loop iteration. Of course those arrays will be garbage collected but it's unnecessary created garbage. The code in the OP's answer is even worse. Instead of using materials and causing an instantiation of all those materials he should just use sharedMaterials without destroying anything. So the proper answer would be:
MeshRenderer rend = g.GetComponent<MeshRenderer>();
Material[] mats = rend.sharedMaterials;
for (int j = 0; j < mats.Length; j++)
{
mats[j] = StoredMaterial[index];
}
rend.sharedMaterials = mats;
This should (also) be the correct answer for the asked question. Cool explanation. Please don't stop answering questions in this matter. Even if the info is just a little too much for the intended purpose, I enjoyed the read and it gave me everything to understand how it works (as much as we can).
Answer by im012 · Jul 09, 2018 at 01:13 PM
You Should Use System.Gc.Collect for this .
I think it should work but this operation is I think to heavy for my device to be done at each update.
System.GC.Collect does NOT collect unreferenced ASSETS. Only Resources.UnloadUnusedAssets() does that, and it also calls System.GC.Collect internally. Also, both of these are potentially heavy function-calls, which should not be used lightly. At least, I don't think it should warrant a GC or any such operation to remove a material from memory.
Answer by madks13 · Jul 03, 2018 at 08:59 AM
Can you give more details on what you're trying to achieve here? Someone might give you another way of doing it. Also, what is StoredMaterial?
If i understood correctly, you are changing object colors based on external data, with update tick of 1s or less? Do you need to update even if the color stays the same?
I may verify if the materials need to be changed but the problem will remains with those wich need to be changed.
Your answer
Follow this Question
Related Questions
Changing the material in an array of materials at run time 2 Answers
How to replace materials in the materials array 3 Answers
Collada import material Main Colors 0 Answers
Is there a way to export animated UVs from a 3d package? 2 Answers
Any suggestions on this approach to multiple objects and materials 0 Answers