DirectX 中的局部定位点传输

在无法使用 Azure 空间定位点的情况下,HoloLens 设备可通过局部定位点传输功能导出要由另一个 HoloLens 设备导入的定位点。

注意

本地定位点传输功能提供的定位点回收没有 Azure 空间定位点可靠,且此方法不支持 iOS 和 Android 设备。

注意

本文中的代码片段当前演示了如何使用 C++/CX,而不是 C++17 兼容的 C++/WinRT,后者在 C++ 全息项目模板中使用。 这些概念与 C++/WinRT 项目等同,但将需要转换代码。

传输空间定位点

可以使用 SpatialAnchorTransferManager 在 Windows Mixed Reality 设备之间传输空间定位点。 通过此 API,可将定位点与在世界中查找该确切位置所需的所有支持性传感器数据捆绑在一起,然后将该捆绑包导入另一台设备。 第二台设备上的应用导入该定位点后,每个应用可以使用该共享空间定位点的坐标系来渲染全息影像,然后这些全息影像将显示在现实世界中的同一位置。

请注意,无法在不同的设备类型之间传输空间定位点,例如,可能无法使用沉浸式头戴显示设备来定位 HoloLens 空间定位点。 此外,传输的定位点与 iOS 或 Android 设备不兼容。

设置应用以使用 spatialPerception 功能

必须向应用授予 SpatialPerception 功能的使用权限,然后它才能使用 SpatialAnchorTransferManager。 这是必要的,因为传输空间定位点涉及到共享在不同的时间在该定位点附近收集的传感器图像,其中可能包含敏感信息。

在应用的 package.appxmanifest 文件中声明此功能。 下面是一个示例:

<Capabilities>
  <uap2:Capability Name="spatialPerception" />
</Capabilities>

此功能来自 uap2 命名空间。 若要在清单中访问此命名空间,请将其作为 xlmns 属性包含在 <Package> 元素中。 下面是一个示例:

<Package
    xmlns="https://schemas.microsoft.com/appx/manifest/foundation/windows10"
    xmlns:mp="https://schemas.microsoft.com/appx/2014/phone/manifest"
    xmlns:uap="https://schemas.microsoft.com/appx/manifest/uap/windows10"
    xmlns:uap2="https://schemas.microsoft.com/appx/manifest/uap/windows10/2"
    IgnorableNamespaces="uap mp"
    >

注意:应用需要在运行时请求该功能,然后才能访问 SpatialAnchor 导出/导入 API。 请参阅以下示例中的 RequestAccessAsync

使用 SpatialAnchorTransferManager 导出定位点数据以将其序列化

代码示例中包含一个帮助器函数,用于导出(序列化)SpatialAnchor 数据。 此导出 API 用于序列化将字符串与定位点相关联的键-值对集合中的所有定位点。

// ExportAnchorDataAsync: Exports a byte buffer containing all of the anchors in the given collection.
//
// This function will place data in a buffer using a std::vector<byte>. The ata buffer contains one or more
// Anchors if one or more Anchors were successfully imported; otherwise, it is ot modified.
//
task<bool> SpatialAnchorImportExportHelper::ExportAnchorDataAsync(
    vector<byte>* anchorByteDataOut,
    IMap<String^, SpatialAnchor^>^ anchorsToExport
    )
{

首先,我们需要设置数据流。 这样,我们便可以 1.) 使用 TryExportAnchorsAsync 将数据放入应用拥有的缓冲区;2.) 将导出的字节缓冲区流(一个 WinRT 数据流)中的数据读取到我们自己的内存缓冲区(一个 std::vector<byte>)中。

// Create a random access stream to process the anchor byte data.
InMemoryRandomAccessStream^ stream = ref new InMemoryRandomAccessStream();
// Get an output stream for the anchor byte stream.
IOutputStream^ outputStream = stream->GetOutputStreamAt(0);

我们需要请求访问空间数据(包括系统导出的定位点)的权限。

