Created
September 4, 2023 19:00
-
-
Save alexanderameye/a27c32d7d7425f2970ef93aec7dc3c30 to your computer and use it in GitHub Desktop.
An AR cursor for Unity3D.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using UnityEngine; | |
using UnityEngine.XR.ARSubsystems; | |
using DG.Tweening; | |
[RequireComponent(typeof(ARHitProvider), typeof(ARPointCloudProvider))] | |
public class ARCursor : MonoBehaviour | |
{ | |
public static ARCursor instance; | |
[Header("Cursor")] | |
[SerializeField] GameObject cursor; | |
[SerializeField] [Range(0f, 20f)] float trackingSpeed = 15f; | |
[SerializeField] [Range(0f, 2f)] float enableDuration = 0.5f; | |
[SerializeField] [Range(0f, 2f)] float disableDuration = 1.2f; | |
[Header("Thresholds")] | |
//NOTE: moveThreshold is useful for snapping, set threshold on snap, remove threshold when moved enough | |
[SerializeField] [Tooltip("Only move cursor if we moved more than this threshold.")] [Range(0f, 1f)] float moveThreshold = 0.03f; | |
[SerializeField] [Tooltip("Hide cursor if less features on screen than this threshold.")] [Range(0, 50)] int featureThreshold = 10; | |
[SerializeField] [Tooltip("Hide cursor after a few seconds if tracking is lost.")] [Range(0, 5)] float timeOutThreshold = 2f; | |
[SerializeField] [Tooltip("Hide cursor beyond this distance.")] [Range(0, 30)] float distanceThreshold = 20f; | |
[SerializeField] [Tooltip("Hysteresis length.")] [Range(0, 5)] float distanceHysteresis = 1f; // NOTE: used to avoid flickering | |
[Header("Tracking")] | |
[SerializeField] [Tooltip("Use PlaneEstimated if you want to snap to mesh.")] TrackableType trackableType; | |
/* cursor position and rotation */ | |
Pose targetPose; | |
public Vector3 Position => targetPose.position; | |
public Quaternion Rotation => targetPose.rotation; | |
Vector3 currentCursorPosition; | |
/* providers */ | |
ARPointCloudProvider pointCloudProvider; | |
ARHitProvider hitProvider; | |
Camera mainCamera; | |
/* coroutines */ | |
IEnumerator displayCursor; | |
IEnumerator moveCursor; | |
bool m_currentlyScaling; | |
void Awake() | |
{ | |
if (instance != null) GameObject.Destroy(instance); | |
else instance = this; | |
DontDestroyOnLoad(this); | |
mainCamera = FindObjectOfType<Camera>(); | |
} | |
private void Start() | |
{ | |
/* get providers */ | |
pointCloudProvider = ARPointCloudProvider.Instance; | |
hitProvider = ARHitProvider.instance; | |
cursor.SetActive(false); | |
displayCursor = DisplayCursor(); | |
StartCoroutine(displayCursor); | |
} | |
public void EnableCursor() | |
{ | |
if (cursor.activeSelf || m_currentlyScaling) return; // don't run if cursor already enabled or currently enabling/disabling | |
DOTween.Sequence() | |
.AppendCallback(() => m_currentlyScaling = true) | |
.AppendCallback(() => cursor.SetActive(true)) | |
.Append(cursor.transform.DOScale(1f, enableDuration)) | |
.AppendCallback(() => m_currentlyScaling = false); | |
} | |
public void DisableCursor() | |
{ | |
if (!cursor.activeSelf || m_currentlyScaling) return; // don't run if cursor already disabled or currently enabling/disabling | |
DOTween.Sequence() | |
.AppendCallback(() => m_currentlyScaling = true) | |
.Append(cursor.transform.DOScale(0f, disableDuration)) | |
.AppendCallback(() => cursor.SetActive(false)) | |
.AppendCallback(() => m_currentlyScaling = false); | |
} | |
public void DoTapFeedback() | |
{ | |
DOTween.Sequence() | |
.Append(cursor.transform.DOScale(0.5f, 0.1f)) | |
.Append(cursor.transform.DOScale(1f, 0.2f)); | |
} | |
private IEnumerator DisplayCursor() | |
{ | |
bool withinDistance = false; | |
while (true) | |
{ | |
if (hitProvider.validHit)// && m_hitProvider.hit.hitType == trackableType) | |
{ | |
targetPose = hitProvider.hit.pose; | |
float distanceFromCamera = Vector3.Distance(targetPose.position, mainCamera.transform.position); | |
float distanceFromTarget = (targetPose.position - currentCursorPosition).magnitude; | |
if (distanceFromTarget < moveThreshold) { } // don't move cursor | |
else | |
{ | |
if (pointCloudProvider.FeatureCount < featureThreshold) DisableCursor(); // low feature count | |
else if (Time.time - pointCloudProvider.LastPointCloudUpdate > timeOutThreshold) DisableCursor(); // lost tracking for a while | |
else if (withinDistance == false && distanceFromCamera > (distanceThreshold - distanceHysteresis * 0.5)) DisableCursor(); // cursor too far away | |
else if (withinDistance == true && distanceFromCamera > (distanceThreshold + distanceHysteresis * 0.5)) | |
{ | |
withinDistance = false; | |
EnableCursor(); | |
} | |
else | |
{ | |
withinDistance = true; | |
EnableCursor(); | |
currentCursorPosition = targetPose.position; | |
cursor.transform.rotation = targetPose.rotation; | |
if (moveCursor != null) StopCoroutine(moveCursor); | |
moveCursor = MoveCursor(targetPose.position); | |
StartCoroutine(moveCursor); | |
} | |
} | |
} | |
else DisableCursor(); | |
yield return null; | |
} | |
} | |
IEnumerator MoveCursor(Vector3 destination) | |
{ | |
float distance = (destination - cursor.transform.position).magnitude; | |
while (distance > 0) | |
{ | |
float step = distance * Time.deltaTime / 0.2f; | |
cursor.transform.position = Vector3.MoveTowards(cursor.transform.position, destination, step); | |
distance = (destination - cursor.transform.position).magnitude; | |
yield return null; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment