If you are wondering how to persist your holograms across sessions, in a way that it was “anchored” into space and you can find it where you left it the last time you were in the app- then this is called spatial persistence.
I explained this the last time around with WorldAnchorManager for HoloLens1 in my older article: https://codeholo.com/2019/02/01/anchoring-your-gameobjects-in-hololens-apps/
Quite possibly you have tried this also with HoloLens2, but with not good results. Here is one way to do it which still works with the HoloLens2, however using WorldAnchorManager which is going to be made obsolete in future versions.
Along with the approach in the above article and the very cryptic notes from Microsoft here: https://docs.microsoft.com/en-us/windows/mixed-reality/develop/unity/persistence-in-unity , I was able to finally use the local anchor approach to save my holograms across sessions.
Why are there no tutorials/working examples from Microsoft you ask? So Microsoft is trying to push it’s own solution for persisting anchors across sessions which is cloud based: Azure spatial anchors I will also try it out and let you know my experience with them. The tutorials and other supporting examples and code is all encouraging us to go this way- however, if you are like me and don’t want all your assets on the cloud somewhere, then you’d prefer a more local approach as described in this article
Ok, let’s get started:
What we would do in this tutorial is use a simple object, like a cube to move around and when we are happy with it’s position, we will anchor it. Then when we close the app and restart it, we should see our cube in the last position we left it. Changing the cube’s position, will delete the previous anchor and re-save it newly.
Note: this article will also feature an update to the older article using ManipulationHandler replaced by the newer, ObjectManipulator
1. I have used Unity 2019.3.4f1 , we still use IL2CPP as Scripting Backend which is the recommended setup for HoloLens 2 apps
2. I have used MRTK V2.4.0 – just the Foundations package is sufficient
3. Create a new project
4. Apply default settings with the MRTK popup that appears
5. Click on Mixed Reality Toolkit at the top and click on Add to Scene and Configure. This will add the Mixed Reality essentials to the project such as the Playspace and the Toolkit.
6. On the MainCamera, change the Skybox to Solid Color
7. On the MixedRealityToolkit, use the MixedRealityToolkitConfigurationprofile which has SpatialAwareness automatically enabled for you. If you don’t want the white wireframe that appears in the app, clone the SpatialAwarenessSystemProfile and clone the MeshObserverProfile and choose None in Display Option.
I also turned off Diagnostics.
Now for a 3D object to manipulate:
I used a simple Cube
So the recommendation from Microsoft for anchoring objects well is to create a GameObject root with a world anchor and have children holograms anchored by it with a local position offset. Let’s do that.
Create a GameObject, call it Root (or anything you want) and drag the cube to it as a child object
Let’s add an ObjectManipulator to the Root. Search from the Assets folder or add it via the AddComponent
In the ObjectManipulator, change the TwoHandedManipulationType to just Move. We only need moving capability in this article.
Ok now we want a Debug panel to display our Anchor logs onto it else we go nuts with trying to figure out what happened with the anchors we are saving 😀
So I just used a NearMenu prefab in the MRTK and modified it a bit and wrote a Debug script to display the logs (I won’t go about explaining these so please refer to the git code here for the entire code)
Now for our AnchorScript:
First, have an empty GameObject in the scene. I named it Manager.
Add an empty script to it named AnchorScript or so.
Ok, AnchorScript needs to first have a reference to our RootGameObject (it’s for the easy understanding of our example, you can also save Anchors passing any GameObject you want at runtime). So create a public GameObject rootGameObject in the code to drag and drop the Root GameObject into the Inspector of the code
So first let’s focus on saving anchors first:
We need 3 steps for that:
– get the store
– check if that anchor is existing, if yes then delete it
– save on the new anchor in its place and save it to the store
In our Start() method, first we need to get our store, because this needs to be done earlier on.
This is the code for that:
where StoreLoaded is a method we write to get the WorldAnchorStore and we assign it to our local WorldAnchorStore in our code
I’m also setting a local bool value storeLoaded to true once we finish loading the store. This we will use later while loading the anchors from the store
So now, this is a very important concept to understand. Once you anchor an object, it won’t move. So the Manipulation scripts will have no effect after anchoring it the first time. So what we need to do is everytime we start the manipulation, we delete the existing anchor, we also delete the anchor from the store (trying to save without deleting will return this.savedRoot as false)
So writing a function, DeleteExistignAnchor() for this which does all of the above:
First we get a reference to the existing WorldAnchor on the rootGameObject. If this is null, i.e it was not anchored before, then we don’t do anything. Else we proceed to delete the anchor so that the object can move again and then delete the anchor ID from the store (in case we had earlier saved it with this id “root”)
We also reset the savedRoot to false else, our code in SaveAnchor() will not proceed with re-saving
For the Saving anchor part:
write another function SaveAnchor() which should be ideally called when the manipulation has ended
In this SaveAnchor(), first we get the anchor associated with the gameObject we want to manipulate, then check if it’s savedRoot is false and if there is an anchor (in case of replacing), else proceed onto to display that anchor was already saved
then, we use the WorldAnchorStore.Save to save the anchor to an ID (string, which we may have previously deleted to replace it, else will be saved first time with this)
let’s print it out to see if this.SavedRoot is actually true. If it is, then we have succesfully (re)saved the anchor.
Now on your ObjectManipulator script, assign these functions to be called when manipulation has started and ended respectively
Now for the LoadAnchors part:
We need 3 steps for that too:
– get the store
– load the store and it’s anchors(IDs will be loaded)
– load the anchor from it’s ID
so, before if you remember, we had saved the bool storeLoaded info. Now this is where it comes handy. Since the store loading may take a few seconds, we ideally do not want to start reading IDs from it before it is fully loaded. Therefore, in our Update() method, we check if this storeLoaded is true, and if yes, proceed to enumerate IDs.
After we finish loading the IDs, we can try to display them one by one – if we are looking for confirmation on what is existing in the store.
Then, the magic of showing anchors from previously saved sessions begins!
You see that we are calling LoadAnchor() after we enumerate the IDs in the store that we just got
What LoadAnchor() does is, gets the store to load the particular anchor ID we want. And then associate that with the GameObject we want.
if here, this.savedRoot returns false, then there was previously no load anchor of that name found in the store. That means we need to do this for the first time. And the store.Load also automatically adds an anchor to the GameObject in context.
Don’t forget to add the rootgameobject in the Inspector to AnchorScript.
Add the DebugText script to the Manager. Then drag and drop a TextMeshPro text onto it to show you the output at runtime.
In your publishing settings, don’t forget to check the SpatialPerception
Ok, that’s it. Now try this out in your device itself because this won’t give you the right results in the editor.
Errors and solutions:
Error: saved root is false
Solution: not exactly an error, but i’ts telling you that you are trying to save 2 anchors onto the same ID. Make sure you delete it before you save
Error: Object doesn’t move on anchoring first time
Solution: Again, not an error but an expected behaviour. You just anchored it, didn’t you? 🙂 Upon OnManipulationStarted(), Destroy the anchor first and then add new anchor component and then try to move.
Error: enumerating IDs happen before WorldAnchorStore is loaded
Solution: set a bool in the StoreLoaded method and check this in Update to make sure, enumerate IDs is called after store is loaded (thanks to the solution from Anders Lundgren: https://www.anderslundgren.se/creating-local-world-anchors-on-hololens-2/)
Error: Sometimes due to the MixedRealityProfile we are using, the Camera settings default to Skybox
Solution: Simply clone the existing Camera Profile and change the display settings to reflect Solid Color instead of Skybox