// Request access to spatial data.
auto accessRequestedTask = create_taskSpatialAnchorTransferManager::RequestAccessAsync()).then([anchorsToExport, utputStream](SpatialPerceptionAccessStatus status)
{
    if (status == SpatialPerceptionAccessStatus::Allowed)
    {
        // Access is allowed.
        // Export the indicated set of anchors.
        return create_task(SpatialAnchorTransferManager::TryExportAnchorsAsync(
            anchorsToExport,
            outputStream
            ));
    }
    else
    {
        // Access is denied.
        return task_from_result<bool>(false);
    }
});

如果我们获得了权限并导出了定位点,则可以读取数据流。 此外还将演示如何创建用于读取数据的 DataReader 和 InputStream。

// Get the input stream for the anchor byte stream.
IInputStream^ inputStream = stream->GetInputStreamAt(0);
// Create a DataReader, to get bytes from the anchor byte stream.
DataReader^ reader = ref new DataReader(inputStream);
return accessRequestedTask.then([anchorByteDataOut, stream, reader](bool nchorsExported)
{
    if (anchorsExported)
    {
        // Get the size of the exported anchor byte stream.
        size_t bufferSize = static_cast<size_t>(stream->Size);
        // Resize the output buffer to accept the data from the stream.
        anchorByteDataOut->reserve(bufferSize);
        anchorByteDataOut->resize(bufferSize);
        // Read the exported anchor store into the stream.
        return create_task(reader->LoadAsync(bufferSize));
    }
    else
    {
        return task_from_result<size_t>(0);
    }

从流中读取字节后,我们可以按此处所示将其保存到我们自己的数据缓冲区中。

}).then([anchorByteDataOut, reader](size_t bytesRead)
{
    if (bytesRead > 0)
    {
        // Read the bytes from the stream, into our data output buffer.
        reader->ReadBytes(Platform::ArrayReference<byte>(&(*anchorByteDataOut)[0], bytesRead));
        return true;
    }
    else
    {
        return false;
    }
});
};

通过使用 SpatialAnchorTransferManager 将定位点数据导入系统来反序列化定位点数据

代码示例中包含一个帮助器函数用于加载先前导出的数据。 此反序列化函数提供键值对的集合,类似于 SpatialAnchorStore 提供的集合 — 只不过这些数据是从另一个源(例如网络套接字)获取的。 可以在脱机存储这些数据之前,使用应用中内存或(如果适用)应用的 SpatialAnchorStore 对其进行处理和推理。

