// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using UltimateXR.Animation.Interpolation; using UltimateXR.Animation.Materials; using UltimateXR.Audio; using UltimateXR.Avatar; using UltimateXR.Core; using UltimateXR.Core.Components; using UltimateXR.Extensions.Unity.Math; using UltimateXR.Extensions.Unity.Render; using UnityEngine; namespace UltimateXR.Examples.FullScene.Doors { /// /// Component that handles the hand scanning required to open an . /// public class HandScanner : UxrComponent { #region Inspector Properties/Serialized Fields [SerializeField] private ArmoredDoor _armoredDoor; [SerializeField] private Renderer _validLight; [SerializeField] private Renderer _invalidLight; [SerializeField] private Renderer _scannerBeam; [SerializeField] private Vector3 _scannerBeamTopLocalPos; [SerializeField] private Vector3 _scannerBeamBottomLocalPos; [SerializeField] private int _beamCount = 5; [SerializeField] [Range(0.0f, 1.0f)] private float _beamTrailDelay = 0.1f; [SerializeField] private UxrEasing _beamEeasing = UxrEasing.EaseInOutQuint; [SerializeField] private Vector3 _beamMaxScale = Vector3.one; [SerializeField] private Renderer _handRendererLeft; [SerializeField] private Renderer _handRendererRight; [SerializeField] private UxrHandSide _defaultHandSide = UxrHandSide.Right; [SerializeField] private BoxCollider _handBoxValidPos; [SerializeField] private float _scanSeconds = 1.5f; [SerializeField] private float _resultSeconds = 2.0f; [SerializeField] private UxrAudioSample _audioScan; [SerializeField] private UxrAudioSample _audioError; [SerializeField] private UxrAudioSample _audioOk; #endregion #region Public Types & Data /// /// Event called right after a hand was scanned. Parameters are the avatar that was scanned and if the scan granted /// access. /// public event Action HandScanned; #endregion #region Unity /// /// Initializes the component. /// protected override void Awake() { base.Awake(); _colorValid = _validLight.sharedMaterial.GetColor(UxrConstants.Shaders.StandardColorVarName); _colorInvalid = _invalidLight.sharedMaterial.GetColor(UxrConstants.Shaders.StandardColorVarName); _handSide = _defaultHandSide; _beamScale = _scannerBeam.transform.localScale; _beams.Add(_scannerBeam); for (int i = 0; i < _beamCount - 1; ++i) { _beams.Add(Instantiate(_scannerBeam.gameObject, _scannerBeam.transform.position, _scannerBeam.transform.rotation, _scannerBeam.transform.parent).GetComponent()); } } /// /// Sets the default scanning state. /// protected override void OnEnable() { base.OnEnable(); _scanReady = true; _validLight.enabled = false; _invalidLight.enabled = false; _handRendererLeft.enabled = _defaultHandSide == UxrHandSide.Left; _handRendererRight.enabled = _defaultHandSide == UxrHandSide.Right; _handRendererLeft.material.color = ColorExt.ColorAlpha(_handRendererLeft.material.color, _defaultHandSide == UxrHandSide.Left ? 1.0f : 0.0f); _handRendererRight.material.color = ColorExt.ColorAlpha(_handRendererRight.material.color, _defaultHandSide == UxrHandSide.Right ? 1.0f : 0.0f); EnableBeams(false); } /// /// Disables the component. /// protected override void OnDisable() { base.OnEnable(); _scanReady = true; _validLight.enabled = false; _invalidLight.enabled = false; EnableBeams(false); } /// /// Updates the component. Performs the scanning process. /// private void Update() { if (UxrAvatar.LocalAvatar == null || !_scanReady) { return; } // Update scanning & beam if (_scanTimer < 0.0f) { if (_armoredDoor != null && _armoredDoor.IsOpen) { // If we are controlling an armored door and it is already open, ignore the hands. } else { // Waiting for hand to be scanned. Look for hand: if (UxrAvatar.LocalAvatar.GetHandBone(UxrHandSide.Left).position.IsInsideBox(_handBoxValidPos)) { StartScan(UxrAvatar.LocalAvatar, UxrHandSide.Left); } else if (UxrAvatar.LocalAvatar.GetHandBone(UxrHandSide.Right).position.IsInsideBox(_handBoxValidPos)) { StartScan(UxrAvatar.LocalAvatar, UxrHandSide.Right); } } } else { // Hand is scanning _scanTimer += Time.deltaTime; EnableBeams(true); for (int i = 0; i < _beams.Count; ++i) { float beamStartTime = i / (_beams.Count == 1 ? 1.0f : _beams.Count - 1.0f) * _beamTrailDelay * _scanSeconds; float beamDuration = _scanSeconds - _beamTrailDelay; float t = Mathf.Clamp01((_scanTimer - beamStartTime) / beamDuration); float tScale = 1.0f - Mathf.Abs(t - 0.5f) * 2.0f; _beams[i].transform.localPosition = Vector3.Lerp(_scannerBeamTopLocalPos, _scannerBeamBottomLocalPos, UxrInterpolator.GetInterpolationFactor(t, _beamEeasing)); _beams[i].transform.localScale = Vector3.Lerp(_beamScale, _beamMaxScale, Mathf.Pow(tScale, 8.0f)); } // Check for conditions if (_avatarScanning == UxrAvatar.LocalAvatar) { if (UxrAvatar.LocalAvatar.GetHandBone(_handSide).position.IsInsideBox(_handBoxValidPos)) { if (!UxrAvatar.LocalAvatar.GetHandBone(UxrUtils.GetOppositeSide(_handSide)).position.IsInsideBox(_handBoxValidPos)) { // Keep scanning if (_scanTimer > _scanSeconds) { ProcessScanResult(UxrAvatar.LocalAvatar, _handSide, true); if (_armoredDoor != null) { _armoredDoor.OpenDoor(false); } } } else { // Opposite hand got in. Aborting. ProcessScanResult(UxrAvatar.LocalAvatar, _handSide, false); } } else { // Scanning hand got out. Aborting. ProcessScanResult(UxrAvatar.LocalAvatar, _handSide, false); } } } // Update scan side if (_handSide == UxrHandSide.Left) { if (_handRendererRight.enabled) { float rightAlpha = _handRendererRight.material.color.a - Time.deltaTime * HandAlphaSwitchSpeed; _handRendererRight.material.color = ColorExt.ColorAlpha(_handRendererRight.material.color, Mathf.Clamp01(rightAlpha)); if (rightAlpha < 0.0f) { _handRendererRight.enabled = false; } } if (!_handRendererLeft.enabled) { _handRendererLeft.enabled = true; } _handRendererLeft.material.color = ColorExt.ColorAlpha(_handRendererLeft.material.color, Mathf.Clamp01(_handRendererLeft.material.color.a + Time.deltaTime * HandAlphaSwitchSpeed)); } else { if (_handRendererLeft.enabled) { float leftAlpha = _handRendererLeft.material.color.a - Time.deltaTime * HandAlphaSwitchSpeed; _handRendererLeft.material.color = ColorExt.ColorAlpha(_handRendererLeft.material.color, Mathf.Clamp01(leftAlpha)); if (leftAlpha < 0.0f) { _handRendererLeft.enabled = false; } } if (!_handRendererRight.enabled) { _handRendererRight.enabled = true; } _handRendererRight.material.color = ColorExt.ColorAlpha(_handRendererRight.material.color, Mathf.Clamp01(_handRendererRight.material.color.a + Time.deltaTime * HandAlphaSwitchSpeed)); } } #endregion #region Private Methods /// /// Starts scanning a hand. /// /// The avatar that started scanning /// The side of the hand being scanned private void StartScan(UxrAvatar avatarScanning, UxrHandSide handSide) { BeginSync(); _scanTimer = 0.0f; _handSide = handSide; _avatarScanning = avatarScanning; _audioScan.Play(transform.position); EndSyncMethod(new object[] { avatarScanning, handSide }); } /// /// Enables/disables the scanning beams. /// /// Whether the beams should be enabled private void EnableBeams(bool enable) { _beams.ForEach(r => r.enabled = enable); } /// /// Processes a scan result. /// /// Avatar that was scanned /// Which hand was scanned /// Whether the access was granted private void ProcessScanResult(UxrAvatar avatar, UxrHandSide handSide, bool isValid) { BeginSync(); EnableBeams(false); _scanReady = false; _scanTimer = -1.0f; if (isValid) { _audioOk.Play(transform.position); _validLight.enabled = true; UxrAnimatedMaterial.AnimateBlinkColor(_validLight.gameObject, UxrConstants.Shaders.StandardColorVarName, _colorValid.WithAlpha(0.0f), _colorValid, UxrAnimatedMaterial.DefaultBlinkFrequency, _resultSeconds, UxrMaterialMode.InstanceOnly, () => { _scanReady = true; _validLight.enabled = false; }); } else { _audioError.Play(transform.position); _invalidLight.enabled = true; UxrAnimatedMaterial.AnimateBlinkColor(_invalidLight.gameObject, UxrConstants.Shaders.StandardColorVarName, _colorInvalid.WithAlpha(0.0f), _colorInvalid, UxrAnimatedMaterial.DefaultBlinkFrequency, _resultSeconds, UxrMaterialMode.InstanceOnly, () => { _scanReady = true; _invalidLight.enabled = false; }); } HandScanned?.Invoke(avatar, handSide, isValid); EndSyncMethod(new object[] { avatar, handSide, isValid }); } #endregion #region Private Types & Data /// /// Controls how fast the hand image will switch from one side to the other when a different hand than the currently /// shown is placed on the scanner. /// private const float HandAlphaSwitchSpeed = 4.0f; private readonly List _beams = new List(); private bool _scanReady = true; private float _scanTimer = -1.0f; private Vector3 _beamScale; private UxrAvatar _avatarScanning; private UxrHandSide _handSide; private Color _colorValid; private Color _colorInvalid; #endregion } }