前言

注意:本文已更新到5.5.1f1版本号

本篇集中学习全息影像“共享”的功能,以实如今同一房间的人,看到“同一个物体”。之所以打引號,是由于。每一个人看到的并不是同一个物体,仅仅是空间位置等信息相同的同类物体而已。

要想实现这个效果,有以下几点须要注意:

  • 需开启设备的Spatial Perception功能(在Player Settings…面板的Publishing Settings > Capabilities中勾选)
  • 需开启设备的网络功能
  • 临时仅仅能两台以上真机測试,无法在Unity中測试(即便是Remoting连接Hololens也不行)
  • 设备在同一房间内(废话)

友情提醒:本章需在多台设备间折腾,把设备休眠时间设置得长一点。会方便非常多。详细方法例如以下:

设备打开。浏览器訪问设备IP,进入:Hololens Device PortalHome菜单下有个Sleep settings。最长设置30分钟。

要实现共享全息影像的效果,主要掌握以下技术点:

  • 使用Socket协议传递数据
  • 理解世界坐标系及空间锚点的使用(WorldAnchor及WorldAnchorStore)
  • Sharing组件的使用(锚点的上传和下载)

Chapter 1 - Unity Setup

  1. 请依照第一篇的教程,完毕项目的创建。
  2. 新建目录:”Assets/_Scenes/Holograms 240/”
  3. 新建场景:”Assets/_Scenes/Holograms 240/Holograms 240.unity
  4. 打开场景,删除默认的Main Camera
  5. 将”Assets/HoloToolkit/Input/Prefabs/HololensCamera.prefab”增加到Hierarchy根级
  6. 将”Assets/HoloToolkit/Input/Prefabs/InputManager.prefab”增加到Hierarchy根级
  7. 将”Assets/HoloToolkit/Input/Prefabs/Cursor/DefaultCursor.prefab”增加到Hierarchy根级
  8. Hierarchy面板根级,增加一个Cube,设置例如以下:

本节完毕!

Chapter 2 - 使用Socket协议传递数据

目标

使用HoloToolkit提供的Socket套件进行数据传输

实践

搭建Socket服务基础环境

首先要说明的是:HoloToolkit提供的Socket套件,使用的是RakNet。对其原理感兴趣的同学。能够去官网查看。

  1. 在下载的HoloToolkit-Unity开发包中。找到:”External\”目录,将其拷贝到项目目录下(与Assets目录同级目录)。

    如图:

  2. 点击Unity主菜单下的:HoloToolkit > Sharing Service > Launch Sharing Service,如图:

  3. 此时将会打开一个Socket服务端。如图所看到的。记录下IP。比如本例为:192.168.0.108

  4. Project面板中,找到:”Assets/HoloToolkit/Sharing/Prefabs/Sharing.prefab”,拖动到Hierarchy根级,并在其Inspector面板中找到Server Address属性,填写上面一步得到的IP地址。如图:

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVyaWNob3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="增加Sharing组件" title="">

    此步相当于为APP增加了一个Socketclient。

以上步骤完毕后,能够点击Playbutton,并观察Socket服务端界面,看是否有设备增加到服务器。

如图:


创建Socket消息传输类

