From 086784eb0ea4f4e687493bb1879c34913194f5ca Mon Sep 17 00:00:00 2001
From: Sylvain Buche <sylvain.buche@orange.com>
Date: Tue, 16 Jul 2024 15:38:16 +0200
Subject: [PATCH] Implementation of the Geospatial module using ARCore API. Fix
 a bug in the trackable selection algorithm. Change the way ARAnchorManager is
 used in the ARCoreAnchors module

---
 Runtime/ARCoreExtensionsConfig.asset          |   2 +-
 .../Resources/ARFAnchorTrackingPrefab.prefab  | 106 ++++++++++
 Runtime/Scripts/WorldAnalysisARFoundation.cs  |  34 +++-
 ...dAnalysisARFoundationModuleARCoreAnchor.cs |  10 +-
 ...rldAnalysisARFoundationModuleGeospatial.cs | 185 ++++++++++++++++++
 5 files changed, 327 insertions(+), 10 deletions(-)
 create mode 100644 Runtime/Scripts/WorldAnalysisARFoundationModuleGeospatial.cs

diff --git a/Runtime/ARCoreExtensionsConfig.asset b/Runtime/ARCoreExtensionsConfig.asset
index 858be38..6b08173 100644
--- a/Runtime/ARCoreExtensionsConfig.asset
+++ b/Runtime/ARCoreExtensionsConfig.asset
@@ -14,5 +14,5 @@ MonoBehaviour:
   m_EditorClassIdentifier: 
   CloudAnchorMode: 1
   SemanticMode: 0
-  GeospatialMode: 0
+  GeospatialMode: 2
   StreetscapeGeometryMode: 0
diff --git a/Runtime/Resources/ARFAnchorTrackingPrefab.prefab b/Runtime/Resources/ARFAnchorTrackingPrefab.prefab
index 4b74c4b..1c0ee73 100644
--- a/Runtime/Resources/ARFAnchorTrackingPrefab.prefab
+++ b/Runtime/Resources/ARFAnchorTrackingPrefab.prefab
@@ -1,5 +1,110 @@
 %YAML 1.1
 %TAG !u! tag:unity3d.com,2011:
+--- !u!1 &919512057412407595
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5663275178603938767}
+  - component: {fileID: 2250489381470411502}
+  - component: {fileID: 5243785181569724411}
+  - component: {fileID: 4959683567476516339}
+  m_Layer: 0
+  m_Name: PurpleSphere
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5663275178603938767
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 919512057412407595}
+  serializedVersion: 2
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 20, y: 20, z: 20}
+  m_ConstrainProportionsScale: 1
+  m_Children: []
+  m_Father: {fileID: 6911754431728604849}
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &2250489381470411502
+MeshFilter:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 919512057412407595}
+  m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &5243785181569724411
+MeshRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 919512057412407595}
+  m_Enabled: 1
+  m_CastShadows: 1
+  m_ReceiveShadows: 1
+  m_DynamicOccludee: 1
+  m_StaticShadowCaster: 0
+  m_MotionVectors: 1
+  m_LightProbeUsage: 1
+  m_ReflectionProbeUsage: 1
+  m_RayTracingMode: 2
+  m_RayTraceProcedural: 0
+  m_RenderingLayerMask: 1
+  m_RendererPriority: 0
+  m_Materials:
+  - {fileID: 2100000, guid: a64202207c2bc6042a40ad749c6eabf1, type: 2}
+  m_StaticBatchInfo:
+    firstSubMesh: 0
+    subMeshCount: 0
+  m_StaticBatchRoot: {fileID: 0}
+  m_ProbeAnchor: {fileID: 0}
+  m_LightProbeVolumeOverride: {fileID: 0}
+  m_ScaleInLightmap: 1
+  m_ReceiveGI: 1
+  m_PreserveUVs: 0
+  m_IgnoreNormalsForChartDetection: 0
+  m_ImportantGI: 0
+  m_StitchLightmapSeams: 1
+  m_SelectedEditorRenderState: 3
+  m_MinimumChartSize: 4
+  m_AutoUVMaxDistance: 0.5
+  m_AutoUVMaxAngle: 89
+  m_LightmapParameters: {fileID: 0}
+  m_SortingLayerID: 0
+  m_SortingLayer: 0
+  m_SortingOrder: 0
+  m_AdditionalVertexStreams: {fileID: 0}
+--- !u!135 &4959683567476516339
+SphereCollider:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 919512057412407595}
+  m_Material: {fileID: 0}
+  m_IncludeLayers:
+    serializedVersion: 2
+    m_Bits: 0
+  m_ExcludeLayers:
+    serializedVersion: 2
+    m_Bits: 0
+  m_LayerOverridePriority: 0
+  m_IsTrigger: 0
+  m_ProvidesContacts: 0
+  m_Enabled: 1
+  serializedVersion: 3
+  m_Radius: 0.5
+  m_Center: {x: 0, y: 0, z: 0}
 --- !u!1 &6911754431728604848
 GameObject:
   m_ObjectHideFlags: 0
