using System.Collections.Generic;
using UnityEngine.Scripting.APIUpdating;
namespace UnityEngine.XR.Interaction.Toolkit.Interactables.Visuals
{
///
/// Simple Interactable Visual component that demonstrates hover or selection state with emissive tinting.
/// Note: requires use of a shader that supports emission (such as Standard shader) with the variant included in the game build.
///
[MovedFrom("UnityEngine.XR.Interaction.Toolkit")]
[AddComponentMenu("XR/Visual/XR Tint Interactable Visual", 11)]
[DisallowMultipleComponent]
[HelpURL(XRHelpURLConstants.k_XRTintInteractableVisual)]
public class XRTintInteractableVisual : MonoBehaviour
{
[SerializeField, Tooltip("Tint color for interactable.")]
Color m_TintColor = Color.yellow;
///
/// The tint color for interactable.
///
public Color tintColor
{
get => m_TintColor;
set => m_TintColor = value;
}
[SerializeField, Tooltip("Tint on hover.")]
bool m_TintOnHover = true;
///
/// Whether this should tint on hover.
///
public bool tintOnHover
{
get => m_TintOnHover;
set => m_TintOnHover = value;
}
[SerializeField, Tooltip("Tint on selection.")]
bool m_TintOnSelection = true;
///
/// Whether this should tint on selection.
///
public bool tintOnSelection
{
get => m_TintOnSelection;
set => m_TintOnSelection = value;
}
[SerializeField, Tooltip("Renderer(s) to use for tinting (will default to any Renderer on the GameObject if not specified).")]
List m_TintRenderers = new List();
///
/// The (s) to use for tinting (will default to any on the if not specified).
///
public List tintRenderers
{
get => m_TintRenderers;
set => m_TintRenderers = value;
}
IXRInteractable m_Interactable;
IXRHoverInteractable m_HoverInteractable;
IXRSelectInteractable m_SelectInteractable;
MaterialPropertyBlock m_TintPropertyBlock;
bool m_EmissionEnabled;
bool m_HasLoggedMaterialInstance;
///
/// Reusable list of type to reduce allocations.
///
static readonly List s_Materials = new List();
///
/// See .
///
protected void Awake()
{
m_Interactable = GetComponent();
if (m_Interactable is Object unityObject && unityObject != null)
{
m_HoverInteractable = m_Interactable as IXRHoverInteractable;
m_SelectInteractable = m_Interactable as IXRSelectInteractable;
if (m_HoverInteractable != null)
{
m_HoverInteractable.firstHoverEntered.AddListener(OnFirstHoverEntered);
m_HoverInteractable.lastHoverExited.AddListener(OnLastHoverExited);
}
if (m_SelectInteractable != null)
{
m_SelectInteractable.firstSelectEntered.AddListener(OnFirstSelectEntered);
m_SelectInteractable.lastSelectExited.AddListener(OnLastSelectExited);
}
}
else
Debug.LogWarning($"Could not find required interactable component on {gameObject} for tint visual." +
" Cannot respond to hover or selection.", this);
if (m_TintRenderers.Count == 0)
{
GetComponents(m_TintRenderers);
if (m_TintRenderers.Count == 0)
Debug.LogWarning($"Could not find required Renderer component on {gameObject} for tint visual.", this);
}
// Determine if Emission is enabled on the material, or if material instances will need
// to be created to enable it.
m_EmissionEnabled = GetEmissionEnabled();
m_TintPropertyBlock = new MaterialPropertyBlock();
// Set initial tint to on if already hovered or selected
if (m_TintOnHover && (m_HoverInteractable?.isHovered ?? false) ||
m_TintOnSelection && (m_SelectInteractable?.isSelected ?? false))
{
SetTint(true);
}
}
///
/// See .
///
protected void OnDestroy()
{
if (m_Interactable is Object unityObject && unityObject != null)
{
if (m_HoverInteractable != null)
{
m_HoverInteractable.firstHoverEntered.RemoveListener(OnFirstHoverEntered);
m_HoverInteractable.lastHoverExited.RemoveListener(OnLastHoverExited);
}
if (m_SelectInteractable != null)
{
m_SelectInteractable.firstSelectEntered.RemoveListener(OnFirstSelectEntered);
m_SelectInteractable.lastSelectExited.RemoveListener(OnLastSelectExited);
}
}
}
///
/// Apply or remove a tint to all Renderers used for tinting.
///
/// Whether to apply a tint when , or remove the tint when .
protected virtual void SetTint(bool on)
{
var emissionColor = on ? m_TintColor * Mathf.LinearToGammaSpace(1f) : Color.black;
if (!m_EmissionEnabled && !m_HasLoggedMaterialInstance)
{
Debug.LogWarning("Emission is not enabled on a Material used by a tint visual, a Material instance will need to be created.", this);
m_HasLoggedMaterialInstance = true;
}
foreach (var render in m_TintRenderers)
{
if (render == null)
continue;
// Create material instances to enable Emission
if (!m_EmissionEnabled)
{
render.GetMaterials(s_Materials);
foreach (var material in s_Materials)
{
if (on)
material.EnableKeyword("_EMISSION");
else
material.DisableKeyword("_EMISSION");
}
s_Materials.Clear();
}
render.GetPropertyBlock(m_TintPropertyBlock);
m_TintPropertyBlock.SetColor(ShaderPropertyLookup.emissionColor, emissionColor);
render.SetPropertyBlock(m_TintPropertyBlock);
}
}
///
/// Gets whether all shared materials on the Renderers used for tinting have emission enabled.
///
/// Returns if all materials used for tinting have emission enabled. Otherwise, returns .
protected virtual bool GetEmissionEnabled()
{
foreach (var render in m_TintRenderers)
{
if (render == null)
continue;
render.GetSharedMaterials(s_Materials);
foreach (var sharedMaterial in s_Materials)
{
if (!sharedMaterial.IsKeywordEnabled("_EMISSION"))
{
s_Materials.Clear();
return false;
}
}
}
s_Materials.Clear();
return true;
}
void OnFirstHoverEntered(HoverEnterEventArgs args)
{
if (m_TintOnHover)
SetTint(true);
}
void OnLastHoverExited(HoverExitEventArgs args)
{
if (m_TintOnHover)
SetTint(m_TintOnSelection && (m_SelectInteractable?.isSelected ?? false));
}
void OnFirstSelectEntered(SelectEnterEventArgs args)
{
if (m_TintOnSelection)
SetTint(true);
}
void OnLastSelectExited(SelectExitEventArgs args)
{
if (m_TintOnSelection)
SetTint(m_TintOnHover && (m_HoverInteractable?.isHovered ?? false));
}
struct ShaderPropertyLookup
{
public static readonly int emissionColor = Shader.PropertyToID("_EmissionColor");
}
}
}