上一步中,我们利用HoloToolkit提供的Socket套件。搭建了基础数据传输环境(包括一个Socket服务端程序和一个Socketclient连接组件)。以下用一个移动Cube的样例来学习怎样同步数据。

  1. 新建目录:”Assets/_Scenes/Holograms 240/Scripts/
  2. 新建脚本:”Assets/_Scenes/Holograms 240/Scripts/Cube240.cs”。附加给Cube,编写脚本例如以下:

    (代码适用:5.5.0f3版本号)

    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler { // 是否正在移动
    bool isMoving = false; // 单击Cube,切换是否移动
    public void OnInputClicked(InputEventData eventData)
    {
    isMoving = !isMoving;
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update () {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    }
    }
    }

    (代码适用:5.5.1f1版本号)

    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler
    {
    // 是否正在移动
    bool isMoving = false; // 单击Cube。切换是否移动
    public void OnInputClicked(InputClickedEventData eventData)
    {
    isMoving = !isMoving;
    } // 假设Cube为移动状态。让其放置在镜头前2米位置
    void Update()
    {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    }
    }
    }

    脚本实现了Cube的移动和放置。能够測试一下效果。

  3. 以下,我们来实现两台设备传递Cube的位置。

  4. Hierarchy面板。创建根级空对象,命名为:”Controller
  5. 建立一个消息传递类。

    新建脚本:”Assets/_Scenes/Holograms 240/Scripts/CustomMessages240.cs,附加给Controller,编辑内容例如以下:

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity;
    using System.Collections.Generic;
    using UnityEngine; public class CustomMessages240 : Singleton<CustomMessages240>
    {
    // 代表当前的Socket连接
    NetworkConnection serverConnection; // 当前连接的事件监听器,这是一个典型的适配器模式,继承自NetworkConnectionListener
    NetworkConnectionAdapter connectionAdapter; // 自己定义消息类型
    public enum CustomMessageID : byte
    {
    // 自己的消息从MessageID.UserMessageIDStart開始编号,避免与MessageID内置消息编号冲突
    // Cube位置消息
    CubePosition = MessageID.UserMessageIDStart,
    Max
    } // 消息处理代理
    public delegate void MessageCallback(NetworkInMessage msg); // 消息处理字典
    public Dictionary<CustomMessageID, MessageCallback> MessageHandlers { get; private set; } // 当前用户在Sorket服务器中的唯一编号(自己主动生成)
    public long LocalUserID { get; private set; } protected override void Awake()
    {
    base.Awake();
    // 初始化消息处理字典
    MessageHandlers = new Dictionary<CustomMessageID, MessageCallback>();
    for (byte index = (byte)MessageID.UserMessageIDStart; index < (byte)CustomMessageID.Max; index++)
    {
    if (!MessageHandlers.ContainsKey((CustomMessageID)index))
    {
    MessageHandlers.Add((CustomMessageID)index, null);
    }
    }
    } void Start () {
    // SharingStage是Sharing组件相应的脚本,内部是对经典的Socketclient的封装。 SharingStage.Instance.SharingManagerConnected += Instance_SharingManagerConnected;
    } private void Instance_SharingManagerConnected(object sender, System.EventArgs e)
    {
    // 初始化消息处理器
    InitializeMessageHandlers();
    } // 初始化消息处理器
    private void InitializeMessageHandlers()
    {
    SharingStage sharingStage = SharingStage.Instance; if (sharingStage == null)
    {
    return;
    } // 获取当前Socket连接
    serverConnection = sharingStage.Manager.GetServerConnection();
    if (serverConnection == null)
    {
    return;
    } // 初始化消息监听
    connectionAdapter = new NetworkConnectionAdapter();
    connectionAdapter.MessageReceivedCallback += ConnectionAdapter_MessageReceivedCallback; // 获取当前用户在Socket服务器中生成的唯一编号
    LocalUserID = sharingStage.Manager.GetLocalUser().GetID(); // 依据每一个自己定义消息。增加监听器
    for (byte index = (byte)MessageID.UserMessageIDStart; index < (byte)CustomMessageID.Max; index++)
    {
    serverConnection.AddListener(index, connectionAdapter);
    }
    } // 接收到服务器端消息的回调处理
    private void ConnectionAdapter_MessageReceivedCallback(NetworkConnection connection, NetworkInMessage msg)
    {
    byte messageType = msg.ReadByte();
    MessageCallback messageHandler = MessageHandlers[(CustomMessageID)messageType];
    if (messageHandler != null)
    {
    messageHandler(msg);
    }
    } protected override void OnDestroy()
    {
    if (serverConnection != null)
    {
    for (byte index = (byte)MessageID.UserMessageIDStart; index < (byte)CustomMessageID.Max; index++)
    {
    serverConnection.RemoveListener(index, connectionAdapter);
    }
    connectionAdapter.MessageReceivedCallback -= ConnectionAdapter_MessageReceivedCallback;
    }
    base.OnDestroy();
    } // 创建一个Out消息(client传递给服务端)
    // 消息格式第一个必须为消息类型,其后再增加自己的数据
    // 我们在全部的消息一開始增加消息发送的用户编号
    private NetworkOutMessage CreateMessage(byte messageType)
    {
    NetworkOutMessage msg = serverConnection.CreateMessage(messageType);
    msg.Write(messageType);
    msg.Write(LocalUserID);
    return msg;
    } // 将Cube位置广播给其它用户
    public void SendCubePosition(Vector3 position)
    {
    if (serverConnection != null && serverConnection.IsConnected())
    {
    // 将Cube的位置写入消息
    NetworkOutMessage msg = CreateMessage((byte)CustomMessageID.CubePosition); msg.Write(position.x);
    msg.Write(position.y);
    msg.Write(position.z); // 将消息广播给其它人
    serverConnection.Broadcast(msg,
    MessagePriority.Immediate, //马上发送
    MessageReliability.ReliableOrdered, //可靠排序数据包
    MessageChannel.Default); // 默认频道
    }
    } // 读取Cube的位置
    public static Vector3 ReadCubePosition(NetworkInMessage msg)
    {
    // 读取用户编号,但不使用
    msg.ReadInt64(); // 依次读取XYZ。这个和发送Cube时。写入參数顺序是一致的
    return new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());
    }
    }
  6. 改动Cube240.cs,内容例如以下:

    (代码适用:5.5.0f3版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler { // 是否正在移动
    bool isMoving = false; // 消息传递类
    CustomMessages240 customMessage; private void Start()
    {
    customMessage = CustomMessages240.Instance; // 指定收到Cube位置消息后的处理方法
    customMessage.MessageHandlers[CustomMessages240.CustomMessageID.CubePosition] = OnCubePositionReceived;
    } private void OnCubePositionReceived(NetworkInMessage msg)
    {
    // 同步Cube位置
    if (!isMoving)
    {
    transform.position = CustomMessages240.ReadCubePosition(msg);
    }
    } // 单击Cube。切换是否移动
    public void OnInputClicked(InputEventData eventData)
    {
    isMoving = !isMoving;
    // 放置Cube后。发送Cube的位置消息给其它人
    if (!isMoving)
    {
    customMessage.SendCubePosition(transform.position);
    }
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update () {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    }
    }
    }

    (代码适用:5.5.1f1版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler
    { // 是否正在移动
    bool isMoving = false; // 消息传递类
    CustomMessages240 customMessage; private void Start()
    {
    customMessage = CustomMessages240.Instance; // 指定收到Cube位置消息后的处理方法
    customMessage.MessageHandlers[CustomMessages240.CustomMessageID.CubePosition] = OnCubePositionReceived;
    } private void OnCubePositionReceived(NetworkInMessage msg)
    {
    // 同步Cube位置
    if (!isMoving)
    {
    transform.position = CustomMessages240.ReadCubePosition(msg);
    }
    } // 单击Cube,切换是否移动
    public void OnInputClicked(InputClickedEventData eventData)
    {
    isMoving = !isMoving;
    // 放置Cube后。发送Cube的位置消息给其它人
    if (!isMoving)
    {
    customMessage.SendCubePosition(transform.position);
    }
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update()
    {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    }
    }
    }
  7. 公布到Hololens设备。启动。同一时候再点击UnityPlaybutton

当Hololens放置完Cube后。Play窗体中的Cube也会发生位置变化。反之亦然。


实时更新Cube的位置

我们仅仅需做少量改动,就能够实现实时传递Cube的位置。

  1. 找到文件”CustomMessages240.cs”的SendCubePosition方法(大概在124行的位置)。改动为:

    // 将Cube位置广播给其它用户
    public void SendCubePosition(Vector3 position, MessageReliability? reliability = MessageReliability.ReliableOrdered)
    {
    if (serverConnection != null && serverConnection.IsConnected())
    {
    // 将Cube的位置写入消息
    NetworkOutMessage msg = CreateMessage((byte)CustomMessageID.CubePosition); msg.Write(position.x);
    msg.Write(position.y);
    msg.Write(position.z); // 将消息广播给其它人
    serverConnection.Broadcast(msg,
    MessagePriority.Immediate, //马上发送
    reliability.Value, //可靠排序数据包
    MessageChannel.Default); // 默认频道
    }
    }
  2. 找到”Cube240.cs”文件的Update方法,改动为:

    // 假设Cube为移动状态。让其放置在镜头前2米位置
    void Update () {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    // 实时传递Cube位置
    customMessage.SendCubePosition(transform.position, MessageReliability.UnreliableSequenced);
    }
    }

再次測试,不论是移动还是放置Cube。两个client都能够实时看到Cube的位置变化。

大家注意到,在同步Cube实时移动时,使用了MessageReliability.UnreliableSequenced(不可靠序列数据包)。而在同步Cube放置时,使用了默认的MessageReliability.ReliableOrdered(可靠排序数据包)。是有原因的。

两种情况相应了两种不同场景,一种是高频的数据同步,第二种是低频的数据同步。

不同场景对消息的可靠性、消息传递序列也有不同的要求。详细请看以下《关于消息传递方式》的说明。

说明

  • 关于消息结构

    这里要注意的是,组装消息时所使用的数据结构和解析消息时所使用的数据结构须要保持一致。

    比方。本例中,组装Cube消息后的数据结构例如以下:

    1. 消息类型,在CreateMessage(byte messageType)方法中的msg.Write(messageType);
    2. 用户编号,在CreateMessage(byte messageType)方法中的msg.Write(LocalUserID);
    3. Cube的X坐标,在SendCubePosition(Vector3 position)方法中的msg.Write(position.x);
    4. Cube的Y坐标,在SendCubePosition(Vector3 position)方法中的msg.Write(position.y);
    5. Cube的Z坐标,在SendCubePosition(Vector3 position)方法中的msg.Write(position.z);

    相同,在解析消息时。也应该依照上面的顺序进行,例如以下:

    1. 消息类型,在ConnectionAdapter_MessageReceivedCallback(NetworkConnection connection, NetworkInMessage msg)方法中的byte messageType = msg.ReadByte();
    2. 用户编号,在ReadCubePosition(NetworkInMessage msg)方法中的msg.ReadInt64();
    3. Cube的X坐标,在ReadCubePosition(NetworkInMessage msg)方法中的”return new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());”
    4. Cube的Y坐标。在ReadCubePosition(NetworkInMessage msg)方法中的”return new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());”
    5. Cube的Z坐标,在ReadCubePosition(NetworkInMessage msg)方法中的”return new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat());”
  • 关于消息传递方式

    • MessageReliability.Reliable

      可靠数据包:数据一定到达。但包可能乱序。适用于开关button等相似场景。
    • MessageReliability.ReliableOrdered

      可靠排序数据包:数据一定到达,且经过排序。但须要等待传输最慢的包。适用于聊天等相似场景。

    • MessageReliability.ReliableSequenced

      可靠序列数据包:数据一定到达,且经过排序,不等待慢包。旧包被抛弃。适用于低频有顺序要求的场景。

      比方:每2000ms更新物体的位置。

    • MessageReliability.Unreliable

      不可靠数据包:数据不一定到达,包也可能乱序。适用于语音通话等相似场景。

    • MessageReliability.UnreliableSequenced

      不可靠序列数据包:数据不一定到达。但经过排序,不等待慢包,旧包被抛弃。适用于高频有顺序要求的场景。比方:每100ms更新物体的位置。

Chapter 3 - 空间锚点的使用

目标

实现固化物体到空间,实现仿真的“共享”物体效果

实践

上一章节中,我们尽管实现了Cube的数据同步,但由于每台设备启动后的參考坐标系不同,导致看到的Cube仍然是独立与设备的(对不齐)。所以,要实现仿真的“共享”效果,肯定须要同步设备的世界坐标系。这一章节。我们将会结合空间扫描、空间锚点,来调整Cube的位置,以实现高仿真的“共享”效果。

准备工作:

  • 需开启设备的Spatial Perception功能(在Player Settings…面板的Publishing Settings > Capabilities中勾选)
  • 两台Hololens
  • 设备在同一房间内