@@ -33,6 +138,7 @@ Transform:
   - {fileID: 6911754433267149346}
   - {fileID: 6911754433204531549}
   - {fileID: 5348077246929615354}
+  - {fileID: 5663275178603938767}
   m_Father: {fileID: 0}
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
 --- !u!1 &6911754432812775762
diff --git a/Runtime/Scripts/WorldAnalysisARFoundation.cs b/Runtime/Scripts/WorldAnalysisARFoundation.cs
index 02484a7..28aa10d 100644
--- a/Runtime/Scripts/WorldAnalysisARFoundation.cs
+++ b/Runtime/Scripts/WorldAnalysisARFoundation.cs
@@ -8,10 +8,11 @@ using System.Collections;
 using System.Linq;
 using ETSI.ARF.OpenAPI.WorldStorage;
 using ETSI.ARF.WorldStorage.REST;
+using Unity.XR.CoreUtils;
 
 //Implementation of the WorldAnalysis interface
 public class WorldAnalysisARFoundation : MonoBehaviour, WorldAnalysisInterface
-{
+{    
     /// <summary>
     /// Dictionnary of susbscription informations for poses, for each item, stored using the UUID of the item (anchor/trackable)
     /// </summary>
@@ -50,6 +51,11 @@ public class WorldAnalysisARFoundation : MonoBehaviour, WorldAnalysisInterface
     /// </summary>
     private bool isChekingAvailabilityRunning = false;
 
+    /// <summary>
+    ///  Anchor manager
+    /// </summary>
+    private ARAnchorManager m_anchorManager;
+
     /// <summary>
     /// Informations regarding a subscription to a pose
     /// </summary>
@@ -73,10 +79,13 @@ public class WorldAnalysisARFoundation : MonoBehaviour, WorldAnalysisInterface
         m_relocalizationInformations = new Dictionary<Guid, ETSI.ARF.OpenAPI.WorldStorage.RelocalizationInformation>();
         m_computedPoses = new Dictionary<Guid, ETSI.ARF.OpenAPI.WorldAnalysis.Pose>();
         m_subscriptionsPoses = new Dictionary<Guid, SubscriptionInfo>();
-
+        
         m_trackableModules = new List<WorldAnalysisARFoundationModule>();
         WorldAnalysisARFoundationModuleImage imageModule = new WorldAnalysisARFoundationModuleImage();
         m_trackableModules.Add(imageModule);
+
+        WorldAnalysisARFoundationModuleGeospatial geospatialModule = new WorldAnalysisARFoundationModuleGeospatial();
+        m_trackableModules.Add(geospatialModule);
 #if UNITY_IOS
         WorldAnalysisARFoundationModuleMesh meshModule = new WorldAnalysisARFoundationModuleMesh();
         m_trackableModules.Add(meshModule);
@@ -158,7 +167,9 @@ public class WorldAnalysisARFoundation : MonoBehaviour, WorldAnalysisInterface
                 List<RelocObjects> notTrackedCandidates = new List<RelocObjects>();
 
                 //Hierarchy of types
-                string[] types = { "MESH", "IMAGE_MARKER", "FIDUCIAL_MARKER", "MAP", "OTHER" }; //TODO : GEOPOSE
+                string[] types = { "MESH", "IMAGE_MARKER", "FIDUCIAL_MARKER", "MAP", "GEOPOSE", "OTHER" };
+
+                List<Guid> keysToRemove = new List<Guid>();
 
                 //We fill in the confidence level lists.
                 foreach (KeyValuePair<Guid, RelocObjects> relocObject in temp)
@@ -178,9 +189,18 @@ public class WorldAnalysisARFoundation : MonoBehaviour, WorldAnalysisInterface
                         {
                             notTrackedCandidates.Add(relocObject.Value);
                         }
+                    }else
+                    {
+                        keysToRemove.Add(relocObject.Key);
                     }
                 }
 
