namespace UnityEngine.XR.Content.Rendering
{
///
/// Draws an outline on an object when highlighting. Can either transition the color and or size of the out line
/// as selected or be instant on.
///
public class OutlineHighlight : MonoBehaviour, IMaterialHighlight
{
enum OutlineSource
{
Shader = 0,
Material
}
const float k_OutlineWidth = 0.005f;
const string k_ShaderColorParameter = "_Color";
const string k_ShaderWidthParameter = "g_flOutlineWidth";
static readonly int k_GFlOutlineWidth = Shader.PropertyToID(k_ShaderWidthParameter);
static readonly int k_Color = Shader.PropertyToID(k_ShaderColorParameter);
#pragma warning disable 649
[SerializeField]
[Tooltip("How the highlight material will be applied to the renderer's material array.")]
MaterialHighlightMode m_HighlightMode = MaterialHighlightMode.Replace;
[SerializeField]
[Tooltip("Selects source for the highlight material. Either using a shader or material.")]
OutlineSource m_OutlineSource = OutlineSource.Shader;
[SerializeField]
[Tooltip("Outline highlight shader to use for highlight material.")]
Shader m_Shader;
[SerializeField]
[Tooltip("Material used for drawing the outline highlight.")]
Material m_HighlightMaterial;
[SerializeField]
[Tooltip("Transition outline width over time")]
bool m_TransitionWidth;
[SerializeField]
[Tooltip("The outline width used if no transition or the end value for transition width of outline")]
[Range(0f, 1f)]
float m_OutlineScale = 1f;
[SerializeField]
[Tooltip("Starting value for transition width of outline")]
[Range(0f, 1f)]
float m_StartingOutlineScale;
[SerializeField]
[Tooltip("Transition outline color over time")]
bool m_TransitionColor;
[SerializeField]
[Tooltip("The outline color used if no transition or the end value for transition color of outline")]
Color m_OutlineColor = new Color(0.3f, 0.6f, 1f, 1f);
[SerializeField]
[Tooltip("Starting value for transition color of outline")]
Color m_StartingOutlineColor = Color.black;
[SerializeField]
[Tooltip("Time it takes to transition from start to end on highlight")]
float m_TransitionDuration = 0.3f;
[SerializeField]
[Tooltip("Use material values for starting color and width")]
bool m_StartWithMaterialValues;
#pragma warning restore 649
Material m_InstanceOutlineMaterial;
bool m_Animating = false;
bool m_AnimatingIn = false;
float m_TransitionTimer = 0.0f;
///
/// Time it takes to transition from start to end on highlight
///
public float transitionDuration
{
get => m_TransitionDuration;
set => m_TransitionDuration = value;
}
///
/// Transition outline width over time
///
public bool transitionWidth
{
get => m_TransitionWidth;
set => m_TransitionWidth = value;
}
///
/// Transition outline color over time
///
public bool transitionColor
{
get => m_TransitionColor;
set => m_TransitionColor = value;
}
///
/// The outline color used if no transition or the end value for transition color of outline
///
public Color outlineColor
{
get => m_OutlineColor;
set => m_OutlineColor = value;
}
///
/// Starting value for transition color of outline
///
public Color startingOutlineColor
{
get => m_StartingOutlineColor;
set => m_StartingOutlineColor = value;
}
///
/// The outline width used if no transition or the end value for transition width of outline
///
public float outlineScale
{
get => m_OutlineScale;
set => m_OutlineScale = value;
}
///
/// Starting value for transition width of outline
///
public float startingOutlineScale
{
get => m_StartingOutlineScale;
set => m_StartingOutlineScale = value;
}
///
/// A 0-1 relative outline scale that takes into account the ideal base outline width,
/// multiplied by the user specified value. This allows for more intuitive adjustment of the value.
/// This is the value used if there is no transition otherwise this is the end value of a transition.
///
float relativeOutlineScale => outlineScale * k_OutlineWidth;
///
/// A 0-1 relative outline scale that takes into account the ideal base outline width,
/// multiplied by the user specified value. This allows for more intuitive adjustment of the value.
/// This is the start value of a transition otherwise this value is not used.
///
float startingRelativeOutlineScale => startingOutlineScale * k_OutlineWidth;
///
/// How the highlight material will be applied to the renderer's material array.
///
public MaterialHighlightMode highlightMode
{
get => m_HighlightMode;
set => m_HighlightMode = value;
}
///
/// Material to use for highlighting
///
public Material highlightMaterial => m_InstanceOutlineMaterial;
void IMaterialHighlight.Initialize()
{
InstantiateHighlightMaterial();
if (m_StartWithMaterialValues)
{
startingOutlineScale = m_HighlightMaterial.GetFloat(k_GFlOutlineWidth) / k_OutlineWidth;
startingOutlineColor = m_HighlightMaterial.GetColor(k_Color);
}
}
void IMaterialHighlight.Deinitialize()
{
if (m_InstanceOutlineMaterial)
{
Destroy(m_InstanceOutlineMaterial);
m_InstanceOutlineMaterial = null;
}
}
protected void OnDestroy()
{
((IMaterialHighlight)(this)).Deinitialize();
}
protected void Update()
{
if (m_Animating)
{
m_TransitionTimer += Time.unscaledDeltaTime;
var transitionPercent = Mathf.Clamp01(m_TransitionTimer / m_TransitionDuration);
var alpha = m_AnimatingIn ? transitionPercent : (1.0f - transitionPercent);
if (m_TransitionWidth)
{
var size = Mathf.Lerp(startingRelativeOutlineScale, relativeOutlineScale, alpha);
m_InstanceOutlineMaterial.SetFloat(k_GFlOutlineWidth, size);
}
if (m_TransitionColor)
{
var color = Color.Lerp(startingOutlineColor, outlineColor, alpha);
m_InstanceOutlineMaterial.SetColor(k_Color, color);
}
if (transitionPercent >= 1.0f)
{
m_TransitionTimer = 0.0f;
m_Animating = false;
}
}
}
void IMaterialHighlight.OnHighlight()
{
if (m_InstanceOutlineMaterial == null)
return;
PlayPulseAnimation();
}
float IMaterialHighlight.OnUnhighlight()
{
if (m_InstanceOutlineMaterial == null)
return 0.0f;
PlayPulseAnimation(false);
if (!m_TransitionWidth && !m_TransitionColor || Mathf.Approximately(m_TransitionDuration, 0f))
return 0.0f;
else
return m_TransitionDuration;
}
///
/// Pulses the highlight - even if it is already active
///
/// Whether the highlight is fading in or out
public void PlayPulseAnimation(bool pulseUp = true)
{
if (!m_TransitionWidth && !m_TransitionColor || Mathf.Approximately(m_TransitionDuration, 0f))
{
m_InstanceOutlineMaterial.SetFloat(k_GFlOutlineWidth, relativeOutlineScale);
m_InstanceOutlineMaterial.SetColor(k_Color, m_OutlineColor);
}
else
{
// If the same animation is already occurring, we just let it play. If it is playing backwards, we seamlessly transition
if (m_Animating)
{
if (m_AnimatingIn != pulseUp)
{
m_TransitionTimer = 1.0f - m_TransitionTimer;
}
}
else
{
m_Animating = true;
m_AnimatingIn = pulseUp;
m_TransitionTimer = 0.0f;
}
}
}
void InstantiateHighlightMaterial()
{
if (m_Shader == null && m_HighlightMaterial == null)
{
Debug.LogError($"{gameObject.name} has no highlight material or shader set!", this);
enabled = false;
return;
}
const string outlineMaterialName = "Outline Material Instance";
switch (m_OutlineSource)
{
case OutlineSource.Material:
if (m_HighlightMaterial == null)
{
Debug.LogError($"{gameObject.name} Outline highlight has no material assigned. Please assign outline material.", this);
enabled = false;
break;
}
m_InstanceOutlineMaterial = new Material(m_HighlightMaterial) { name = outlineMaterialName };
break;
case OutlineSource.Shader:
if (m_Shader == null)
{
Debug.LogError($"{gameObject.name} Outline highlight has no shader assigned. Please assign outline shader. ", this);
enabled = false;
break;
}
m_InstanceOutlineMaterial = new Material(m_Shader) { name = outlineMaterialName };
break;
default:
Debug.LogError($"{gameObject.name} Outline highlight has an invalid highlight mode {m_OutlineSource}.", this);
enabled = false;
break;
}
}
protected void OnValidate()
{
if (m_TransitionDuration < 0f)
{
m_TransitionDuration = 0f;
}
}
}
}