Hi there!
Here’s another post about a topic I feel isn’t well enough covered: Material Property Blocks.
I’ve read here and there the following question in different forms:
How do I manage to give renderers unique material values without manually creating material variants?
So, let’s consider the following example: a Sprite Unlit Shader Graph, featuring Properties to fill the Sprite with a Radial Gradient.
It’s really just an example, the following will work with any Renderer type.
I’m using Unity 6 (6000.0.0b16) here, so I made the properties Local, but unchecked Show In Inspector, since I don’t want to set those properties from the Material Inspector, but from a C# component on the Sprite Renderer.
In earlier version, you may just set them as Global. It’ll have a similar effect as Global Properties (Uniforms) can still be overridden locally with a Material Property Block.
We can then assign the generated Material to several Sprites. I don’t even need to create a Material Asset, since it has no parameters other than those we’ll set from C#.
Now, how do we go about setting unique values per Renderer?
First, we need to make a new MonoBehaviour with parameters we can set in the Inspector.
using UnityEngine;
[RequireComponent(typeof(Renderer))] // <--- this ensures we have a Renderer
public class MaterialPropertyBlockDemo : MonoBehaviour
{
[SerializeField, ColorUsage(true, true)] Color _colorA = Color.yellow;
[SerializeField, ColorUsage(true, true)] Color _colorB = Color.cyan;
[SerializeField] Vector2 _center;
[SerializeField] float _radius = .5f;
[SerializeField, Range(.1f, 5f)] float _power = 1f;
[SerializeField] bool _useTexture;
[SerializeField] Texture2D _texture;
}
We then need to hold a reference to the Renderer, and a MaterialPropertyBlock.
We can declare two methods, one to initialize references, and another to update properties, that we can call from within Unity Messages like OnValidate() which gets called whenever you make a change to the values.
Renderer _renderer;
MaterialPropertyBlock _materialPropertyBlock;
private void Awake()
{
Init();
}
private void Start()
{
UpdatePropertyBlock();
}
private void OnValidate()
{
Init();
UpdatePropertyBlock();
}
private void Reset()
{
Init();
UpdatePropertyBlock();
}
private void Init()
{
if (_renderer == null)
_renderer = GetComponent<Renderer>();
if (_materialPropertyBlock == null)
_materialPropertyBlock = new MaterialPropertyBlock();
}
void UpdatePropertyBlock()
{
}
Now, to update the values, all we have to do is set them in the MaterialPropertyBlock and set the Renderer’s Property Block.
void UpdatePropertyBlock()
{
_materialPropertyBlock.SetColor("_colorA", _colorA);
_materialPropertyBlock.SetColor("_colorB", _colorB);
_materialPropertyBlock.SetVector("_Center", _center);
_materialPropertyBlock.SetFloat("_Radius", _radius);
_materialPropertyBlock.SetFloat("_Power", _power);
_materialPropertyBlock.SetFloat("_UseTexture", _useTexture?1:0);
_materialPropertyBlock.SetTexture("_Texture", _texture);
_renderer.SetPropertyBlock(_materialPropertyBlock);
}
Since materialPropertyBlock.SetTexture() cannot be passed a null reference, we want to make sure it is not null.
We can do this with a C# Property like :
[SerializeField] bool _useTexture;
[SerializeField] Texture2D _texture;
public bool UseTexture { get => _useTexture && _texture != null; }
And use that property to set the “_UseTexture” Property, and not assign a texture if it’s null or not needed.
_materialPropertyBlock.SetFloat("_UseTexture", UseTexture?1:0);
if (UseTexture)
_materialPropertyBlock.SetTexture("_Texture", _texture);
Now each Sprite can share the same Material, yet use unique values.
Should we want to animate those properties, we could call UpdatePropertyBlock() from Update()
private void Update()
{
UpdatePropertyBlock(); // this allows for animated properties
}
And shall we want to preview the animation when not in Play mode, scrubbing a Timeline for example, we’d need to add the [ExecuteAlways] attribute to our component.
[ExecuteAlways] // this allows for animated properties preview in Timeline when in Edit Mode
public class MaterialPropertyBlockDemo : MonoBehaviour
One last optimization we’d want to do then, is to store nameIDs using Shader.PropertyToID().
int _colorA_id, _colorB_id, _center_id, _radius_id, _power_id, _useTexture_id, _texture_id;
private void Init()
{
if (_renderer == null)
_renderer = GetComponent<Renderer>();
if (_materialPropertyBlock == null)
_materialPropertyBlock = new MaterialPropertyBlock();
_colorA_id = Shader.PropertyToID("_ColorA");
_colorB_id = Shader.PropertyToID("_ColorB");
_center_id = Shader.PropertyToID("_Center");
_radius_id = Shader.PropertyToID("_Radius");
_power_id = Shader.PropertyToID("_Power");
_useTexture_id = Shader.PropertyToID("_UseTexture");
_texture_id = Shader.PropertyToID("_Texture");
}
void UpdatePropertyBlock()
{
_materialPropertyBlock.SetColor(_colorA_id, _colorA);
_materialPropertyBlock.SetColor(_colorB_id, _colorB);
_materialPropertyBlock.SetVector(_center_id, _center);
_materialPropertyBlock.SetFloat(_radius_id, _radius);
_materialPropertyBlock.SetFloat(_power_id, _power);
_materialPropertyBlock.SetFloat(_useTexture_id, UseTexture?1:0);
if (UseTexture)
_materialPropertyBlock.SetTexture(_texture_id, _texture);
_renderer.SetPropertyBlock(_materialPropertyBlock);
}
I hope this helps. Your feedback is welcome.