// ImportAnchorDataAsync: Imports anchors from a byte buffer that was previously exported.
//
// This function will import all anchors from a data buffer into an in-memory ollection of key, value
// pairs that maps String objects to SpatialAnchor objects. The Spatial nchorStore is not affected by
// this function unless you provide it as the target collection for import.
//
task<bool> SpatialAnchorImportExportHelper::ImportAnchorDataAsync(
    std::vector<byte>& anchorByteDataIn,
    IMap<String^, SpatialAnchor^>^ anchorMapOut
    )
{

首先,需要创建流对象来访问定位点数据。 我们要将数据从我们的缓冲区写入系统缓冲区,因此需要创建一个写入内存中数据流的 DataWriter,以实现将定位点作为 SpatialAnchors 从字节缓冲区放入到系统中的目标。

// Create a random access stream for the anchor data.
InMemoryRandomAccessStream^ stream = ref new InMemoryRandomAccessStream();
// Get an output stream for the anchor data.
IOutputStream^ outputStream = stream->GetOutputStreamAt(0);
// Create a writer, to put the bytes in the stream.
DataWriter^ writer = ref new DataWriter(outputStream);

同样,我们需要确保应用有权导出空间定位点数据,其中可能包括有关用户环境的私密信息。

// Request access to transfer spatial anchors.
return create_task(SpatialAnchorTransferManager::RequestAccessAsync()).then(
    [&anchorByteDataIn, writer](SpatialPerceptionAccessStatus status)
{
    if (status == SpatialPerceptionAccessStatus::Allowed)
    {
        // Access is allowed.

如果允许访问,我们可将字节从缓冲区写入系统数据流。

// Write the bytes to the stream.
        byte* anchorDataFirst = &anchorByteDataIn[0];
        size_t anchorDataSize = anchorByteDataIn.size();
        writer->WriteBytes(Platform::ArrayReference<byte>(anchorDataFirst, anchorDataSize));
        // Store the stream.
        return create_task(writer->StoreAsync());
    }
    else
    {
        // Access is denied.
        return task_from_result<size_t>(0);
    }

如果我们成功地在数据流中存储了字节,则可以尝试使用 SpatialAnchorTransferManager 导入该数据。

}).then([writer, stream](unsigned int bytesWritten)
{
    if (bytesWritten > 0)
    {
        // Try to import anchors from the byte stream.
        return create_task(writer->FlushAsync())
            .then([stream](bool dataWasFlushed)
        {
            if (dataWasFlushed)
            {
                // Get the input stream for the anchor data.
                IInputStream^ inputStream = stream->GetInputStreamAt(0);
                return create_task(SpatialAnchorTransferManager::TryImportAnchorsAsync(inputStream));
            }
            else
            {
                return task_from_result<IMapView<String^, SpatialAnchor^>^>(nullptr);
            }
        });
    }
    else
    {
        return task_from_result<IMapView<String^, SpatialAnchor^>^>(nullptr);
    }

如果能够导入数据,则我们会获得一个将字符串与定位点相关联的键值-对的映射视图。 可将其载入我们自己的内存中数据集合,并使用该集合来查找所需的定位点。

}).then([anchorMapOut](task<Windows::Foundation::Collections::IMapView<String^, SpatialAnchor^>^>  previousTask)
{
    try
    {
        auto importedAnchorsMap = previousTask.get();
        // If the operation was successful, we get a set of imported anchors.
        if (importedAnchorsMap != nullptr)
        {
            for each (auto& pair in importedAnchorsMap)
            {
                // Note that you could look for specific anchors here, if you know their key values.
                auto const& id = pair->Key;
                auto const& anchor = pair->Value;
                // Append "Remote" to the end of the anchor name for disambiguation.
                std::wstring idRemote(id->Data());
                idRemote += L"Remote";
                String^ idRemoteConst = ref new String (idRemote.c_str());
                // Store the anchor in the current in-memory anchor map.
                anchorMapOut->Insert(idRemoteConst, anchor);
            }
            return true;
        }
    }
    catch (Exception^ exception)
    {
        OutputDebugString(L"Error: Unable to import the anchor data buffer bytes into the in-memory anchor collection.\n");
    }
    return false;
});
}

注意:仅仅是能够导入某个定位点并不一定意味着可以直接使用该定位点。 定位点可能位于不同的房间,或者完全位于另一个物理位置;只有在接收定位点的设备具有足够的有关创建该定位点的环境的视觉信息,可以还原该定位点相对于已知当前环境的位置之后,才能定位该定位点。 在继续尝试将定位点用于实时内容之前,客户端实现应尝试相对于局部坐标系或参考系来定位定位点。 例如,尝试周期性地相对于当前坐标系定位定位点,直到该定位点开始可定位。

特殊注意事项

TryExportAnchorsAsync API 允许将多个 SpatialAnchor 导出到同一个不透明的二进制 Blob。 但是,Blob 包含的数据存在细微差别,具体取决于在单个调用中导出的是单个 SpatialAnchor 还是多个 SpatialAnchor。

导出单个 SpatialAnchor

Blob 包含 SpatialAnchor 附近环境的表示形式,因此可以在导入 SpatialAnchor 的设备上识别该环境。 导入完成后,新的 SpatialAnchor 可供设备使用。 假设用户最近出现在定位点附近,则该定位点是可定位的,并可以渲染附加到 SpatialAnchor 的全息影像。 这些全息影像将显示在与导出 SpatialAnchor 的原始设备上相同的物理位置。

Export of a single SpatialAnchor

导出多个 SpatialAnchor

