- Home /
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[i].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?
Answer by Zarenityx · Aug 02, 2018 at 08:54 PM
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!
But I see that if I run the game as release mode, then it won't modify my release-mode-asset on disk. So I think it would be cool if I can adapt this behavior for editor mode.
That would be interesting, although I'm not sure what what this would really let you do that wouldn't be better as an editor script. I feel like it would just make testing a nightmare. But I'd be interested if you found a use for it.
Fairly disturbing behaviour, this shouldn't really happen.
It doesn't has much to do with reference/ value types rather with serialization (of course you manipulate the actual instance of the material, and changes should apply to all objects using said material, but it shouldn't be directly connected with the serialized data on your hard drive($$anonymous$$aterial asset or any other asset as matter of hand)), meaning the material are serialized upon change or on Play$$anonymous$$ode ter$$anonymous$$ation.
Could be the way they are loaded or reloaded , maybe for synchronization reasons, beats me, hopefully not to easy the editing.
On what other assets did you noticed this behaviour?
It's not actually internally connected to what's on your hard drive really, I just didn't want to make my answer even longer. As far as I can tell the material is loaded into memory when you're messing around with it in the editor, and that memory can of course be changed (thus the reference issue). The Unity editor marks any assets that have changed as 'dirty', and then updates the disk version accordingly.
While I can see your point that it can have unintended side effects, not having it means not being able to edit materials while playing your game, which could really hurt the iteration process for designers and artists. And for coders, it's a pretty simple consideration to handle.
Answer by goldbug · May 01, 2019 at 02:35 AM
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;
}
}
}
Sorry, i haven't understand where/how use that. Can you give me an explanation?
Thanks!
Still very useful in 2019.3, thanks! To address the previous comment: create a folder Assets/Editor and put it in there. It will work automatically.
I tried this out... but when I save the project, it still alters all the materials.... the material appears modified in git, how to fix please ?
I can confirm this doesn't work at all using Unity 2019.4.21f1
Looking at it now, I don't even get how it's supposed to work to begin with. This would prevent it writing changes for materials if you clicked Save Project while also having it running in the editor. However, once you stopped running it, it wouldn't revert the changes. The next time you saved your project, those changes would be saved.
I guess this worked for the OP because their code set the materials back to their initial state. However, if you stopped playing partway through the app or it crashed, you'd still wind up with changed materials and this script wouldn't prevent the save.
Answer by terriblyfun · Jan 20 at 06:47 PM
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.