Shared space

The HoloLens uses a concept known as spatial anchors to map out the environment. These anchors are reference points used by the device to build a coordinate system and used to position holograms. Using multiple anchors allows the device to track large areas. These anchors can also be persisted and shared; sharing an anchor, along with its supporting understanding of the environment, is what we will cover in this section. 

Just to recap, our goal is to allow multiple users to review a design outside the screen. To enable this, we need to have a single user place the hologram and share this location with other users that join, such that all users are viewing the hologram from the same place. 

We are already placing the hologram, so our first task will be extending this such that when the hologram is initially placed, we attach a WorldAnchor to it and, once attached, export and transfer the data to the BlenderLIVE service where it can be shared with subsequent users that join. After finishing this, we will look at how we can import an existing WorldAnchor to automatically place the hologram. 

Jump into the SceneManager, navigate to the PlacementManager_OnObjectPlaced method, and make the following amendments:

  private void PlacementManager_OnObjectPlaced(BaseBlenderGameObject bgo)
{
SceneStatus.Instance.SetText("");

BaseBlenderGameObject.Anchored onAnchoredHandler = null;
onAnchoredHandler = (BaseBlenderGameObject caller, byte[] data) =>
{
caller.OnAnchored -= onAnchoredHandler;
BlenderServiceManager.Instance.SendBlenderGameObjectWorldAnchor(caller, data);
};

bgo.OnAnchored += onAnchoredHandler;
bgo.AnchorAtPosition(bgo.transform.position);
}

Creating an anchor in Unity is simply a matter of attaching a WorldAnchor component to the GameObject you want to anchor, which we will do via the AnchorAtPosition method next. Our main intention in creating an anchor is to create a shared space. For this, we need to export and share it. This is an asynchronous task, which we will implement shortly in the AnchoredBlenderGameObject class. Once exported, we will fire the OnAnchored event, which we are registering here, passing the serialized data. Once received, we pass it to the BlenderLIVE service so that it can be shared with the other users that join our session.

Open the script AnchoredBlenderGameObject, navigate to the comment // TODO Implement exporting the WorldAnchor within the AnchorAtPosition method, and make the following amendments:

 public override bool AnchorAtPosition(Vector3 position)
{
if (exporting || importing)
{
return false;
}

if (worldAnchor)
{
Destroy(worldAnchor);
}

gameObject.transform.position = position;

#if WINDOWS_UWP

worldAnchor = gameObject.AddComponent<WorldAnchor>();

if (worldAnchor.isLocated)
{
StartExportingWorldAnchor();
}
else
{
worldAnchor.OnTrackingChanged += WorldAnchor_OnTrackingChanged;
}

exporting = true;

#endif

return true;
}

As mentioned earlier, in Unity creating an anchor is as simple as adding the WorldAnchor component to the GameObject. After attaching the WorldAnchor, we determine whether the anchor is located or not; in other words, is HoloLens currently tracking the object or not. If so, we call StartExportingWorldAnchor which, as the name suggests, takes care of exporting the WorldAnchor. If not located, we register for the OnTrackingChanged events and patiently wait for the anchor to be tracked before calling StartExportingWorldAnchor to kick off the process. Let's now implement the WorldAnchor_OnTrackingChanged method. Add the following method to the AnchoredBlenderGameObject class:

private void WorldAnchor_OnTrackingChanged(WorldAnchor self, bool located)
{
if (located)
{
worldAnchor.OnTrackingChanged -= WorldAnchor_OnTrackingChanged;
StartExportingWorldAnchor();
}
}

