Why changing material shader at runtime also affect my asset on disk?

I change shader via script

public Shader m_TwoSideShader;

public void ChangeShader()
{
                MeshRenderer meshRenderer = GetComponent<MeshRenderer>();
                Material[] materials = meshRenderer.sharedMaterials;
                for (int i = 0; i < materials.Length; ++i)
                {
                    materials*.shader = m_TwoSideShader;*

}
}
I think everything should return to original after exit play mode, but after I exit play mode, I see that my asset’s material also change the shader. Is this a bug?
Note: this also happen when I change terrain height & texture map at run-time, but I think very few games would change terrain at run-time so Unity wouldn’t care about this, but now this also happen to material shader too?

This is due to using sharedMaterials instead of materials in your case, but it seems like your more general question is why this happens at all.

As far as I can tell, this is not a bug. When you exit play mode, all of the fields in all of your components will be reset, but assets won’t. This is generally a good thing, since you generally shouldn’t be modifying assets at runtime, but it would be very nice to be able to modify them in the editor and see the change at runtime, adjust it to something nice, then keep it that way. Try changing material parameters by hand in play mode. They don’t reset, so you can edit a material the way you see it in-game.

The issue here is that Material is a class, and thus a reference type. If you’re unfamiliar with the idea of reference vs value types, a brief overview: Value types contain their value itself. Bool, float, etc. are value types, as are structs. Setting “Value A = Value B” makes A a copy of B. Changing one doesn’t affect the other. By contrast, reference types have their actual value somewhere in memory, but themselves are just a pointer to that value. Setting “Reference A = Reference B” makes A and B refer to the same value. Changing one changes both. There are plenty of more in-depth posts on this distinction, and if you aren’t familiar with it I recommend looking it up. It’s a very important and powerful part of C# as a language (although in no way unique to C# either).

So the reason you’re seeing your materials change:

MeshRenderer.sharedMaterials is an array of references to a Material asset. If you set a sharedMaterial, this works just fine, since you’re just changing what that field refers to, which gets reset after play mode. But if you modify a sharedMaterial, you aren’t modifying the reference, you’re modifying the value it points to: the asset itself. This applies to terrains, textures, any asset really, even prefabs.

So how can this be fixed?

There are a couple of ways, but both require that you operate on a copy of the material. The simplest way to do this, of course, is to copy each material in the editor and apply the new shader, then use your script to set the sharedMaterial to that material. Of course, this doesn’t scale well, and I’d assume if you’re doing this in a script that that’s a concern. Fortunately, there is Material.CopyPropertiesFromMaterial, which you can use to make a copy in code, then set the material to that. This will create a new material instance that you can modify to your heart’s content without touching the material you copied it from. This solves the problem, but has a few problems:

  • It breaks batching. So would switching the material out, but at least that could batch with other objects using that material. Using a runtime material won’t. You’ll be getting 1 draw call per object unless you’re doing instancing.
  • You won’t be able to see changes made to the source material in the new material until you copy it again.
  • The copy process isn’t super slow, but it’s not something to be doing every frame either. Do it when necessary. Startup or when something changes is fine.

Hope this helps! Good luck on your project!

This annoys me to no end. I animate materials at runtime, and they are persisted causing a lot of changes for my version control.

Here is my solution. Add this script in an Editor folder:

using System.IO;
using System.Linq;
using UnityEditor;

// prevents materials from being saved while we are in play mode
public class PlayModeMaterials : UnityEditor.AssetModificationProcessor
{
    static string[] OnWillSaveAssets(string[] paths)
    {
        if (EditorApplication.isPlaying)
        {
            return paths.Where(path => Path.GetExtension(path) != ".mat").ToArray();
        }
        else
        {
            return paths;
        }
    }
}

If you want to change a single material, set properties directly on the renderer.material.


If you truly need to change the shader of an instance during runtime (this is generally discouraged in game design), create or reference from the editor a different material with said shader. Change the material to that.


If you need to change the shader of a material for ALL instances using that material, you would use renderer.sharedMaterial instead of renderer.material. You would then set its values or change the shader directly.


Finally, if you need the shared values to revert back to what it was in playmode, you would utilize the renderer.CopyPropertiesFromMaterial method, as the best answer described. First you create a new material with the exact same shader and properties, with the original material as constructor parameter. Then on OnApplicationQuit, you will revert the material back into its original state using the cached material. Below is an example code:

     Renderer rd;
 #if UNITY_EDITOR
     Material mat;
 #endif

     void Start()
     {
         rd = GetComponent<Renderer>();
     #if UNITY_EDITOR
         mat = new Material(rd.sharedMaterial.shader);
     #endif
         rd.sharedMaterial.SetVector("_TestVector3", new Vector3(1.0f, 0.0f, 0.0f));
     }
 
 #if UNITY_EDITOR
     void OnApplicationQuit() {
         rd.sharedMaterial.CopyPropertiesFromMaterial(mat);
     }
 #endif

Macros just ensure that this code will not execute in released version, since play mode doesn’t mean anything in build mode.

This is an annoying feature of trying to debug apps that need to modify a material, especially one shared by several different objects. Like if I want to change all my objects of that type from one color to another to highlight them.

The main way around this is to put in a block protected by a compiler directive. This block instantiates the material so you’re working on a copy. This is similar to the suggestion above only it works behind the scenes and doesn’t get compiled into your final build.

This is an example bit of code you could either put in the object’s pre-existing script (you probably already have one on it, if you’re modifying the material anyway). Or you could put it in a standalone script and attach it to the object:

    private void Awake()
    {
#if UNITY_EDITOR
        // make copy if you're in the editor to avoid editing the saved one
        // simply accessing .material the first time causes an instantiation 
        var material = GetComponent<MeshRenderer>().material; 
#endif
    }

If you wanted to do this for several objects and maintain static batching, you’d want to do it more like this in one central script:

#if UNITY_EDITOR
        // make copy if you're in the editor to avoid editing the saved one
        // this finds all the objects of a certain type and reassigns their material to all point to a single new
        // instance, so batching is maintained
        Material material = null;
        // This doesn't have to be GameObject.FindGameObjectsWithTag(). It can be any code that can find the list of
        // objects you actually care about.
        var objs = GameObject.FindGameObjectsWithTag("projectile");
        foreach (var obj in objs)
        {
            if (!material)
            {
                // accessing .material causes it to create an instance
                material = obj.GetComponent<MeshRenderer>().material;
            }
            else
            {
                // assign the previous instance to all the other ones
                obj.GetComponent<MeshRenderer>().sharedMaterial = material;
            }
        }
#endif

Now they all share the same material and can have it all changed with no effects on your material on disk. And since this is a compiler directive, none of this will be included in your finished app.