diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f5a374771d79b22857a7c702efb05664a6979c..991a3f1f1ec396cb636bfa7e55bce43e8d5e579b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## 1.0.1 - 2025-06-24 +### Added + +- Public property `isActivated` in `AprilTagDetector` +- Public method `AddTag`, `RemoveTag`, and `SetTagSizes` in `AprilTagDetector` +- Tooltip for `AprilTagDetector` serialized fields +- `DebugLog` utility based on custom scripting symbols ## 1.0.0 - 2025-06-17 diff --git a/README.md b/README.md index df2b8e9f81c52e2106065665af3e258e505ddf88..bbc7ab9975a823572f84544da87d5cee61f71475 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,18 @@ This package is available in the `HYPER` scoped registry. Please follow [this snippet] to add the registry to your project. +## How to use + +### Enable debug messages + +Add `HYPER_PCA_APRILTAG_DEBUG_LOG` to **Project Settings** > **Player** > **Script Compilation** > **Scripting Define Symbols**. + +## Attribution + +- Correct Blips by CogFireStudios -- https://freesound.org/s/531510/ -- License: Creative Commons 0 + [jp.keijiro.apriltag]: https://github.com/keijiro/jp.keijiro.apriltag [LICENSE.BSD-2-Clause]: https://git-st.inf.tu-dresden.de/hyper/pca-apriltag/-/blob/main/LICENSE.BSD-2-Clause [Unity-PassthroughCameraApiSamples]: https://github.com/oculus-samples/Unity-PassthroughCameraApiSamples/tree/main/Assets/PassthroughCameraApiSamples [LICENSE.MIT]: https://git-st.inf.tu-dresden.de/hyper/pca-apriltag/-/blob/main/LICENSE.MIT [this snippet]: https://git-st.inf.tu-dresden.de/hyper/core/-/snippets/14 - -## Attribution - -- Correct Blips by CogFireStudios -- https://freesound.org/s/531510/ -- License: Creative Commons 0 diff --git a/Runtime/Scripts/AprilTagDetector.cs b/Runtime/Scripts/AprilTagDetector.cs index eb4efff43d7a3c0a222595074241a031ea593093..f52ebac63cfe8c9859d344bf5e4e31cabe0a863c 100644 --- a/Runtime/Scripts/AprilTagDetector.cs +++ b/Runtime/Scripts/AprilTagDetector.cs @@ -10,18 +10,46 @@ namespace Hyper.PCAAprilTag { public class AprilTagDetector : MonoBehaviour { + /// <summary> + /// Indicates whether the AprilTag detector is currently activated. + /// </summary> + public bool isActivated { get; private set; } + public static event Action<IEnumerable<TagPoseSize>> OnTagsDetected; - [SerializeField] private bool isContinuous; - [SerializeField] private List<IdSizePair> tagSizes = new(); - [SerializeField] private int frameDelay = 7; - [SerializeField] private int numAveragedPoses = 10; - [SerializeField] private float maxLinearSpeed = 0.1f; // meters per second - [SerializeField] private float maxAngularSpeed = 10f; // degrees per second - [SerializeField] private float timeout = 2f; - [SerializeField] private int decimation = 4; + [SerializeField, Tooltip("If true, the detector will continuously search for tags until Deactivate() is called.")] + private bool isContinuous; + + [SerializeField, Tooltip("If true, plays a sound when tags are detected.")] + private bool playSuccessSound = true; + + [SerializeField, Tooltip("List of tag IDs and their sizes. " + + "Only tags listed here will be processed by the AprilTag detector.")] + private List<IdSizePair> tagSizes = new(); + + [SerializeField, Tooltip("Use the past frameDelay camera poses when detecting tags " + + "to compensate for WebCamTexture latency.")] + private int frameDelay = 7; + + [SerializeField, Tooltip("Number of poses to average for each detected tag. " + + "Higher values can improve stability but may introduce latency.")] + private int numAveragedPoses = 10; + + [SerializeField, Tooltip("Maximum linear speed of the headset before detection is skipped. " + + "This is to avoid bad detections when the headset is moving too fast.")] + private float maxLinearSpeed = 0.1f; // meters per second + + [SerializeField, Tooltip("Maximum angular speed of the headset before detection is skipped. " + + "This is to avoid bad detections when the headset is rotating too fast.")] + private float maxAngularSpeed = 10f; // degrees per second + + // [SerializeField] private float timeout = 2f; // TODO + + [SerializeField, Tooltip("Decimation factor for the image processing. " + + "Higher values reduce the resolution of the input image, " + + "which can speed up processing but may reduce accuracy.")] + private int decimation = 1; - private bool _isActivated; private float _verticalFOV; private TagDetector _detector; private WebCamTextureManager _webCamTextureManager; @@ -30,14 +58,13 @@ namespace Hyper.PCAAprilTag private Thread _detectorProcessImageThread; private List<TagPoseSize> _detectedTags = new(); private Lifo<Pose> _cameraPoses; - private Dictionary<int, Lifo<TagPoseSize>> _tagPoseSizeDict = new(); + private readonly Dictionary<int, Lifo<TagPoseSize>> _tagPoseSizeDict = new(); private AudioSource _doneAudioSource; - private Vector3 lastPosition; - private Quaternion lastRotation; - private float lastTime; - - private Transform headset; + private Transform _headset; + private Vector3 _lastPosition; + private Quaternion _lastRotation; + private float _lastTime; private void Awake() { @@ -65,10 +92,10 @@ namespace Hyper.PCAAprilTag var cameraIntrinsics = PassthroughCameraUtils.GetCameraIntrinsics(CameraEye); _verticalFOV = 2 * Mathf.Atan2(cameraIntrinsics.Resolution.y, 2 * cameraIntrinsics.FocalLength.y); - headset = Camera.main!.transform; - lastPosition = headset.position; - lastRotation = headset.rotation; - lastTime = Time.time; + _headset = Camera.main!.transform; + _lastPosition = _headset.position; + _lastRotation = _headset.rotation; + _lastTime = Time.time; } private void OnDestroy() @@ -85,6 +112,7 @@ namespace Hyper.PCAAprilTag private void LateUpdate() { + // TODO. This might be a problem if the Camera Rig is moved. _cameraPoses.Push(PassthroughCameraUtils.GetCameraPoseInWorld(CameraEye)); if (_detectedTags.Any()) @@ -103,44 +131,44 @@ namespace Hyper.PCAAprilTag if (!isContinuous && _tagPoseSizeDict.First().Value.Count >= numAveragedPoses) { Deactivate(); - _doneAudioSource.Play(); + if (playSuccessSound) _doneAudioSource.Play(); _tagPoseSizeDict.Clear(); } OnTagsDetected?.Invoke(AvgDetectedTags()); } - if (!_isActivated || !_webCamTextureManager.WebCamTexture || + if (!isActivated || !_webCamTextureManager.WebCamTexture || _detectorProcessImageThread is { IsAlive: true }) return; var currentTime = Time.time; - var deltaTime = currentTime - lastTime; + var deltaTime = currentTime - _lastTime; if (deltaTime > 0f) { // Linear velocity - float linearSpeed = Vector3.Distance(headset.position, lastPosition) / deltaTime; + var linearSpeed = Vector3.Distance(_headset.position, _lastPosition) / deltaTime; // Angular velocity - Quaternion deltaRotation = headset.rotation * Quaternion.Inverse(lastRotation); + var deltaRotation = _headset.rotation * Quaternion.Inverse(_lastRotation); deltaRotation.ToAngleAxis(out float angleInDegrees, out _); - float angularSpeed = angleInDegrees / deltaTime; + var angularSpeed = angleInDegrees / deltaTime; // Update - lastPosition = headset.position; - lastRotation = headset.rotation; - lastTime = currentTime; + _lastPosition = _headset.position; + _lastRotation = _headset.rotation; + _lastTime = currentTime; // Check thresholds if (linearSpeed > maxLinearSpeed) { - Debug.LogWarning($"[Camera] Headset moving too fast (linear): {linearSpeed:F2} m/s"); + DebugLog.Log(LogType.Warning, $"[Camera] Headset moving too fast (linear): {linearSpeed:F2} m/s"); return; } if (angularSpeed > maxAngularSpeed) { - Debug.LogWarning($"[Camera] Headset rotating too fast: {angularSpeed:F2} deg/s"); + DebugLog.Log(LogType.Warning, $"[Camera] Headset rotating too fast: {angularSpeed:F2} deg/s"); return; } } @@ -160,28 +188,28 @@ namespace Hyper.PCAAprilTag _detectorProcessImageThread.Start(); } - private void ProcessImage(Color32[] image, Matrix4x4 cameraToWorldMatrix) - { - _detector.ProcessImage(image, _verticalFOV, _tagSizes, cameraToWorldMatrix); - _detectedTags = _detector.GetDetectedTagsCopy(); - } - + /// <summary> + /// Activate the AprilTag detector and start the WebCamTexture. + /// </summary> public void Activate() { - if (_isActivated) return; + if (isActivated) return; - _isActivated = true; + isActivated = true; _webCamTextureManager.WebCamTexture.Play(); } + /// <summary> + /// Deactivate the AprilTag detector and stop the WebCamTexture. + /// </summary> public void Deactivate() { - _isActivated = false; + isActivated = false; _webCamTextureManager.WebCamTexture.Stop(); } /// <summary> - /// Method to wait for the camera to be ready and then activate the AprilTag detector. + /// Wait for the camera to be ready and then activate the AprilTag detector. /// Useful if AprilTag detection should run when the application starts. /// </summary> public void WaitForCamReadyAndActivate() => @@ -199,6 +227,72 @@ namespace Hyper.PCAAprilTag } } + /// <summary> + /// Add a new tag with its ID and size. + /// </summary> + /// <param name="id">Tag ID.</param> + /// <param name="size">Tag size.</param> + /// <returns>Success boolean.</returns> + public bool AddTag(int id, float size) + { + if (tagSizes.Any(pair => pair.id == id)) + { + DebugLog.Log(LogType.Warning, $"Tag with ID {id} already exists."); + return false; + } + + tagSizes.Add(new IdSizePair(id, size)); + _tagSizes = TagSizesDictionary(); + return true; + } + + /// <summary> + /// Remove a tag by its ID. + /// </summary> + /// <param name="id">Tag ID.</param> + /// <returns></returns> + public bool RemoveTag(int id) + { + if (tagSizes.Any(pair => pair.id == id)) + { + tagSizes.Remove(tagSizes.First(pair => pair.id == id)); + _tagSizes = TagSizesDictionary(); + return true; + } + + DebugLog.Log(LogType.Warning, $"Tag with ID {id} does not exist."); + return false; + } + + /// <summary> + /// Set the tagSizes list. + /// </summary> + /// <param name="ids">Array of tag IDs.</param> + /// <param name="sizes">Array of tag sizes.</param> + /// <returns>Success boolean.</returns> + public bool SetTagSizes(int[] ids, float[] sizes) + { + if (ids.Length != sizes.Length) + { + DebugLog.Log(LogType.Error, "IDs and sizes arrays must have the same length."); + return false; + } + + tagSizes.Clear(); + for (var i = 0; i < ids.Length; i++) + { + tagSizes.Add(new IdSizePair(ids[i], sizes[i])); + } + _tagSizes = TagSizesDictionary(); + return true; + } + + private void ProcessImage(Color32[] image, Matrix4x4 cameraToWorldMatrix) + { + _detector.ProcessImage(image, _verticalFOV, _tagSizes, cameraToWorldMatrix); + _detectedTags = _detector.GetDetectedTagsCopy(); + } + private Dictionary<int, float> TagSizesDictionary() { var dict = new Dictionary<int, float>(); diff --git a/Runtime/Scripts/DebugLog.cs b/Runtime/Scripts/DebugLog.cs new file mode 100644 index 0000000000000000000000000000000000000000..f63b85faa700955c50b56c9a8c76c3ccd1eb25b4 --- /dev/null +++ b/Runtime/Scripts/DebugLog.cs @@ -0,0 +1,34 @@ +using Debug = UnityEngine.Debug; +using System; +using System.Diagnostics; +using UnityEngine; + +namespace Hyper.PCAAprilTag +{ + /// <summary> + /// A utility class for logging debug messages. + /// To enable, add "HYPER_PCA_APRILTAG_DEBUG_LOG" + /// to Project Settings > Player > Script Compilation > Scripting Define Symbols + /// </summary> + public static class DebugLog + { + [Conditional("HYPER_PCA_APRILTAG_DEBUG_LOG")] + public static void Log(LogType mType, string message) + { + switch (mType) + { + case LogType.Error: + Debug.LogError(message); + break; + case LogType.Warning: + Debug.LogWarning(message); + break; + case LogType.Log: + Debug.Log(message); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mType), mType, null); + } + } + } +} diff --git a/Runtime/Scripts/DebugLog.cs.meta b/Runtime/Scripts/DebugLog.cs.meta new file mode 100644 index 0000000000000000000000000000000000000000..99eabe4531c930f120d44ea4ea9b5c9aa0fca919 --- /dev/null +++ b/Runtime/Scripts/DebugLog.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 28ff872694bba47d899df394223ec8d0 \ No newline at end of file diff --git a/Runtime/Scripts/IdSizePair.cs b/Runtime/Scripts/IdSizePair.cs index 892c12bab2e3373b63bcad951c50f4f144e79fc2..73a374fc2aee1234a443e9ba51b12b29286e563b 100644 --- a/Runtime/Scripts/IdSizePair.cs +++ b/Runtime/Scripts/IdSizePair.cs @@ -3,9 +3,30 @@ using System; namespace Hyper.PCAAprilTag { [Serializable] - public struct IdSizePair + public struct IdSizePair : IEquatable<IdSizePair> { public int id; public float size; + + public IdSizePair(int id, float size) + { + this.id = id; + this.size = size; + } + + public bool Equals(IdSizePair other) + { + return id == other.id; + } + + public override bool Equals(object obj) + { + return obj is IdSizePair other && Equals(other); + } + + public override int GetHashCode() + { + return id; + } } } diff --git a/Runtime/Scripts/KeijiroAprilTag/Utils.cs b/Runtime/Scripts/KeijiroAprilTag/Utils.cs index 150920c28551d09a92761eb2844f4992b41667bc..69c7c60fe0d68e2c0f713dc539db1c6995b32d7d 100644 --- a/Runtime/Scripts/KeijiroAprilTag/Utils.cs +++ b/Runtime/Scripts/KeijiroAprilTag/Utils.cs @@ -1,4 +1,3 @@ -using AprilTag; using System.Collections.Generic; using Unity.Collections; using Unity.Mathematics; diff --git a/Runtime/Scripts/Lifo.cs b/Runtime/Scripts/Lifo.cs index 480ec6df4577b8b0122ab5a97ca2df92609afc86..8988e45c0af051438e75f76d8eb20e81806fc435 100644 --- a/Runtime/Scripts/Lifo.cs +++ b/Runtime/Scripts/Lifo.cs @@ -1,45 +1,52 @@ using System.Collections.Generic; using System.Linq; -// LIFO (Last-In-First-Out) List -public class Lifo<T> +namespace Hyper.PCAAprilTag { - private readonly int _capacity; - private readonly List<T> _list; - - public Lifo(int capacity) + /// <summary> + /// LIFO (Last-In-First-Out) List + /// </summary> + /// <typeparam name="T"></typeparam> + public class Lifo<T> { - _capacity = capacity; - _list = new List<T>(capacity); - } + private readonly int _capacity; + private readonly List<T> _list; - public void Push(T item) - { - if (_list.Count >= _capacity) + public Lifo(int capacity) { - _list.RemoveAt(0); + _capacity = capacity; + _list = new List<T>(capacity); } - _list.Add(item); - } - public T Pop() - { - if (_list.Count == 0) return default; + public void Push(T item) + { + if (_list.Count >= _capacity) + { + _list.RemoveAt(0); + } - var item = _list[0]; - _list.RemoveAt(0); - return item; - } + _list.Add(item); + } - public T Top => _list.Last(); + public T Pop() + { + if (_list.Count == 0) return default; - public T this[int index] => _list[index]; + var item = _list[0]; + _list.RemoveAt(0); + return item; + } - public bool IsEmpty => _list.Count == 0; + public T Top => _list.Last(); - public bool IsFull => _list.Count >= _capacity; + public T this[int index] => _list[index]; - public int Count => _list.Count; + public bool IsEmpty => _list.Count == 0; - public List<T> ToList() => new List<T>(_list); + public bool IsFull => _list.Count >= _capacity; + + public int Count => _list.Count; + + public List<T> ToList() => new List<T>(_list); + } } diff --git a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs deleted file mode 100644 index 67b1e2fc1e4c050ba74e8ed3fe4f4c878998034a..0000000000000000000000000000000000000000 --- a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Meta Platforms, Inc. and affiliates. - -using UnityEngine; - -public static class PassthroughCameraDebugger -{ - public enum DebuglevelEnum - { - ALL, - NONE, - ONLY_ERROR, - ONLY_LOG, - ONLY_WARNING - } - - public static DebuglevelEnum DebugLevel = DebuglevelEnum.ALL; - - /// <summary> - /// Send debug information to Unity console based on DebugType and DebugLevel - /// </summary> - /// <param name="mType"></param> - /// <param name="message"></param> - public static void DebugMessage(LogType mType, string message) - { - switch (mType) - { - case LogType.Error: - if (DebugLevel is DebuglevelEnum.ALL or DebuglevelEnum.ONLY_ERROR) - { - Debug.LogError(message); - } - break; - case LogType.Log: - if (DebugLevel is DebuglevelEnum.ALL or DebuglevelEnum.ONLY_LOG) - { - Debug.Log(message); - } - break; - case LogType.Warning: - if (DebugLevel is DebuglevelEnum.ALL or DebuglevelEnum.ONLY_WARNING) - { - Debug.LogWarning(message); - } - break; - } - } -} diff --git a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs.meta b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs.meta deleted file mode 100644 index 89ad076bd4b424d46834c221f4b7d144aa5c639c..0000000000000000000000000000000000000000 --- a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraDebugger.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3a8ac5b7682fa4c2491fe8382784bad5 \ No newline at end of file diff --git a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraPermissions.cs b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraPermissions.cs index fb166ba29c4d37f24e36eec121497a42a22affb1..9d816289d2b3686bdf37db1323aa12cf6eae4ad6 100644 --- a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraPermissions.cs +++ b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraPermissions.cs @@ -5,84 +5,89 @@ using System.Linq; using UnityEngine; #if UNITY_ANDROID using UnityEngine.Android; -using PCD = PassthroughCameraDebugger; #pragma warning disable CS0618 // Type or member is obsolete #endif -/// <summary> -/// PLEASE NOTE: Unity doesn't support requesting multiple permissions at the same time with <see cref="Permission.RequestUserPermissions"/> on Android. -/// This component is a sample and shouldn't be used simultaneously with other scripts that manage Android permissions. -/// </summary> -public class PassthroughCameraPermissions : MonoBehaviour +namespace Hyper.PCAAprilTag { - [SerializeField] public List<string> PermissionRequestsOnStartup = new() { OVRPermissionsRequester.ScenePermission }; - - public static readonly string[] CameraPermissions = - { - "android.permission.CAMERA", // Required to use WebCamTexture object. - "horizonos.permission.HEADSET_CAMERA" // Required to access the Passthrough Camera API in Horizon OS v74 and above. - }; - - public static bool? HasCameraPermission { get; private set; } - private static bool s_askedOnce; - -#if UNITY_ANDROID /// <summary> - /// Request camera permission if the permission is not authorized by the user. + /// PLEASE NOTE: Unity doesn't support requesting multiple permissions at the same time with <see cref="Permission.RequestUserPermissions"/> on Android. + /// This component is a sample and shouldn't be used simultaneously with other scripts that manage Android permissions. /// </summary> - public void AskCameraPermissions() + public class PassthroughCameraPermissions : MonoBehaviour { - if (s_askedOnce) - { - return; - } - s_askedOnce = true; - if (IsAllCameraPermissionsGranted()) + [SerializeField] + public List<string> PermissionRequestsOnStartup = new() { OVRPermissionsRequester.ScenePermission }; + + public static readonly string[] CameraPermissions = { - HasCameraPermission = true; - PCD.DebugMessage(LogType.Log, "PCA: All camera permissions granted."); - } - else + "android.permission.CAMERA", // Required to use WebCamTexture object. + "horizonos.permission.HEADSET_CAMERA" // Required to access the Passthrough Camera API in Horizon OS v74 and above. + }; + + public static bool? HasCameraPermission { get; private set; } + private static bool s_askedOnce; + +#if UNITY_ANDROID + /// <summary> + /// Request camera permission if the permission is not authorized by the user. + /// </summary> + public void AskCameraPermissions() { - PCD.DebugMessage(LogType.Log, "PCA: Requesting camera permissions."); + if (s_askedOnce) + { + return; + } - var callbacks = new PermissionCallbacks(); - callbacks.PermissionDenied += PermissionCallbacksPermissionDenied; - callbacks.PermissionGranted += PermissionCallbacksPermissionGranted; - callbacks.PermissionDeniedAndDontAskAgain += PermissionCallbacksPermissionDenied; + s_askedOnce = true; + if (IsAllCameraPermissionsGranted()) + { + HasCameraPermission = true; + DebugLog.Log(LogType.Log, "PCA: All camera permissions granted."); + } + else + { + DebugLog.Log(LogType.Log, "PCA: Requesting camera permissions."); - // It's important to request all necessary permissions in one request because only one 'PermissionCallbacks' instance is supported at a time. - var allPermissions = CameraPermissions.Concat(PermissionRequestsOnStartup).ToArray(); - Permission.RequestUserPermissions(allPermissions, callbacks); - } - } + var callbacks = new PermissionCallbacks(); + callbacks.PermissionDenied += PermissionCallbacksPermissionDenied; + callbacks.PermissionGranted += PermissionCallbacksPermissionGranted; + callbacks.PermissionDeniedAndDontAskAgain += PermissionCallbacksPermissionDenied; - /// <summary> - /// Permission Granted callback - /// </summary> - /// <param name="permissionName"></param> - private static void PermissionCallbacksPermissionGranted(string permissionName) - { - PCD.DebugMessage(LogType.Log, $"PCA: Permission {permissionName} Granted"); + // It's important to request all necessary permissions in one request because only one 'PermissionCallbacks' instance is supported at a time. + var allPermissions = CameraPermissions.Concat(PermissionRequestsOnStartup).ToArray(); + Permission.RequestUserPermissions(allPermissions, callbacks); + } + } - // Only initialize the WebCamTexture object if both permissions are granted - if (IsAllCameraPermissionsGranted()) + /// <summary> + /// Permission Granted callback + /// </summary> + /// <param name="permissionName"></param> + private static void PermissionCallbacksPermissionGranted(string permissionName) { - HasCameraPermission = true; + DebugLog.Log(LogType.Log, $"PCA: Permission {permissionName} Granted"); + + // Only initialize the WebCamTexture object if both permissions are granted + if (IsAllCameraPermissionsGranted()) + { + HasCameraPermission = true; + } } - } - /// <summary> - /// Permission Denied callback. - /// </summary> - /// <param name="permissionName"></param> - private static void PermissionCallbacksPermissionDenied(string permissionName) - { - PCD.DebugMessage(LogType.Warning, $"PCA: Permission {permissionName} Denied"); - HasCameraPermission = false; - s_askedOnce = false; - } + /// <summary> + /// Permission Denied callback. + /// </summary> + /// <param name="permissionName"></param> + private static void PermissionCallbacksPermissionDenied(string permissionName) + { + DebugLog.Log(LogType.Warning, $"PCA: Permission {permissionName} Denied"); + HasCameraPermission = false; + s_askedOnce = false; + } - private static bool IsAllCameraPermissionsGranted() => CameraPermissions.All(Permission.HasUserAuthorizedPermission); + private static bool IsAllCameraPermissionsGranted() => + CameraPermissions.All(Permission.HasUserAuthorizedPermission); #endif + } } diff --git a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraUtils.cs b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraUtils.cs index b5ae67f1c3168dd37cf23f886fecc3131a055a3e..93cc547cbc3371bea62a9fb01017aa2d627ab739 100644 --- a/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraUtils.cs +++ b/Runtime/Scripts/PassthroughCameraAPISample/PassthroughCameraUtils.cs @@ -6,362 +6,378 @@ using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; -public static class PassthroughCameraUtils +namespace Hyper.PCAAprilTag { - // The Horizon OS starts supporting PCA with v74. - public const int MINSUPPORTOSVERSION = 74; + public static class PassthroughCameraUtils + { + // The Horizon OS starts supporting PCA with v74. + public const int MINSUPPORTOSVERSION = 74; - // The only pixel format supported atm - private const int YUV_420_888 = 0x00000023; + // The only pixel format supported atm + private const int YUV_420_888 = 0x00000023; - private static AndroidJavaObject s_currentActivity; - private static AndroidJavaObject s_cameraManager; - private static bool? s_isSupported; - private static int? s_horizonOsVersion; + private static AndroidJavaObject s_currentActivity; + private static AndroidJavaObject s_cameraManager; + private static bool? s_isSupported; + private static int? s_horizonOsVersion; - // Caches - internal static readonly Dictionary<PassthroughCameraEye, (string id, int index)> CameraEyeToCameraIdMap = new(); - private static readonly ConcurrentDictionary<PassthroughCameraEye, List<Vector2Int>> s_cameraOutputSizes = new(); - private static readonly ConcurrentDictionary<string, AndroidJavaObject> s_cameraCharacteristicsMap = new(); - private static readonly OVRPose?[] s_cachedCameraPosesRelativeToHead = new OVRPose?[2]; + // Caches + internal static readonly Dictionary<PassthroughCameraEye, (string id, int index)> + CameraEyeToCameraIdMap = new(); - /// <summary> - /// Get the Horizon OS version number on the headset - /// </summary> - public static int? HorizonOSVersion - { - get + private static readonly ConcurrentDictionary<PassthroughCameraEye, List<Vector2Int>> + s_cameraOutputSizes = new(); + + private static readonly ConcurrentDictionary<string, AndroidJavaObject> s_cameraCharacteristicsMap = new(); + private static readonly OVRPose?[] s_cachedCameraPosesRelativeToHead = new OVRPose?[2]; + + /// <summary> + /// Get the Horizon OS version number on the headset + /// </summary> + public static int? HorizonOSVersion { - if (!s_horizonOsVersion.HasValue) + get { - var vrosClass = new AndroidJavaClass("vros.os.VrosBuild"); - s_horizonOsVersion = vrosClass.CallStatic<int>("getSdkVersion"); + if (!s_horizonOsVersion.HasValue) + { + var vrosClass = new AndroidJavaClass("vros.os.VrosBuild"); + s_horizonOsVersion = vrosClass.CallStatic<int>("getSdkVersion"); #if OVR_INTERNAL_CODE // 10000 means that the build doesn't have a proper release version, and it is still in Mainline, // not in a release branch. #endif // OVR_INTERNAL_CODE - if (s_horizonOsVersion == 10000) - { - s_horizonOsVersion = -1; + if (s_horizonOsVersion == 10000) + { + s_horizonOsVersion = -1; + } } - } - return s_horizonOsVersion.Value != -1 ? s_horizonOsVersion.Value : null; + return s_horizonOsVersion.Value != -1 ? s_horizonOsVersion.Value : null; + } } - } - /// <summary> - /// Returns true if the current headset supports Passthrough Camera API - /// </summary> - public static bool IsSupported - { - get + /// <summary> + /// Returns true if the current headset supports Passthrough Camera API + /// </summary> + public static bool IsSupported { - if (!s_isSupported.HasValue) + get { - var headset = OVRPlugin.GetSystemHeadsetType(); - return (headset == OVRPlugin.SystemHeadset.Meta_Quest_3 || - headset == OVRPlugin.SystemHeadset.Meta_Quest_3S) && - (!HorizonOSVersion.HasValue || HorizonOSVersion >= MINSUPPORTOSVERSION); - } + if (!s_isSupported.HasValue) + { + var headset = OVRPlugin.GetSystemHeadsetType(); + return (headset == OVRPlugin.SystemHeadset.Meta_Quest_3 || + headset == OVRPlugin.SystemHeadset.Meta_Quest_3S) && + (!HorizonOSVersion.HasValue || HorizonOSVersion >= MINSUPPORTOSVERSION); + } - return s_isSupported.Value; + return s_isSupported.Value; + } } - } - - /// <summary> - /// Provides a list of resolutions supported by the passthrough camera. Developers should use one of those - /// when initializing the camera. - /// </summary> - /// <param name="cameraEye">The passthrough camera</param> - public static List<Vector2Int> GetOutputSizes(PassthroughCameraEye cameraEye) - { - return s_cameraOutputSizes.GetOrAdd(cameraEye, GetOutputSizesInternal(cameraEye)); - } - - /// <summary> - /// Returns the camera intrinsics for a specified passthrough camera. All the intrinsics values are provided - /// in pixels. The resolution value is the maximum resolution available for the camera. - /// </summary> - /// <param name="cameraEye">The passthrough camera</param> - public static PassthroughCameraIntrinsics GetCameraIntrinsics(PassthroughCameraEye cameraEye) - { - var cameraCharacteristics = GetCameraCharacteristics(cameraEye); - var intrinsicsArr = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_INTRINSIC_CALIBRATION"); - - // Querying the camera resolution for which the intrinsics are provided - // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE - // This is a Rect of 4 elements: [bottom, left, right, top] with (0,0) at top-left corner. - using var sensorSize = GetCameraValueByKey<AndroidJavaObject>(cameraCharacteristics, "SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE"); - return new PassthroughCameraIntrinsics + /// <summary> + /// Provides a list of resolutions supported by the passthrough camera. Developers should use one of those + /// when initializing the camera. + /// </summary> + /// <param name="cameraEye">The passthrough camera</param> + public static List<Vector2Int> GetOutputSizes(PassthroughCameraEye cameraEye) { - FocalLength = new Vector2(intrinsicsArr[0], intrinsicsArr[1]), - PrincipalPoint = new Vector2(intrinsicsArr[2], intrinsicsArr[3]), - Resolution = new Vector2Int(sensorSize.Get<int>("right"), sensorSize.Get<int>("bottom")), - Skew = intrinsicsArr[4] - }; - } + return s_cameraOutputSizes.GetOrAdd(cameraEye, GetOutputSizesInternal(cameraEye)); + } - /// <summary> - /// Returns an Android Camera2 API's cameraId associated with the passthrough camera specified in the argument. - /// </summary> - /// <param name="cameraEye">The passthrough camera</param> - /// <exception cref="ApplicationException">Throws an exception if the code was not able to find cameraId</exception> - public static string GetCameraIdByEye(PassthroughCameraEye cameraEye) - { - _ = EnsureInitialized(); + /// <summary> + /// Returns the camera intrinsics for a specified passthrough camera. All the intrinsics values are provided + /// in pixels. The resolution value is the maximum resolution available for the camera. + /// </summary> + /// <param name="cameraEye">The passthrough camera</param> + public static PassthroughCameraIntrinsics GetCameraIntrinsics(PassthroughCameraEye cameraEye) + { + var cameraCharacteristics = GetCameraCharacteristics(cameraEye); + var intrinsicsArr = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_INTRINSIC_CALIBRATION"); - return !CameraEyeToCameraIdMap.TryGetValue(cameraEye, out var value) - ? throw new ApplicationException($"Cannot find cameraId for the eye {cameraEye}") - : value.id; - } + // Querying the camera resolution for which the intrinsics are provided + // https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE + // This is a Rect of 4 elements: [bottom, left, right, top] with (0,0) at top-left corner. + using var sensorSize = GetCameraValueByKey<AndroidJavaObject>(cameraCharacteristics, + "SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE"); - /// <summary> - /// Returns the world pose of a passthrough camera at a given time. - /// The LENS_POSE_TRANSLATION and LENS_POSE_ROTATION keys in 'android.hardware.camera2' are relative to the origin, so they can be cached to improve performance. - /// </summary> - /// <param name="cameraEye">The passthrough camera</param> - /// <returns>The passthrough camera's world pose</returns> - public static Pose GetCameraPoseInWorld(PassthroughCameraEye cameraEye) - { - var index = cameraEye == PassthroughCameraEye.Left ? 0 : 1; + return new PassthroughCameraIntrinsics + { + FocalLength = new Vector2(intrinsicsArr[0], intrinsicsArr[1]), + PrincipalPoint = new Vector2(intrinsicsArr[2], intrinsicsArr[3]), + Resolution = new Vector2Int(sensorSize.Get<int>("right"), sensorSize.Get<int>("bottom")), + Skew = intrinsicsArr[4] + }; + } - if (s_cachedCameraPosesRelativeToHead[index] == null) + /// <summary> + /// Returns an Android Camera2 API's cameraId associated with the passthrough camera specified in the argument. + /// </summary> + /// <param name="cameraEye">The passthrough camera</param> + /// <exception cref="ApplicationException">Throws an exception if the code was not able to find cameraId</exception> + public static string GetCameraIdByEye(PassthroughCameraEye cameraEye) { - var cameraId = GetCameraIdByEye(cameraEye); - using var cameraCharacteristics = s_cameraManager.Call<AndroidJavaObject>("getCameraCharacteristics", cameraId); + _ = EnsureInitialized(); - var cameraTranslation = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_POSE_TRANSLATION"); - var p_headFromCamera = new Vector3(cameraTranslation[0], cameraTranslation[1], -cameraTranslation[2]); - - var cameraRotation = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_POSE_ROTATION"); - var q_cameraFromHead = new Quaternion(-cameraRotation[0], -cameraRotation[1], cameraRotation[2], cameraRotation[3]); + return !CameraEyeToCameraIdMap.TryGetValue(cameraEye, out var value) + ? throw new ApplicationException($"Cannot find cameraId for the eye {cameraEye}") + : value.id; + } - var q_headFromCamera = Quaternion.Inverse(q_cameraFromHead); + /// <summary> + /// Returns the world pose of a passthrough camera at a given time. + /// The LENS_POSE_TRANSLATION and LENS_POSE_ROTATION keys in 'android.hardware.camera2' are relative to the origin, so they can be cached to improve performance. + /// </summary> + /// <param name="cameraEye">The passthrough camera</param> + /// <returns>The passthrough camera's world pose</returns> + public static Pose GetCameraPoseInWorld(PassthroughCameraEye cameraEye) + { + var index = cameraEye == PassthroughCameraEye.Left ? 0 : 1; - s_cachedCameraPosesRelativeToHead[index] = new OVRPose + if (s_cachedCameraPosesRelativeToHead[index] == null) { - position = p_headFromCamera, - orientation = q_headFromCamera - }; - } + var cameraId = GetCameraIdByEye(cameraEye); + using var cameraCharacteristics = + s_cameraManager.Call<AndroidJavaObject>("getCameraCharacteristics", cameraId); - var headFromCamera = s_cachedCameraPosesRelativeToHead[index].Value; - var worldFromHead = OVRPlugin.GetNodePoseStateImmediate(OVRPlugin.Node.Head).Pose.ToOVRPose(); - var worldFromCamera = worldFromHead * headFromCamera; - worldFromCamera.orientation *= Quaternion.Euler(180, 0, 0); + var cameraTranslation = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_POSE_TRANSLATION"); + var p_headFromCamera = new Vector3(cameraTranslation[0], cameraTranslation[1], -cameraTranslation[2]); - return new Pose(worldFromCamera.position, worldFromCamera.orientation); - } + var cameraRotation = GetCameraValueByKey<float[]>(cameraCharacteristics, "LENS_POSE_ROTATION"); + var q_cameraFromHead = new Quaternion(-cameraRotation[0], -cameraRotation[1], cameraRotation[2], + cameraRotation[3]); - /// <summary> - /// Returns a 3D ray in the world space which starts from the passthrough camera origin and passes through the - /// 2D camera pixel. - /// </summary> - /// <param name="cameraEye">The passthrough camera</param> - /// <param name="screenPoint">A 2D point on the camera texture. The point is positioned relative to the - /// maximum available camera resolution. This resolution can be obtained using <see cref="GetCameraIntrinsics"/> - /// or <see cref="GetOutputSizes"/> methods. - /// </param> - public static Ray ScreenPointToRayInWorld(PassthroughCameraEye cameraEye, Vector2Int screenPoint) - { - var rayInCamera = ScreenPointToRayInCamera(cameraEye, screenPoint); - var cameraPoseInWorld = GetCameraPoseInWorld(cameraEye); - var rayDirectionInWorld = cameraPoseInWorld.rotation * rayInCamera.direction; - return new Ray(cameraPoseInWorld.position, rayDirectionInWorld); - } + var q_headFromCamera = Quaternion.Inverse(q_cameraFromHead); - /// <summary> - /// Returns a 3D ray in the camera space which starts from the passthrough camera origin - which is always - /// (0, 0, 0) - and passes through the 2D camera pixel. - /// </summary> - /// <param name="cameraEye">The passthrough camera</param> - /// <param name="screenPoint">A 2D point on the camera texture. The point is positioned relative to the - /// maximum available camera resolution. This resolution can be obtained using <see cref="GetCameraIntrinsics"/> - /// or <see cref="GetOutputSizes"/> methods. - /// </param> - public static Ray ScreenPointToRayInCamera(PassthroughCameraEye cameraEye, Vector2Int screenPoint) - { - var intrinsics = GetCameraIntrinsics(cameraEye); - var directionInCamera = new Vector3 - { - x = (screenPoint.x - intrinsics.PrincipalPoint.x) / intrinsics.FocalLength.x, - y = (screenPoint.y - intrinsics.PrincipalPoint.y) / intrinsics.FocalLength.y, - z = 1 - }; + s_cachedCameraPosesRelativeToHead[index] = new OVRPose + { + position = p_headFromCamera, + orientation = q_headFromCamera + }; + } - return new Ray(Vector3.zero, directionInCamera); - } + var headFromCamera = s_cachedCameraPosesRelativeToHead[index].Value; + var worldFromHead = OVRPlugin.GetNodePoseStateImmediate(OVRPlugin.Node.Head).Pose.ToOVRPose(); + var worldFromCamera = worldFromHead * headFromCamera; + worldFromCamera.orientation *= Quaternion.Euler(180, 0, 0); - #region Private methods + return new Pose(worldFromCamera.position, worldFromCamera.orientation); + } - internal static bool EnsureInitialized() - { - if (CameraEyeToCameraIdMap.Count == 2) + /// <summary> + /// Returns a 3D ray in the world space which starts from the passthrough camera origin and passes through the + /// 2D camera pixel. + /// </summary> + /// <param name="cameraEye">The passthrough camera</param> + /// <param name="screenPoint">A 2D point on the camera texture. The point is positioned relative to the + /// maximum available camera resolution. This resolution can be obtained using <see cref="GetCameraIntrinsics"/> + /// or <see cref="GetOutputSizes"/> methods. + /// </param> + public static Ray ScreenPointToRayInWorld(PassthroughCameraEye cameraEye, Vector2Int screenPoint) { - return true; + var rayInCamera = ScreenPointToRayInCamera(cameraEye, screenPoint); + var cameraPoseInWorld = GetCameraPoseInWorld(cameraEye); + var rayDirectionInWorld = cameraPoseInWorld.rotation * rayInCamera.direction; + return new Ray(cameraPoseInWorld.position, rayDirectionInWorld); } - Debug.Log($"PCA: PassthroughCamera - Initializing..."); - using var activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); - s_currentActivity = activityClass.GetStatic<AndroidJavaObject>("currentActivity"); - s_cameraManager = s_currentActivity.Call<AndroidJavaObject>("getSystemService", "camera"); - Assert.IsNotNull(s_cameraManager, "Camera manager has not been provided by the Android system"); + /// <summary> + /// Returns a 3D ray in the camera space which starts from the passthrough camera origin - which is always + /// (0, 0, 0) - and passes through the 2D camera pixel. + /// </summary> + /// <param name="cameraEye">The passthrough camera</param> + /// <param name="screenPoint">A 2D point on the camera texture. The point is positioned relative to the + /// maximum available camera resolution. This resolution can be obtained using <see cref="GetCameraIntrinsics"/> + /// or <see cref="GetOutputSizes"/> methods. + /// </param> + public static Ray ScreenPointToRayInCamera(PassthroughCameraEye cameraEye, Vector2Int screenPoint) + { + var intrinsics = GetCameraIntrinsics(cameraEye); + var directionInCamera = new Vector3 + { + x = (screenPoint.x - intrinsics.PrincipalPoint.x) / intrinsics.FocalLength.x, + y = (screenPoint.y - intrinsics.PrincipalPoint.y) / intrinsics.FocalLength.y, + z = 1 + }; + + return new Ray(Vector3.zero, directionInCamera); + } - var cameraIds = GetCameraIdList(); - Debug.Log($"PCA: PassthroughCamera - cameraId list is {string.Join(", ", cameraIds)}"); + #region Private methods - for (var idIndex = 0; idIndex < cameraIds.Length; idIndex++) + internal static bool EnsureInitialized() { - var cameraId = cameraIds[idIndex]; - CameraSource? cameraSource = null; - CameraPosition? cameraPosition = null; - - var cameraCharacteristics = GetCameraCharacteristics(cameraId); - using var keysList = cameraCharacteristics.Call<AndroidJavaObject>("getKeys"); - var size = keysList.Call<int>("size"); - for (var i = 0; i < size; i++) + if (CameraEyeToCameraIdMap.Count == 2) { - using var key = keysList.Call<AndroidJavaObject>("get", i); - var keyName = key.Call<string>("getName"); + return true; + } - if (string.Equals(keyName, "com.meta.extra_metadata.camera_source", StringComparison.OrdinalIgnoreCase)) + DebugLog.Log(LogType.Log, $"PCA: PassthroughCamera - Initializing..."); + using var activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); + s_currentActivity = activityClass.GetStatic<AndroidJavaObject>("currentActivity"); + s_cameraManager = s_currentActivity.Call<AndroidJavaObject>("getSystemService", "camera"); + Assert.IsNotNull(s_cameraManager, "Camera manager has not been provided by the Android system"); + + var cameraIds = GetCameraIdList(); + DebugLog.Log(LogType.Log, $"PCA: PassthroughCamera - cameraId list is {string.Join(", ", cameraIds)}"); + + for (var idIndex = 0; idIndex < cameraIds.Length; idIndex++) + { + var cameraId = cameraIds[idIndex]; + CameraSource? cameraSource = null; + CameraPosition? cameraPosition = null; + + var cameraCharacteristics = GetCameraCharacteristics(cameraId); + using var keysList = cameraCharacteristics.Call<AndroidJavaObject>("getKeys"); + var size = keysList.Call<int>("size"); + for (var i = 0; i < size; i++) { - // Both `com.meta.extra_metadata.camera_source` and `com.meta.extra_metadata.camera_source` are - // custom camera fields which are stored as arrays of size 1, instead of single values. - // We have to read those values correspondingly - var cameraSourceArr = GetCameraValueByKey<sbyte[]>(cameraCharacteristics, key); - if (cameraSourceArr == null || cameraSourceArr.Length != 1) - continue; - - cameraSource = (CameraSource)cameraSourceArr[0]; + using var key = keysList.Call<AndroidJavaObject>("get", i); + var keyName = key.Call<string>("getName"); + + if (string.Equals(keyName, "com.meta.extra_metadata.camera_source", + StringComparison.OrdinalIgnoreCase)) + { + // Both `com.meta.extra_metadata.camera_source` and `com.meta.extra_metadata.camera_source` are + // custom camera fields which are stored as arrays of size 1, instead of single values. + // We have to read those values correspondingly + var cameraSourceArr = GetCameraValueByKey<sbyte[]>(cameraCharacteristics, key); + if (cameraSourceArr == null || cameraSourceArr.Length != 1) + continue; + + cameraSource = (CameraSource)cameraSourceArr[0]; + } + else if (string.Equals(keyName, "com.meta.extra_metadata.position", + StringComparison.OrdinalIgnoreCase)) + { + var cameraPositionArr = GetCameraValueByKey<sbyte[]>(cameraCharacteristics, key); + if (cameraPositionArr == null || cameraPositionArr.Length != 1) + continue; + + cameraPosition = (CameraPosition)cameraPositionArr[0]; + } } - else if (string.Equals(keyName, "com.meta.extra_metadata.position", StringComparison.OrdinalIgnoreCase)) - { - var cameraPositionArr = GetCameraValueByKey<sbyte[]>(cameraCharacteristics, key); - if (cameraPositionArr == null || cameraPositionArr.Length != 1) - continue; - cameraPosition = (CameraPosition)cameraPositionArr[0]; + if (!cameraSource.HasValue || !cameraPosition.HasValue || + cameraSource.Value != CameraSource.Passthrough) + continue; + + switch (cameraPosition) + { + case CameraPosition.Left: + DebugLog.Log(LogType.Log, $"PCA: Found left passthrough cameraId = {cameraId}"); + CameraEyeToCameraIdMap[PassthroughCameraEye.Left] = (cameraId, idIndex); + break; + case CameraPosition.Right: + DebugLog.Log(LogType.Log, $"PCA: Found right passthrough cameraId = {cameraId}"); + CameraEyeToCameraIdMap[PassthroughCameraEye.Right] = (cameraId, idIndex); + break; + default: + throw new ApplicationException($"Cannot parse Camera Position value {cameraPosition}"); } } - if (!cameraSource.HasValue || !cameraPosition.HasValue || cameraSource.Value != CameraSource.Passthrough) - continue; + return CameraEyeToCameraIdMap.Count == 2; + } - switch (cameraPosition) - { - case CameraPosition.Left: - Debug.Log($"PCA: Found left passthrough cameraId = {cameraId}"); - CameraEyeToCameraIdMap[PassthroughCameraEye.Left] = (cameraId, idIndex); - break; - case CameraPosition.Right: - Debug.Log($"PCA: Found right passthrough cameraId = {cameraId}"); - CameraEyeToCameraIdMap[PassthroughCameraEye.Right] = (cameraId, idIndex); - break; - default: - throw new ApplicationException($"Cannot parse Camera Position value {cameraPosition}"); - } + internal static bool IsPassthroughEnabled() + { + return OVRManager.IsInsightPassthroughSupported() && + OVRManager.IsInsightPassthroughInitialized() && + OVRManager.instance.isInsightPassthroughEnabled; } - return CameraEyeToCameraIdMap.Count == 2; - } + private static string[] GetCameraIdList() + { + return s_cameraManager.Call<string[]>("getCameraIdList"); + } - internal static bool IsPassthroughEnabled() - { - return OVRManager.IsInsightPassthroughSupported() && - OVRManager.IsInsightPassthroughInitialized() && - OVRManager.instance.isInsightPassthroughEnabled; - } + private static List<Vector2Int> GetOutputSizesInternal(PassthroughCameraEye cameraEye) + { + _ = EnsureInitialized(); - private static string[] GetCameraIdList() - { - return s_cameraManager.Call<string[]>("getCameraIdList"); - } + var cameraId = GetCameraIdByEye(cameraEye); + var cameraCharacteristics = GetCameraCharacteristics(cameraId); + using var configurationMap = + GetCameraValueByKey<AndroidJavaObject>(cameraCharacteristics, "SCALER_STREAM_CONFIGURATION_MAP"); + var outputSizes = configurationMap.Call<AndroidJavaObject[]>("getOutputSizes", YUV_420_888); - private static List<Vector2Int> GetOutputSizesInternal(PassthroughCameraEye cameraEye) - { - _ = EnsureInitialized(); + var result = new List<Vector2Int>(); + foreach (var outputSize in outputSizes) + { + var width = outputSize.Call<int>("getWidth"); + var height = outputSize.Call<int>("getHeight"); + result.Add(new Vector2Int(width, height)); + } - var cameraId = GetCameraIdByEye(cameraEye); - var cameraCharacteristics = GetCameraCharacteristics(cameraId); - using var configurationMap = - GetCameraValueByKey<AndroidJavaObject>(cameraCharacteristics, "SCALER_STREAM_CONFIGURATION_MAP"); - var outputSizes = configurationMap.Call<AndroidJavaObject[]>("getOutputSizes", YUV_420_888); + foreach (var obj in outputSizes) + { + obj?.Dispose(); + } - var result = new List<Vector2Int>(); - foreach (var outputSize in outputSizes) - { - var width = outputSize.Call<int>("getWidth"); - var height = outputSize.Call<int>("getHeight"); - result.Add(new Vector2Int(width, height)); + return result; } - foreach (var obj in outputSizes) + private static AndroidJavaObject GetCameraCharacteristics(string cameraId) { - obj?.Dispose(); + return s_cameraCharacteristicsMap.GetOrAdd(cameraId, + _ => s_cameraManager.Call<AndroidJavaObject>("getCameraCharacteristics", cameraId)); } - return result; - } - - private static AndroidJavaObject GetCameraCharacteristics(string cameraId) - { - return s_cameraCharacteristicsMap.GetOrAdd(cameraId, - _ => s_cameraManager.Call<AndroidJavaObject>("getCameraCharacteristics", cameraId)); - } + private static AndroidJavaObject GetCameraCharacteristics(PassthroughCameraEye eye) + { + var cameraId = GetCameraIdByEye(eye); + return GetCameraCharacteristics(cameraId); + } - private static AndroidJavaObject GetCameraCharacteristics(PassthroughCameraEye eye) - { - var cameraId = GetCameraIdByEye(eye); - return GetCameraCharacteristics(cameraId); - } + private static T GetCameraValueByKey<T>(AndroidJavaObject cameraCharacteristics, string keyStr) + { + using var key = cameraCharacteristics.GetStatic<AndroidJavaObject>(keyStr); + return GetCameraValueByKey<T>(cameraCharacteristics, key); + } - private static T GetCameraValueByKey<T>(AndroidJavaObject cameraCharacteristics, string keyStr) - { - using var key = cameraCharacteristics.GetStatic<AndroidJavaObject>(keyStr); - return GetCameraValueByKey<T>(cameraCharacteristics, key); - } + private static T GetCameraValueByKey<T>(AndroidJavaObject cameraCharacteristics, AndroidJavaObject key) + { + return cameraCharacteristics.Call<T>("get", key); + } - private static T GetCameraValueByKey<T>(AndroidJavaObject cameraCharacteristics, AndroidJavaObject key) - { - return cameraCharacteristics.Call<T>("get", key); - } + private enum CameraSource + { + Passthrough = 0 + } - private enum CameraSource - { - Passthrough = 0 - } + private enum CameraPosition + { + Left = 0, + Right = 1 + } - private enum CameraPosition - { - Left = 0, - Right = 1 + #endregion Private methods } - #endregion Private methods -} - -/// <summary> -/// Contains camera intrinsics, which describe physical characteristics of a passthrough camera -/// </summary> -public struct PassthroughCameraIntrinsics -{ /// <summary> - /// The focal length in pixels + /// Contains camera intrinsics, which describe physical characteristics of a passthrough camera /// </summary> - public Vector2 FocalLength; - /// <summary> - /// The principal point from the top-left corner of the image, expressed in pixels - /// </summary> - public Vector2 PrincipalPoint; - /// <summary> - /// The resolution in pixels for which the intrinsics are defined - /// </summary> - public Vector2Int Resolution; - /// <summary> - /// The skew coefficient which represents the non-perpendicularity of the image sensor's x and y axes - /// </summary> - public float Skew; + public struct PassthroughCameraIntrinsics + { + /// <summary> + /// The focal length in pixels + /// </summary> + public Vector2 FocalLength; + + /// <summary> + /// The principal point from the top-left corner of the image, expressed in pixels + /// </summary> + public Vector2 PrincipalPoint; + + /// <summary> + /// The resolution in pixels for which the intrinsics are defined + /// </summary> + public Vector2Int Resolution; + + /// <summary> + /// The skew coefficient which represents the non-perpendicularity of the image sensor's x and y axes + /// </summary> + public float Skew; + } } diff --git a/Runtime/Scripts/PassthroughCameraAPISample/WebCamTextureManager.cs b/Runtime/Scripts/PassthroughCameraAPISample/WebCamTextureManager.cs index 6c2b1a89182b31c9f9c246c2f0c2e5634abc3634..40dfd1442fc9572dc7112d79999624d92966a17d 100644 --- a/Runtime/Scripts/PassthroughCameraAPISample/WebCamTextureManager.cs +++ b/Runtime/Scripts/PassthroughCameraAPISample/WebCamTextureManager.cs @@ -4,143 +4,157 @@ using System.Collections; using System.Linq; using UnityEngine; using UnityEngine.Assertions; -using PCD = PassthroughCameraDebugger; -public class WebCamTextureManager : MonoBehaviour +namespace Hyper.PCAAprilTag { - [SerializeField] public PassthroughCameraEye Eye = PassthroughCameraEye.Left; - [SerializeField, Tooltip("The requested resolution of the camera may not be supported by the chosen camera. In such cases, the closest available values will be used.\n\n" + - "When set to (0,0), the highest supported resolution will be used.")] - public Vector2Int RequestedResolution; - [SerializeField] public PassthroughCameraPermissions CameraPermissions; + public class WebCamTextureManager : MonoBehaviour + { + [SerializeField] public PassthroughCameraEye Eye = PassthroughCameraEye.Left; - /// <summary> - /// Returns <see cref="WebCamTexture"/> reference if required permissions were granted and this component is enabled. Else, returns null. - /// </summary> - public WebCamTexture WebCamTexture { get; private set; } + [SerializeField, Tooltip( + "The requested resolution of the camera may not be supported by the chosen camera. In such cases, the closest available values will be used.\n\n" + + "When set to (0,0), the highest supported resolution will be used.")] + public Vector2Int RequestedResolution; - public bool IsReady { get; private set; } + [SerializeField] public PassthroughCameraPermissions CameraPermissions; - private bool m_hasPermission; + /// <summary> + /// Returns <see cref="WebCamTexture"/> reference if required permissions were granted and this component is enabled. Else, returns null. + /// </summary> + public WebCamTexture WebCamTexture { get; private set; } - private void Awake() - { - PCD.DebugMessage(LogType.Log, $"{nameof(WebCamTextureManager)}.{nameof(Awake)}() was called"); - Assert.AreEqual(1, FindObjectsByType<WebCamTextureManager>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length, - $"PCA: Passthrough Camera: more than one {nameof(WebCamTextureManager)} component. Only one instance is allowed at a time. Current instance: {name}"); -#if UNITY_ANDROID - CameraPermissions.AskCameraPermissions(); -#endif - } + public bool IsReady { get; private set; } - private void OnEnable() - { - PCD.DebugMessage(LogType.Log, $"PCA: {nameof(OnEnable)}() was called"); - if (!PassthroughCameraUtils.IsSupported) + private bool m_hasPermission; + + private void Awake() { - PCD.DebugMessage(LogType.Log, "PCA: Passthrough Camera functionality is not supported by the current device." + - $" Disabling {nameof(WebCamTextureManager)} object"); - enabled = false; - return; + DebugLog.Log(LogType.Log, $"{nameof(WebCamTextureManager)}.{nameof(Awake)}() was called"); + Assert.AreEqual(1, + FindObjectsByType<WebCamTextureManager>(FindObjectsInactive.Include, FindObjectsSortMode.None).Length, + $"PCA: Passthrough Camera: more than one {nameof(WebCamTextureManager)} component. Only one instance is allowed at a time. Current instance: {name}"); +#if UNITY_ANDROID + CameraPermissions.AskCameraPermissions(); +#endif } - m_hasPermission = PassthroughCameraPermissions.HasCameraPermission == true; - if (!m_hasPermission) + private void OnEnable() { - PCD.DebugMessage(LogType.Error, - $"PCA: Passthrough Camera requires permission(s) {string.Join(" and ", PassthroughCameraPermissions.CameraPermissions)}. Waiting for them to be granted..."); - return; - } + DebugLog.Log(LogType.Log, $"PCA: {nameof(OnEnable)}() was called"); + if (!PassthroughCameraUtils.IsSupported) + { + DebugLog.Log(LogType.Log, + "PCA: Passthrough Camera functionality is not supported by the current device." + + $" Disabling {nameof(WebCamTextureManager)} object"); + enabled = false; + return; + } - PCD.DebugMessage(LogType.Log, "PCA: All permissions have been granted"); - _ = StartCoroutine(InitializeWebCamTexture()); - } + m_hasPermission = PassthroughCameraPermissions.HasCameraPermission == true; + if (!m_hasPermission) + { + DebugLog.Log(LogType.Error, + $"PCA: Passthrough Camera requires permission(s) {string.Join(" and ", PassthroughCameraPermissions.CameraPermissions)}. Waiting for them to be granted..."); + return; + } - private void OnDisable() - { - PCD.DebugMessage(LogType.Log, $"PCA: {nameof(OnDisable)}() was called"); - StopCoroutine(InitializeWebCamTexture()); - if (WebCamTexture != null) + DebugLog.Log(LogType.Log, "PCA: All permissions have been granted"); + _ = StartCoroutine(InitializeWebCamTexture()); + } + + private void OnDisable() { - WebCamTexture.Stop(); - Destroy(WebCamTexture); - WebCamTexture = null; + DebugLog.Log(LogType.Log, $"PCA: {nameof(OnDisable)}() was called"); + StopCoroutine(InitializeWebCamTexture()); + if (WebCamTexture != null) + { + WebCamTexture.Stop(); + Destroy(WebCamTexture); + WebCamTexture = null; + } } - } - private void Update() - { - if (!m_hasPermission) + private void Update() { - if (PassthroughCameraPermissions.HasCameraPermission != true) - return; + if (!m_hasPermission) + { + if (PassthroughCameraPermissions.HasCameraPermission != true) + return; - m_hasPermission = true; - _ = StartCoroutine(InitializeWebCamTexture()); + m_hasPermission = true; + _ = StartCoroutine(InitializeWebCamTexture()); + } } - } - private IEnumerator InitializeWebCamTexture() - { - // Check if Passhtrough is present in the scene and is enabled - var ptLayer = FindAnyObjectByType<OVRPassthroughLayer>(); - if (ptLayer == null || !PassthroughCameraUtils.IsPassthroughEnabled()) + private IEnumerator InitializeWebCamTexture() { - PCD.DebugMessage(LogType.Error, "Passthrough must be enabled to use the Passthrough Camera API."); - yield break; - } + // Check if Passhtrough is present in the scene and is enabled + var ptLayer = FindAnyObjectByType<OVRPassthroughLayer>(); + if (ptLayer == null || !PassthroughCameraUtils.IsPassthroughEnabled()) + { + DebugLog.Log(LogType.Error, "Passthrough must be enabled to use the Passthrough Camera API."); + yield break; + } #if !UNITY_6000_OR_NEWER - // There is a bug on Unity 2022 that causes a crash if you don't wait a frame before initializing the WebCamTexture. - // Waiting for one frame is important and prevents the bug. - yield return new WaitForEndOfFrame(); + // There is a bug on Unity 2022 that causes a crash if you don't wait a frame before initializing the WebCamTexture. + // Waiting for one frame is important and prevents the bug. + yield return new WaitForEndOfFrame(); #endif - while (true) - { - var devices = WebCamTexture.devices; - if (PassthroughCameraUtils.EnsureInitialized() && PassthroughCameraUtils.CameraEyeToCameraIdMap.TryGetValue(Eye, out var cameraData)) + while (true) { - if (cameraData.index < devices.Length) + var devices = WebCamTexture.devices; + if (PassthroughCameraUtils.EnsureInitialized() && + PassthroughCameraUtils.CameraEyeToCameraIdMap.TryGetValue(Eye, out var cameraData)) { - var deviceName = devices[cameraData.index].name; - WebCamTexture webCamTexture; - if (RequestedResolution == Vector2Int.zero) - { - var largestResolution = PassthroughCameraUtils.GetOutputSizes(Eye).OrderBy(static size => size.x * size.y).Last(); - webCamTexture = new WebCamTexture(deviceName, largestResolution.x, largestResolution.y); - } - else + if (cameraData.index < devices.Length) { - webCamTexture = new WebCamTexture(deviceName, RequestedResolution.x, RequestedResolution.y); + var deviceName = devices[cameraData.index].name; + WebCamTexture webCamTexture; + if (RequestedResolution == Vector2Int.zero) + { + var largestResolution = PassthroughCameraUtils.GetOutputSizes(Eye) + .OrderBy(static size => size.x * size.y).Last(); + webCamTexture = new WebCamTexture(deviceName, largestResolution.x, largestResolution.y); + } + else + { + webCamTexture = new WebCamTexture(deviceName, RequestedResolution.x, RequestedResolution.y); + } + + webCamTexture.Play(); + var currentResolution = new Vector2Int(webCamTexture.width, webCamTexture.height); + if (RequestedResolution != Vector2Int.zero && RequestedResolution != currentResolution) + { + DebugLog.Log(LogType.Warning, + $"WebCamTexture created, but '{nameof(RequestedResolution)}' {RequestedResolution} is not supported. Current resolution: {currentResolution}."); + } + + WebCamTexture = webCamTexture; + DebugLog.Log(LogType.Log, + $"WebCamTexture created, texturePtr: {WebCamTexture.GetNativeTexturePtr()}, size: {WebCamTexture.width}/{WebCamTexture.height}"); + + webCamTexture.Stop(); + IsReady = true; + + yield break; } - webCamTexture.Play(); - var currentResolution = new Vector2Int(webCamTexture.width, webCamTexture.height); - if (RequestedResolution != Vector2Int.zero && RequestedResolution != currentResolution) - { - PCD.DebugMessage(LogType.Warning, $"WebCamTexture created, but '{nameof(RequestedResolution)}' {RequestedResolution} is not supported. Current resolution: {currentResolution}."); - } - WebCamTexture = webCamTexture; - PCD.DebugMessage(LogType.Log, $"WebCamTexture created, texturePtr: {WebCamTexture.GetNativeTexturePtr()}, size: {WebCamTexture.width}/{WebCamTexture.height}"); - - webCamTexture.Stop(); - IsReady = true; - - yield break; } - } - PCD.DebugMessage(LogType.Error, $"Requested camera is not present in WebCamTexture.devices: {string.Join(", ", devices)}."); - yield return null; + DebugLog.Log(LogType.Error, + $"Requested camera is not present in WebCamTexture.devices: {string.Join(", ", devices)}."); + yield return null; + } } } -} -/// <summary> -/// Defines the position of a passthrough camera relative to the headset -/// </summary> -public enum PassthroughCameraEye -{ - Left, - Right + /// <summary> + /// Defines the position of a passthrough camera relative to the headset + /// </summary> + public enum PassthroughCameraEye + { + Left, + Right + } } diff --git a/package.json b/package.json index e58fb13df48ae03dffafecc82e6fb51894b5941c..287e423581fdcd9d0bf3313266034446fc99cd04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "de.tu-dresden.hyper.pca-apriltag", - "version": "1.0.0", + "version": "1.0.1", "displayName": "HYPER PCA AprilTag", "description": "This package provides AprilTag detection using Passthrough Camera API.", "unity": "6000.0",