320 lines
12 KiB
C#
320 lines
12 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Microsoft.MixedReality.Toolkit.Utilities
|
|
{
|
|
/// <summary>
|
|
/// Utility component to animate and visualize a light that can be used with
|
|
/// the "MixedRealityToolkit/Standard" shader "_ProximityLight" feature.
|
|
/// </summary>
|
|
[ExecuteInEditMode]
|
|
[HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/rendering/proximity-light")]
|
|
[AddComponentMenu("Scripts/MRTK/Core/ProximityLight")]
|
|
public class ProximityLight : MonoBehaviour
|
|
{
|
|
// Two proximity lights are supported at this time. Excess lights will
|
|
// be held in a queue and addded to the shader in a first-in, first-out
|
|
// manner.
|
|
private const int proximityLightCount = 2;
|
|
private const int proximityLightQueueSize = 6;
|
|
private const int proximityLightDataSize = 6;
|
|
private static List<ProximityLight> activeProximityLights = new List<ProximityLight>(proximityLightQueueSize);
|
|
private static Vector4[] proximityLightData = new Vector4[proximityLightCount * proximityLightDataSize];
|
|
private static int proximityLightDataID;
|
|
private static int lastProximityLightUpdate = -1;
|
|
|
|
[Serializable]
|
|
public class LightSettings
|
|
{
|
|
/// <summary>
|
|
/// Specifies the radius of the ProximityLight effect when near to a surface.
|
|
/// </summary>
|
|
public float NearRadius
|
|
{
|
|
get { return nearRadius; }
|
|
set { nearRadius = value; }
|
|
}
|
|
|
|
[Header("Proximity Settings")]
|
|
[Tooltip("Specifies the radius of the ProximityLight effect when near to a surface.")]
|
|
[SerializeField]
|
|
[Range(0.0f, 1.0f)]
|
|
private float nearRadius = 0.05f;
|
|
|
|
/// <summary>
|
|
/// Specifies the radius of the ProximityLight effect when far from a surface.
|
|
/// </summary>
|
|
public float FarRadius
|
|
{
|
|
get { return farRadius; }
|
|
set { farRadius = value; }
|
|
}
|
|
|
|
[Tooltip("Specifies the radius of the ProximityLight effect when far from a surface.")]
|
|
[SerializeField]
|
|
[Range(0.0f, 1.0f)]
|
|
private float farRadius = 0.2f;
|
|
|
|
/// <summary>
|
|
/// Specifies the distance a ProximityLight must be from a surface to be considered near.
|
|
/// </summary>
|
|
public float NearDistance
|
|
{
|
|
get { return nearDistance; }
|
|
set { nearDistance = value; }
|
|
}
|
|
|
|
[Tooltip("Specifies the distance a ProximityLight must be from a surface to be considered near.")]
|
|
[SerializeField]
|
|
[Range(0.0f, 1.0f)]
|
|
private float nearDistance = 0.02f;
|
|
|
|
/// <summary>
|
|
/// When a ProximityLight is near, the smallest size percentage from the far size it can shrink to.
|
|
/// </summary>
|
|
public float MinNearSizePercentage
|
|
{
|
|
get { return minNearSizePercentage; }
|
|
set { minNearSizePercentage = value; }
|
|
}
|
|
|
|
[Tooltip("When a ProximityLight is near, the smallest size percentage from the far size it can shrink to.")]
|
|
[SerializeField]
|
|
[Range(0.0f, 1.0f)]
|
|
private float minNearSizePercentage = 0.35f;
|
|
|
|
/// <summary>
|
|
/// The color of the ProximityLight gradient at the center (RGB) and (A) is gradient extent.
|
|
/// </summary>
|
|
public Color CenterColor
|
|
{
|
|
get { return centerColor; }
|
|
set { centerColor = value; }
|
|
}
|
|
|
|
[Header("Color Settings")]
|
|
[Tooltip("The color of the ProximityLight gradient at the center (RGB) and (A) is gradient extent.")]
|
|
[ColorUsageAttribute(true, true)]
|
|
[SerializeField]
|
|
private Color centerColor = new Color(54.0f / 255.0f, 142.0f / 255.0f, 250.0f / 255.0f, 0.0f / 255.0f);
|
|
|
|
/// <summary>
|
|
/// The color of the ProximityLight gradient at the center (RGB) and (A) is gradient extent.
|
|
/// </summary>
|
|
public Color MiddleColor
|
|
{
|
|
get { return middleColor; }
|
|
set { middleColor = value; }
|
|
}
|
|
|
|
[Tooltip("The color of the ProximityLight gradient at the middle (RGB) and (A) is gradient extent.")]
|
|
[SerializeField]
|
|
[ColorUsageAttribute(true, true)]
|
|
private Color middleColor = new Color(47.0f / 255.0f, 132.0f / 255.0f, 255.0f / 255.0f, 51.0f / 255.0f);
|
|
|
|
/// <summary>
|
|
/// The color of the ProximityLight gradient at the center (RGB) and (A) is gradient extent.
|
|
/// </summary>
|
|
public Color OuterColor
|
|
{
|
|
get { return outerColor; }
|
|
set { outerColor = value; }
|
|
}
|
|
|
|
[Tooltip("The color of the ProximityLight gradient at the outer (RGB) and (A) is gradient extent.")]
|
|
[SerializeField]
|
|
[ColorUsageAttribute(true, true)]
|
|
private Color outerColor = new Color((82.0f * 3.0f) / 255.0f, (31.0f * 3.0f) / 255.0f, (191.0f * 3.0f) / 255.0f, 255.0f / 255.0f);
|
|
}
|
|
|
|
public LightSettings Settings
|
|
{
|
|
get { return settings; }
|
|
set { settings = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
private LightSettings settings = new LightSettings();
|
|
|
|
private float pulseTime;
|
|
private float pulseFade;
|
|
|
|
/// <summary>
|
|
/// Initiates a pulse, if one is not already occurring, which simulates a user touching a surface.
|
|
/// </summary>
|
|
/// <param name="pulseDuration">How long in seconds should the pulse animate over.</param>
|
|
/// <param name="fadeBegin">At what point during the pulseDuration should the pulse begin to fade out as a percentage. Range should be [0, 1].</param>
|
|
/// <param name="fadeSpeed">The speed to fade in and out.</param>
|
|
public void Pulse(float pulseDuration = 0.2f, float fadeBegin = 0.8f, float fadeSpeed = 10.0f)
|
|
{
|
|
if (pulseTime <= 0.0f)
|
|
{
|
|
StartCoroutine(PulseRoutine(pulseDuration, fadeBegin, fadeSpeed));
|
|
}
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
AddProximityLight(this);
|
|
UpdateProximityLights(true);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
RemoveProximityLight(this);
|
|
UpdateProximityLights(true);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void Update()
|
|
{
|
|
if (Application.isPlaying)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Initialize();
|
|
UpdateProximityLights();
|
|
}
|
|
#endif // UNITY_EDITOR
|
|
|
|
private void LateUpdate()
|
|
{
|
|
UpdateProximityLights();
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
if (!enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector3[] directions = new Vector3[] { Vector3.right, Vector3.left, Vector3.up, Vector3.down, Vector3.forward, Vector3.back };
|
|
|
|
Gizmos.color = new Color(Settings.CenterColor.r, Settings.CenterColor.g, Settings.CenterColor.b);
|
|
Gizmos.DrawWireSphere(transform.position, Settings.NearRadius);
|
|
|
|
foreach (Vector3 direction in directions)
|
|
{
|
|
Gizmos.DrawIcon(transform.position + direction * Settings.NearRadius, string.Empty, false);
|
|
}
|
|
|
|
Gizmos.color = new Color(Settings.OuterColor.r, Settings.OuterColor.g, Settings.OuterColor.b);
|
|
Gizmos.DrawWireSphere(transform.position, Settings.FarRadius);
|
|
|
|
foreach (Vector3 direction in directions)
|
|
{
|
|
Gizmos.DrawIcon(transform.position + direction * Settings.FarRadius, string.Empty, false);
|
|
}
|
|
}
|
|
|
|
private static void AddProximityLight(ProximityLight light)
|
|
{
|
|
activeProximityLights.Add(light);
|
|
}
|
|
|
|
private static void RemoveProximityLight(ProximityLight light)
|
|
{
|
|
activeProximityLights.Remove(light);
|
|
}
|
|
|
|
private static void Initialize()
|
|
{
|
|
proximityLightDataID = Shader.PropertyToID("_ProximityLightData");
|
|
}
|
|
|
|
private static void UpdateProximityLights(bool forceUpdate = false)
|
|
{
|
|
if (lastProximityLightUpdate == -1)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
if (!forceUpdate && (Time.frameCount == lastProximityLightUpdate))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int lightIndex, queueIndex;
|
|
for (lightIndex = queueIndex = 0; queueIndex < activeProximityLights.Count && lightIndex < proximityLightCount; ++queueIndex)
|
|
{
|
|
ProximityLight light = activeProximityLights[queueIndex];
|
|
|
|
if (light)
|
|
{
|
|
int dataIndex = lightIndex++ * proximityLightDataSize;
|
|
|
|
proximityLightData[dataIndex] = new Vector4(light.transform.position.x,
|
|
light.transform.position.y,
|
|
light.transform.position.z,
|
|
1.0f);
|
|
float pulseScaler = 1.0f + light.pulseTime;
|
|
proximityLightData[dataIndex + 1] = new Vector4(light.Settings.NearRadius * pulseScaler,
|
|
1.0f / Mathf.Clamp(light.Settings.FarRadius * pulseScaler, 0.001f, 1.0f),
|
|
1.0f / Mathf.Clamp(light.Settings.NearDistance * pulseScaler, 0.001f, 1.0f),
|
|
Mathf.Clamp01(light.Settings.MinNearSizePercentage));
|
|
proximityLightData[dataIndex + 2] = new Vector4(light.Settings.NearDistance * light.pulseTime,
|
|
Mathf.Clamp01(1.0f - light.pulseFade),
|
|
0.0f,
|
|
0.0f);
|
|
proximityLightData[dataIndex + 3] = light.Settings.CenterColor;
|
|
proximityLightData[dataIndex + 4] = light.Settings.MiddleColor;
|
|
proximityLightData[dataIndex + 5] = light.Settings.OuterColor;
|
|
}
|
|
}
|
|
|
|
for (; lightIndex < proximityLightCount; ++lightIndex)
|
|
{
|
|
int dataIndex = lightIndex * proximityLightDataSize;
|
|
proximityLightData[dataIndex] = Vector4.zero;
|
|
}
|
|
|
|
Shader.SetGlobalVectorArray(proximityLightDataID, proximityLightData);
|
|
|
|
lastProximityLightUpdate = Time.frameCount;
|
|
}
|
|
|
|
private IEnumerator PulseRoutine(float pulseDuration, float fadeBegin, float fadeSpeed)
|
|
{
|
|
float pulseTimer = 0.0f;
|
|
|
|
while (pulseTimer < pulseDuration)
|
|
{
|
|
pulseTimer += Time.deltaTime;
|
|
pulseTime = pulseTimer / pulseDuration;
|
|
|
|
if (pulseTime > fadeBegin)
|
|
{
|
|
pulseFade += Time.deltaTime;
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
while (pulseFade < 1.0f)
|
|
{
|
|
pulseFade += Time.deltaTime * fadeSpeed;
|
|
|
|
yield return null;
|
|
}
|
|
|
|
pulseTime = 0.0f;
|
|
|
|
while (pulseFade > 0.0f)
|
|
{
|
|
pulseFade -= Time.deltaTime * fadeSpeed;
|
|
|
|
yield return null;
|
|
}
|
|
|
|
pulseFade = 0.0f;
|
|
}
|
|
}
|
|
}
|