+                // Remove null trackables from temp
+                foreach (Guid key in keysToRemove)
+                {
+                    temp.Remove(key);
+                }
+
                 //uses the types[] array indexes as key, and UUIDs as values
                 List<KeyValuePair<string, Guid>> typesSortedList = new List<KeyValuePair<string, Guid>>();
                 foreach (var relocObject in temp)
@@ -365,7 +385,10 @@ public class WorldAnalysisARFoundation : MonoBehaviour, WorldAnalysisInterface
             List<ETSI.ARF.OpenAPI.WorldStorage.Capability> capabilities = new List<ETSI.ARF.OpenAPI.WorldStorage.Capability>();
             foreach (ETSI.ARF.OpenAPI.WorldAnalysis.Capability capability in currentCapabilities)
             {
-                capabilities.Add(WorldAnalysisUnityHelper.ConvertWorldAnalysisCapability(capability));
+                if(capability != null)
+                {
+                    capabilities.Add(WorldAnalysisUnityHelper.ConvertWorldAnalysisCapability(capability));
+                }
             }
 
             /// Collect relocalization information
@@ -544,7 +567,7 @@ public class WorldAnalysisARFoundation : MonoBehaviour, WorldAnalysisInterface
     /// <returns></returns>
     private Guid SelectTrackableWithTypeAndDistance(List<RelocObjects> candidates, List<KeyValuePair<string, Guid>> typesSortedList, UnityEngine.Vector3 cameraTransformPos)
     {
-        float bestDistance = 100.0f; //valeur par d�faut  = grande distance (� changer)
+        float bestDistance = 10000000.0f; //default value : long distance (to change ?)
         Guid selectedTrackableUUID = Guid.Empty;
 
         //if only one trackable is candidate
@@ -602,7 +625,6 @@ public class WorldAnalysisARFoundation : MonoBehaviour, WorldAnalysisInterface
                 }
             }
         }
-
         return selectedTrackableUUID;
     }
 
diff --git a/Runtime/Scripts/WorldAnalysisARFoundationModuleARCoreAnchor.cs b/Runtime/Scripts/WorldAnalysisARFoundationModuleARCoreAnchor.cs
index 9bd2d3e..1d5fb50 100644
--- a/Runtime/Scripts/WorldAnalysisARFoundationModuleARCoreAnchor.cs
+++ b/Runtime/Scripts/WorldAnalysisARFoundationModuleARCoreAnchor.cs
@@ -30,9 +30,13 @@ public class WorldAnalysisARFoundationModuleARCoreAnchor : WorldAnalysisARFounda
     public void Initialize()
     {
         XROrigin origin = UnityEngine.Object.FindAnyObjectByType<XROrigin>();
-        m_anchorManager = origin.gameObject.AddComponent<ARAnchorManager>();
-        GameObject anchorPrefab = (GameObject)Resources.Load("ARFAnchorTrackingPrefab");
-        m_anchorManager.anchorPrefab = anchorPrefab;
+        m_anchorManager = origin.gameObject.GetComponent<ARAnchorManager>();
+        if (m_anchorManager == null )
+        {
+            m_anchorManager = origin.gameObject.AddComponent<ARAnchorManager>();
+            GameObject anchorPrefab = (GameObject)Resources.Load("ARFAnchorTrackingPrefab");
+            m_anchorManager.anchorPrefab = anchorPrefab;
+        }
         m_anchorManager.anchorsChanged += OnTrackedCloudAnchorChanged;
     }
 
