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!