When I load a scene, in-game, I want to run through all the materials and set their “_SkyColor”. The code below does this perfectly. However, there is a side effect! When I stop running the game in the editor the changes are still there, and have been written to the .mat files!
Is there a way of writing to the shared material without it writing back to the source files?
public class LevelColourScript
{
static public void SetMaterials()
{
GameObject[] gameObjects = (GameObject[]) Object.FindSceneObjectsOfType( typeof( GameObject ) );
foreach( GameObject gameObject in gameObjects )
{
Debug.Log( string.Format( "LevelColourScript::SetMaterials - Found {0}", gameObject.name ) );
Renderer[] renderers = gameObject.GetComponents<Renderer>();
foreach( Renderer renderer in renderers )
{
if( renderer.sharedMaterial.HasProperty( "_SkyColor" ) )
{
renderer.sharedMaterial.SetColor ( "_SkyColor", new Vector4( 1.00f, 0.00f, 0.00f, 1.00f ) );
}
}
}
}
}
Apparently, this is by design (though I don’t think it’s a very good design choice):
What I took from the above link, is that you should basically re-instantiate a copy of any sharedMaterial in your start method, as such:
protected void Start() {
myObject.renderer.sharedMaterial = new Material(myObject.renderer.sharedMaterial);
}
What this does is allow you to still use materials in the editor, but create a copy of it at runtime. You can then use .sharedMaterial.setColor() etc in your Update() method (as you would expect), which will only modify the in-memory copy of the material, resetting it back to the original material once you stop debugging.
This is really how it should work in unity itself - your code shouldn’t explicitly modify an asset file, unless you call some kind of saveToDisk() method on it.
As a side note, it seems that RenderSettings.skybox is implicitly a shared material - changing a value on it will modify the .mat file applied to the skybox. To get around this, I used a similar line in my Start method:
RenderSettings.skybox = new Material(RenderSettings.skybox);
The behaviour of sharedMaterial is as intended, you should make adjustments to material instead. To minimise the number of unique materials you create in doing this you could try reading from the sharedMaterial property, using this to build a cache of adjusted materials to be applied to the renderers.
using UnityEngine;
using MaterialCache = System.Collections.Generic.Dictionary<UnityEngine.Material, UnityEngine.Material>;
public class LevelColourScript
{
static public void SetMaterials()
{
MaterialCache cache = new MaterialCache();
GameObject[] gameObjects = Object.FindObjectsOfType<GameObject>();
foreach( GameObject gameObject in gameObjects )
{
Debug.Log( string.Format( "LevelColourScript::SetMaterials - Found {0}", gameObject.name ) );
Renderer[] renderers = gameObject.GetComponents<Renderer>();
foreach( Renderer renderer in renderers )
{
if( renderer.sharedMaterial.HasProperty( "_SkyColor" ) )
{
Material adjusted;
if (cache.TryGetValue(renderer.sharedMaterial, out adjusted)) {
// The adjusted material is already available, write this instance.
renderer.material = adjusted;
} else {
// Not cached yet, instantiate a new material from this renderer and cache it
// for later reuse.
adjusted = renderer.material;
adjusted.SetColor( "_SkyColor", new Vector4( 1.00f, 0.00f, 0.00f, 1.00f ) );
cache.Add(renderer.sharedMaterial, adjusted);
}
}
}
}
}
}
using UnityEngine;
using System.Collections;
using Vexe.Runtime.Extensions;
using MaterialCache = System.Collections.Generic.Dictionary<UnityEngine.Material, UnityEngine.Material>;
public class SharedMaterialReinstantiater : MonoBehaviour {
private static readonly MaterialCache _cache = new MaterialCache();
void Awake() {
var renderer1 = GetComponent<Renderer>();
Material cachedMat;
if (!_cache.TryGetValue(renderer1.sharedMaterial, out cachedMat)) {
cachedMat = new Material(renderer1.sharedMaterial);
Debug.Log("Found new material " + renderer1.sharedMaterial.name);
_cache.Add(renderer1.sharedMaterial, cachedMat);
}
renderer1.sharedMaterial = cachedMat;
}
}