My original goal was to create a component similar to CanvasGroup in the UI system that would work for Sprites. Then I needed it to work with MeshText components as well - that's where I encountered the error described in my question.
Given my original goal, the code was tested on those components (sprites & text) and I can't guarantee that it works correctly with any other materials. For example, transparency of some materials wouldn't be changed if their RenderingMode is not Fade or Transparent, or their shader is not "Transparent/Diffuse" (it can be changed with material.shader = Shader.Find("Transparent/Diffuse"); or in the Inspector).
AlphaGroup.cs
using System.Linq;
using UnityEditor;
using UnityEngine;
/// <summary>
/// Controls the transparency of the entire object (including all children) as `CanvasGroup` does in the UI system.
/// Executes both in play- & edit- modes.
/// </summary>
[DisallowMultipleComponent]
[ExecuteAlways]
public class AlphaGroup : MonoBehaviour
{
[Range(0f, 1f)]
[SerializeField]
private float _alpha = 1f;
private float _prevAlpha;
private Renderer[] _renderers;
private void Awake()
{
_renderers = GetComponentsInChildren<Renderer>();
}
private void Update()
{
UpdateAlpha();
}
private void UpdateAlpha()
{
if (_prevAlpha == _alpha)
return;
for (int i = 0; i < _renderers.Length; i++)
{
var renderer = _renderers[i];
if (renderer is SpriteRenderer spriteRenderer)
{
var color = spriteRenderer.color;
color.a = _alpha;
spriteRenderer.color = color;
}
else if (renderer is MeshRenderer meshRenderer)
{
IsCopyMaterial isCopy = renderer.gameObject.GetComponent<IsCopyMaterial>();
Material[] materials;
if (isCopy)
{
materials = meshRenderer.sharedMaterials;
}
else
{
materials = meshRenderer.sharedMaterials.Select(m => Instantiate(m)).ToArray();
for (int j = 0; j < materials.Length; j++)
{
var material = materials[j];
material.name = meshRenderer.sharedMaterials[j].name + " (copy)";
}
meshRenderer.sharedMaterials = materials;
meshRenderer.gameObject.AddComponent<IsCopyMaterial>(); // Create a copy of the renderer materials only once.
}
foreach (Material material in materials)
{
if (!material.HasProperty("_Color"))
continue;
var color = material.color;
color.a = _alpha;
material.color = color;
}
}
}
_prevAlpha = _alpha;
}
}
IsCopyMaterial.cs
/// <summary>
/// An indicator that the material has been copied.
/// </summary>
[DisallowMultipleComponent]
public class IsCopyMaterial : MonoBehaviour
{
}
sharedMaterialdoesn't suit because it will change transparency for all the objects which use the same material in the scene. I will read it. \$\endgroup\$Instantiate(material)then I should set it to therenderer.material, in this case, who is responsible for destroying the previous instance ofrenderer.materialwhich is replaced with a new one? \$\endgroup\$