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