- Home /
How to stop renderer.sharedMaterial.SetColor writing to source .mat files?
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 ) );
}
}
}
}
}
Thanks for reading.
Jamie
Just come across this myself. If I can't find anything, I think I'm going to write an editor script that resets all my shared$$anonymous$$aterial values to their default values when I stop debugging the game.
If you write to material, rather than shared$$anonymous$$aterial, then the changes you make will be temporary. If you want to $$anonymous$$imise the number of unique materials that you create in your loop doing this you could use a Dictionary to build a cache, reading from shared$$anonymous$$aterial and writing back to material.
I was under the impression that if you accessed renderer.material (read or write), you instantly created a copy of it? Which isn't always what you want to do... whenever I use shared$$anonymous$$aterial, I want to update the material itself, rather than the instance.
Accessing .shared$$anonymous$$aterial should really only be temporary too - it should revert back to the original value once you stop debugging. This is causing havoc with my source control system at the moment - I can't even debug my scene without it changing a file.
This is why I was suggesting reading from shared$$anonymous$$aterial to build your cache and only write to material when necessary. I will write some code to make this explicit.
Answer by Spikeh · Dec 07, 2013 at 06:30 PM
Apparently, this is by design (though I don't think it's a very good design choice):
http://answers.unity3d.com/questions/487908/modifying-material-modifies-the-actual-mat-file.html
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);
Answer by Draitch · Dec 07, 2013 at 07:33 PM
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);
}
}
}
}
}
}
Accessing and setting .material is inefficient in some circumstances (such as modifying the colour of cloud particles, like I'm doing) - it generates more draw calls, as a unique material will be generated for each renderer, even if you set all instances to the same colour - the second you read or write to .material.
The in memory method in the link I posted above is closer to what's needed here - .shared$$anonymous$$aterial is still set in code, but the prefab itself won't be modified (as a "new" instance of the prefab is instantiated). It's not quite right for my use, but it's given me more than enough to go on.
Not really, since the method you describe explicitly creates a new 'shared$$anonymous$$aterial' per object in its Start method. This reduces opportunities for batching, due to the duplication of materials, and so will lead to more draw calls. It will, however, dodge the problem of writing modified values back to disk in the original material. At best, it's wasteful in instantiating materials which are later dropped from memory by later updates to that property. That seems unattractive from the point of view of unnecessary allocation and garbage collection activity.
The use of cache in the method I describe both avoids modifying the original materials and via the cache potentially reduces the number of materials that need to be instantiated.
Right you are, I hadn't thought of applying the script to multiple objects (all my shared$$anonymous$$aterial stuff is contained within a single script that will only ever apply to a single object).
I suppose I'm arguing a point that doesn't apply to this question (but solves my personal issue). $$anonymous$$y issue with your code is that you're accessing .material, which doesn't really solve the problem of modifying .shared$$anonymous$$aterial and not modifying the underlying asset file.
I'm updating a number of shared$$anonymous$$aterial properties constantly, every frame - meaning that the efficiency of using a cache is instantly negated. It would make much more sense to create some kind of Shared$$anonymous$$aterial$$anonymous$$anager, which runs after all other scripts and applies a .shared$$anonymous$$aterial to all those objects that share that material, in the same vain as my snippet - a bit like the code contained in the answer on the original link I posted.
I can't think of a single instance where I would want to modify an asset file through code. The Unity implementation is counter intuitive.
You could say it solves the problem by deliberately not touching the shared$$anonymous$$aterial property and running into Unity's preferred way of treating this property.
Once again assu$$anonymous$$g multiple objects referencing the same material, the benefit of the cache would not be negated by the frequent updates you mention. In fact, I would use it in an implementation of the manager that you mention: iterating over the cached entries and making changes there per frame.
Although I can think of times in which I might wish to procedurally modify assets permanently, these moments do not occur when my game clients are executing and so I agree that Unity's behaviour is somewhat perverse.
Your points are all valid :) Learned a lot from this little thread :)
I've just spent a bit of time fixing a bug I just noticed in my game - I have a "glowing" material attached to a vast number of game objects in my game (collectable items). It constantly changes colour via a tween to inform the player that the item can be picked up. Anyway, I just realised (thanks to your comments) that I was actually creating a shared$$anonymous$$aterial per object - so I've basically created a static class that contains an instantiated instance of the material:
using UnityEngine;
/// <summary>
/// Provides shared material constants for batching reuse.
/// </summary>
public static class $$anonymous$$aterialConstants {
public static readonly $$anonymous$$aterial CollectableGlow$$anonymous$$aterial = new $$anonymous$$aterial(Resources.Load(ResourceConstants.Shader_CollectableGlow) as $$anonymous$$aterial);
}
I then apply the tween to the material instance above in a base class Start() method (but check if the tween is already running first, so it only applies it once), which considerably reduces the draw calls in my scene!
glowRenderer.shared$$anonymous$$aterial = $$anonymous$$aterialConstants.CollectableGlow$$anonymous$$aterial;
if(glowRenderer.shared$$anonymous$$aterial != null) {
// Check if a tween has already been initiated on the shared glow material
if(!HOTween.IsTweening($$anonymous$$aterialConstants.CollectableGlow$$anonymous$$aterial)) {
// Set shader to transparent colour to start with
$$anonymous$$aterialConstants.CollectableGlow$$anonymous$$aterial.SetColor("_OutlineColor", new Color(GlowColor.r, GlowColor.g, GlowColor.b, 0));
// Initiate the glow tween on the shared material
HOTween.To($$anonymous$$aterialConstants.CollectableGlow$$anonymous$$aterial, GlowSpeed, new TweenParms()
.Prop("shader", new PlugSetColor(GlowColor).Property("_OutlineColor"))
.Ease(EaseType.Linear)
.Loops(-1, LoopType.Yoyo));
}
} else {
throw new Exception("Collectable glowRenderer material is null");
}
I suppose this is a form of caching, but using a named instance, rather than type checking.
Answer by astracat111 · Dec 04, 2017 at 01:39 AM
Just add this to a global script (attached to your starting main camera), it worked for me:
foreach (GameObject gameObject in GameObject.FindObjectsOfType(typeof(GameObject))) {
if (gameObject.GetComponent<Renderer> () != null && gameObject.GetComponent<Renderer> ().sharedMaterial != null) {
gameObject.GetComponent<Renderer> ().sharedMaterial = new Material (gameObject.GetComponent<Renderer> ().sharedMaterial);
}
}
That's a horrible idea. In this case you can simply use the material property. The point of using shared$$anonymous$$aterial is that all instances use the same material. If you create a seperate material instance for each gameobject nothing can be batched by Unity.
Answer by LouisHong · Jul 24, 2016 at 03:00 PM
After reading both answers, I've written a much easier to use script that is just as efficient.
https://gist.github.com/loolo78/cadb84af3150a707b47f7c9c9a2dce6a
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;
}
}