// 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.Toolkit.Rendering
{
///
/// 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.
///
[HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/rendering/material-instance")]
[ExecuteAlways, RequireComponent(typeof(Renderer))]
[AddComponentMenu("Scripts/MRTK/Core/MaterialInstance")]
public class MaterialInstance : MonoBehaviour
{
///
/// 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(); }
}
///
/// Whether to use a cached copy of cachedRenderer.sharedMaterials or call sharedMaterials on the Renderer directly.
/// Enabling the option will lead to better performance but you must turn it off before modifying sharedMaterials of the Renderer.
///
public bool CacheSharedMaterialsFromRenderer
{
get
{
return cacheSharedMaterialsFromRenderer;
}
set
{
if (cacheSharedMaterialsFromRenderer != value)
{
if (value)
{
cachedSharedMaterials = CachedRenderer.sharedMaterials;
}
else
{
cachedSharedMaterials = null;
}
cacheSharedMaterialsFromRenderer = value;
}
}
}
private Renderer CachedRenderer
{
get
{
if (cachedRenderer == null)
{
cachedRenderer = GetComponent();
if (CacheSharedMaterialsFromRenderer)
{
cachedSharedMaterials = cachedRenderer.sharedMaterials;
}
}
return cachedRenderer;
}
}
private Material[] CachedRendererSharedMaterials
{
get
{
if (CacheSharedMaterialsFromRenderer)
{
if (cachedSharedMaterials == null)
{
cachedSharedMaterials = cachedRenderer.sharedMaterials;
}
return cachedSharedMaterials;
}
else
{
return cachedRenderer.sharedMaterials;
}
}
set
{
if (CacheSharedMaterialsFromRenderer)
{
cachedSharedMaterials = value;
}
cachedRenderer.sharedMaterials = value;
}
}
private Renderer cachedRenderer = null;
[SerializeField, HideInInspector]
private Material[] defaultMaterials = null;
private Material[] instanceMaterials = null;
private Material[] cachedSharedMaterials = null;
private bool initialized = false;
private bool materialsInstanced = false;
[SerializeField]
[Tooltip("Whether to use a cached copy of cachedRenderer.sharedMaterials or call sharedMaterials on the Renderer directly. " +
"Enabling the option will lead to better performance but you must turn it off before modifying sharedMaterials of the Renderer.")]
private bool cacheSharedMaterialsFromRenderer = false;
private readonly HashSet