using System; using System.Collections.Generic; using System.Diagnostics; using UnityEngine.Assertions; using UnityEngine.Scripting.APIUpdating; namespace UnityEngine.XR.Interaction.Toolkit.Locomotion.Comfort { /// /// Represents the parameters to control the tunneling vignette material and customize its effects. /// /// /// /// [Serializable] [MovedFrom("UnityEngine.XR.Interaction.Toolkit")] public sealed class VignetteParameters { [SerializeField] [Range(0f, Defaults.apertureSizeMax)] float m_ApertureSize = Defaults.apertureSizeDefault; /// /// The diameter of the inner transparent circle of the tunneling vignette. /// /// /// When multiple providers trigger the tunneling vignette animation, the one with the smallest aperture size /// will be used. The range of this value is [0, 1], where 1 represents having no vignette effect. /// public float apertureSize { get => m_ApertureSize; set => m_ApertureSize = value; } [SerializeField] [Range(0f, Defaults.featheringEffectMax)] float m_FeatheringEffect = Defaults.featheringEffectDefault; /// /// The degree of smoothly blending the edges between the aperture and full visual cut-off. /// Set this to a non-zero value to add a gradual transition from the transparent aperture to the black vignette edges. /// public float featheringEffect { get => m_FeatheringEffect; set => m_FeatheringEffect = value; } [SerializeField] float m_EaseInTime = Defaults.easeInTimeDefault; /// /// The transition time (in seconds) of easing in the tunneling vignette. /// Set this to a non-zero value to reduce the potential distraction from instantaneously changing the user's /// field of view when beginning the vignette. /// public float easeInTime { get => m_EaseInTime; set => m_EaseInTime = value; } [SerializeField] float m_EaseOutTime = Defaults.easeOutTimeDefault; /// /// The transition time (in seconds) of easing out the tunneling vignette. /// Set this to a non-zero value to reduce the potential distraction from instantaneously changing the user's /// field of view when ending the vignette. /// public float easeOutTime { get => m_EaseOutTime; set => m_EaseOutTime = value; } [SerializeField] bool m_EaseInTimeLock = Defaults.easeInTimeLockDefault; /// /// Persists the easing-in transition until it is complete. /// Enable this option if you want the easing-in transition to persist until it is complete. /// This can be useful for instant changes, such as snap turn and teleportation, to trigger /// the full tunneling effect without easing out the vignette partway through the easing in process. /// public bool easeInTimeLock { get => m_EaseInTimeLock; set => m_EaseInTimeLock = value; } [SerializeField] float m_EaseOutDelayTime = Defaults.easeOutDelayTimeDefault; /// /// The delay time (in seconds) before starting to ease out of the tunneling vignette. /// public float easeOutDelayTime { get => m_EaseOutDelayTime; set => m_EaseOutDelayTime = value; } [SerializeField] Color m_VignetteColor = Defaults.vignetteColorDefault; /// /// The primary color of the visual cut-off area of the vignette. /// public Color vignetteColor { get => m_VignetteColor; set => m_VignetteColor = value; } [SerializeField] Color m_VignetteColorBlend = Defaults.vignetteColorBlendDefault; /// /// The optional color to add color blending to the visual cut-off area of the vignette. /// public Color vignetteColorBlend { get => m_VignetteColorBlend; set => m_VignetteColorBlend = value; } [SerializeField] [Range(Defaults.apertureVerticalPositionMin, Defaults.apertureVerticalPositionMax)] float m_ApertureVerticalPosition = Defaults.apertureVerticalPositionDefault; /// /// The vertical position offset of the vignette. /// Changing this value will change the local y-position of the GameObject that this script is attached to. /// public float apertureVerticalPosition { get => m_ApertureVerticalPosition; set => m_ApertureVerticalPosition = value; } /// /// Provides default values for . /// internal static class Defaults { /// /// The default maximum value of . /// public const float apertureSizeMax = 1f; /// /// The default maximum value of . /// public const float featheringEffectMax = 1f; /// /// The default maximum value of . /// public const float apertureVerticalPositionMax = 0.2f; /// /// The default minimum value of . /// public const float apertureVerticalPositionMin = -apertureVerticalPositionMax; /// /// The default value of . /// public const float apertureSizeDefault = 0.7f; /// /// The default value of . /// public const float featheringEffectDefault = 0.2f; /// /// The default value of . /// public const float easeInTimeDefault = 0.3f; /// /// The default value of . /// public const float easeOutTimeDefault = 0.3f; /// /// The default value of . /// public const bool easeInTimeLockDefault = false; /// /// The default value of . /// public const float easeOutDelayTimeDefault = 0f; /// /// (Read Only) The default value of . /// public static readonly Color vignetteColorDefault = Color.black; /// /// (Read Only) The default value of . /// public static readonly Color vignetteColorBlendDefault = Color.black; /// /// The default value of . /// public const float apertureVerticalPositionDefault = 0f; /// /// (Read Only) The that represents the default effect for the vignette. /// public static readonly VignetteParameters defaultEffect = new VignetteParameters { apertureSize = apertureSizeDefault, featheringEffect = featheringEffectDefault, easeInTime = easeInTimeDefault, easeOutTime = easeOutTimeDefault, easeInTimeLock = easeInTimeLockDefault, easeOutDelayTime = easeOutDelayTimeDefault, vignetteColor = vignetteColorDefault, vignetteColorBlend = vignetteColorBlendDefault, apertureVerticalPosition = apertureVerticalPositionDefault, }; /// /// (Read Only) The that represents no effect for the vignette. /// public static readonly VignetteParameters noEffect = new VignetteParameters { apertureSize = apertureSizeMax, featheringEffect = 0f, easeInTime = 0f, easeOutTime = 0f, easeInTimeLock = false, easeOutDelayTime = 0f, vignetteColor = vignetteColorDefault, vignetteColorBlend = vignetteColorBlendDefault, apertureVerticalPosition = apertureVerticalPositionDefault, }; } /// /// Copies the parameter values from the given . /// /// The to copy values from. /// Throws when is . public void CopyFrom(VignetteParameters parameters) { if (parameters == null) throw new ArgumentNullException(nameof(parameters)); apertureSize = parameters.apertureSize; featheringEffect = parameters.featheringEffect; easeInTime = parameters.easeInTime; easeOutTime = parameters.easeOutTime; easeInTimeLock = parameters.easeInTimeLock; easeOutDelayTime = parameters.easeOutDelayTime; vignetteColor = parameters.vignetteColor; vignetteColorBlend = parameters.vignetteColorBlend; apertureVerticalPosition = parameters.apertureVerticalPosition; } } /// /// Options for displaying easing transitions of the tunneling vignette effect /// to reduce potential distractions from instantaneously changing the user's field of view. /// [MovedFrom("UnityEngine.XR.Interaction.Toolkit")] public enum EaseState { /// /// Display the normal state with no vignette effect (e.g., when the user is idling). /// NotEasing, /// /// Display the ease-in transition from the normal state to the tunneled field of view, /// (e.g., when the user started or is continuously moving). /// EasingIn, /// /// Continue displaying the ease-in transition after an ease-out transition is triggered /// and switch to the ease-out transition state after the ease-in transition is complete. /// EasingInHoldBeforeEasingOut, /// /// Delay the start of ease-out transition. /// EasingOutDelay, /// /// Display the ease-out transition from the tunneled field of view to the normal state, /// (e.g., when the user completed moving). /// EasingOut, } /// /// An interface that provides needed to control the tunneling vignette effect. /// [MovedFrom("UnityEngine.XR.Interaction.Toolkit")] public interface ITunnelingVignetteProvider { /// /// Represents the parameter values that this provider wants to set for the tunneling vignette effect. /// A value of indicates the should be used. /// VignetteParameters vignetteParameters { get; } } /// /// Represents an with a . /// [Serializable] [MovedFrom("UnityEngine.XR.Interaction.Toolkit")] public class LocomotionVignetteProvider : ITunnelingVignetteProvider { [SerializeField] LocomotionProvider m_LocomotionProvider; /// /// The to trigger the tunneling vignette effects based on its . /// public LocomotionProvider locomotionProvider { get => m_LocomotionProvider; set => m_LocomotionProvider = value; } [SerializeField] bool m_Enabled; /// /// Whether to enable this to trigger the tunneling vignette effects. /// public bool enabled { get => m_Enabled; set => m_Enabled = value; } [SerializeField] bool m_OverrideDefaultParameters; /// /// If enabled, Unity will override the value of /// and instead use the customized defined by this class. /// /// public bool overrideDefaultParameters { get => m_OverrideDefaultParameters; set => m_OverrideDefaultParameters = value; } [SerializeField] VignetteParameters m_OverrideParameters = new VignetteParameters(); /// /// The that this uses to control the vignette /// when the property to override is enabled. /// /// public VignetteParameters overrideParameters { get => m_OverrideParameters; set => m_OverrideParameters = value; } /// public VignetteParameters vignetteParameters => m_OverrideDefaultParameters ? m_OverrideParameters : null; } /// /// Provides methods for components to control the tunneling vignette material. /// [AddComponentMenu("XR/Locomotion/Tunneling Vignette Controller", 11)] [HelpURL((XRHelpURLConstants.k_TunnelingVignetteController))] [MovedFrom("UnityEngine.XR.Interaction.Toolkit")] public class TunnelingVignetteController : MonoBehaviour { const string k_DefaultShader = "VR/TunnelingVignette"; static class ShaderPropertyLookup { public static readonly int apertureSize = Shader.PropertyToID("_ApertureSize"); public static readonly int featheringEffect = Shader.PropertyToID("_FeatheringEffect"); public static readonly int vignetteColor = Shader.PropertyToID("_VignetteColor"); public static readonly int vignetteColorBlend = Shader.PropertyToID("_VignetteColorBlend"); } [SerializeField] VignetteParameters m_DefaultParameters = new VignetteParameters(); /// /// The default of this . /// public VignetteParameters defaultParameters { get => m_DefaultParameters; set => m_DefaultParameters = value; } [SerializeField] VignetteParameters m_CurrentParameters = new VignetteParameters(); /// /// (Read Only) The current that is controlling the tunneling vignette material. /// public VignetteParameters currentParameters => m_CurrentParameters; [SerializeField] List m_LocomotionVignetteProviders = new List(); /// /// List to store instances that trigger the tunneling vignette on their locomotion state changes. /// public List locomotionVignetteProviders { get => m_LocomotionVignetteProviders; set => m_LocomotionVignetteProviders = value; } /// /// Represents a record of an and its dynamically updated values. /// class ProviderRecord { public ITunnelingVignetteProvider provider { get; } public EaseState easeState { get; set; } = EaseState.NotEasing; public float dynamicApertureSize { get; set; } = VignetteParameters.Defaults.apertureSizeMax; public bool easeInLockEnded { get; set; } public float dynamicEaseOutDelayTime { get; set; } public ProviderRecord(ITunnelingVignetteProvider provider) { this.provider = provider; } } /// /// List to keep the records of all the instances of this controller and their dynamically updated values. /// readonly List m_ProviderRecords = new List(); MeshRenderer m_MeshRender; MeshFilter m_MeshFilter; Material m_SharedMaterial; MaterialPropertyBlock m_VignettePropertyBlock; /// /// Queues an to trigger the ease-in vignette effect. /// /// The that contains information of . /// /// Unity will automatically sort all providers by their aperture size to prioritize the control from the one with the smallest aperture size if /// multiple providers are calling this method. /// public void BeginTunnelingVignette(ITunnelingVignetteProvider provider) { foreach (var record in m_ProviderRecords) { if (record.provider == provider) { record.easeState = EaseState.EasingIn; return; } } m_ProviderRecords.Add(new ProviderRecord(provider) { easeState = EaseState.EasingIn }); } /// /// Queues an to trigger the ease-out vignette effect. /// /// The that contains information of . /// /// Unity will automatically sort all providers by their aperture size to prioritize the control from the one with the smallest aperture size if /// multiple providers are calling this method. /// public void EndTunnelingVignette(ITunnelingVignetteProvider provider) { var parameters = provider.vignetteParameters ?? m_DefaultParameters; // Check if this provider is already in our record. foreach (var record in m_ProviderRecords) { if (record.provider == provider) { // Update the record. record.easeState = parameters.easeInTimeLock && !record.easeInLockEnded ? EaseState.EasingInHoldBeforeEasingOut : parameters.easeOutDelayTime > 0f && record.dynamicEaseOutDelayTime < parameters.easeOutDelayTime ? EaseState.EasingOutDelay : EaseState.EasingOut; return; } } // Otherwise, add the new provider to the record and use its parameters to determine its EaseState. var easeState = parameters.easeInTimeLock ? EaseState.EasingInHoldBeforeEasingOut : parameters.easeOutDelayTime > 0f ? EaseState.EasingOutDelay : EaseState.EasingOut; m_ProviderRecords.Add(new ProviderRecord(provider) { easeState = easeState }); } /// /// (Editor Only) Previews a vignette effect in Editor with the given . /// /// The to preview in Editor. [Conditional("UNITY_EDITOR")] internal void PreviewInEditor(VignetteParameters previewParameters) { // Avoid previewing when inspecting the prefab asset, which may cause the editor constantly refreshing. // Only preview it when it is in the scene or in the prefab window. if (!Application.isPlaying && gameObject.activeInHierarchy) UpdateTunnelingVignette(previewParameters); } /// /// See . /// protected virtual void Awake() { #if UNITY_EDITOR UnityEditor.SceneVisibilityManager.instance.DisablePicking(gameObject, false); #endif m_CurrentParameters.CopyFrom(VignetteParameters.Defaults.noEffect); UpdateTunnelingVignette(VignetteParameters.Defaults.noEffect); } /// /// See . /// [Conditional("UNITY_EDITOR")] protected virtual void Reset() { m_DefaultParameters.CopyFrom(VignetteParameters.Defaults.defaultEffect); m_CurrentParameters.CopyFrom(VignetteParameters.Defaults.noEffect); UpdateTunnelingVignette(m_DefaultParameters); } /// /// See . /// protected virtual void Update() { // Add (only if not already) providers to the list that keeps track of their aperture sizes and ease-out delay time. // Queue their EaseStates to begin/end the easing transitions according to their LocomotionStates. if (m_LocomotionVignetteProviders.Count > 0) { foreach (var provider in m_LocomotionVignetteProviders) { var locomotionProvider = provider.locomotionProvider; if (!provider.enabled || locomotionProvider == null) continue; if (locomotionProvider.isLocomotionActive) BeginTunnelingVignette(provider); else if (locomotionProvider.locomotionState == LocomotionState.Ended) EndTunnelingVignette(provider); } } if (m_ProviderRecords.Count == 0) return; // Max aperture size for no effect const float apertureSizeMax = VignetteParameters.Defaults.apertureSizeMax; // Compute dynamic parameter values for all providers and update their records. foreach (var record in m_ProviderRecords) { var provider = record.provider; var parameters = provider.vignetteParameters ?? m_DefaultParameters; var currentSize = record.dynamicApertureSize; switch (record.easeState) { case EaseState.NotEasing: { record.dynamicApertureSize = apertureSizeMax; record.dynamicEaseOutDelayTime = 0f; record.easeInLockEnded = false; break; } case EaseState.EasingIn: { var desiredEaseInTime = Mathf.Max(parameters.easeInTime, 0f); var desiredEaseInSize = parameters.apertureSize; record.easeInLockEnded = false; if (desiredEaseInTime > 0f && currentSize > desiredEaseInSize) { var updatedSize = currentSize + (desiredEaseInSize - apertureSizeMax) / desiredEaseInTime * Time.unscaledDeltaTime; record.dynamicApertureSize = updatedSize < desiredEaseInSize ? desiredEaseInSize : updatedSize; } else { record.dynamicApertureSize = desiredEaseInSize; } break; } case EaseState.EasingInHoldBeforeEasingOut: { if (!record.easeInLockEnded) { var desiredEaseInTime = Mathf.Max(parameters.easeInTime, 0f); var desiredEaseInSize = parameters.apertureSize; if (desiredEaseInTime > 0f && currentSize > desiredEaseInSize) { var updatedSize = currentSize + (desiredEaseInSize - apertureSizeMax) / desiredEaseInTime * Time.unscaledDeltaTime; record.dynamicApertureSize = updatedSize < desiredEaseInSize ? desiredEaseInSize : updatedSize; } else { record.easeInLockEnded = true; if (parameters.easeOutDelayTime > 0f && record.dynamicEaseOutDelayTime < parameters.easeOutDelayTime) { record.easeState = EaseState.EasingOutDelay; goto case EaseState.EasingOutDelay; } record.easeState = EaseState.EasingOut; goto case EaseState.EasingOut; } } else { if (parameters.easeOutDelayTime > 0f) { record.easeState = EaseState.EasingOutDelay; goto case EaseState.EasingOutDelay; } record.easeState = EaseState.EasingOutDelay; goto case EaseState.EasingOut; } break; } case EaseState.EasingOutDelay: { var currentDelayTime = record.dynamicEaseOutDelayTime; var desiredEaseOutDelayTime = Mathf.Max(parameters.easeOutDelayTime, 0f); if (desiredEaseOutDelayTime > 0f && currentDelayTime < desiredEaseOutDelayTime) { currentDelayTime += Time.unscaledDeltaTime; record.dynamicEaseOutDelayTime = currentDelayTime > desiredEaseOutDelayTime ? desiredEaseOutDelayTime : currentDelayTime; } if (record.dynamicEaseOutDelayTime >= desiredEaseOutDelayTime) { record.easeState = EaseState.EasingOut; goto case EaseState.EasingOut; } break; } case EaseState.EasingOut: { var desiredEaseOutTime = Mathf.Max(parameters.easeOutTime, 0f); var startSize = parameters.apertureSize; if (desiredEaseOutTime > 0f && currentSize < apertureSizeMax) { var updatedSize = currentSize + (apertureSizeMax - startSize) / desiredEaseOutTime * Time.unscaledDeltaTime; record.dynamicApertureSize = updatedSize > apertureSizeMax ? apertureSizeMax : updatedSize; } else { record.dynamicApertureSize = apertureSizeMax; } if (record.dynamicApertureSize >= apertureSizeMax) record.easeState = EaseState.NotEasing; break; } default: Assert.IsTrue(false, $"Unhandled {nameof(EaseState)}={record.easeState}"); break; } } // Find the minimum dynamic aperture size among all providers and update the current parameters with its associated vignette parameters. var minDynamicApertureSize = apertureSizeMax; ProviderRecord minRecord = null; foreach (var record in m_ProviderRecords) { var apertureSize = record.dynamicApertureSize; if (apertureSize < minDynamicApertureSize) { minRecord = record; minDynamicApertureSize = apertureSize; } } if (minRecord != null) m_CurrentParameters.CopyFrom(minRecord.provider.vignetteParameters ?? m_DefaultParameters); m_CurrentParameters.apertureSize = minDynamicApertureSize; // Update the visuals of the tunneling vignette. UpdateTunnelingVignette(m_CurrentParameters); } /// /// Updates the tunneling vignette with the vignette parameters. /// /// The uses to update the material values. /// /// Use this method with caution when other instances are updating the material simultaneously. /// Calling this method will automatically try to set up the material and its renderer for the if it is not set up already. /// void UpdateTunnelingVignette(VignetteParameters parameters) { if (parameters == null) parameters = m_DefaultParameters; if (TrySetUpMaterial()) { m_MeshRender.GetPropertyBlock(m_VignettePropertyBlock); m_VignettePropertyBlock.SetFloat(ShaderPropertyLookup.apertureSize, parameters.apertureSize); m_VignettePropertyBlock.SetFloat(ShaderPropertyLookup.featheringEffect, parameters.featheringEffect); m_VignettePropertyBlock.SetColor(ShaderPropertyLookup.vignetteColor, parameters.vignetteColor); m_VignettePropertyBlock.SetColor(ShaderPropertyLookup.vignetteColorBlend, parameters.vignetteColorBlend); m_MeshRender.SetPropertyBlock(m_VignettePropertyBlock); } // Update the Transform y-position to match apertureVerticalPosition var thisTransform = transform; var localPosition = thisTransform.localPosition; if (!Mathf.Approximately(localPosition.y, parameters.apertureVerticalPosition)) { localPosition.y = parameters.apertureVerticalPosition; thisTransform.localPosition = localPosition; } } bool TrySetUpMaterial() { if (m_MeshRender == null) m_MeshRender = GetComponent(); if (m_MeshRender == null) m_MeshRender = gameObject.AddComponent(); if (m_VignettePropertyBlock == null) m_VignettePropertyBlock = new MaterialPropertyBlock(); if (m_MeshFilter == null) m_MeshFilter = GetComponent(); if (m_MeshFilter == null) m_MeshFilter = gameObject.AddComponent(); if (m_MeshFilter.sharedMesh == null) { Debug.LogWarning("The default mesh for the TunnelingVignetteController is not set. " + "Make sure to import it from the Tunneling Vignette Sample of XR Interaction Toolkit.", this); return false; } if (m_MeshRender.sharedMaterial == null) { var defaultShader = Shader.Find(k_DefaultShader); if (defaultShader == null) { Debug.LogWarning("The default material for the TunnelingVignetteController is not set, and the default Shader: " + k_DefaultShader + " cannot be found. Make sure they are imported from the Tunneling Vignette Sample of XR Interaction Toolkit.", this); return false; } Debug.LogWarning("The default material for the TunnelingVignetteController is not set. " + "Make sure it is imported from the Tunneling Vignette Sample of XR Interaction Toolkit. + " + "Try creating a material using the default Shader: " + k_DefaultShader, this); m_SharedMaterial = new Material(defaultShader) { name = "DefaultTunnelingVignette", }; m_MeshRender.sharedMaterial = m_SharedMaterial; } else { m_SharedMaterial = m_MeshRender.sharedMaterial; } return true; } } }