与导出单个 SpatialAnchor 类似,Blob 包含所有指定的 SpatialAnchor 附近环境的表示形式。 此外,Blob 包含有关所包括的 SpatialAnchor 之间的连接的信息(如果这些 SpatialAnchor 位于同一物理空间中)。 这意味着,如果导入了两个附近的 SpatialAnchor,则即使设备仅识别第一个 SpatialAnchor 周围的环境,也可以定位附加到第二个 SpatialAnchor 的全息影像,因为 Blob 中包含了足够的数据用于计算两个 SpatialAnchor 之间的转换。 如果分开导出了两个 SpatialAnchor(对 TryExportSpatialAnchors 的两次单独调用),则 Blob 中可能没有足够的数据,因此在定位第一个全息影像时,无法定位附加到第二个 SpatialAnchor 的全息影像。

Multiple anchors exported using a single TryExportAnchorsAsync callMultiple anchors exported using a separate TryExportAnchorsAsync call for each anchor

示例:使用 Windows::Networking::StreamSocket 发送定位点数据

此处提供了一个示例,用于演示如何通过 TCP 网络发送导出的定位点数据,以此使用这些数据。 此示例取自 HolographicSpatialAnchorTransferSample。

WinRT StreamSocket 类使用 PPL 任务库。 发生网络错误时,错误将使用重新引发的异常返回到链中的下一个任务。 该异常包含指示错误状态的 HRESULT。

通过 TCP 使用 Windows::Networking::StreamSocketListener 来发送导出的定位点数据

创建一个用于侦听连接的服务器实例。

void SampleAnchorTcpServer::ListenForConnection()
{
    // Make a local copy to avoid races with Closed events.
    StreamSocketListener^ streamSocketListener = m_socketServer;
    if (streamSocketListener == nullptr)
    {
        OutputDebugString(L"Server listening for client.\n");
        // Create the web socket connection.
        streamSocketListener = ref new StreamSocketListener();
        streamSocketListener->Control->KeepAlive = true;
        streamSocketListener->BindEndpointAsync(
            SampleAnchorTcpCommon::m_serverHost,
            SampleAnchorTcpCommon::m_tcpPort
            );
        streamSocketListener->ConnectionReceived +=
            ref new Windows::Foundation::TypedEventHandler<StreamSocketListener^, StreamSocketListenerConnectionReceivedEventArgs^>(
                std::bind(&SampleAnchorTcpServer::OnConnectionReceived, this, _1, _2)
                );
        m_socketServer = streamSocketListener;
    }
    else
    {
        OutputDebugString(L"Error: Stream socket listener not created.\n");
    }
}

接收连接时,使用客户端套接字连接来发送定位点数据。

void SampleAnchorTcpServer::OnConnectionReceived(StreamSocketListener^ listener, StreamSocketListenerConnectionReceivedEventArgs^ args)
{
    m_socketForClient = args->Socket;
    if (m_socketForClient != nullptr)
    {
        // In this example, when the client first connects, we catch it up to the current state of our anchor set.
        OutputToClientSocket(m_spatialAnchorHelper->GetAnchorMap());
    }
}

现在,我们可以开始发送包含导出的定位点数据的数据流。

void SampleAnchorTcpServer::OutputToClientSocket(IMap<String^, SpatialAnchor^>^ anchorsToSend)
{
    m_anchorTcpSocketStreamWriter = ref new DataWriter(m_socketForClient->OutputStream);
    OutputDebugString(L"Sending stream to client.\n");
    SendAnchorDataStream(anchorsToSend).then([this](task<bool> previousTask)
    {
        try
        {
            bool success = previousTask.get();
            if (success)
            {
                OutputDebugString(L"Anchor data sent!\n");
            }
            else
            {
                OutputDebugString(L"Error: Anchor data not sent.\n");
            }
        }
        catch (Exception^ exception)
        {
            HandleException(exception);
            OutputDebugString(L"Error: Anchor data was not sent.\n");
        }
    });
}

在发送流本身之前,必须先发送一个标头数据包。 此标头数据包的长度必须是固定的,并且必须指示字节的变量数组(定位点数据流)的长度;在此示例中,我们不会发送其他标头数据,因此标头的长度为 4 个字节,包含一个 32 位无符号整数。

