Skip to content

Instantly share code, notes, and snippets.

@alexanderameye
Created September 4, 2023 19:00
Show Gist options
  • Save alexanderameye/a27c32d7d7425f2970ef93aec7dc3c30 to your computer and use it in GitHub Desktop.
Save alexanderameye/a27c32d7d7425f2970ef93aec7dc3c30 to your computer and use it in GitHub Desktop.
An AR cursor for Unity3D.
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