diff --git a/Runtime/Scripts/WorldAnalysisARFoundationModuleGeospatial.cs b/Runtime/Scripts/WorldAnalysisARFoundationModuleGeospatial.cs
new file mode 100644
index 0000000..d57391e
--- /dev/null
+++ b/Runtime/Scripts/WorldAnalysisARFoundationModuleGeospatial.cs
@@ -0,0 +1,185 @@
+#if UNITY_ANDROID && ETSIARF_ARCORE_EXTENSIONS
+using System;
+using System.Collections.Generic;
+using Google.XR.ARCoreExtensions;
+using Unity.XR.CoreUtils;
+using UnityEngine;
+using UnityEngine.XR.ARFoundation;
+using UnityEngine.XR.ARSubsystems;
+using static WorldAnalysisARFoundationModule;
+
+public class WorldAnalysisARFoundationModuleGeospatial : WorldAnalysisARFoundationModule 
+{
+    /// <summary>
+    ///  Anchor manager
+    /// </summary>
+    private ARAnchorManager m_anchorManager;
+
+    private AREarthManager m_arEarthManager;
+
+    private Dictionary<string, TrackableInfo> m_trackedGeospatialAnchors = new Dictionary<string, TrackableInfo>();
+
+    private bool geospatialSupported = true;
+
+    /// Correspondance between local trackable and etsi uuid
+    /// </summary>
+    private Dictionary<string, string> m_localIdToEtsiId = new Dictionary<string, string>();
+
+    /// <summary>
+    /// Initialize ARCore geospatial anchors tracking module
+    /// </summary>
+    public void Initialize()
+    {
+        XROrigin origin = UnityEngine.Object.FindAnyObjectByType<XROrigin>();
+        m_anchorManager = origin.gameObject.GetComponent<ARAnchorManager>();
+        if (m_anchorManager == null)
+        {
+            m_anchorManager = origin.gameObject.AddComponent<ARAnchorManager>();
+            GameObject anchorPrefab = (GameObject)Resources.Load("ARFAnchorTrackingPrefab");
+            m_anchorManager.anchorPrefab = anchorPrefab;
+        }
+        m_arEarthManager = origin.gameObject.AddComponent<AREarthManager>();
+        m_anchorManager.anchorsChanged += OnTrackedGeospatialAnchorChanged;   
+    }
+
+    /// <summary>
+    /// 
+    /// </summary>
+    /// <param name="uuid"></param>
+    /// <returns></returns>
+    public TrackableInfo GetPoseTrackable(Guid uuid)
+    {
+        if (m_trackedGeospatialAnchors.ContainsKey(uuid.ToString()))
+        {
+            return m_trackedGeospatialAnchors[uuid.ToString()];
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    /// <summary>
+    /// 
+    /// </summary>
+    /// <param name="trackable"></param>
+    /// <returns></returns>
+    public bool AddTrackable(ETSI.ARF.OpenAPI.WorldStorage.Trackable trackable)
+    {
+        if (!geospatialSupported) return false;
+
+        if (trackable.TrackableType != ETSI.ARF.OpenAPI.WorldStorage.TrackableType.GEOPOSE)
+        {
+            return false;
+        }
+        CreateGeosptialAnchor(trackable);
+  
+        return true;
+    }
+
+
+    public async void CreateGeosptialAnchor(ETSI.ARF.OpenAPI.WorldStorage.Trackable trackable)
+    {
+        if (geospatialSupported)
+        {
+            while (ARSession.state == ARSessionState.CheckingAvailability || ARSession.state == ARSessionState.None || ARSession.state == ARSessionState.SessionInitializing || m_arEarthManager.EarthState != EarthState.Enabled || m_arEarthManager.EarthTrackingState != TrackingState.Tracking)
+            {
+                await System.Threading.Tasks.Task.Delay(100);
+            }
+            if (m_arEarthManager.IsGeospatialModeSupported(GeospatialMode.Enabled) != FeatureSupported.Supported)
+            {
+                Debug.Log("Support : " + m_arEarthManager.IsGeospatialModeSupported(GeospatialMode.Enabled));
+                geospatialSupported = false;
+            }
+            else
+            {
+                double[] values = new double[trackable.TrackablePayload.Length / sizeof(double)];
+
+                for (int i = 0; i < values.Length; i++)
+                {
+                    values[i] = BitConverter.ToDouble(trackable.TrackablePayload, i * sizeof(double));
+                }
+
+                Quaternion rotation = Quaternion.Euler((float)values[3], (float)values[4], (float)values[5]);
+
+                if (m_arEarthManager.EarthTrackingState == TrackingState.Tracking)
+                {
+                    var anchor = m_anchorManager.AddAnchor(values[0], values[1], values[2], rotation);
+                    m_localIdToEtsiId.Add(anchor.trackableId.subId2.ToString("X16"), trackable.UUID.ToString());
+                }
+            }
+        }
+    }
+
+
+    /// <summary>
+    /// 
+    /// </summary>
+    /// <returns></returns>
+    public ETSI.ARF.OpenAPI.WorldAnalysis.Capability GetSupportedCapability() //s'occuper de ça (fonction doc)
+    {
+        if (!geospatialSupported) return null;
+
+        ETSI.ARF.OpenAPI.WorldAnalysis.Capability capabilityGeospatialAnchor = new ETSI.ARF.OpenAPI.WorldAnalysis.Capability();
+        capabilityGeospatialAnchor.TrackableType = ETSI.ARF.OpenAPI.WorldAnalysis.TrackableType.GEOPOSE;
+        ETSI.ARF.OpenAPI.WorldAnalysis.EncodingInformationStructure encodingInformation = new ETSI.ARF.OpenAPI.WorldAnalysis.EncodingInformationStructure();
+        encodingInformation.DataFormat = ETSI.ARF.OpenAPI.WorldAnalysis.EncodingInformationStructureDataFormat.ARCORE;
+        encodingInformation.Version = "1.01";
+        capabilityGeospatialAnchor.EncodingInformation = encodingInformation;
+        capabilityGeospatialAnchor.Framerate = 30; // Not particularly specified on ARKit and ARCore
+        capabilityGeospatialAnchor.Latency = 0; // Not particularly specified on ARKit and ARCore
+        capabilityGeospatialAnchor.Accuracy = 1; // Not particularly specified on ARKit and ARCore
+        return capabilityGeospatialAnchor;
+    }
+
+    /// <summary>
+    /// 
+    /// </summary>
+    /// <param name="eventArgs"></param>
+    private void OnTrackedGeospatialAnchorChanged(ARAnchorsChangedEventArgs eventArgs)
+    {
+        foreach (var trackedGeospatialAnchor in eventArgs.updated)
+        {
+            Vector3 position = trackedGeospatialAnchor.transform.position;
+            Quaternion rotation = trackedGeospatialAnchor.transform.rotation;
+
+            TrackableInfo info = new TrackableInfo();
+            info.name = trackedGeospatialAnchor.name;
+            string localId = trackedGeospatialAnchor.trackableId.subId1.ToString("X16");// there must be a better way : does it work every time?
+
+            if (m_localIdToEtsiId.ContainsKey(localId))
+            {
+                info.name = m_localIdToEtsiId[localId];
+            }
+            else
+            {
+                Debug.Log("ARCore Geospatial Anchor No correspondance for Local Anchor " + localId);
+                continue;
+            }
+
+            if (trackedGeospatialAnchor.trackingState == TrackingState.Tracking)
+            {
+                info.state = ETSI.ARF.OpenAPI.WorldAnalysis.PoseEstimationState.OK;
+                info.confidence = 100;
+            }
+            else if (trackedGeospatialAnchor.trackingState == TrackingState.Limited)
+            {
+                info.state = ETSI.ARF.OpenAPI.WorldAnalysis.PoseEstimationState.OK;
+                info.confidence = 50;
+            }
+            else
+            {
+                info.state = ETSI.ARF.OpenAPI.WorldAnalysis.PoseEstimationState.FAILURE; //ADD NOT_TRACKED ?
+                info.confidence = 0;
+            }
+            info.timeStamp = unchecked((int)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
+            info.position = position;
+            info.rotation = rotation;
+            info.trackableType = ETSI.ARF.OpenAPI.WorldAnalysis.TrackableType.GEOPOSE;
+                       
+            m_trackedGeospatialAnchors[info.name] = info;
+        }
+    }
+}
+
+#endif
\ No newline at end of file
-- 
GitLab