Concurrency::task<bool> SampleAnchorTcpServer::SendAnchorDataLengthMessage(size_t dataStreamLength)
{
    unsigned int arrayLength = dataStreamLength;
    byte* data = reinterpret_cast<byte*>(&arrayLength);
    m_anchorTcpSocketStreamWriter->WriteBytes(Platform::ArrayReference<byte>(data, SampleAnchorTcpCommon::c_streamHeaderByteArrayLength));
    return create_task(m_anchorTcpSocketStreamWriter->StoreAsync()).then([this](unsigned int bytesStored)
    {
        if (bytesStored > 0)
        {
            OutputDebugString(L"Anchor data length stored in stream; Flushing stream.\n");
            return create_task(m_anchorTcpSocketStreamWriter->FlushAsync());
        }
        else
        {
            OutputDebugString(L"Error: Anchor data length not stored in stream.\n");
            return task_from_result<bool>(false);
        }
    });
}
Concurrency::task<bool> SampleAnchorTcpServer::SendAnchorDataStreamIMap<String^, SpatialAnchor^>^ anchorsToSend)
{
    return SpatialAnchorImportExportHelper::ExportAnchorDataAsync(
        &m_exportedAnchorStoreBytes,
        anchorsToSend
        ).then([this](bool anchorDataExported)
    {
        if (anchorDataExported)
        {
            const size_t arrayLength = m_exportedAnchorStoreBytes.size();
            if (arrayLength > 0)
            {
                OutputDebugString(L"Anchor data was exported; sending data stream length message.\n");
                return SendAnchorDataLengthMessage(arrayLength);
            }
        }
        OutputDebugString(L"Error: Anchor data was not exported.\n");
        // No data to send.
        return task_from_result<bool>(false);

将以字节为单位的流长度发送到客户端后,可以继续将数据流本身写入套接字流。 这会导致将定位点存储字节发送到客户端。

}).then([this](bool dataLengthSent)
    {
        if (dataLengthSent)
        {
            OutputDebugString(L"Data stream length message sent; writing exported anchor store bytes to stream.\n");
            m_anchorTcpSocketStreamWriter->WriteBytes(Platform::ArrayReference<byte>(&m_exportedAnchorStoreBytes[0], m_exportedAnchorStoreBytes.size()));
            return create_task(m_anchorTcpSocketStreamWriter->StoreAsync());
        }
        else
        {
            OutputDebugString(L"Error: Data stream length message not sent.\n");
            return task_from_result<size_t>(0);
        }
    }).then([this](unsigned int bytesStored)
    {
        if (bytesStored > 0)
        {
            PrintWstringToDebugConsole(
                std::to_wstring(bytesStored) +
                L" bytes of anchor data written and stored to stream; flushing stream.\n"
                );
        }
        else
        {
            OutputDebugString(L"Error: No anchor data bytes were written to the stream.\n");
        }
        return task_from_result<bool>(false);
    });
}

如本主题前面所述,我们必须准备好处理包含网络错误状态消息的异常。 对于意外的错误,我们可以按此处所示将异常信息写入调试控制台。 如果代码示例无法完成连接或者无法完成定位点数据发送,这些信息将为我们提供有关发生的问题的线索。

void SampleAnchorTcpServer::HandleException(Exception^ exception)
{
    PrintWstringToDebugConsole(
        std::wstring(L"Connection error: ") +
        exception->ToString()->Data() +
        L"\n"
        );
}

通过 TCP 使用 Windows::Networking::StreamSocket 来接收导出的定位点数据

首先必须连接到服务器。 此代码示例演示如何创建和配置 StreamSocket,以及如何创建可用于通过套接字连接获取网络数据的 DataReader。

注意:如果运行此示例代码,请确保在启动客户端之前配置并启动服务器

