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"); } } }