// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections; using System.Collections.Generic; using UnityEngine; /// /// Utility component to animate and visualize a light that can be used with /// the "SDK/Standard" shader "_ProximityLight" feature. /// [ExecuteInEditMode] public class ProximityLight : MonoBehaviour { // Two proximity lights are supported at this time. private const int proximityLightCount = 2; private const int proximityLightDataSize = 6; private static List activeProximityLights = new List(proximityLightCount); private static Vector4[] proximityLightData = new Vector4[proximityLightCount * proximityLightDataSize]; private static int proximityLightDataID; private static int lastProximityLightUpdate = -1; [Serializable] public class LightSettings { /// /// Specifies the radius of the ProximityLight effect when near to a surface. /// 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; /// /// Specifies the radius of the ProximityLight effect when far from a surface. /// 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; /// /// Specifies the distance a ProximityLight must be from a surface to be considered near. /// 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; /// /// When a ProximityLight is near, the smallest size percentage from the far size it can shrink to. /// 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; /// /// The color of the ProximityLight gradient at the center (RGB) and (A) is gradient extent. /// 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); /// /// The color of the ProximityLight gradient at the center (RGB) and (A) is gradient extent. /// 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); /// /// The color of the ProximityLight gradient at the center (RGB) and (A) is gradient extent. /// 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; /// /// Initiates a pulse, if one is not already occurring, which simulates a user touching a surface. /// /// How long in seconds should the pulse animate over. /// At what point during the pulseDuration should the pulse begin to fade out as a percentage. Range should be [0, 1]. /// The speed to fade in and out. 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); } private void OnDisable() { pulseTime = pulseFade = 0; 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) { if (activeProximityLights.Count >= proximityLightCount) { Debug.LogWarningFormat("Max proximity light count ({0}) exceeded.", proximityLightCount); } 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; } for (int i = 0; i < proximityLightCount; ++i) { ProximityLight light = (i >= activeProximityLights.Count) ? null : activeProximityLights[i]; int dataIndex = i * proximityLightDataSize; if (light) { 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; } else { 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; } }