task<bool> SampleAnchorTcpClient::ConnectToServer()
{
    // Make a local copy to avoid races with Closed events.
    StreamSocket^ streamSocket = m_socketClient;
    // Have we connected yet?
    if (m_socketClient == nullptr)
    {
        OutputDebugString(L"Client is attempting to connect to server.\n");
        EndpointPair^ endpointPair = ref new EndpointPair(
            SampleAnchorTcpCommon::m_clientHost,
            SampleAnchorTcpCommon::m_tcpPort,
            SampleAnchorTcpCommon::m_serverHost,
            SampleAnchorTcpCommon::m_tcpPort
            );
        // Create the web socket connection.
        m_socketClient = ref new StreamSocket();
        // The client connects to the server.
        return create_task(m_socketClient->ConnectAsync(endpointPair, SocketProtectionLevel::PlainSocket)).then([this](task<void> previousTask)
        {
            try
            {
                // Try getting all exceptions from the continuation chain above this point.
                previousTask.get();
                m_anchorTcpSocketStreamReader = ref new DataReader(m_socketClient->InputStream);
                OutputDebugString(L"Client connected!\n");
                m_anchorTcpSocketStreamReader->InputStreamOptions = InputStreamOptions::ReadAhead;
                WaitForAnchorDataStream();
                return true;
            }
            catch (Exception^ exception)
            {
                if (exception->HResult == 0x80072741)
                {
                    // This code sample includes a very simple implementation of client/server
                    // endpoint detection: if the current instance tries to connect to itself,
                    // it is determined to be the server.
                    OutputDebugString(L"Starting up the server instance.\n");
                    // When we return false, we'll start up the server instead.
                    return false;
                }
                else if ((exception->HResult == 0x8007274c) || // connection timed out
                    (exception->HResult == 0x80072740)) // connection maxed at server end
                {
                    // If the connection timed out, try again.
                    ConnectToServer();
                }
                else if (exception->HResult == 0x80072741)
                {
                    // No connection is possible.
                }
                HandleException(exception);
                return true;
            }
        });
    }
    else
    {
        OutputDebugString(L"A StreamSocket connection to a server already exists.\n");
        return task_from_result<bool>(true);
    }
}

建立连接后,可以等待服务器发送数据。 为此,可以在流数据读取器上调用 LoadAsync。

收到的第一组字节应该始终是标头数据包,它指示定位点数据流字节长度,如上一部分所述。

void SampleAnchorTcpClient::WaitForAnchorDataStream()
{
    if (m_anchorTcpSocketStreamReader == nullptr)
    {
        // We have not connected yet.
        return;
    }
    OutputDebugString(L"Waiting for server message.\n");
    // Wait for the first message, which specifies the byte length of the string data.
    create_task(m_anchorTcpSocketStreamReader->LoadAsync(SampleAnchorTcpCommon::c_streamHeaderByteArrayLength)).then([this](unsigned int numberOfBytes)
    {
        if (numberOfBytes > 0)
        {
            OutputDebugString(L"Server message incoming.\n");
            return ReceiveAnchorDataLengthMessage();
        }
        else
        {
            OutputDebugString(L"0-byte async task received, awaiting server message again.\n");
            WaitForAnchorDataStream();
            return task_from_result<size_t>(0);
        }

...

task<size_t> SampleAnchorTcpClient::ReceiveAnchorDataLengthMessage()
{
    byte data[4];
    m_anchorTcpSocketStreamReader->ReadBytes(Platform::ArrayReference<byte>(data, SampleAnchorTcpCommon::c_streamHeaderByteArrayLength));
    unsigned int lengthMessageSize = *reinterpret_cast<unsigned int*>(data);
    if (lengthMessageSize > 0)
    {
        OutputDebugString(L"One or more anchors to be received.\n");
        return task_from_result<size_t>(lengthMessageSize);
    }
    else
    {
        OutputDebugString(L"No anchors to be received.\n");
        ConnectToServer();
    }
    return task_from_result<size_t>(0);
}

收到标头数据包后,我们便知道了预期有多少字节的定位点数据。 可以继续从流中读取这些字节。

}).then([this](size_t dataStreamLength)
    {
        if (dataStreamLength > 0)
        {
            std::wstring debugMessage = std::to_wstring(dataStreamLength);
            debugMessage += L" bytes of anchor data incoming.\n";
            OutputDebugString(debugMessage.c_str());
            // Prepare to receive the data stream in one or more pieces.
            m_anchorStreamLength = dataStreamLength;
            m_exportedAnchorStoreBytes.clear();
            m_exportedAnchorStoreBytes.resize(m_anchorStreamLength);
            OutputDebugString(L"Loading byte stream.\n");
            return ReceiveAnchorDataStream();
        }
        else
        {
            OutputDebugString(L"Error: Anchor data size not received.\n");
            ConnectToServer();
            return task_from_result<bool>(false);
        }
    });
}