An WorldAnchor is a serialized description of a specific point in space, including supporting environment data; anchors can be fairly large packets of data. To export, we call the ExportAsync method of the WorldAnchorTransferBatch. As the name suggests, the exporting takes places asynchronously using the anchor (or anchors) batched inside the WorldAnchorTransferBatch class along with two delegates that are notified as data becomes available and when the task is finished. The serialized batched data is passed as chunks to the SerializationDataAvailableDelegate handler; here, we will cache each chunk and, once finished, pass the full array of data to the OnAnchored event. For caching, we will simply use a generic list using bytes arrays as the generic. Let's add this variable now to the AnchoredBlenderGameObject class: 

    private List<byte> worldAnchorBuffer = new List<byte>();

And now, let's implement the method responsible for preparing and starting the export. Add the following to the StartExportingWorldAnchor method: 

private void StartExportingWorldAnchor()
{
if (worldAnchor == null)
{
worldAnchor = gameObject.GetComponent<WorldAnchor>();
}

if (worldAnchor == null)
{
return;
}

worldAnchorBuffer.Clear();

WorldAnchorTransferBatch transferBatch = new
WorldAnchorTransferBatch();
transferBatch.AddWorldAnchor(gameObject.name, worldAnchor);
WorldAnchorTransferBatch.ExportAsync(transferBatch,
OnExportDataAvailable, OnExportComplete);
}

Here, we are clearing our buffer of any previous exports and creating WorldAnchorTransferBatch, a structure simply used to bundle WorldAnchors together for exporting. We add the WorldAnchor we want to export using the AddWorldAnchor method and being exported by calling ExportAsync, passing in our batch and handlers, currently missing at present. Let's fix that now by adding both to our AnchoredBlenderGameObject class, starting with the OnExportDataAvailable method: 

 private void OnExportDataAvailable(byte[] data)
{
worldAnchorBuffer.AddRange(data);
}

As mentioned earlier, WorldAnchorTransferBatch will pass through chunks of the serialised anchor. Here, we are simply caching them in our worldAnchorBuffer list. 

The SerializationCompleteDelegate handler is called once exporting is complete. Complete here means having finished the task rather than having successfully exported the WorldAnchor. It encodes the result of the export using the enumeration SerializationCompletionReason. If its value is equal to Succeeded, then we know we have successfully exported the anchor; otherwise, we need to handle the exception (omitted in this example as with most exception handling):

 private void OnExportComplete(SerializationCompletionReason  
completionReason)
{
exporting = false;

if (completionReason == SerializationCompletionReason.Succeeded)
{
RaiseOnAnchored(worldAnchorBuffer.ToArray());
}
else
{
// TODO: handle expectational case
}
}

Once we have successfully completed exporting, we call RaiseOnAnchored, passing along the cached data. This, in turn, will fire OnAnchored, which SceneManager is eagerly waiting for. 

This now completes the setting and exporting of the WorldAnchor, which other HoloLens users can import so everyone is working in a shared space. The following figure summarizes the main steps of the process: 

Our next task is to handle the other side, that is, importing. This occurs when we receive a Blender object with an existing WorldAnchor. Similar to exporting, the entry point of this process begins in SceneManager with the bulk of the logic encapsulated in the AnchoredBlenderGameObject class.

The process of storing and transferring this data is abstracted away in the BlenderServiceManager and associated classes; I encourage you to explore these to get a better understanding of how this is handled.  

There are two cases when we need to check whether a received Blender object has an associated anchor. The first time a Blender object is retrieved (created) as well as every time the object is updated. Starting with the creation use case, back in SceneManager, locate the method BlenderServiceManager_OnBlenderGameObjectCreated and make the following amendments:

 private void BSM_OnBlenderGameObjectCreated(BaseBlenderGameObject bgo)
{
if (bgo.IsAnchored)
{
SceneStatus.Instance.SetText("");

if (bgo.GetComponent<WorldAnchor>())
{
if (PlacementManager.Instance.RemoveObjectForPlacement(bgo.name))
{
SceneStatus.Instance.SetText("Blender object placed", MEDIUM);
}
}
}
else
{
SceneStatus.Instance.SetText("Blender object ready for placementnAir-tap on a suitable surface to place", MEDIUM);
PlacementManager.Instance.AddObjectForPlacement(bgo);
}
}

