// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Microsoft.MixedReality.GraphicsTools
{
///
/// The MaterialInstance behavior aides in tracking instance material lifetime and automatically destroys instanced materials for the user.
/// This utility component can be used as a replacement to Renderer.material or
/// Renderer.materials. When invoking Unity's Renderer.material(s), Unity
/// automatically instantiates new materials. It is the caller's responsibility to destroy the materials when a material is no longer needed or the game object is
/// destroyed. The MaterialInstance behavior helps avoid material leaks and keeps material allocation paths consistent during edit and run time.
///
[ExecuteAlways, RequireComponent(typeof(Renderer))]
[AddComponentMenu("Scripts/GraphicsTools/MaterialInstance")]
public class MaterialInstance : MonoBehaviour
{
///
/// Default postfix applied to all instance material names.
///
public static readonly string InstancePostfix = " (Instance)";
///
/// Returns true if a material is instanced (this is currently tracked by a material having the InstancePostfix in the name).
///
public static bool IsInstance(Material material)
{
return ((material != null) && material.name.Contains(InstancePostfix));
}
///
/// Creates a new material instance based on a source material. The name is updated to reflect being an instance.
///
public static Material Instance(Material source)
{
if (source == null)
{
return null;
}
if (IsInstance(source))
{
Debug.LogWarning($"The material ({source.name}) is already instanced and is being instanced multiple times.");
}
Material output = new Material(source);
output.name += InstancePostfix;
return output;
}
///
/// Returns the first instantiated Material assigned to the renderer, similar to Renderer.material.
/// If any owner is specified the instanced material(s) will not be released until all owners are released. When a material
/// is no longer needed ReleaseMaterial should be called with the matching owner.
///
/// An optional owner to track instance ownership.
/// The first instantiated Material.
public Material AcquireMaterial(Object owner = null, bool instance = true)
{
if (owner != null)
{
materialOwners.Add(owner);
}
if (instance)
{
AcquireInstances();
}
if (instanceMaterials?.Length > 0)
{
return instanceMaterials[0];
}
return null;
}
///
/// Returns all the instantiated materials of this object, similar to Renderer.materials.
/// If any owner is specified the instanced material(s) will not be released until all owners are released. When a material
/// is no longer needed ReleaseMaterial should be called with the matching owner.
///
/// An optional owner to track instance ownership.
/// Should this acquisition attempt to instance materials?
/// All the instantiated materials.
public Material[] AcquireMaterials(Object owner = null, bool instance = true)
{
if (owner != null)
{
materialOwners.Add(owner);
}
if (instance)
{
AcquireInstances();
}
return instanceMaterials;
}
///
/// Relinquishes ownership of a material instance. This should be called when a material is no longer needed
/// after acquire ownership with AcquireMaterial(s).
///
/// The same owner which originally acquire ownership via AcquireMaterial(s).
/// Should this acquisition attempt to instance materials?
/// When ownership count hits zero should the MaterialInstance component be destroyed?
public void ReleaseMaterial(Object owner, bool autoDestroy = true)
{
materialOwners.Remove(owner);
if (autoDestroy && materialOwners.Count == 0)
{
DestroySafe(this);
// OnDestroy not called on inactive objects
if (!gameObject.activeInHierarchy)
{
RestoreRenderer();
}
}
}
///
/// Returns the first instantiated Material assigned to the renderer, similar to Renderer.material.
///
public Material Material
{
get { return AcquireMaterial(); }
}
///
/// Returns all the instantiated materials of this object, similar to Renderer.materials.
///
public Material[] Materials
{
get { return AcquireMaterials(); }
}
private Renderer CachedRenderer
{
get
{
if (cachedRenderer == null)
{
cachedRenderer = GetComponent();
}
return cachedRenderer;
}
}
private Renderer cachedRenderer = null;
[SerializeField, HideInInspector]
private Material[] defaultMaterials = null;
private Material[] instanceMaterials = null;
private bool initialized = false;
private bool materialsInstanced = false;
private readonly HashSet