using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Serialization; namespace UDE_HAND_INTERACTION { public class FingerPressInteractor : InteractDetector { private class SurfaceHitCache { private readonly struct HitInfo { public readonly bool IsValid; public readonly SurfaceHit Hit; public HitInfo(bool isValid, SurfaceHit hit) { IsValid = isValid; Hit = hit; } } private Dictionary _surfacePatchHitCache; private Dictionary _backingSurfaceHitCache; private Vector3 _origin; public bool GetPatchHit(FingerPressInteractable interactable, out SurfaceHit hit) { if (!_surfacePatchHitCache.ContainsKey(interactable)) { bool isValid = interactable.SurfacePatch .ClosestSurfacePoint(_origin, out SurfaceHit patchHit); HitInfo info = new HitInfo(isValid, patchHit); _surfacePatchHitCache.Add(interactable, info); } hit = _surfacePatchHitCache[interactable].Hit; return _surfacePatchHitCache[interactable].IsValid; } public bool GetBackingHit(FingerPressInteractable interactable, out SurfaceHit hit) { if (!_backingSurfaceHitCache.ContainsKey(interactable)) { bool isValid = interactable.SurfacePatch.BackingSurface .ClosestSurfacePoint(_origin, out SurfaceHit backingHit); HitInfo info = new HitInfo(isValid, backingHit); _backingSurfaceHitCache.Add(interactable, info); } hit = _backingSurfaceHitCache[interactable].Hit; return _backingSurfaceHitCache[interactable].IsValid; } public SurfaceHitCache() { _surfacePatchHitCache = new Dictionary(); _backingSurfaceHitCache = new Dictionary(); } public void Reset(Vector3 origin) { _origin = origin; _surfacePatchHitCache.Clear(); _backingSurfaceHitCache.Clear(); } } [SerializeField] private Transform TipTransform; //public Vector3 _localPositionOffset; [SerializeField] private float _radius = 0.005f; private float _touchReleaseThreshold = 0.002f; [FormerlySerializedAs("_zThreshold")] private float _equalDistanceThreshold = 0.001f; public Vector3 ClosestPoint { get; private set; } public Vector3 TouchPoint { get; private set; } public Vector3 TouchNormal { get; private set; } public float Radius => _radius; public Vector3 Origin { get; private set; } private Vector3 _previousFingerPressOrigin; private FingerPressInteractable _previousCandidate = null; private FingerPressInteractable _hitInteractable = null; private FingerPressInteractable _recoilInteractable = null; private Vector3 _previousSurfacePointLocal; private Vector3 _firstTouchPointLocal; private Vector3 _targetTouchPointLocal; private Vector3 _easeTouchPointLocal; private bool _isRecoiled; private bool _isDragging; private ProgressCurve _dragEaseCurve; private ProgressCurve _pinningResyncCurve; private Vector3 _dragCompareSurfacePointLocal; private float _maxDistanceFromFirstTouchPoint; private float _recoilVelocityExpansion; private float _selectMaxDepth; private float _reEnterDepth; private float _lastUpdateTime; private Func _timeProvider; private bool _isPassedSurface; public bool IsPassedSurface { get { return _isPassedSurface; } set { bool previousValue = _isPassedSurface; _isPassedSurface = value; if (value != previousValue) { WhenPassedSurfaceChanged.Invoke(value); } } } public Action WhenPassedSurfaceChanged = delegate { }; private SurfaceHitCache _hitCache; private Dictionary _previousSurfaceTransformMap; private float _previousDragCurveProgress; private float _previousPinningCurveProgress; protected override void Awake() { base.Awake(); _timeProvider = () => Time.time; } protected override void Start() { base.Start(); this.AssertField(TipTransform, nameof(TipTransform)); this.AssertField(_timeProvider, nameof(_timeProvider)); _dragEaseCurve = new ProgressCurve(); _pinningResyncCurve = new ProgressCurve(); _hitCache = new SurfaceHitCache(); _previousSurfaceTransformMap = new Dictionary(); } protected override void DoPreprocess() { base.DoPreprocess(); _previousFingerPressOrigin = Origin; Origin = TipTransform.position; _hitCache.Reset(Origin); } protected override void DoPostprocess() { base.DoPostprocess(); var interactables = FingerPressInteractable.Registry.List(this); foreach (FingerPressInteractable interactable in interactables) { _previousSurfaceTransformMap[interactable] = interactable.SurfacePatch.BackingSurface.Transform.worldToLocalMatrix; } _lastUpdateTime = _timeProvider(); } protected override bool ComputeShouldSelect() { if (_recoilInteractable != null) { float depth = ComputeFingerPressDepth(_recoilInteractable, Origin); _reEnterDepth = Mathf.Min(depth + _recoilInteractable.RecoilAssist.ReEnterDistance, _reEnterDepth); _hitInteractable = depth > _reEnterDepth ? _recoilInteractable : null; } return _hitInteractable != null; } protected override bool ComputeShouldUnselect() { return _hitInteractable == null; } private bool GetBackingHit(FingerPressInteractable interactable, out SurfaceHit hit) { return _hitCache.GetBackingHit(interactable, out hit); } private bool GetPatchHit(FingerPressInteractable interactable, out SurfaceHit hit) { return _hitCache.GetPatchHit(interactable, out hit); } private bool InteractableInRange(FingerPressInteractable interactable) { if (!_previousSurfaceTransformMap.ContainsKey(interactable)) { return true; } Vector3 previousLocalFingerPressOrigin = _previousSurfaceTransformMap[interactable].MultiplyPoint(_previousFingerPressOrigin); Vector3 adjustedFingerPressOrigin = interactable.SurfacePatch.BackingSurface.Transform.TransformPoint(previousLocalFingerPressOrigin); float hoverDistance = interactable == Interactable ? Mathf.Max(interactable.ExitHoverTangent, interactable.ExitHoverNormal) : Mathf.Max(interactable.EnterHoverTangent, interactable.EnterHoverNormal); float moveDistance = Vector3.Distance(Origin, adjustedFingerPressOrigin); float maxDistance = moveDistance + Radius + hoverDistance + _equalDistanceThreshold + interactable.CloseDistanceThreshold; return interactable.SurfacePatch.ClosestSurfacePoint(Origin, out _, maxDistance); } protected override void DoHoverUpdate() { if (_interactable != null && GetBackingHit(_interactable, out SurfaceHit backingHit)) { TouchPoint = backingHit.Point; TouchNormal = backingHit.Normal; } if (_recoilInteractable != null) { bool withinSurface = SurfaceUpdate(_recoilInteractable); if (!withinSurface) { _isRecoiled = false; _recoilInteractable = null; _recoilVelocityExpansion = 0; IsPassedSurface = false; return; } if (ShouldCancel(_recoilInteractable)) { GenerateDetectorEvent(DetectorEventType.Cancel, _recoilInteractable); _previousFingerPressOrigin = Origin; _previousCandidate = null; _hitInteractable = null; _recoilInteractable = null; _recoilVelocityExpansion = 0; IsPassedSurface = false; _isRecoiled = false; } } } protected override FingerPressInteractable ComputeCandidate() { if (_recoilInteractable != null) { return _recoilInteractable; } if (_hitInteractable != null) { return _hitInteractable; } FingerPressInteractable closestInteractable = ComputeSelectCandidate(); if (closestInteractable != null) { _hitInteractable = closestInteractable; _previousCandidate = closestInteractable; return _hitInteractable; } closestInteractable = ComputeHoverCandidate(); _previousCandidate = closestInteractable; return closestInteractable; } private FingerPressInteractable ComputeSelectCandidate() { FingerPressInteractable closestInteractable = null; float closestNormalDistance = float.MaxValue; float closestTangentDistance = float.MaxValue; var interactables = FingerPressInteractable.Registry.List(this); foreach (FingerPressInteractable interactable in interactables) { if (!InteractableInRange(interactable) || !GetBackingHit(interactable, out SurfaceHit backingHit) || !GetPatchHit(interactable, out SurfaceHit patchHit)) { continue; } Matrix4x4 previousSurfaceMatrix = _previousSurfaceTransformMap.ContainsKey(interactable) ? _previousSurfaceTransformMap[interactable] : interactable.SurfacePatch.BackingSurface.Transform.worldToLocalMatrix; Vector3 localFingerPressOrigin = previousSurfaceMatrix.MultiplyPoint(_previousFingerPressOrigin); Vector3 adjustedFingerPressOrigin = interactable.SurfacePatch.BackingSurface.Transform.TransformPoint(localFingerPressOrigin); if (!PassesEnterHoverDistanceCheck(adjustedFingerPressOrigin, interactable)) { continue; } Vector3 moveDirection = Origin - adjustedFingerPressOrigin; float magnitude = moveDirection.magnitude; if (magnitude == 0f) { continue; } moveDirection /= magnitude; Ray ray = new(adjustedFingerPressOrigin, moveDirection); Vector3 closestSurfaceNormal = backingHit.Normal; if (Vector3.Dot(moveDirection, closestSurfaceNormal) < 0f) { bool hit = interactable.SurfacePatch.BackingSurface.Raycast(ray, out SurfaceHit surfaceHit); hit = hit && surfaceHit.Distance <= magnitude; if (!hit) { float distance = ComputeDistanceAbove(interactable, Origin); if (distance <= 0) { Vector3 closestSurfacePointToOrigin = backingHit.Point; hit = true; surfaceHit = new SurfaceHit() { Point = closestSurfacePointToOrigin, Normal = backingHit.Normal, Distance = distance }; } } if (hit) { float tangentDistance = ComputeTangentDistance(interactable, surfaceHit.Point); if (tangentDistance > (interactable != _previousCandidate ? interactable.EnterHoverTangent : interactable.ExitHoverTangent)) { continue; } float normalDistance = Vector3.Dot(adjustedFingerPressOrigin - surfaceHit.Point, surfaceHit.Normal); bool normalDistanceEqual = Mathf.Abs(normalDistance - closestNormalDistance) < _equalDistanceThreshold; if (normalDistance > closestNormalDistance + interactable.CloseDistanceThreshold) { continue; } if (closestInteractable == null || normalDistance < closestNormalDistance - closestInteractable.CloseDistanceThreshold) { closestNormalDistance = normalDistance; closestTangentDistance = tangentDistance; closestInteractable = interactable; continue; } if (tangentDistance < closestTangentDistance) { closestNormalDistance = normalDistance; closestTangentDistance = tangentDistance; closestInteractable = interactable; } } } } if (closestInteractable != null) { GetBackingHit(closestInteractable, out SurfaceHit backingHitClosest); GetPatchHit(closestInteractable, out SurfaceHit patchHitClosest); ClosestPoint = patchHitClosest.Point; TouchPoint = backingHitClosest.Point; TouchNormal = backingHitClosest.Normal; foreach (FingerPressInteractable interactable in interactables) { if (interactable == closestInteractable) { continue; } if (!InteractableInRange(interactable) || !GetBackingHit(interactable, out SurfaceHit backingHit) || !GetPatchHit(interactable, out SurfaceHit patchHit)) { continue; } Matrix4x4 previousSurfaceMatrix = _previousSurfaceTransformMap.ContainsKey(interactable) ? _previousSurfaceTransformMap[interactable] : interactable.SurfacePatch.BackingSurface.Transform.worldToLocalMatrix; Vector3 localFingerPressOrigin = previousSurfaceMatrix.MultiplyPoint(_previousFingerPressOrigin); Vector3 adjustedFingerPressOrigin = interactable.SurfacePatch.BackingSurface.Transform.TransformPoint(localFingerPressOrigin); if (!PassesEnterHoverDistanceCheck(adjustedFingerPressOrigin, interactable)) { continue; } Vector3 backingToTouchPoint = TouchPoint - backingHit.Point; float normalDistance = Vector3.Dot(backingToTouchPoint, backingHit.Normal); if (normalDistance <= 0 || normalDistance > interactable.CloseDistanceThreshold) { continue; } float tangentDistance = ComputeTangentDistance(interactable, TouchPoint); if (tangentDistance > interactable.EnterHoverTangent || tangentDistance > closestTangentDistance) { continue; } return null; } } return closestInteractable; } private bool PassesEnterHoverDistanceCheck(Vector3 position, FingerPressInteractable interactable) { if (interactable == _previousCandidate) { return true; } float distanceThreshold = 0f; if (interactable.MinThresholds.Enabled) { distanceThreshold = Mathf.Min(interactable.MinThresholds.MinNormal, MinFingerPressDepth(interactable)); } return ComputeDistanceAbove(interactable, position) > distanceThreshold; } public float MinFingerPressDepth(FingerPressInteractable interactable) { float minDepth = interactable.ExitHoverNormal; foreach (FingerPressInteractor fingerPressInteractor in interactable.Interactors) { float normalDistance = ComputeFingerPressDepth(interactable, fingerPressInteractor.Origin); minDepth = Mathf.Min(normalDistance, minDepth); } return minDepth; } private FingerPressInteractable ComputeHoverCandidate() { FingerPressInteractable closestInteractable = null; float closestNormalDistance = float.MaxValue; float closestTangentDistance = float.MaxValue; var interactables = FingerPressInteractable.Registry.List(this); foreach (FingerPressInteractable interactable in interactables) { if (!InteractableInRange(interactable) || !GetBackingHit(interactable, out SurfaceHit backingHit) || !GetPatchHit(interactable, out SurfaceHit patchHit)) { continue; } if (!PassesEnterHoverDistanceCheck(Origin, interactable) && !PassesEnterHoverDistanceCheck(_previousFingerPressOrigin, interactable)) { continue; } Vector3 closestSurfacePoint = backingHit.Point; Vector3 closestSurfaceNormal = backingHit.Normal; Vector3 surfaceToPoint = Origin - closestSurfacePoint; float magnitude = surfaceToPoint.magnitude; if (magnitude != 0f) { if (Vector3.Dot(surfaceToPoint, closestSurfaceNormal) > 0f) { float normalDistance = ComputeDistanceAbove(interactable, Origin); if (normalDistance > (_previousCandidate != interactable ? interactable.EnterHoverNormal : interactable.ExitHoverNormal)) { continue; } float tangentDistance = ComputeTangentDistance(interactable, Origin); if (tangentDistance > (_previousCandidate != interactable ? interactable.EnterHoverTangent : interactable.ExitHoverTangent)) { continue; } bool normalDistanceEqual = Mathf.Abs(normalDistance - closestNormalDistance) < _equalDistanceThreshold; if (normalDistance > closestNormalDistance + interactable.CloseDistanceThreshold) { continue; } if (closestInteractable == null || normalDistance < closestNormalDistance - closestInteractable.CloseDistanceThreshold) { closestInteractable = interactable; closestNormalDistance = normalDistance; closestTangentDistance = tangentDistance; continue; } if (tangentDistance < closestTangentDistance) { closestInteractable = interactable; closestNormalDistance = normalDistance; closestTangentDistance = tangentDistance; } } } } if (closestInteractable != null) { GetBackingHit(closestInteractable, out SurfaceHit backingHitClosest); GetPatchHit(closestInteractable, out SurfaceHit patchHitClosest); ClosestPoint = patchHitClosest.Point; TouchPoint = backingHitClosest.Point; TouchNormal = backingHitClosest.Normal; } return closestInteractable; } protected override void InteractableSelected(FingerPressInteractable interactable) { if (interactable != null && GetBackingHit(interactable, out SurfaceHit backingHit)) { _previousSurfacePointLocal = _firstTouchPointLocal = _easeTouchPointLocal = _targetTouchPointLocal = interactable.SurfacePatch.BackingSurface.Transform.InverseTransformPoint(TouchPoint); Vector3 lateralComparePoint = backingHit.Point; _dragCompareSurfacePointLocal = interactable.SurfacePatch.BackingSurface.Transform.InverseTransformPoint(lateralComparePoint); _dragEaseCurve.Copy(interactable.DragThresholds.DragEaseCurve); _pinningResyncCurve.Copy(interactable.PositionPinning.ResyncCurve); _isDragging = false; _isRecoiled = false; _maxDistanceFromFirstTouchPoint = 0; _selectMaxDepth = 0; } IsPassedSurface = true; base.InteractableSelected(interactable); } protected override void HandleDisabled() { _hitInteractable = null; base.HandleDisabled(); } protected override Pose ComputeDetectorPose() { if (Interactable == null) { return Pose.identity; } if (!Interactable.ClosestBackingSurfaceHit(TouchPoint, out SurfaceHit hit)) { return Pose.identity; } return new Pose(TouchPoint, Quaternion.LookRotation(hit.Normal)); } private float ComputeDistanceAbove(FingerPressInteractable interactable, Vector3 point) { return SurfaceUtils.ComputeDistanceAbove(interactable.SurfacePatch, point, _radius); } private float ComputeFingerPressDepth(FingerPressInteractable interactable, Vector3 point) { return SurfaceUtils.ComputeDepth(interactable.SurfacePatch, point, _radius); } private float ComputeTangentDistance(FingerPressInteractable interactable, Vector3 point) { return SurfaceUtils.ComputeTangentDistance(interactable.SurfacePatch, point, _radius); } protected virtual bool SurfaceUpdate(FingerPressInteractable interactable) { if (interactable == null) { return false; } if (!GetBackingHit(interactable, out SurfaceHit backingHit)) { return false; } if (ComputeDistanceAbove(interactable, Origin) > _touchReleaseThreshold) { return false; } bool wasRecoiled = _isRecoiled; _isRecoiled = _hitInteractable == null && _recoilInteractable != null; Vector3 closestSurfacePoint = backingHit.Point; Vector3 positionOnSurfaceLocal = interactable.SurfacePatch.BackingSurface.Transform.InverseTransformPoint(closestSurfacePoint); if (interactable.DragThresholds.Enabled) { float worldDepthDelta = Mathf.Abs(ComputeFingerPressDepth(interactable, Origin) - ComputeFingerPressDepth(interactable, _previousFingerPressOrigin)); Vector3 positionDeltaLocal = positionOnSurfaceLocal - _previousSurfacePointLocal; Vector3 positionDeltaWorld = interactable.SurfacePatch.BackingSurface.Transform.TransformVector(positionDeltaLocal); bool isZMotion = worldDepthDelta > positionDeltaWorld.magnitude && worldDepthDelta > interactable.DragThresholds.DragNormal; if (isZMotion) { _dragCompareSurfacePointLocal = positionOnSurfaceLocal; } if (!_isDragging) { if (!isZMotion) { Vector3 surfaceDeltaLocal = positionOnSurfaceLocal - _dragCompareSurfacePointLocal; Vector3 surfaceDeltaWorld = interactable.SurfacePatch.BackingSurface.Transform.TransformVector(surfaceDeltaLocal); if (surfaceDeltaWorld.magnitude > interactable.DragThresholds.DragTangent) { _isDragging = true; _dragEaseCurve.Start(); _previousDragCurveProgress = 0; _targetTouchPointLocal = positionOnSurfaceLocal; } } } else { if (isZMotion) { _isDragging = false; } else { _targetTouchPointLocal = positionOnSurfaceLocal; } } } else { _targetTouchPointLocal = positionOnSurfaceLocal; } Vector3 pinnedTouchPointLocal = _targetTouchPointLocal; if (interactable.PositionPinning.Enabled) { if (!_isRecoiled) { Vector3 deltaFromCaptureLocal = pinnedTouchPointLocal - _firstTouchPointLocal; Vector3 deltaFromCaptureWorld = interactable.SurfacePatch.BackingSurface.Transform.TransformVector(deltaFromCaptureLocal); _maxDistanceFromFirstTouchPoint = Mathf.Max(deltaFromCaptureWorld.magnitude, _maxDistanceFromFirstTouchPoint); float deltaAsPercent = 1; if (interactable.PositionPinning.MaxPinDistance != 0f) { deltaAsPercent = Mathf.Clamp01(_maxDistanceFromFirstTouchPoint / interactable.PositionPinning.MaxPinDistance); deltaAsPercent = interactable.PositionPinning.PinningEaseCurve.Evaluate(deltaAsPercent); } pinnedTouchPointLocal = _firstTouchPointLocal + deltaFromCaptureLocal * deltaAsPercent; } else { if (!wasRecoiled) { _pinningResyncCurve.Start(); _previousPinningCurveProgress = 0; } float pinningCurveProgress = _pinningResyncCurve.Progress(); if (pinningCurveProgress != 1f) { float deltaProgress = pinningCurveProgress - _previousPinningCurveProgress; Vector3 delta = pinnedTouchPointLocal - _easeTouchPointLocal; pinnedTouchPointLocal = _easeTouchPointLocal + deltaProgress / (1f - _previousPinningCurveProgress) * delta; _previousPinningCurveProgress = pinningCurveProgress; } } } float dragCurveProgress = _dragEaseCurve.Progress(); if (dragCurveProgress != 1f) { float deltaProgress = dragCurveProgress - _previousDragCurveProgress; Vector3 delta = pinnedTouchPointLocal - _easeTouchPointLocal; _easeTouchPointLocal += deltaProgress / (1f - _previousDragCurveProgress) * delta; _previousDragCurveProgress = dragCurveProgress; } else { _easeTouchPointLocal = pinnedTouchPointLocal; } TouchPoint = interactable.SurfacePatch.BackingSurface.Transform.TransformPoint(_easeTouchPointLocal); interactable.ClosestBackingSurfaceHit(TouchPoint, out SurfaceHit hit); TouchNormal = hit.Normal; _previousSurfacePointLocal = positionOnSurfaceLocal; return true; } protected virtual bool ShouldCancel(FingerPressInteractable interactable) { return (interactable.CancelSelectNormal > 0.0f && ComputeFingerPressDepth(interactable, Origin) > interactable.CancelSelectNormal) || (interactable.CancelSelectTangent > 0.0f && ComputeTangentDistance(interactable, Origin) > interactable.CancelSelectTangent); } protected virtual bool ShouldRecoil(FingerPressInteractable interactable) { if (!interactable.RecoilAssist.Enabled) { return false; } float depth = ComputeFingerPressDepth(interactable, Origin); float deltaTime = _timeProvider() - _lastUpdateTime; float recoilExitDistance = interactable.RecoilAssist.ExitDistance; if (interactable.RecoilAssist.UseVelocityExpansion) { Vector3 frameDeltaWorld = Origin - _previousFingerPressOrigin; float normalVelocity = Mathf.Max(0, Vector3.Dot(frameDeltaWorld, -TouchNormal)); normalVelocity = deltaTime > 0 ? normalVelocity / deltaTime : 0f; float adjustment = Mathf.InverseLerp( interactable.RecoilAssist.VelocityExpansionMinSpeed, interactable.RecoilAssist.VelocityExpansionMaxSpeed, normalVelocity); float targetRecoilVelocityExpansion = Mathf.Clamp01(adjustment) * interactable.RecoilAssist.VelocityExpansionDistance; if (targetRecoilVelocityExpansion > _recoilVelocityExpansion) { _recoilVelocityExpansion = targetRecoilVelocityExpansion; } else { float decayRate = interactable.RecoilAssist.VelocityExpansionDecayRate * deltaTime; _recoilVelocityExpansion = Math.Max(targetRecoilVelocityExpansion, _recoilVelocityExpansion - decayRate); } recoilExitDistance += _recoilVelocityExpansion; } if (depth > _selectMaxDepth) { _selectMaxDepth = depth; } else { if (interactable.RecoilAssist.UseDynamicDecay) { Vector3 frameDeltaWorld = Origin - _previousFingerPressOrigin; Vector3 normalDeltaWorld = Vector3.Project(frameDeltaWorld, TouchNormal); float normalRatio = frameDeltaWorld.sqrMagnitude > 0.0000001f ? normalDeltaWorld.magnitude / frameDeltaWorld.magnitude : 1f; float decayFactor = interactable.RecoilAssist.DynamicDecayCurve.Evaluate(normalRatio); _selectMaxDepth = Mathf.Lerp(_selectMaxDepth, depth, decayFactor * deltaTime); } if (depth < _selectMaxDepth - recoilExitDistance) { _reEnterDepth = depth + interactable.RecoilAssist.ReEnterDistance; return true; } } return false; } protected override void DoSelectUpdate() { bool withinSurface = SurfaceUpdate(_selectedInteractable); if (!withinSurface) { _hitInteractable = null; IsPassedSurface = _recoilInteractable != null; return; } if (ShouldCancel(_selectedInteractable)) { GenerateDetectorEvent(DetectorEventType.Cancel, _selectedInteractable); _previousFingerPressOrigin = Origin; _previousCandidate = null; _hitInteractable = null; _recoilInteractable = null; _recoilVelocityExpansion = 0; IsPassedSurface = false; _isRecoiled = false; return; } if (ShouldRecoil(_selectedInteractable)) { _hitInteractable = null; _recoilInteractable = _selectedInteractable; _selectMaxDepth = 0; } } } }