Previously, we were only concerned with passing the created object to PlacementManager. PlacementManager would then take responsibility for placing the object. This time, we first check whether the object has a WorldAnchor. If not, then we follow the same path as we had previously, but if the object does have a WorldAnchor, we first ensure that PlacementManager doesn't have the object in its queue by calling the method RemoveObjectForPlacement and then simply notifying the user that the object has been placed. Placement occurs when the associated WorldAnchor is imported and attached to the GameObject. This occurs when the serialised data transmitted from the BlenderLIVE service is bounded to the GameObject, which in turn calls the SetAnchor method of the AnchoredBlenderGameObject class, passing it the serialized WorldAnchor. It is here we will start with our next chunk of amendments. Make the following amendments to the SetAnchor method: 

 public override bool SetAnchor(byte[] data)
{
if (exporting || importing)
{
return false;
}

#if WINDOWS_UWP

importing = true;

worldAnchorBuffer.Clear();
worldAnchorBuffer.AddRange(data);

importAttempts = WorldAnchorImportAttempts;
WorldAnchorTransferBatch.ImportAsync(data, OnImportComplete);

#endif

return true;
}

We first check to see whether we are currently either importing or exporting. If so, we return; otherwise, we continue the importing process. Within this class, we have defined a constant called WorldAnchorImportAttempts and the importAttempts variable. The former defines the import attempts we will try before, essentially, giving up. The latter, importAttempts, is the current number of attempts remaining for the current import. The importing itself is abstracted away in the ImportAsync method of the WorldAnchorTransferBatch class. We simply need to pass it the serialized data and a callback delegate, which we will implement now:

 private void OnImportComplete(SerializationCompletionReason   
completionReason, WorldAnchorTransferBatch deserializedTransferBatch)
{
importing = false;

if (completionReason != SerializationCompletionReason.Succeeded)
{
if (importAttempts > 0)
{
importing = true;
importAttempts--;
WorldAnchorTransferBatch.ImportAsync(worldAnchorBuffer.ToArray(),
OnImportComplete);
}
return;
}

worldAnchor = deserializedTransferBatch.LockObject(gameObject.name,
gameObject);
}

First, we check to see whether the import was successful or not. If not, then we attempt it again until we have reached our threshold. Otherwise, we call the LockObject method of the WorldAnchorTransferBatch instance. We pass this method the name of the associated WorldAnchor and the GameObject we want be to anchored (or locked if we use their naming convention). 

We have one final amendment to make before we have extended our application across multiple devices; if you remember, I mentioned that WorldAnchor could be attached when BlenderGameObject was created or updated. Here, we have taking care of cases when it's created and, now, we will handle the case for when BlenderGameObject is updated, which occurs if two users are connected to the BlenderLIVE service when the object hasn't yet been placed.

As soon as one user places the object, the update method will be called on the other, passing along the newly created WorldAnchor. Jump back in the SceneManager class and make the following amendments to the BlenderServiceManager_OnBlenderGameObjectUpdated method:

 private void BSM_OnBlenderGameObjectUpdated(BaseBlenderGameObject bgo)
{
SceneStatus.Instance.SetText("");
if (bgo.IsAnchored)
{
if (PlacementManager.Instance.RemoveObjectForPlacement(bgo.name))
{
SceneStatus.Instance.SetText("Blender object placed", MEDIUM);
}
}
}

As we did with the BSM_OnBlenderGameObjectCreated method, we test whether an anchor exists and, if so, ensure that it is removed from the PlacementManager queue; and we finally, notify the user that the object has been placed.

With that block of code now finished, we have now extended Blender beyond the flat screen and into the real world, giving designers a better, more natural, forum to communicate and collaborate on. Grab a friend nearby with the device and build and deploy to test.

In the next section, we take advantage of spatial sound to make discovering the holograms easier. 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.142.146