// %BANNER_BEGIN% // --------------------------------------------------------------------- // %COPYRIGHT_BEGIN% // Copyright (c) 2022-2023 Magic Leap, Inc. All Rights Reserved. // Use of this file is governed by the Magic Leap 2 Software License Agreement, located here: https://www.magicleap.com/software-license-agreement-ml2 // Terms and conditions applicable to third-party materials accompanying this distribution may also be found in the top-level NOTICE file appearing herein. // %COPYRIGHT_END% // --------------------------------------------------------------------- // %BANNER_END% #if !UNITY_EDITOR && UNITY_ANDROID #define ML2 #endif using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; #if ML2 using UnityEngine.XR.MagicLeap; using System.Threading.Tasks; #endif using static UnityEngine.XR.MagicLeap.MLMarkerTracker; namespace MagicLeap.Spectator { public class MLArucoMarkerDetector : MarkerDetector { #region Inspector variables #pragma warning disable 0414 [SerializeField, Tooltip("The aruco / april dictionary to use.")] private ArucoDictionaryName dictionary = ArucoDictionaryName.DICT_6X6_250; [SerializeField, Tooltip("Aruco marker size to use (in meters).")] private float markerSize = 0.15f; [SerializeField, Tooltip("A prefab to use to identify markers.")] private GameObject markerPrefab = null; [Space] [SerializeField, Tooltip("A hint to the back-end for the max frames per second to be analyzed.")] private FPSHint fpsHint = FPSHint.Low; [SerializeField, Tooltip("A hint to the back-end for the resolution of the image to be analyzed.")] private ResolutionHint resolutionHint = ResolutionHint.High; [SerializeField, Tooltip("A hint to the back-end for what camera system to use.")] private CameraHint cameraHint = CameraHint.RGB; [SerializeField, Tooltip("A hint to the back-end for the corner refinement method used during analysis.")] private CornerRefineMethod cornerRefineMethod = CornerRefineMethod.Subpix; [SerializeField, Tooltip("A hint to the back-end to use edge refinement during analysis.")] private bool useEdgeRefinement = true; [SerializeField, Tooltip("Wait for a given amount of time before removing unobsereved trackers.")] private bool removeWithTimeStamp = false; [SerializeField, Tooltip("The timeout duration before removing unobserved trackers. Only used if removeMarkersUsingTimeStamps is set.")] private float trackerTimeout = 0.5f; #pragma warning restore 0414 #endregion #region Private classes private class Marker { public GameObject gameObject { get; private set; } public float Timestamp { get; private set; } public uint Id { get; private set; } public string name => gameObject.name; public Marker(GameObject markerObject) { gameObject = markerObject; } public bool Update(MarkerData data, Camera mainCam) { Timestamp = Time.time; Id = data.ArucoData.Id; var position = data.Pose.position; var rotation = data.Pose.rotation; bool didChange = gameObject.transform.localPosition != position || gameObject.transform.localRotation != rotation; gameObject.transform.localPosition = position; gameObject.transform.localRotation = rotation; gameObject.SetActive(true); return didChange; } } #endregion #region Private variables private Dictionary markerMap = new Dictionary(); private SafeBool hasPermission = new SafeBool(); private Camera mainCam = null; #endregion #region Public properties public ArucoDictionaryName Dictionary { get { return dictionary; } set { foreach (var marker in markerMap.Values) Destroy(marker.gameObject); markerMap.Clear(); dictionary = value; #if ML2 UpdateSettings(); #endif } } public float MarkerSize { get { return markerSize; } set { markerSize = value; #if ML2 UpdateSettings(); #endif } } #endregion #region MonoBehaviour methods public void Awake() { GetMainCam(SceneManager.GetActiveScene(), LoadSceneMode.Single); SceneManager.sceneLoaded += GetMainCam; if (markerPrefab == null) { Debug.LogWarning("MLMarkerDetector: Awake() - Missing marker prefab!"); return; } #if ML2 if (!MLPermissions.CheckPermission(MLPermission.MarkerTracking).IsOk) { var callbacks = new MLPermissions.Callbacks(); callbacks.OnPermissionGranted += x => { hasPermission.Set(true); Debug.Log("MLArucoMarkerDetector: Awake() - MarkerTracking permission granted"); if (enabled) StartTracking(); }; MLPermissions.RequestPermission(MLPermission.MarkerTracking, callbacks); } else { hasPermission.Set(true); Debug.Log("MLArucoMarkerDetector: Awake() - MarkerTracking permission granted"); } #endif } private void OnDestroy() { SceneManager.sceneLoaded -= GetMainCam; } public void OnEnable() { Debug.Log("MLArucoMarkerDetector: OnEnable()"); #if ML2 StartTracking(); #endif } private void OnDisable() { Debug.Log("MLArucoMarkerDetector: OnDisable()"); #if ML2 StopTracking(); #endif foreach (var marker in markerMap.Values) Destroy(marker.gameObject); markerMap.Clear(); } #if ML2 private void Update() { UpdateVisibleTrackers(); } #endif #endregion #region Marker methods #if ML2 private async void StartTracking() { if (!hasPermission) return; if (state != State.Stopped) return; state = State.Starting; MLMarkerTracker.OnMLMarkerTrackerResultsFoundArray += OnMLMarkerTrackerResultsFoundArray; await StartScanningAsync(); state = State.Running; UpdateSettings(); } private async void StopTracking() { if (!hasPermission) return; if (state != State.Running) return; state = State.Stopping; MLMarkerTracker.OnMLMarkerTrackerResultsFoundArray -= OnMLMarkerTrackerResultsFoundArray; await StopScanningAsync(); state = State.Stopped; } private async void UpdateSettings() { if (!hasPermission) return; if (state != State.Running) return; var profile = TrackerSettings.CustomProfile.Create( fpsHint, resolutionHint, cameraHint, FullAnalysisIntervalHint.Slow, cornerRefineMethod, useEdgeRefinement); var settings = TrackerSettings.Create( true, MarkerType.Aruco_April, markerSize, dictionary, markerSize, Profile.Custom, profile); await SetSettingsAsync(settings); } #endif private void OnMLMarkerTrackerResultsFoundArray(MarkerData[] dataArray) { if (mainCam.transform.parent != null) { transform.position = mainCam.transform.parent.position; transform.rotation = mainCam.transform.parent.rotation; } else { transform.position = Vector3.zero; transform.rotation = Quaternion.identity; } if (!removeWithTimeStamp) { RemoveNotVisibleTrackers(dataArray); } foreach (MarkerData data in dataArray) { ProcessSingleMarker(data); } } private void ProcessSingleMarker(MarkerData data) { ArucoData aruco = data.ArucoData; if (!markerMap.TryGetValue(aruco.Id, out Marker marker)) { var markerObject = Instantiate(markerPrefab); markerObject.transform.parent = transform; markerObject.transform.localScale = new Vector3(markerSize, markerSize, 1.0f); markerObject.name = $"{aruco.Dictionary} - {aruco.Id}"; marker = new Marker(markerObject); markerMap.Add(aruco.Id, marker); } // If our marker data has changed, invoke our marker event if (marker.Update(data, mainCam)) markerEvent?.Invoke(marker.name, marker.gameObject); } private void UpdateVisibleTrackers() { if (removeWithTimeStamp) { UpdateVisibleTrackersByTimeStamp(); } } private void UpdateVisibleTrackersByTimeStamp() { List removed = new List(); foreach (var marker in markerMap.Values) { if (!(marker.Timestamp - Time.time > trackerTimeout)) continue; Destroy(marker.gameObject); removed.Add(marker.Id); } foreach (var id in removed) markerMap.Remove(id); } private void RemoveNotVisibleTrackers(MarkerData[] dataArray) { List removed = new List(); foreach (var marker in markerMap.Values) { if (!dataArray.Any(x => x.ArucoData.Id == marker.Id)) { Destroy(marker.gameObject); removed.Add(marker.Id); } } foreach (var id in removed) markerMap.Remove(id); } private void RemoveAllTrackers() { List removed = new List(); foreach (var marker in markerMap.Values) { Destroy(marker.gameObject); removed.Add(marker.Id); } foreach (var id in removed) markerMap.Remove(id); } #endregion #region Private methods private void GetMainCam(Scene scene, LoadSceneMode mode) { mainCam = Camera.main; } #endregion } }