下面是用于接收定位点数据流的代码。 同样,我们首先从流中加载字节;此操作可能需要一段时间才能完成,因为 StreamSocket 需要等待从网络接收这么多的字节。

加载操作完成后,我们可以读取这么多的字节。 如果收到的定位点数据流的字节数符合预期,则我们可以继续导入定位点数据;否则肯定是出现了某种错误。 例如,如果服务器实例在完成发送数据流之前终止,或者在客户端接收整个数据流之前网络出现故障,则就会发生这种情况。

task<bool> SampleAnchorTcpClient::ReceiveAnchorDataStream()
{
    if (m_anchorStreamLength > 0)
    {
        // First, we load the bytes from the network socket.
        return create_task(m_anchorTcpSocketStreamReader->LoadAsync(m_anchorStreamLength)).then([this](size_t bytesLoadedByStreamReader)
        {
            if (bytesLoadedByStreamReader > 0)
            {
                // Once the bytes are loaded, we can read them from the stream.
                m_anchorTcpSocketStreamReader->ReadBytes(Platform::ArrayReference<byte>(&m_exportedAnchorStoreBytes[0],
                    bytesLoadedByStreamReader));
                // Check status.
                if (bytesLoadedByStreamReader == m_anchorStreamLength)
                {
                    // The whole stream has arrived. We can process the data.
                    // Informational message of progress complete.
                    std::wstring infoMessage = std::to_wstring(bytesLoadedByStreamReader);
                    infoMessage += L" bytes read out of ";
                    infoMessage += std::to_wstring(m_anchorStreamLength);
                    infoMessage += L" total bytes; importing the data.\n";
                    OutputDebugStringW(infoMessage.c_str());
                    // Kick off a thread to wait for a new message indicating another incoming anchor data stream.
                    WaitForAnchorDataStream();
                    // Process the data for the stream we just received.
                    return SpatialAnchorImportExportHelper::ImportAnchorDataAsync(m_exportedAnchorStoreBytes, m_spatialAnchorHelper->GetAnchorMap());
                }
                else
                {
                    OutputDebugString(L"Error: Fewer than expected anchor data bytes were received.\n");
                }
            }
            else
            {
                OutputDebugString(L"Error: No anchor bytes were received.\n");
            }
            return task_from_result<bool>(false);
        });
    }
    else
    {
        OutputDebugString(L"Warning: A zero-length data buffer was sent.\n");
        return task_from_result<bool>(false);
    }
}

同样,我们必须准备好处理未知的网络错误。

void SampleAnchorTcpClient::HandleException(Exception^ exception)
{
    std::wstring error = L"Connection error: ";
    error += exception->ToString()->Data();
    error += L"\n";
    OutputDebugString(error.c_str());
}

就这么简单! 现在,你应该已有足够的信息来尝试定位通过网络收到的定位点。 同样请注意,客户端必须有足够的空间视觉跟踪数据才能成功定位定位点;如果不能马上定位,请先等待片刻时间。 如果仍然无法定位,请让服务器发送更多定位点,并使用网络通信来议定一个适合客户端的定位点。 可以通过下载 HolographicSpatialAnchorTransferSample,配置客户端和服务器 IP 并将其部署到客户端和服务器 HoloLens 设备来尝试这种做法。

另请参阅