原理:

  1. 两台设备在同一房间开启空间扫描,得到基本一致的世界坐标參考系
  2. 当中一台设备在世界坐标系中设置一个锚点(坐标),并绑定到APP中的一个物体上(一般为一个根节点(0, 0, 0))。全部物体作为这个根节点的子集。

  3. 这台设备开设房间(事实上就是自己的世界坐标參考系。房间包括上面的锚点)。并将锚点上传至服务器
  4. 其它设备增加房间。并下载房间中的锚点信息
  5. 将锚点信息绑定到自己APP的根节点上(0, 0, 0)
  6. 之后通过上文提到的Socket技术,传递子集中的各种数据(比方:LocalPosition等)

详细实施

  1. Cube拖放到Controller上,作为子集
  2. Project面板中,找到”Assets/HoloToolkit/ShatialMapping/Prefabs/SpatialMapping.prefab”,拖放到Hierarchy根级
  3. 为了方便測试。我们放置一个文本,显示測试信息。

    将”Assets/HoloToolkit/Utilities/Prefabs/FPSDisplay”拖放到Hierarchy根级,点击FPSDisplay下的FPSText,去掉FPS Display脚本

  4. 新建脚本ImportExportAnchorManager240.cs,并附加给Controller,内容例如以下:

    (代码适用:5.5.0f3版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.VR.WSA.Persistence;
    using UnityEngine.VR.WSA.Sharing;
    using System;
    using UnityEngine.VR.WSA;
    using HoloToolkit.Unity.SpatialMapping; public class ImportExportAnchorManager240 : Singleton<ImportExportAnchorManager240> { /// <summary>
    /// 建立共享坐标系过程中的各种状态
    /// </summary>
    private enum ImportExportState
    {
    // 总体状态
    /// <summary>
    /// 開始
    /// </summary>
    Start,
    /// <summary>
    /// 已完毕
    /// </summary>
    Ready,
    /// <summary>
    /// 失败
    /// </summary>
    Failed,
    // 本地锚点存储器状态
    /// <summary>
    /// 本地锚点存储器正在初始化
    /// </summary>
    AnchorStore_Initializing,
    /// <summary>
    /// 本地锚点存储器已初始化完毕(在状态机中)
    /// </summary>
    AnchorStore_Initialized,
    /// <summary>
    /// 房间API已初始化完毕(在状态机中)
    /// </summary>
    RoomApiInitialized,
    // Anchor creation values
    /// <summary>
    /// 须要初始锚点(在状态机中)
    /// </summary>
    InitialAnchorRequired,
    /// <summary>
    /// 正在创建初始锚点
    /// </summary>
    CreatingInitialAnchor,
    /// <summary>
    /// 准备导出初始锚点(在状态机中)
    /// </summary>
    ReadyToExportInitialAnchor,
    /// <summary>
    /// 正在上传初始锚点
    /// </summary>
    UploadingInitialAnchor,
    // Anchor values
    /// <summary>
    /// 已请求数据
    /// </summary>
    DataRequested,
    /// <summary>
    /// 数据已准备(在状态机中)
    /// </summary>
    DataReady,
    /// <summary>
    /// 导入中
    /// </summary>
    Importing
    } /// <summary>
    /// 当前状态
    /// </summary>
    private ImportExportState currentState = ImportExportState.Start; /// <summary>
    /// 上次状态。用来測试的。代码在Update中
    /// </summary>
    private ImportExportState lastState = ImportExportState.Start; /// <summary>
    /// 当前状态名
    /// </summary>
    public string StateName
    {
    get
    {
    return currentState.ToString();
    }
    } /// <summary>
    /// 共享坐标系是否已经建立完毕
    /// </summary>
    public bool AnchorEstablished
    {
    get
    {
    return currentState == ImportExportState.Ready;
    }
    } /// <summary>
    /// 序列化坐标锚点并进行设备间的传输
    /// </summary>
    private WorldAnchorTransferBatch sharedAnchorInterface; /// <summary>
    /// 下载的原始锚点数据
    /// </summary>
    private byte[] rawAnchorData = null; /// <summary>
    /// 本地锚点存储器
    /// </summary>
    private WorldAnchorStore anchorStore = null; /// <summary>
    /// 保存我们正在导出的锚点名称
    /// </summary>
    public string ExportingAnchorName = "anchor-1234567890"; /// <summary>
    /// 正在导出的锚点数据
    /// </summary>
    private List<byte> exportingAnchorBytes = new List<byte>(); /// <summary>
    /// 共享服务是否已经准备好,这个是上传和下载锚点数据的前提条件
    /// </summary>
    private bool sharingServiceReady = false; /// <summary>
    /// 共享服务中的房间管理器
    /// </summary>
    private RoomManager roomManager; /// <summary>
    /// 当前房间(锚点将会保存在房间中)
    /// </summary>
    private Room currentRoom; /// <summary>
    /// 有时我们会发现一些非常小非常小的锚点数据。这些往往没法使用。所以我们设置一个最小的可信任大小值
    /// </summary>
    private const uint minTrustworthySerializedAnchorDataSize = 100000; /// <summary>
    /// 房间编号
    /// </summary>
    private const long roomID = 8675309; /// <summary>
    /// 房间管理器的各种事件监听
    /// </summary>
    private RoomManagerAdapter roomManagerCallbacks; protected override void Awake()
    {
    base.Awake();
    // 開始初始化本地锚点存储器
    currentState = ImportExportState.AnchorStore_Initializing;
    WorldAnchorStore.GetAsync(AnchorStoreReady);
    } /// <summary>
    /// 本地锚点存储器已准备好
    /// </summary>
    /// <param name="store">本地锚点存储器</param>
    private void AnchorStoreReady(WorldAnchorStore store)
    {
    Debug.Log("本地锚点存储器(WorldAnchorStore)已准备好 - AnchorStoreReady(WorldAnchorStore store)"); anchorStore = store;
    currentState = ImportExportState.AnchorStore_Initialized;
    } private void Start()
    {
    bool isObserverRunning = SpatialMappingManager.Instance.IsObserverRunning();
    Debug.Log("空间扫描状态:" + isObserverRunning);
    if (!isObserverRunning)
    {
    SpatialMappingManager.Instance.StartObserver();
    } // 共享管理器是否已经连接
    SharingStage.Instance.SharingManagerConnected += Instance_SharingManagerConnected; // 是否增加到当前会话中(此事件在共享管理器连接之后才会触发)
    SharingSessionTracker.Instance.SessionJoined += Instance_SessionJoined;
    } #region 共享管理器连接成功后的一系列处理 // 共享管理器连接事件
    private void Instance_SharingManagerConnected(object sender, EventArgs e)
    {
    Debug.Log("共享管理器连接成功 - Instance_SharingManagerConnected(object sender, EventArgs e)"); // 从共享管理器中获取房间管理器
    roomManager = SharingStage.Instance.Manager.GetRoomManager(); // 房间管理器的事件监听
    roomManagerCallbacks = new RoomManagerAdapter(); // 房间中锚点下载完毕事件
    roomManagerCallbacks.AnchorsDownloadedEvent += RoomManagerCallbacks_AnchorsDownloadedEvent;
    // 房间中锚点上传完毕事件
    roomManagerCallbacks.AnchorUploadedEvent += RoomManagerCallbacks_AnchorUploadedEvent; // 为房间管理器增加上面的事件监听
    roomManager.AddListener(roomManagerCallbacks);
    } // 房间中锚点上传完毕事件
    private void RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)
    {
    if (successful)
    {
    Debug.Log("房间锚点上传完毕 - RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)"); // 房间锚点上传成功后,空间坐标共享机制建立完毕
    currentState = ImportExportState.Ready;
    }
    else
    {
    Debug.Log("房间锚点上传失败 - RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)"); // 房间锚点上传失败
    Debug.Log("Anchor Upload Failed!" + failureReason);
    currentState = ImportExportState.Failed;
    }
    } // 房间中锚点下载完毕事件
    private void RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)
    {
    if (successful)
    {
    Debug.Log("房间锚点下载完毕 - RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)"); // 房间锚点下载完毕
    // 获取锚点数据长度
    int datasize = request.GetDataSize();
    // 将下载的锚点数据缓存到数组中
    rawAnchorData = new byte[datasize]; request.GetData(rawAnchorData, datasize); // 保存完锚点数据,能够開始准备数据传输
    currentState = ImportExportState.DataReady;
    }
    else
    {
    Debug.Log("锚点下载失败!" + failureReason + " - RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)"); // 锚点下载失败,又一次開始请求锚点数据
    MakeAnchorDataRequest();
    }
    } /// <summary>
    /// 请求锚点数据
    /// </summary>
    private void MakeAnchorDataRequest()
    {
    if (roomManager.DownloadAnchor(currentRoom, new XString(ExportingAnchorName)))
    {
    // 下载锚点完毕
    currentState = ImportExportState.DataRequested;
    }
    else
    {
    currentState = ImportExportState.Failed;
    }
    } #endregion #region 成功增加当前会话后的一系列处理 // 增加当前会话完毕
    private void Instance_SessionJoined(object sender, SharingSessionTracker.SessionJoinedEventArgs e)
    {
    SharingSessionTracker.Instance.SessionJoined -= Instance_SessionJoined; // 稍等一下。将共享服务状态设置为正常,即能够開始同步锚点了
    Invoke("MarkSharingServiceReady", 5);
    } /// <summary>
    /// 将共享服务状态设置为正常
    /// </summary>
    private void MarkSharingServiceReady()
    {
    sharingServiceReady = true; #if UNITY_EDITOR || UNITY_STANDALONE InitRoomApi(); #endif } /// <summary>
    /// 初始化房间,直到增加到房间中(Update中会持续调用)
    /// </summary>
    private void InitRoomApi()
    {
    int roomCount = roomManager.GetRoomCount();
    if (roomCount == 0)
    {
    Debug.Log("未找到房间 - InitRoomApi()"); // 假设当前会话中,没有获取到不论什么房间
    if (LocalUserHasLowestUserId())
    {
    // 假设当前用户编号最小,则创建房间
    currentRoom = roomManager.CreateRoom(new XString("DefaultRoom"), roomID, false);
    // 房间创建好,准备载入本地的初始锚点。供其它人共享
    currentState = ImportExportState.InitialAnchorRequired; Debug.Log("我是房主,创建房间完毕 - InitRoomApi()");
    }
    }
    else
    {
    for (int i = 0; i < roomCount; i++)
    {
    // 获取第一个房间为当前房间
    currentRoom = roomManager.GetRoom(i);
    if (currentRoom.GetID() == roomID)
    {
    // 增加当前房间
    roomManager.JoinRoom(currentRoom);
    // TODO: 增加房间,房间API初始化完毕。准备同步初始锚点
    currentState = ImportExportState.RoomApiInitialized; Debug.Log("找到房间并增加! - InitRoomApi()"); return;
    }
    }
    }
    } /// <summary>
    /// 推断当前用户编号是不是全部用户中最小的
    /// </summary>
    /// <returns></returns>
    private bool LocalUserHasLowestUserId()
    {
    for (int i = 0; i < SharingSessionTracker.Instance.UserIds.Count; i++)
    {
    if (SharingSessionTracker.Instance.UserIds[i] < CustomMessages240.Instance.LocalUserID)
    {
    return false;
    }
    } return true;
    } #endregion // Update中处理各种状态(简单状态机)
    private void Update()
    {
    if (currentState != lastState)
    {
    Debug.Log("状态变化:" + lastState.ToString() + " > " + currentState.ToString());
    lastState = currentState;
    } switch (currentState)
    {
    case ImportExportState.AnchorStore_Initialized:
    // 本地锚点存储器初始化完毕
    // 假设成功增加当前会话。则開始载入房间
    if (sharingServiceReady)
    {
    InitRoomApi();
    }
    break;
    case ImportExportState.RoomApiInitialized:
    // 房间已载入完毕,開始载入锚点信息
    StartAnchorProcess();
    break;
    case ImportExportState.DataReady:
    // 锚点数据下载完毕后,開始导入锚点数据
    currentState = ImportExportState.Importing;
    WorldAnchorTransferBatch.ImportAsync(rawAnchorData, ImportComplete);
    break;
    case ImportExportState.InitialAnchorRequired:
    // 房主房间创建完毕后,须要创建初始锚点共享给他人
    currentState = ImportExportState.CreatingInitialAnchor;
    // 创建本地锚点
    CreateAnchorLocally();
    break;
    case ImportExportState.ReadyToExportInitialAnchor:
    // 准备导出初始锚点
    currentState = ImportExportState.UploadingInitialAnchor;
    // 运行导出
    Export();
    break;
    }
    } /// <summary>
    /// 房主将本地锚点共享给其它人
    /// </summary>
    private void Export()
    {
    // 获取锚点,这个组件会在CreateAnchorLocally()中自己主动增加
    WorldAnchor anchor = GetComponent<WorldAnchor>(); if (anchor == null)
    {
    return;
    } // 本地保存该锚点
    if (anchorStore.Save(ExportingAnchorName, anchor))
    {
    // 将锚点导出
    sharedAnchorInterface = new WorldAnchorTransferBatch();
    sharedAnchorInterface.AddWorldAnchor(ExportingAnchorName, anchor);
    WorldAnchorTransferBatch.ExportAsync(sharedAnchorInterface, WriteBuffer, ExportComplete);
    }
    else
    {
    currentState = ImportExportState.InitialAnchorRequired;
    }
    } /// <summary>
    /// 房主导出锚点成功
    /// </summary>
    /// <param name="completionReason"></param>
    private void ExportComplete(SerializationCompletionReason completionReason)
    {
    if (completionReason == SerializationCompletionReason.Succeeded && exportingAnchorBytes.Count > minTrustworthySerializedAnchorDataSize)
    {
    // 将锚点数据上传至当前房间中
    roomManager.UploadAnchor(
    currentRoom,
    new XString(ExportingAnchorName),
    exportingAnchorBytes.ToArray(),
    exportingAnchorBytes.Count);
    }
    else
    {
    currentState = ImportExportState.InitialAnchorRequired;
    }
    } private void WriteBuffer(byte[] data)
    {
    exportingAnchorBytes.AddRange(data);
    } /// <summary>
    /// 房主在本地创建一个新的锚点
    /// </summary>
    private void CreateAnchorLocally()
    {
    Debug.Log("開始创建本地锚点"); // 增加世界锚点组件
    WorldAnchor anchor = GetComponent<WorldAnchor>();
    if (anchor == null)
    {
    anchor = gameObject.AddComponent<WorldAnchor>();
    } if (anchor.isLocated)
    {
    // 房主自己定位好本地锚点后。准备导出给其它人
    currentState = ImportExportState.ReadyToExportInitialAnchor;
    }
    else
    {
    anchor.OnTrackingChanged += WorldAnchorForExport_OnTrackingChanged;
    }
    } private void WorldAnchorForExport_OnTrackingChanged(WorldAnchor self, bool located)
    {
    if (located)
    {
    // 房主自己定位好本地锚点后,准备导出给其它人
    currentState = ImportExportState.ReadyToExportInitialAnchor;
    }
    else
    {
    // 房主自己的锚点定位失败,则同步总体失败
    currentState = ImportExportState.Failed;
    } self.OnTrackingChanged -= WorldAnchorForExport_OnTrackingChanged;
    } /// <summary>
    /// 锚点数据下载完毕后,開始导入锚点数据
    /// </summary>
    /// <param name="completionReason"></param>
    /// <param name="deserializedTransferBatch"></param>
    private void ImportComplete(SerializationCompletionReason completionReason, WorldAnchorTransferBatch deserializedTransferBatch)
    {
    if (completionReason == SerializationCompletionReason.Succeeded && deserializedTransferBatch.GetAllIds().Length > 0)
    {
    // 成功导入锚点
    // 获取第一个锚点名称
    bool hasAnchorName = false;
    string[] anchorNames = deserializedTransferBatch.GetAllIds();
    foreach (var an in anchorNames)
    {
    if (an == ExportingAnchorName)
    {
    hasAnchorName = true;
    break;
    }
    } if (!hasAnchorName)
    {
    currentState = ImportExportState.DataReady;
    return;
    } // 保存锚点到本地
    WorldAnchor anchor = deserializedTransferBatch.LockObject(ExportingAnchorName, gameObject);
    if (anchor.isLocated)
    {
    if(anchorStore.Save(ExportingAnchorName, anchor))
    {
    currentState = ImportExportState.Ready;
    }
    else
    {
    currentState = ImportExportState.DataReady;
    } }
    else
    {
    anchor.OnTrackingChanged += WorldAnchorForImport_OnTrackingChanged;
    }
    }
    else
    {
    // 未成功导入,则设置为DataReady。准备在下一帧再次导入。直到导入完毕
    currentState = ImportExportState.DataReady;
    }
    } private void WorldAnchorForImport_OnTrackingChanged(WorldAnchor self, bool located)
    {
    if (located)
    {
    WorldAnchor anchor = GetComponent<WorldAnchor>();
    if (anchorStore.Save(ExportingAnchorName, anchor))
    {
    currentState = ImportExportState.Ready;
    }
    else
    {
    currentState = ImportExportState.DataReady;
    }
    }
    else
    {
    currentState = ImportExportState.Failed;
    } self.OnTrackingChanged -= WorldAnchorForImport_OnTrackingChanged;
    } /// <summary>
    /// 载入锚点信息
    /// </summary>
    private void StartAnchorProcess()
    {
    Debug.Log("正在获取房间锚点…… - StartAnchorProcess()"); // 检查当前房间有无锚点
    int anchorCount = currentRoom.GetAnchorCount(); if (anchorCount > 0)
    {
    bool isRoomAnchorExists = false; for (int i = 0; i < anchorCount; i++)
    {
    string roomAnchor = currentRoom.GetAnchorName(i).GetString();
    if (roomAnchor == ExportingAnchorName)
    {
    isRoomAnchorExists = true;
    break;
    }
    } if (isRoomAnchorExists)
    {
    Debug.Log("获取房间锚点成功!開始下载锚点");
    // 获取房间锚点信息成功后,開始下载锚点数据
    MakeAnchorDataRequest();
    }
    }
    } protected override void OnDestroy()
    {
    if (SharingStage.Instance != null)
    {
    SharingStage.Instance.SharingManagerConnected -= Instance_SharingManagerConnected;
    } if (roomManagerCallbacks != null)
    {
    roomManagerCallbacks.AnchorsDownloadedEvent -= RoomManagerCallbacks_AnchorsDownloadedEvent;
    roomManagerCallbacks.AnchorUploadedEvent -= RoomManagerCallbacks_AnchorUploadedEvent; if (roomManager != null)
    {
    roomManager.RemoveListener(roomManagerCallbacks);
    }
    } base.OnDestroy();
    }
    }

    (代码适用:5.5.1f1版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.VR.WSA.Persistence;
    using UnityEngine.VR.WSA.Sharing;
    using System;
    using UnityEngine.VR.WSA;
    using HoloToolkit.Unity.SpatialMapping;
    using System.Text; public class ImportExportAnchorManager240 : Singleton<ImportExportAnchorManager240>
    {
    /// <summary>
    /// 建立共享坐标系过程中的各种状态
    /// </summary>
    private enum ImportExportState
    {
    // 总体状态
    /// <summary>
    /// 開始
    /// </summary>
    Start,
    /// <summary>
    /// 已完毕
    /// </summary>
    Ready,
    /// <summary>
    /// 失败
    /// </summary>
    Failed,
    // 本地锚点存储器状态
    /// <summary>
    /// 本地锚点存储器正在初始化
    /// </summary>
    AnchorStore_Initializing,
    /// <summary>
    /// 本地锚点存储器已初始化完毕(在状态机中)
    /// </summary>
    AnchorStore_Initialized,
    /// <summary>
    /// 房间API已初始化完毕(在状态机中)
    /// </summary>
    RoomApiInitialized,
    // Anchor creation values
    /// <summary>
    /// 须要初始锚点(在状态机中)
    /// </summary>
    InitialAnchorRequired,
    /// <summary>
    /// 正在创建初始锚点
    /// </summary>
    CreatingInitialAnchor,
    /// <summary>
    /// 准备导出初始锚点(在状态机中)
    /// </summary>
    ReadyToExportInitialAnchor,
    /// <summary>
    /// 正在上传初始锚点
    /// </summary>
    UploadingInitialAnchor,
    // Anchor values
    /// <summary>
    /// 已请求数据
    /// </summary>
    DataRequested,
    /// <summary>
    /// 数据已准备(在状态机中)
    /// </summary>
    DataReady,
    /// <summary>
    /// 导入中
    /// </summary>
    Importing
    } /// <summary>
    /// 当前状态
    /// </summary>
    private ImportExportState currentState = ImportExportState.Start; /// <summary>
    /// 上次状态。用来測试的,代码在Update中
    /// </summary>
    private ImportExportState lastState = ImportExportState.Start; /// <summary>
    /// 当前状态名
    /// </summary>
    public string StateName
    {
    get
    {
    return currentState.ToString();
    }
    } /// <summary>
    /// 共享坐标系是否已经建立完毕
    /// </summary>
    public bool AnchorEstablished
    {
    get
    {
    return currentState == ImportExportState.Ready;
    }
    } /// <summary>
    /// 序列化坐标锚点并进行设备间的传输
    /// </summary>
    private WorldAnchorTransferBatch sharedAnchorInterface; /// <summary>
    /// 下载的原始锚点数据
    /// </summary>
    private byte[] rawAnchorData = null; /// <summary>
    /// 本地锚点存储器
    /// </summary>
    private WorldAnchorStore anchorStore = null; /// <summary>
    /// 保存我们正在导出的锚点名称
    /// </summary>
    public string ExportingAnchorName = "anchor-1234567890"; /// <summary>
    /// 正在导出的锚点数据
    /// </summary>
    private List<byte> exportingAnchorBytes = new List<byte>(); /// <summary>
    /// 共享服务是否已经准备好。这个是上传和下载锚点数据的前提条件
    /// </summary>
    private bool sharingServiceReady = false; /// <summary>
    /// 共享服务中的房间管理器
    /// </summary>
    private RoomManager roomManager; /// <summary>
    /// 当前房间(锚点将会保存在房间中)
    /// </summary>
    private Room currentRoom; /// <summary>
    /// 有时我们会发现一些非常小非常小的锚点数据,这些往往没法使用,所以我们设置一个最小的可信任大小值
    /// </summary>
    private const uint minTrustworthySerializedAnchorDataSize = 100000; /// <summary>
    /// 房间编号
    /// </summary>
    private const long roomID = 8675309; /// <summary>
    /// 房间管理器的各种事件监听
    /// </summary>
    private RoomManagerAdapter roomManagerCallbacks; /// <summary>
    /// 锚点上传完毕事件
    /// </summary>
    public event Action<bool> AnchorUploaded; /// <summary>
    /// 锚点载入完毕事件
    /// </summary>
    public event Action AnchorLoaded; private TextMesh lblMsg;
    private StringBuilder sb = new StringBuilder();
    private void debug(string msg)
    {
    Debug.Log(msg);
    sb.AppendLine(msg);
    } protected override void Awake()
    {
    base.Awake(); lblMsg = GameObject.Find("FPSText").GetComponent<TextMesh>(); // 開始初始化本地锚点存储器
    currentState = ImportExportState.AnchorStore_Initializing;
    WorldAnchorStore.GetAsync(AnchorStoreReady);
    } /// <summary>
    /// 本地锚点存储器已准备好
    /// </summary>
    /// <param name="store">本地锚点存储器</param>
    private void AnchorStoreReady(WorldAnchorStore store)
    {
    debug("本地锚点存储器(WorldAnchorStore)已准备好 - AnchorStoreReady(WorldAnchorStore store)"); anchorStore = store;
    currentState = ImportExportState.AnchorStore_Initialized;
    } private void Start()
    { bool isObserverRunning = SpatialMappingManager.Instance.IsObserverRunning();
    debug("空间扫描状态:" + isObserverRunning); if (!isObserverRunning)
    {
    SpatialMappingManager.Instance.StartObserver();
    } // 共享管理器是否已经连接
    SharingStage.Instance.SharingManagerConnected += Instance_SharingManagerConnected; // 是否增加到当前会话中(此事件在共享管理器连接之后才会触发)
    SharingStage.Instance.SessionsTracker.CurrentUserJoined += SessionsTracker_CurrentUserJoined;
    SharingStage.Instance.SessionsTracker.CurrentUserLeft += SessionsTracker_CurrentUserLeft;
    } #region 共享管理器连接成功后的一系列处理 // 共享管理器连接事件
    private void Instance_SharingManagerConnected(object sender, EventArgs e)
    {
    debug("共享管理器连接成功 - Instance_SharingManagerConnected(object sender, EventArgs e)"); // 从共享管理器中获取房间管理器
    roomManager = SharingStage.Instance.Manager.GetRoomManager(); // 房间管理器的事件监听
    roomManagerCallbacks = new RoomManagerAdapter(); // 房间中锚点下载完毕事件
    roomManagerCallbacks.AnchorsDownloadedEvent += RoomManagerCallbacks_AnchorsDownloadedEvent;
    // 房间中锚点上传完毕事件
    roomManagerCallbacks.AnchorUploadedEvent += RoomManagerCallbacks_AnchorUploadedEvent; // 为房间管理器增加上面的事件监听
    roomManager.AddListener(roomManagerCallbacks);
    } // 房间中锚点上传完毕事件
    private void RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)
    {
    if (successful)
    {
    debug("房间锚点上传完毕 - RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)"); // 房间锚点上传成功后。空间坐标共享机制建立完毕
    currentState = ImportExportState.Ready;
    }
    else
    {
    debug("房间锚点上传失败 - RoomManagerCallbacks_AnchorUploadedEvent(bool successful, XString failureReason)"); // 房间锚点上传失败
    debug("Anchor Upload Failed!" + failureReason);
    currentState = ImportExportState.Failed;
    } if (AnchorUploaded != null)
    {
    AnchorUploaded(successful);
    }
    } // 房间中锚点下载完毕事件
    private void RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)
    {
    if (successful)
    {
    debug("房间锚点下载完毕 - RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)"); // 房间锚点下载完毕
    // 获取锚点数据长度
    int datasize = request.GetDataSize(); // 将下载的锚点数据缓存到数组中
    rawAnchorData = new byte[datasize]; request.GetData(rawAnchorData, datasize); // 保存完锚点数据,能够開始准备数据传输
    currentState = ImportExportState.DataReady;
    }
    else
    {
    debug("锚点下载失败! " + failureReason + " - RoomManagerCallbacks_AnchorsDownloadedEvent(bool successful, AnchorDownloadRequest request, XString failureReason)"); // 锚点下载失败,又一次開始请求锚点数据
    MakeAnchorDataRequest();
    }
    } /// <summary>
    /// 请求锚点数据
    /// </summary>
    private void MakeAnchorDataRequest()
    {
    if (roomManager.DownloadAnchor(currentRoom, new XString(ExportingAnchorName)))
    {
    // 下载锚点完毕
    currentState = ImportExportState.DataRequested;
    }
    else
    {
    currentState = ImportExportState.Failed;
    }
    } #endregion #region 成功增加当前会话后的一系列处理 // 增加当前会话完毕
    private void SessionsTracker_CurrentUserJoined(Session session)
    {
    SharingStage.Instance.SessionsTracker.CurrentUserJoined -= SessionsTracker_CurrentUserJoined; // 稍等一下,将共享服务状态设置为正常,即能够開始同步锚点了
    Invoke("MarkSharingServiceReady", 5);
    } // 退出当前会话
    private void SessionsTracker_CurrentUserLeft(Session session)
    {
    sharingServiceReady = false;
    if (anchorStore != null)
    {
    currentState = ImportExportState.AnchorStore_Initialized;
    }
    else
    {
    currentState = ImportExportState.AnchorStore_Initializing;
    }
    } /// <summary>
    /// 将共享服务状态设置为正常
    /// </summary>
    private void MarkSharingServiceReady()
    {
    sharingServiceReady = true; #if UNITY_EDITOR || UNITY_STANDALONE InitRoomApi(); #endif } /// <summary>
    /// 初始化房间,直到增加到房间中(Update中会持续调用)
    /// </summary>
    private void InitRoomApi()
    {
    int roomCount = roomManager.GetRoomCount(); if (roomCount == 0)
    {
    debug("未找到房间 - InitRoomApi()"); // 假设当前会话中,没有获取到不论什么房间
    if (LocalUserHasLowestUserId())
    {
    // 假设当前用户编号最小,则创建房间
    currentRoom = roomManager.CreateRoom(new XString("DefaultRoom"), roomID, false);
    // 房间创建好,准备载入本地的初始锚点。供其它人共享
    currentState = ImportExportState.InitialAnchorRequired; debug("我是房主,创建房间完毕 - InitRoomApi()");
    }
    }
    else
    {
    for (int i = 0; i < roomCount; i++)
    {
    currentRoom = roomManager.GetRoom(i);
    if (currentRoom.GetID() == roomID)
    {
    // 增加当前房间
    roomManager.JoinRoom(currentRoom);
    // TODO: 增加房间,房间API初始化完毕,准备同步初始锚点
    currentState = ImportExportState.RoomApiInitialized; debug("找到房间并增加! - InitRoomApi()"); return;
    }
    }
    }
    } /// <summary>
    /// 推断当前用户编号是不是全部用户中最小的
    /// </summary>
    /// <returns></returns>
    private bool LocalUserHasLowestUserId()
    {
    if (SharingStage.Instance == null)
    {
    return false;
    }
    if (SharingStage.Instance.SessionUsersTracker != null)
    {
    List<User> currentUsers = SharingStage.Instance.SessionUsersTracker.CurrentUsers;
    for (int i = 0; i < currentUsers.Count; i++)
    {
    if (currentUsers[i].GetID() < CustomMessages240.Instance.LocalUserID)
    {
    return false;
    }
    }
    }
    return true;
    } #endregion // Update中处理各种状态(简单状态机)
    private void Update()
    {
    if (currentState != lastState)
    {
    debug("状态变化:" + lastState.ToString() + " > " + currentState.ToString());
    lastState = currentState;
    } lblMsg.text = sb.ToString(); switch (currentState)
    {
    case ImportExportState.AnchorStore_Initialized:
    // 本地锚点存储器初始化完毕
    // 假设成功增加当前会话。则開始载入房间
    if (sharingServiceReady)
    {
    InitRoomApi();
    }
    break;
    case ImportExportState.RoomApiInitialized:
    // 房间已载入完毕,開始载入锚点信息
    StartAnchorProcess();
    break;
    case ImportExportState.DataReady:
    // 锚点数据下载完毕后。開始导入锚点数据
    currentState = ImportExportState.Importing;
    WorldAnchorTransferBatch.ImportAsync(rawAnchorData, ImportComplete);
    break;
    case ImportExportState.InitialAnchorRequired:
    // 房主房间创建完毕后,须要创建初始锚点共享给他人
    currentState = ImportExportState.CreatingInitialAnchor;
    // 创建本地锚点
    CreateAnchorLocally();
    break;
    case ImportExportState.ReadyToExportInitialAnchor:
    // 准备导出初始锚点
    currentState = ImportExportState.UploadingInitialAnchor;
    // 运行导出
    Export();
    break;
    }
    } /// <summary>
    /// 房主将本地锚点共享给其它人
    /// </summary>
    private void Export()
    {
    // 获取锚点,这个组件会在CreateAnchorLocally()中自己主动增加
    WorldAnchor anchor = GetComponent<WorldAnchor>(); anchorStore.Clear();
    // 本地保存该锚点
    if (anchor != null && anchorStore.Save(ExportingAnchorName, anchor))
    {
    debug("保存锚点完毕,准备导出! - Export()");
    // 将锚点导出
    sharedAnchorInterface = new WorldAnchorTransferBatch();
    sharedAnchorInterface.AddWorldAnchor(ExportingAnchorName, anchor);
    WorldAnchorTransferBatch.ExportAsync(sharedAnchorInterface, WriteBuffer, ExportComplete);
    }
    else
    {
    debug("保存本地锚点失败。 - Export()"); currentState = ImportExportState.InitialAnchorRequired;
    }
    } /// <summary>
    /// 房主导出锚点成功
    /// </summary>
    /// <param name="completionReason"></param>
    private void ExportComplete(SerializationCompletionReason completionReason)
    {
    if (completionReason == SerializationCompletionReason.Succeeded && exportingAnchorBytes.Count > minTrustworthySerializedAnchorDataSize)
    {
    // 将锚点数据上传至当前房间中
    roomManager.UploadAnchor(
    currentRoom,
    new XString(ExportingAnchorName),
    exportingAnchorBytes.ToArray(),
    exportingAnchorBytes.Count);
    }
    else
    {
    debug("导出锚点出错!" + completionReason.ToString());
    currentState = ImportExportState.InitialAnchorRequired;
    }
    } private void WriteBuffer(byte[] data)
    {
    exportingAnchorBytes.AddRange(data);
    } /// <summary>
    /// 房主在本地创建一个新的锚点
    /// </summary>
    private void CreateAnchorLocally()
    {
    debug("開始创建本地锚点"); // 增加世界锚点组件
    WorldAnchor anchor = GetComponent<WorldAnchor>();
    if (anchor == null)
    {
    anchor = gameObject.AddComponent<WorldAnchor>();
    } if (anchor.isLocated)
    {
    // 房主自己定位好本地锚点后,准备导出给其它人
    currentState = ImportExportState.ReadyToExportInitialAnchor;
    }
    else
    {
    anchor.OnTrackingChanged += WorldAnchorForExport_OnTrackingChanged;
    }
    } private void WorldAnchorForExport_OnTrackingChanged(WorldAnchor self, bool located)
    {
    if (located)
    {
    // 房主自己定位好本地锚点后,准备导出给其它人
    currentState = ImportExportState.ReadyToExportInitialAnchor;
    }
    else
    {
    // 房主自己的锚点定位失败,则同步总体失败
    currentState = ImportExportState.Failed;
    } self.OnTrackingChanged -= WorldAnchorForExport_OnTrackingChanged;
    } /// <summary>
    /// 锚点数据下载完毕后。開始导入锚点数据
    /// </summary>
    /// <param name="completionReason"></param>
    /// <param name="deserializedTransferBatch"></param>
    private void ImportComplete(SerializationCompletionReason completionReason, WorldAnchorTransferBatch deserializedTransferBatch)
    {
    if (completionReason == SerializationCompletionReason.Succeeded && deserializedTransferBatch.GetAllIds().Length > 0)
    {
    // 成功导入锚点
    // 获取第一个锚点名称
    bool hasAnchorName = false;
    string[] anchorNames = deserializedTransferBatch.GetAllIds();
    foreach (var an in anchorNames)
    {
    if (an == ExportingAnchorName)
    {
    hasAnchorName = true;
    break;
    }
    } if (!hasAnchorName)
    {
    currentState = ImportExportState.DataReady;
    return;
    } // 保存锚点到本地
    WorldAnchor anchor = deserializedTransferBatch.LockObject(ExportingAnchorName, gameObject);
    if (anchor.isLocated)
    {
    if (anchorStore.Save(ExportingAnchorName, anchor))
    {
    currentState = ImportExportState.Ready;
    }
    else
    {
    currentState = ImportExportState.DataReady;
    } }
    else
    {
    anchor.OnTrackingChanged += WorldAnchorForImport_OnTrackingChanged;
    }
    }
    else
    {
    // 未成功导入,则设置为DataReady。准备在下一帧再次导入,直到导入完毕
    currentState = ImportExportState.DataReady;
    }
    } private void WorldAnchorForImport_OnTrackingChanged(WorldAnchor self, bool located)
    {
    if (located)
    {
    WorldAnchor anchor = GetComponent<WorldAnchor>();
    if (anchorStore.Save(ExportingAnchorName, anchor))
    {
    currentState = ImportExportState.Ready;
    }
    else
    {
    currentState = ImportExportState.DataReady;
    }
    }
    else
    {
    currentState = ImportExportState.Failed;
    } self.OnTrackingChanged -= WorldAnchorForImport_OnTrackingChanged;
    } /// <summary>
    /// 载入锚点信息
    /// </summary>
    private void StartAnchorProcess()
    {
    debug("正在获取房间锚点…… - StartAnchorProcess()"); // 检查当前房间有无锚点
    int anchorCount = currentRoom.GetAnchorCount(); if (anchorCount > 0)
    {
    bool isRoomAnchorExists = false; for (int i = 0; i < anchorCount; i++)
    {
    string roomAnchor = currentRoom.GetAnchorName(i).GetString();
    if (roomAnchor == ExportingAnchorName)
    {
    isRoomAnchorExists = true;
    break;
    }
    } if (isRoomAnchorExists)
    {
    debug("获取房间锚点成功!開始下载锚点");
    // 获取房间锚点信息成功后,開始下载锚点数据
    MakeAnchorDataRequest();
    }
    }
    } protected override void OnDestroy()
    {
    if (SharingStage.Instance != null)
    {
    SharingStage.Instance.SharingManagerConnected -= Instance_SharingManagerConnected;
    if (SharingStage.Instance.SessionsTracker != null)
    {
    SharingStage.Instance.SessionsTracker.CurrentUserJoined -= SessionsTracker_CurrentUserJoined;
    SharingStage.Instance.SessionsTracker.CurrentUserLeft -= SessionsTracker_CurrentUserLeft;
    }
    } if (roomManagerCallbacks != null)
    {
    roomManagerCallbacks.AnchorsDownloadedEvent -= RoomManagerCallbacks_AnchorsDownloadedEvent;
    roomManagerCallbacks.AnchorUploadedEvent -= RoomManagerCallbacks_AnchorUploadedEvent; if (roomManager != null)
    {
    roomManager.RemoveListener(roomManagerCallbacks);
    } roomManagerCallbacks.Dispose();
    roomManagerCallbacks = null;
    } if (roomManager != null)
    {
    roomManager.Dispose();
    roomManager = null;
    } base.OnDestroy();
    }
    }

    代码有点多,但理解起来并不困难,核心就是一个维护一个简单状态机。我写好了凝视,然后还画了张状态图帮助大家理解,例如以下:

  5. 由于Cube已经作为Controller的子集。我们将之前传递Position改为传递LocalPosition,改动Cube240.cs内容例如以下:

    (代码适用:5.5.0f3版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler { // 是否正在移动
    bool isMoving = false; // 消息传递类
    CustomMessages240 customMessage; private void Start()
    {
    customMessage = CustomMessages240.Instance; // 指定收到Cube位置变化消息后的处理方法
    customMessage.MessageHandlers[CustomMessages240.CustomMessageID.CubePosition] = OnCubePositionReceived;
    } private void OnCubePositionReceived(NetworkInMessage msg)
    {
    // 同步Cube位置
    if (!isMoving)
    {
    transform.localPosition = CustomMessages240.ReadCubePosition(msg);
    }
    } // 单击Cube。切换是否移动
    public void OnInputClicked(InputEventData eventData)
    {
    isMoving = !isMoving;
    // 放置Cube后,发送Cube的位置消息给其它人
    if (!isMoving)
    {
    customMessage.SendCubePosition(transform.localPosition);
    }
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update () {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    customMessage.SendCubePosition(transform.localPosition, MessageReliability.UnreliableSequenced);
    }
    }
    }

    (代码适用:5.5.1f1版本号)

    using HoloToolkit.Sharing;
    using HoloToolkit.Unity.InputModule;
    using UnityEngine; public class Cube240 : MonoBehaviour, IInputClickHandler
    { // 是否正在移动
    bool isMoving = false; // 消息传递类
    CustomMessages240 customMessage; private void Start()
    {
    customMessage = CustomMessages240.Instance; // 指定收到Cube位置变化消息后的处理方法
    customMessage.MessageHandlers[CustomMessages240.CustomMessageID.CubePosition] = OnCubePositionReceived;
    } private void OnCubePositionReceived(NetworkInMessage msg)
    {
    // 同步Cube位置
    if (!isMoving)
    {
    transform.localPosition = CustomMessages240.ReadCubePosition(msg);
    }
    } // 单击Cube。切换是否移动
    public void OnInputClicked(InputClickedEventData eventData)
    {
    isMoving = !isMoving;
    // 放置Cube后,发送Cube的位置消息给其它人
    if (!isMoving)
    {
    customMessage.SendCubePosition(transform.localPosition);
    }
    } // 假设Cube为移动状态,让其放置在镜头前2米位置
    void Update()
    {
    if (isMoving)
    {
    transform.position = Camera.main.transform.position + Camera.main.transform.forward * 2f;
    customMessage.SendCubePosition(transform.localPosition, MessageReliability.UnreliableSequenced);
    }
    }
    }

    本节完毕。

将代码公布到两台设备上。进行測试!

注意,本例中一開始并未同步Cube位置,须要某台设备移动Cube后才干看见效果。

完毕后,Hierarchy结构相似:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVyaWNob3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="Hierarchy结构" title="">

说明

学习官方教程的过程中,有非常大几率会遇到多台设备物体尽管能够正常显示Cube。也能正常移动Cube。但各设备的Cube并没有重叠。移动Cube时,可能还有一台设备的Cube是往不同的方向移动。要问为什么?由于官方代码中充满了以下这样的代码:

roomManager.GetRoom(0)
currentRoom.GetAnchorName(0)
wat.GetAllIds()[0]

入坑的同学如今应该已经明确了,当出现多个房间或者多个锚点时。设备总是拿第一个。造成设备间同步的并不一定是同一个房间或者锚点。

小结

这一节太伤元气,小结就写成问答形式了。

  1. 问: 为什么真机扫描不出来空间模型?

    答: 请检查是否开启Spatial Perception功能(终于须要检查Package.appxmanifest文件里节点是否增加了我们须要的设备特性),以下是我測试时的配置:

    <Capabilities>
    <Capability Name="internetClient" />
    <Capability Name="internetClientServer" />
    <Capability Name="privateNetworkClientServer" />
    <uap2:Capability Name="spatialPerception" />
    <DeviceCapability Name="microphone" />
    </Capabilities>

    另外。请检查是否放置了SpatialMapping.prefab,并打开Auto Start Observer

  2. 问: 出现”SpatialAnchorTransferManager denied access to WorldAnchor serialization”的提示

    答: 首先,请不要在Unity里測试(无论是不是Remoting),否则这个现象肯定会出现

    其次。看第1点。

  3. 问: 仅仅能用多台真机进行測试吗?

    答: 眼下是这样,主要是由于World Anchor
  4. 问: 设备中共享的物体,并没有重叠?

    答: 好吧,请注意代码逻辑,这要求设备扫描了同一空间。增加了同一房间,共享了同一锚点。锚点附加同一物体(要求真高!)。

  5. 问: SpatialMapping好卡啊

    答: 临时先关闭Draw Visual Meshes
  6. 问: 设备測试的时候,设备休眠时间太短,在哪里能够设置得长一点?

    答: 设备打开,浏览器訪问设备IP,进入:Hololens Device PortalHome菜单下有个Sleep settings,最长设置30分钟。

參考文档

官方教程Holograms 240:https://developer.microsoft.com/en-us/windows/mixed-reality/holograms_240


VR/AR/MR技术交流QQ群:594299228

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVyaWNob3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="VR/AR/MR技术交流QQ群:594299228" title="">

Hololens官方教程精简版 - 08. Sharing holograms(共享全息影像)的更多相关文章

  1. Flask 教程精简版之一(系列片)

    Flask 教程精简版之一(系列片) 现在连教程都有精简版 准备 1.要学会 Flask 之前必须掌握 Python 基本使用. 2.会使用简单的 HTML 效果更加 3.若想练气功必须先自暴自弃 简 ...

  2. QQ音乐官方定制精简版v1.3.6 纯净无广告

    介绍 近期腾讯推出了QQ音乐简洁版.顾名思义,QQ音乐简洁版就是官方精简后的版本,没有内置任何广告.完全专注于听歌,不存在直播.K歌.短视频等花里胡哨的内容.如有违规,请删删.. 结尾附pc端 QQ音 ...

  3. 微软官方的精简版Windows 7——Windows Thin PC

    Windows Thin PC是微软的Window 7的精简版本,它的一个非常明显的特点是系统资源占用小.如下是官方的系统配置需求: CPU:    主频1 GHz 内存:    1GB(实际占用约5 ...

  4. 电脑公司最新GHOST WIN7系统32,64位优化精简版下载

    系统来自系统妈:http://www.xitongma.com 电脑公司最新GHOST win7系统32位优化精简版V2016年3月 系统概述 电脑公司ghost win7 x86(32位)万能装机版 ...

  5. WIN7X64SP1极限精简版by双心

     WIN7X64SP1极限精简版by双心 http://bbs.wuyou.net/forum.php?mod=viewthread&tid=405044&page=1&ext ...

  6. 【新手/零基础】Hexo+Gitee个人博客搭建教程--详细版

    前言 点此转到--精简版 可能很多小伙伴都有搭建一个属于自己的博客的想法.但是经常是无奈于自己匮乏的知识.但是,每个老手都是新手过来的,再困难的事情,只要肯花一点时间都可以办成. 本次教程分为详细版和 ...

  7. TeamViewer12.0.71503(远程控制软件)精简版 单文件企业版介绍

    TeamViewer 是一款能在任何防火墙和 NAT 代理的后台用于远程控制,桌面共享和文件传输的简单且快速的解决方案.为了连接到另一台计算机,只需要在两台计算机上同时运行 TeamViewer 即可 ...

  8. [异常解决] ubuntu上安装虚拟机遇到的问题(vmware坑了,virtual-box简单安装,在virtual-box中安装精简版win7)

    利用周末时间将整个电脑格式化,换成了ubuntu系统- 所谓:扫清屋子再请客! 但是有些软件只在win上有,于是还是考虑装个虚拟机来个——逐步过度策略,一点点地从win上转移到linux上 我的系统是 ...

  9. TeamViewer12.0.71503(远程控制软件)精简版单文件企业版介绍

    TeamViewer 是一款能在任何防火墙和 NAT 代理的后台用于远程控制,桌面共享和文件传输的简单且快速的解决方案.为了连接到另一台计算机,只需要在两台计算机上同时运行 TeamViewer 即可 ...

随机推荐

  1. docker 镜像的导入导出

    今天使用docker部署asp.net core应用程序时,发现当我们做好基础镜像之后需要把镜像导出到正式环境,因此学习了一下如何从docker中导出镜像: 1.首先通过docker images命令 ...

  2. CVX安装使用

    CVX下载 下载地址 使用手册 Using Gurobi with CVX Using MOSEK with CVX CVX安装 下载压缩文件后解压缩至任意地址,打开matlab,进入解压缩后的地址, ...

  3. Eclipse Maven 创建Hello World Web项目

    通过Eclipse创建Maven Web项目的简单步骤 先决条件 (Prerequisites) 1,JDK  environment, 具体的安装JDK的步骤和环境配置一般网上都有,这里就不在赘述. ...

  4. Unity的Json解析<二>–写Json文件

    本文章由cartzhang编写,转载请注明出处. 所有权利保留. 文章链接:http://blog.csdn.net/cartzhang/article/details/50378805 作者:car ...

  5. 表单标签 fieldset legent

    书写表单时可以提供简单样式的标签 <fieldset> <legent></legent> <input type="text" > ...

  6. Mysql学习总结(33)——阿里云centos配置MySQL主从复制

    1.安装jdk1.8 首先确定没有安装过jdk 2.yum –y list java*查询系统自带的jdk安装包情况. 3.安装jdk1.8 4. 验证安装结果. 安装mysql 1. rpm -Uv ...

  7. 洛谷 P3133 [USACO16JAN]无线电联系Radio Contact

    P3133 [USACO16JAN]无线电联系Radio Contact 题目描述 Farmer John has lost his favorite cow bell, and Bessie the ...

  8. hdu Swipe Bo(bfs+状态压缩)错了多次的题

    Swipe Bo Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total S ...

  9. 安卓ProgressBar水平进度条的颜色设置

    安卓系统提供了水平进度条ProgressBar的样式,而我们在实际开发中,差点儿不可能使用默认的样式.原因就是"太丑"^_^ 所以我们在很多其它的时候须要对其颜色进行自己定义,主要 ...

  10. pyspark kafka createDirectStream和createStream 区别

    from pyspark.streaming.kafka import KafkaUtils kafkaStream = KafkaUtils.createStream(streamingContex ...