-
Notifications
You must be signed in to change notification settings - Fork 72
/
Copy pathSpatialAnchorsSample.cs
356 lines (302 loc) · 15.6 KB
/
SpatialAnchorsSample.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.Azure.SpatialAnchors; // for SessionUpdatedEventArgs
using Microsoft.Azure.SpatialAnchors.Unity; // for SpatialAnchorManager
using Microsoft.MixedReality.OpenXR.Sample; // for PersistableAnchorVisuals
using System; // for Enum
using System.Collections.Generic; // for List
using System.Threading.Tasks; // for Task
using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.ARFoundation;
namespace Microsoft.MixedReality.OpenXR.ASASample
{
[RequireComponent(typeof(ARAnchorManager))]
[RequireComponent(typeof(SpatialAnchorManager))]
public class SpatialAnchorsSample : MonoBehaviour
{
private bool[] m_wasHandAirTapping = { false, false };
[SerializeField]
[Tooltip("The prefab used to represent an anchored object.")]
private GameObject m_sampleSpatialAnchorPrefab = null;
/// <summary>
/// Used to manage Azure Spatial Anchor sessions and queries in a Unity scene
/// </summary>
private SpatialAnchorManager m_cloudSpatialAnchorManager;
/// <summary>
/// The SpatialAnchorManager must have its session created after the Start() method.
/// Instead, we create it if it has not been created while starting the session.
/// </summary>
private bool m_cloudSpatialAnchorManagerSessionCreated = false;
/// <summary>
/// All anchor GameObjects found by or manually created in this demo. Used to ensure these are all
/// destroyed when the session is stopped.
/// </summary>
private List<GameObject> m_foundOrCreatedAnchorObjects = new List<GameObject>();
/// <summary>
/// If a new cloud session is started after we have previously saved cloud anchors, this spatial
/// anchor watcher will rediscover them.
/// </summary>
private CloudSpatialAnchorWatcher m_cloudSpatialAnchorWatcher;
/// <summary>
/// The Ids of all cloud spatial anchors created during this demo. Used to when creating a watcher
/// to re-discover these anchors after stopping and starting the cloud session.
/// </summary>
private List<string> m_cloudSpatialAnchorIDs = new List<string>();
/// <summary>
/// Setup references to other components on this GameObject.
/// </summary>
private void Awake()
{
m_cloudSpatialAnchorManager = GetComponent<SpatialAnchorManager>();
}
/// <summary>
/// Ensure this sample scene is properly configured, then create the spatial anchor manager session.
/// </summary>
private void Start()
{
// Ensure the ARAnchorManager is properly setup for this sample
ARAnchorManager arAnchorManager = GetComponent<ARAnchorManager>();
if (!arAnchorManager.enabled || arAnchorManager.subsystem == null)
{
Debug.LogError($"ARAnchorManager not enabled or available; sample anchor functionality will not be enabled.");
return;
}
// Ensure anchor prefabs are properly setup for this sample
if (arAnchorManager.anchorPrefab != null)
{
// When using ASA, ARAnchors are managed internally and should not be instantiated with custom behaviors
Debug.LogError("The anchor prefab for ARAnchorManager must be set to null.");
return;
}
if (m_sampleSpatialAnchorPrefab == null)
{
// Since the ARAnchorManager cannot have a prefab set, this script handles anchor prefab instantiation instead
Debug.LogError($"{nameof(m_sampleSpatialAnchorPrefab)} reference has not been set. Make sure it has been added to the scene and wired up to {this.name}.");
return;
}
// Ensure the SpatialAnchorManager is properly setup for this sample
if (string.IsNullOrWhiteSpace(m_cloudSpatialAnchorManager.SpatialAnchorsAccountId) ||
string.IsNullOrWhiteSpace(m_cloudSpatialAnchorManager.SpatialAnchorsAccountKey) ||
string.IsNullOrWhiteSpace(m_cloudSpatialAnchorManager.SpatialAnchorsAccountDomain))
{
Debug.LogError($"{nameof(SpatialAnchorManager.SpatialAnchorsAccountId)}, {nameof(SpatialAnchorManager.SpatialAnchorsAccountKey)} and {nameof(SpatialAnchorManager.SpatialAnchorsAccountDomain)} must be set on {nameof(SpatialAnchorManager)}");
return;
}
m_cloudSpatialAnchorManager.LogDebug += (sender, args) => Debug.Log($"Debug: {args.Message}");
m_cloudSpatialAnchorManager.Error += (sender, args) => Debug.LogError($"Error: {args.ErrorMessage}");
m_cloudSpatialAnchorManager.AnchorLocated += SpatialAnchorManagerAnchorLocated;
m_cloudSpatialAnchorManager.LocateAnchorsCompleted += (sender, args) => Debug.Log("Locate anchors completed!");
Debug.Log($"Tap the button below to create and start the session.");
}
/// <summary>
/// Check for any air taps from either hand.
/// </summary>
private void Update()
{
for (int i = 0; i < 2; i++)
{
InputDevice device = InputDevices.GetDeviceAtXRNode((i == 0) ? XRNode.RightHand : XRNode.LeftHand);
bool isTapping;
if (!device.TryGetFeatureValue(CommonUsages.primaryButton, out isTapping))
{
continue;
}
if (isTapping && !m_wasHandAirTapping[i])
{
OnAirTapped(device);
}
m_wasHandAirTapping[i] = isTapping;
}
}
/// <summary>
/// When an air tap is detected, check for a nearby anchor. If there is an anchor nearby, we save
/// it to or delete it from the cloud. If there is no anchor nearby, we create a new local anchor.
/// </summary>
private void OnAirTapped(InputDevice device)
{
if (m_cloudSpatialAnchorManager == null || !m_cloudSpatialAnchorManager.IsSessionStarted)
{
return;
}
Vector3 position;
if (!device.TryGetFeatureValue(CommonUsages.devicePosition, out position))
{
return;
}
// First, check if there is a nearby anchor to save/delete
GameObject nearestAnchor = FindNearestObject(m_foundOrCreatedAnchorObjects, position, 0.1f);
if (nearestAnchor != null)
{
if (nearestAnchor.GetComponent<PersistableAnchorVisuals>().Persisted)
{
DeleteAnchorFromCloudAsync(nearestAnchor);
}
else
{
SaveAnchorToCloudAsync(nearestAnchor);
}
return;
}
// If there's no anchor nearby, create a new local one
Vector3 headPosition;
if (!InputDevices.GetDeviceAtXRNode(XRNode.Head).TryGetFeatureValue(CommonUsages.devicePosition, out headPosition))
headPosition = Vector3.zero;
Debug.Log($"Creating new local anchor: {position:F3}");
Quaternion orientationTowardsHead = Quaternion.LookRotation(position - headPosition, Vector3.up);
GameObject newGameObject = Instantiate(m_sampleSpatialAnchorPrefab, position, orientationTowardsHead);
m_foundOrCreatedAnchorObjects.Add(newGameObject);
}
/// <summary>
/// Find the nearest gameObject to a position from a list of gameObjects, within an acceptable maximumDistance.
/// </summary>
private GameObject FindNearestObject(List<GameObject> gameObjects, Vector3 position, float maximumDistance)
{
GameObject nearestObject = null;
float nearestObjectDist = maximumDistance;
foreach (var gameObject in gameObjects)
{
float dist = Vector3.Distance(position, gameObject.transform.position);
if (dist < nearestObjectDist)
{
nearestObject = gameObject;
nearestObjectDist = dist;
}
}
return nearestObject;
}
public async void OnStartSessionButtonPressed() => await StartSessionAsync();
public void OnStopSessionButtonPressed() => StopAndCleanupSession();
private void OnDestroy() => StopAndCleanupSession(); // When this sample is destroyed, ensure the session is cleaned up.
/// <summary>
/// Start the session and begin searching for anchors previously saved.
/// </summary>
private async Task StartSessionAsync()
{
// CreateSessionAsync cannot be called during Start(), since the SpatialAnchorManager may not have Start()ed yet itself.
// Instead, we ensure the session is created before we start it.
if (!m_cloudSpatialAnchorManagerSessionCreated)
{
await m_cloudSpatialAnchorManager.CreateSessionAsync();
m_cloudSpatialAnchorManagerSessionCreated = true;
}
if (m_cloudSpatialAnchorManager.IsSessionStarted)
{
Debug.LogWarning("Cannot start session; session already started.");
return;
}
// Start the session
Debug.Log("Starting session...");
await m_cloudSpatialAnchorManager.StartSessionAsync();
Debug.Log($"Session started! Air tap to create a local anchor.");
// And create the watcher to look for any created anchors
if (m_cloudSpatialAnchorIDs.Count > 0)
{
Debug.Log($"Creating watcher to look for {m_cloudSpatialAnchorIDs.Count} spatial anchors");
AnchorLocateCriteria anchorLocateCriteria = new AnchorLocateCriteria();
anchorLocateCriteria.Identifiers = m_cloudSpatialAnchorIDs.ToArray();
m_cloudSpatialAnchorWatcher = m_cloudSpatialAnchorManager.Session.CreateWatcher(anchorLocateCriteria);
Debug.Log($"Watcher created!");
}
}
/// <summary>
/// Stop the session and cleanup any anchors created.
/// </summary>
private void StopAndCleanupSession()
{
// Clean up the sample scenario, destroying any anchors
foreach (GameObject gameObject in m_foundOrCreatedAnchorObjects)
{
Destroy(gameObject);
}
m_foundOrCreatedAnchorObjects.Clear();
// Stop the session
if (!m_cloudSpatialAnchorManager.IsSessionStarted)
{
Debug.LogWarning("Cannot stop session; session has not started.");
return;
}
Debug.Log("Stopping session...");
m_cloudSpatialAnchorManager.StopSession();
m_cloudSpatialAnchorManager.Session.Reset();
// Stop the watcher, if it exists
if (m_cloudSpatialAnchorWatcher != null)
{
m_cloudSpatialAnchorWatcher.Stop();
m_cloudSpatialAnchorWatcher = null;
}
Debug.Log("Session stopped!");
}
/// <summary>
/// For each anchor located by the spatial anchor manager, instantiate and setup a corresponding GameObject.
/// </summary>
private void SpatialAnchorManagerAnchorLocated(object sender, AnchorLocatedEventArgs args)
{
Debug.Log($"Anchor recognized as a possible anchor {args.Identifier} {args.Status}");
if (args.Status == LocateAnchorStatus.Located)
{
UnityDispatcher.InvokeOnAppThread(() =>
{
CloudSpatialAnchor cloudSpatialAnchor = args.Anchor;
Pose anchorPose = cloudSpatialAnchor.GetPose();
// The method CloudToNative below adds a new ARAnchor component to the GameObject,
// replacing any existing ARAnchor component. Scripts on prefabs intended for use
// with ASA should wait until Start() to obtain references to ARAnchor components.
GameObject anchorGameObject = Instantiate(m_sampleSpatialAnchorPrefab, anchorPose.position, anchorPose.rotation);
anchorGameObject.AddComponent<CloudNativeAnchor>().CloudToNative(cloudSpatialAnchor);
anchorGameObject.GetComponent<PersistableAnchorVisuals>().Name = cloudSpatialAnchor.Identifier;
anchorGameObject.GetComponent<PersistableAnchorVisuals>().Persisted = true;
m_foundOrCreatedAnchorObjects.Add(anchorGameObject);
});
}
}
/// <summary>
/// Save a local ARAnchor as a cloud anchor.
/// </summary>
private async void SaveAnchorToCloudAsync(GameObject gameObject)
{
CloudNativeAnchor cloudNativeAnchor = gameObject.AddComponent<CloudNativeAnchor>();
await cloudNativeAnchor.NativeToCloud();
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
// In this sample app, the cloud anchors are deleted explicitly.
// Here, we show how to set an anchor to expire automatically.
cloudSpatialAnchor.Expiration = DateTimeOffset.Now.AddDays(7);
while (!m_cloudSpatialAnchorManager.IsReadyForCreate)
{
await Task.Delay(1000);
float createProgress = m_cloudSpatialAnchorManager.SessionStatus.RecommendedForCreateProgress;
Debug.Log($"Move your device to capture more environment data: {createProgress:0%}");
}
// Now that the cloud spatial anchor has been prepared, we can try the actual save here
Debug.Log($"Saving cloud anchor...");
await m_cloudSpatialAnchorManager.CreateAnchorAsync(cloudSpatialAnchor);
bool saveSucceeded = cloudSpatialAnchor != null;
if (!saveSucceeded)
{
Debug.LogError("Failed to save, but no exception was thrown.");
return;
}
m_cloudSpatialAnchorIDs.Add(cloudSpatialAnchor.Identifier);
Debug.Log($"Saved cloud anchor: {cloudSpatialAnchor.Identifier}");
// Update the visuals of the gameobject
gameObject.GetComponent<PersistableAnchorVisuals>().Name = cloudSpatialAnchor.Identifier;
gameObject.GetComponent<PersistableAnchorVisuals>().Persisted = true;
}
/// <summary>
/// Delete the cloud anchor corresponding to a local ARAnchor.
/// </summary>
private async void DeleteAnchorFromCloudAsync(GameObject gameObject)
{
CloudNativeAnchor cloudNativeAnchor = gameObject.GetComponent<CloudNativeAnchor>();
CloudSpatialAnchor cloudSpatialAnchor = cloudNativeAnchor.CloudAnchor;
Debug.Log($"Deleting cloud anchor: {cloudSpatialAnchor.Identifier}");
await m_cloudSpatialAnchorManager.DeleteAnchorAsync(cloudSpatialAnchor);
m_cloudSpatialAnchorIDs.Remove(cloudSpatialAnchor.Identifier);
Destroy(cloudNativeAnchor);
Debug.Log($"Cloud anchor deleted!");
// Update the visuals of the gameobject
gameObject.GetComponent<PersistableAnchorVisuals>().Name = "";
gameObject.GetComponent<PersistableAnchorVisuals>().Persisted = false;
}
}
}