DirectX 中的坐标系统

注意

本文与旧版 WinRT 原生 API 相关。 对于新的本机应用项目,建议使用 OpenXR API

坐标系是通过 Windows Mixed Reality API 来理解空间的基础。

如今的坐姿 VR 或单房间 VR 设备会为其所跟踪的空间建立一个主坐标系。 混合现实设备(如 HoloLens)是针对大型未定义环境设计的,设备会在用户四处走动时发现并了解其环境。 在不断了解用户的房间的过程中,设备会慢慢适应,但会导致坐标系在应用生命周期内改变相互关系。 Windows Mixed Reality 支持一系列设备,从坐姿沉浸式头戴显示设备到附加到世界的参照系设备,不一而足。

注意

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

Windows 中的空间坐标系

在 Windows 中,用于推断实际坐标系的核心类型为 SpatialCoordinateSystem。 此类型的实例可表示任意坐标系,方便获取转换矩阵数据,你可以使用后者在两个坐标系之间进行转换,而不必了解每个坐标系的详细信息。

返回空间信息的方法会接受 SpatialCoordinateSystem 参数,让你确定将这些坐标返回到其中是最有用的坐标系。 在用户的环境中,空间信息表示为点、射线或体积,这些坐标的单位将始终为米。

SpatialCoordinateSystem 与其他坐标系(包括那些表示设备位置的系统)存在动态关系。 在任何时候,设备都可以定位某些坐标系而不是其他坐标系。 对于大多数坐标系,应用必须准备好处理无法对这些坐标系进行定位的时间段。

应用程序不应直接创建 SpatialCoordinateSystem,而应通过感知 API 来使用 SpatialCoordinateSystem。 感知 API 中有三个主要的坐标系源,其中的每个源都映射到坐标系页上所述的一个概念:

这些对象返回的所有坐标系都是右手的,其中 +y 指向上,+x 指向右,+z 指向后。 通过将你的左手或右手手指指向正 x 方向,然后弯曲手指,将其指向正 y 方向,你可以记住正 z 轴指向的方向。 你的拇指指向的方向(无论指向你还是相反方向)就是正 z 轴为坐标系统所指的方向。 下面的图例显示了这两个坐标系统。

Left-hand and right-hand coordinate systems
左手和右手坐标系

使用 SpatialLocator 类创建一个附加的或静止的参照系,以根据 HoloLens 位置将其引导到 SpatialCoordinateSystem 中。 若要详细了解此过程,请继续阅读下一部分。

使用空间场地在世界中放置全息影像

使用静态 SpatialStageFrameOfReference::Current 属性访问不透明 Windows Mixed Reality 沉浸式头戴显示设备的坐标系。 此 API 提供:

  • 坐标系
  • 有关玩家是坐姿模式玩家还是走动模式玩家的信息
  • 玩家是走动模式玩家时四处走动的安全区域的边界
  • 指示头戴显示设备是否为定向设备。
  • 用于对空间场地进行更新的事件处理程序。

首先,我们获取空间场地并订阅其更新:

空间场地初始化代码

SpatialStageManager::SpatialStageManager(
    const std::shared_ptr<DX::DeviceResources>& deviceResources, 
    const std::shared_ptr<SceneController>& sceneController)
    : m_deviceResources(deviceResources), m_sceneController(sceneController)
{
    // Get notified when the stage is updated.
    m_spatialStageChangedEventToken = SpatialStageFrameOfReference::CurrentChanged +=
        ref new EventHandler<Object^>(std::bind(&SpatialStageManager::OnCurrentChanged, this, _1));

    // Make sure to get the current spatial stage.
    OnCurrentChanged(nullptr);
}

在 OnCurrentChanged 方法中,应用应该会检查空间场地并更新玩家体验。 在此示例中,我们对用户指定的场地边界和起始位置以及场地的视图范围和运动属性范围进行了可视化。 如果无法提供场地,我们还会回退到自己的静止坐标系。

空间场地更新代码

void SpatialStageManager::OnCurrentChanged(Object^ /*o*/)
{
    // The event notifies us that a new stage is available.
    // Get the current stage.
    m_currentStage = SpatialStageFrameOfReference::Current;

    // Clear previous content.
    m_sceneController->ClearSceneObjects();

    if (m_currentStage != nullptr)
    {
        // Obtain stage geometry.
        auto stageCoordinateSystem = m_currentStage->CoordinateSystem;
        auto boundsVertexArray = m_currentStage->TryGetMovementBounds(stageCoordinateSystem);

        // Visualize the area where the user can move around.
        std::vector<float3> boundsVertices;
        boundsVertices.resize(boundsVertexArray->Length);
        memcpy(boundsVertices.data(), boundsVertexArray->Data, boundsVertexArray->Length * sizeof(float3));
        std::vector<unsigned short> indices = TriangulatePoints(boundsVertices);
        m_stageBoundsShape =
            std::make_shared<SceneObject>(
                    m_deviceResources,
                    reinterpret_cast<std::vector<XMFLOAT3>&>(boundsVertices),
                    indices,
                    XMFLOAT3(DirectX::Colors::SeaGreen),
                    stageCoordinateSystem);
        m_sceneController->AddSceneObject(m_stageBoundsShape);

        // In this sample, we draw a visual indicator for some spatial stage properties.
        // If the view is forward-only, the indicator is a half circle pointing forward - otherwise, it
        // is a full circle.
        // If the user can walk around, the indicator is blue. If the user is seated, it is red.

        // The indicator is rendered at the origin - which is where the user declared the center of the
        // stage to be during setup - above the plane of the stage bounds object.
        float3 visibleAreaCenter = float3(0.f, 0.001f, 0.f);

        // Its shape depends on the look direction range.
        std::vector<float3> visibleAreaIndicatorVertices;
        if (m_currentStage->LookDirectionRange == SpatialLookDirectionRange::ForwardOnly)
        {
            // Half circle for forward-only look direction range.
            visibleAreaIndicatorVertices = CreateCircle(visibleAreaCenter, 0.25f, 9, XM_PI);
        }
        else
        {
            // Full circle for omnidirectional look direction range.
            visibleAreaIndicatorVertices = CreateCircle(visibleAreaCenter, 0.25f, 16, XM_2PI);
        }

        // Its color depends on the movement range.
        XMFLOAT3 visibleAreaColor;
        if (m_currentStage->MovementRange == SpatialMovementRange::NoMovement)
        {
            visibleAreaColor = XMFLOAT3(DirectX::Colors::OrangeRed);
        }
        else
        {
            visibleAreaColor = XMFLOAT3(DirectX::Colors::Aqua);
        }

        std::vector<unsigned short> visibleAreaIndicatorIndices = TriangulatePoints(visibleAreaIndicatorVertices);

        // Visualize the look direction range.
        m_stageVisibleAreaIndicatorShape =
            std::make_shared<SceneObject>(
                    m_deviceResources,
                    reinterpret_cast<std::vector<XMFLOAT3>&>(visibleAreaIndicatorVertices),
                    visibleAreaIndicatorIndices,
                    visibleAreaColor,
                    stageCoordinateSystem);
        m_sceneController->AddSceneObject(m_stageVisibleAreaIndicatorShape);
    }
    else
    {
        // No spatial stage was found.
        // Fall back to a stationary coordinate system.
        auto locator = SpatialLocator::GetDefault();
        if (locator)
        {
            m_stationaryFrameOfReference = locator->CreateStationaryFrameOfReferenceAtCurrentLocation();

            // Render an indicator, so that we know we fell back to a mode without a stage.
            std::vector<float3> visibleAreaIndicatorVertices;
            float3 visibleAreaCenter = float3(0.f, -2.0f, 0.f);
            visibleAreaIndicatorVertices = CreateCircle(visibleAreaCenter, 0.125f, 16, XM_2PI);
            std::vector<unsigned short> visibleAreaIndicatorIndices = TriangulatePoints(visibleAreaIndicatorVertices);
            m_stageVisibleAreaIndicatorShape =
                std::make_shared<SceneObject>(
                    m_deviceResources,
                    reinterpret_cast<std::vector<XMFLOAT3>&>(visibleAreaIndicatorVertices),
                    visibleAreaIndicatorIndices,
                    XMFLOAT3(DirectX::Colors::LightSlateGray),
                    m_stationaryFrameOfReference->CoordinateSystem);
            m_sceneController->AddSceneObject(m_stageVisibleAreaIndicatorShape);
        }
    }
}

以顺时针顺序提供定义场地边界的顶点集。 当用户靠近边界时,Windows Mixed Reality shell 会在边界处绘制围栏,但你可能希望出于你自己的目的而将可走动区域三角形化。 以下算法可用于将场地三角形化。

空间场地三角形化代码

std::vector<unsigned short> SpatialStageManager::TriangulatePoints(std::vector<float3> const& vertices)
{
    size_t const& vertexCount = vertices.size();

    // Segments of the shape are removed as they are triangularized.
    std::vector<bool> vertexRemoved;
    vertexRemoved.resize(vertexCount, false);
    unsigned int vertexRemovedCount = 0;

    // Indices are used to define triangles.
    std::vector<unsigned short> indices;

    // Decompose into convex segments.
    unsigned short currentVertex = 0;
    while (vertexRemovedCount < (vertexCount - 2))
    {
        // Get next triangle:
        // Start with the current vertex.
        unsigned short index1 = currentVertex;

        // Get the next available vertex.
        unsigned short index2 = index1 + 1;

        // This cycles to the next available index.
        auto CycleIndex = [=](unsigned short indexToCycle, unsigned short stopIndex)
        {
            // Make sure the index does not exceed bounds.
            if (indexToCycle >= unsigned short(vertexCount))
            {
                indexToCycle -= unsigned short(vertexCount);
            }

            while (vertexRemoved[indexToCycle])
            {
                // If the vertex is removed, go to the next available one.
                ++indexToCycle;

                // Make sure the index does not exceed bounds.
                if (indexToCycle >= unsigned short(vertexCount))
                {
                    indexToCycle -= unsigned short(vertexCount);
                }

                // Prevent cycling all the way around.
                // Should not be needed, as we limit with the vertex count.
                if (indexToCycle == stopIndex)
                {
                    break;
                }
            }

            return indexToCycle;
        };
        index2 = CycleIndex(index2, index1);

        // Get the next available vertex after that.
        unsigned short index3 = index2 + 1;
        index3 = CycleIndex(index3, index1);

        // Vertices that may define a triangle inside the 2D shape.
        auto& v1 = vertices[index1];
        auto& v2 = vertices[index2];
        auto& v3 = vertices[index3];

        // If the projection of the first segment (in clockwise order) onto the second segment is 
        // positive, we know that the clockwise angle is less than 180 degrees, which tells us 
        // that the triangle formed by the two segments is contained within the bounding shape.
        auto v2ToV1 = v1 - v2;
        auto v2ToV3 = v3 - v2;
        float3 normalToV2ToV3 = { -v2ToV3.z, 0.f, v2ToV3.x };
        float projectionOntoNormal = dot(v2ToV1, normalToV2ToV3);
        if (projectionOntoNormal >= 0)
        {
            // Triangle is contained within the 2D shape.

            // Remove peak vertex from the list.
            vertexRemoved[index2] = true;
            ++vertexRemovedCount;

            // Create the triangle.
            indices.push_back(index1);
            indices.push_back(index2);
            indices.push_back(index3);

            // Continue on to the next outer triangle.
            currentVertex = index3;
        }
        else
        {
            // Triangle is a cavity in the 2D shape.
            // The next triangle starts at the inside corner.
            currentVertex = index2;
        }
    }

    indices.shrink_to_fit();
    return indices;
}

使用静止参照系在世界中放置全息影像

SpatialStationaryFrameOfReference 类表示一个参照系,该参照系在用户四处移动时相对于用户的环境保持静止。 此参照系优先确保坐标在设备附近保持稳定。 SpatialStationaryFrameOfReference 的一项关键用途是在渲染全息影像时充当渲染引擎内的基础世界坐标系。

若要获取 SpatialStationaryFrameOfReference,请使用 SpatialLocator 类并调用 CreateStationaryFrameOfReferenceAtCurrentLocation

在 Windows Holographic 应用模板代码中:

           // The simplest way to render world-locked holograms is to create a stationary reference frame
           // when the app is launched. This is roughly analogous to creating a "world" coordinate system
           // with the origin placed at the device's position as the app is launched.
           referenceFrame = locator.CreateStationaryFrameOfReferenceAtCurrentLocation();
  • 静止参照系旨在提供相对于总体空间的最适位置。 该参照系中的各个位置都允许略微偏移。 这是正常现象,因为设备会了解有关环境的详细信息。
  • 需要精确放置单独的全息影像时,应使用 SpatialAnchor 将各个全息影像定位到现实世界中的某个位置,例如用户指示的特别感兴趣的点。 定位点位置不会偏移,但可以更正;定位点将使用更正后在下一帧中开始的更正位置。

使用空间定位点在世界中放置全息影像

空间定位点是在现实世界的特定位置放置全息影像的绝佳方式,系统可确保定位点在一段时间内保持不变。 本主题说明如何创建和使用定位点,以及如何使用定位点数据。

你可以在所选的 SpatialCoordinateSystem 内的任何位置和方向创建 SpatialAnchor。 设备现在必须能够找到该坐标系,而且系统不得达到其空间定位点限制。

定义后,SpatialAnchor 的坐标系会不断调整,以保持其初始位置的精确位置和方向。 然后,你可以使用此 SpatialAnchor 来渲染在该确切位置的用户环境中显示为固定不动的全息影像。

使定位点保持在原来位置的调整效果会随着与定位点的距离的增加而放大。 如果某个定位点距离其原始位置大约 3 米以上,则应避免相对于该定位点来渲染内容。

CoordinateSystem 属性会获取一个坐标系,你可以使用该系统相对于定位点来放置内容,并在设备调整定位点的确切位置时应用缓动。

使用 RawCoordinateSystem 属性和相应的 RawCoordinateSystemAdjusted 事件来自行管理这些调整。

保留并共享空间定位点

可以使用 SpatialAnchorStore 类在本地保留 SpatialAnchor,然后在同一 HoloLens 设备上的未来应用会话中恢复它。

使用 Azure 空间定位点,可以从本地 SpatialAnchor 创建持久的云定位点,让应用可以跨多个 HoloLens、iOS 和 Android 设备进行定位。 通过在多个设备之间共享公用空间定位点,每个用户都可以实时查看相对于同一物理位置中的该定位点渲染的内容。

还可以使用 Azure 空间定位点在 HoloLens、iOS 和 Android 设备上实现异步全息影像持久性。 通过共享持久的云空间定位点,多个设备可以随着时间推移观察相同的持久全息影像,即使这些设备没有同时出现,也是如此。

若要开始在 HoloLens 应用中构建共享体验,请参阅 Azure 空间定位点 HoloLens 快速入门(只需 5 分钟)。

启动并运行 Azure 空间定位点后,可以在 HoloLens 中创建和定位定位点。 演练也适用于 Android 和 iOS,使你能够在所有设备上共享相同的定位点。

为全息内容创建 SpatialAnchor

对于此代码示例,我们修改了 Windows Holographic 应用模板,使之能够在检测到“按下”手势时创建定位点。 然后,在渲染过程中将立方体放在定位点处。

由于帮助程序类支持多个定位点,因此,我们可以放置所需数量的立方体,以使用此代码示例!

注意

定位点的 ID 是你在应用中控制的内容。 在此示例中,我们创建了一个命名方案,该方案在顺序上基于当前存储在应用的定位点集合中的定位点数。

   // Check for new input state since the last frame.
   SpatialInteractionSourceState^ pointerState = m_spatialInputHandler->CheckForInput();
   if (pointerState != nullptr)
   {
       // Try to get the pointer pose relative to the SpatialStationaryReferenceFrame.
       SpatialPointerPose^ pointerPose = pointerState->TryGetPointerPose(currentCoordinateSystem);
       if (pointerPose != nullptr)
       {
           // When a Pressed gesture is detected, the anchor will be created two meters in front of the user.

           // Get the gaze direction relative to the given coordinate system.
           const float3 headPosition = pointerPose->Head->Position;
           const float3 headDirection = pointerPose->Head->ForwardDirection;

           // The anchor position in the StationaryReferenceFrame.
           static const float distanceFromUser = 2.0f; // meters
           const float3 gazeAtTwoMeters = headPosition + (distanceFromUser * headDirection);

           // Create the anchor at position.
           SpatialAnchor^ anchor = SpatialAnchor::TryCreateRelativeTo(currentCoordinateSystem, gazeAtTwoMeters);

           if ((anchor != nullptr) && (m_spatialAnchorHelper != nullptr))
           {
               // In this example, we store the anchor in an IMap.
               auto anchorMap = m_spatialAnchorHelper->GetAnchorMap();

               // Create an identifier for the anchor.
               String^ id = ref new String(L"HolographicSpatialAnchorStoreSample_Anchor") + anchorMap->Size;

               anchorMap->Insert(id->ToString(), anchor);
           }
       }
   }

异步加载和缓存 SpatialAnchorStore

我们来看一下如何编写有助于处理此持久性的 SampleSpatialAnchorHelper 类,其中包括:

  • 存储一系列按 Platform::String 键进行索引的内存中定位点。
  • 从系统的 SpatialAnchorStore 加载与本地内存中集合保持分离的定位点。
  • 将本地内存中集合的定位点保存到 SpatialAnchorStore(当应用选择这样做时)。

下面介绍了如何将 SpatialAnchor 对象保存在 SpatialAnchorStore 中。

当此类启动时,我们以异步方式请求 SpatialAnchorStore。 这涉及到 API 加载定位点存储时的系统 I/O,此 API 设置为异步 API,因此 I/O 是非阻塞性的。

   // Request the spatial anchor store, which is the WinRT object that will accept the imported anchor data.
   return create_task(SpatialAnchorManager::RequestStoreAsync())
       .then([](task<SpatialAnchorStore^> previousTask)
   {
       std::shared_ptr<SampleSpatialAnchorHelper> newHelper = nullptr;

       try
       {
           SpatialAnchorStore^ anchorStore = previousTask.get();

           // Once the SpatialAnchorStore has been loaded by the system, we can create our helper class.

           // Using "new" to access private constructor
           newHelper = std::shared_ptr<SampleSpatialAnchorHelper>(new SampleSpatialAnchorHelper(anchorStore));

           // Now we can load anchors from the store.
           newHelper->LoadFromAnchorStore();
       }
       catch (Exception^ exception)
       {
           PrintWstringToDebugConsole(
               std::wstring(L"Exception while loading the anchor store: ") +
               exception->Message->Data() +
               L"\n"
               );
       }

       // Return the initialized class instance.
       return newHelper;
   });

你将获得一个可用于保存定位点的 SpatialAnchorStore。 这是一个 IMapView,用于将字符串形式的键值与属于 SpatialAnchor 的数据值关联。 在示例代码中,我们将它存储在私有类成员变量中,该变量可通过帮助程序类的公共函数进行访问。

   SampleSpatialAnchorHelper::SampleSpatialAnchorHelper(SpatialAnchorStore^ anchorStore)
   {
       m_anchorStore = anchorStore;
       m_anchorMap = ref new Platform::Collections::Map<String^, SpatialAnchor^>();
   }

注意

不要忘记挂接这些暂停/恢复事件以保存和加载定位点存储。

   void HolographicSpatialAnchorStoreSampleMain::SaveAppState()
   {
       // For example, store information in the SpatialAnchorStore.
       if (m_spatialAnchorHelper != nullptr)
       {
           m_spatialAnchorHelper->TrySaveToAnchorStore();
       }
   }
   void HolographicSpatialAnchorStoreSampleMain::LoadAppState()
   {
       // For example, load information from the SpatialAnchorStore.
       LoadAnchorStore();
   }

将内容保存到定位点存储

当系统挂起应用时,需要将空间定位点保存到定位点存储。 还可以选择在其他时间将定位点保存到定位点存储,因为你发现它是应用的实现所必需的。

准备尝试将内存中定位点保存到 SpatialAnchorStore 时,可以循环访问集合并尝试保存每个定位点。

   // TrySaveToAnchorStore: Stores all anchors from memory into the app's anchor store.
   //
   // For each anchor in memory, this function tries to store it in the app's AnchorStore. The operation will fail if
   // the anchor store already has an anchor by that name.
   //
   bool SampleSpatialAnchorHelper::TrySaveToAnchorStore()
   {
       // This function returns true if all the anchors in the in-memory collection are saved to the anchor
       // store. If zero anchors are in the in-memory collection, we will still return true because the
       // condition has been met.
       bool success = true;

       // If access is denied, 'anchorStore' will not be obtained.
       if (m_anchorStore != nullptr)
       {
           for each (auto& pair in m_anchorMap)
           {
               auto const& id = pair->Key;
               auto const& anchor = pair->Value;

               // Try to save the anchors.
               if (!m_anchorStore->TrySave(id, anchor))
               {
                   // This may indicate the anchor ID is taken, or the anchor limit is reached for the app.
                   success=false;
               }
           }
       }

       return success;
   }

应用恢复时从定位点存储加载内容

可以在应用恢复时将 AnchorStore 中保存的定位点从定位点存储的 IMapView 传输到你自己的 SpatialAnchors 内存中数据库,以还原这些定位点,也可以随时这样做。

若要从 SpatialAnchorStore 还原定位点,请将感兴趣的每个定位点还原到你自己的内存中集合。

需要你自己的 SpatialAnchors 内存中数据库,以将字符串与创建的 SpatialAnchors 关联。 在示例代码中,我们选择使用 Windows::Foundation::Collections::IMap 来存储定位点,从而轻松地对 SpatialAnchorStore 使用相同的键和数据值。

   // This is an in-memory anchor list that is separate from the anchor store.
   // These anchors may be used, reasoned about, and so on before committing the collection to the store.
   Windows::Foundation::Collections::IMap<Platform::String^, Windows::Perception::Spatial::SpatialAnchor^>^ m_anchorMap;

注意

还原的定位点可能无法立刻找到。 例如,它可能是某个单独房间中的定位点,或者根本就不在同一个建筑物中。 在使用从 AnchorStore 检索的定位点之前,应测试它们是否可定位。


注意

在此示例代码中,我们从 AnchorStore 检索所有定位点。 这不是一项要求;应用也可以通过使用对你的实现有意义的字符串键值来选取和选择特定的定位点子集。

   // LoadFromAnchorStore: Loads all anchors from the app's anchor store into memory.
   //
   // The anchors are stored in memory using an IMap, which stores anchors using a string identifier. Any string can be used as
   // the identifier; it can have meaning to the app, such as "Game_Leve1_CouchAnchor," or it can be a GUID that is generated
   // by the app.
   //
   void SampleSpatialAnchorHelper::LoadFromAnchorStore()
   {
       // If access is denied, 'anchorStore' will not be obtained.
       if (m_anchorStore != nullptr)
       {
           // Get all saved anchors.
           auto anchorMapView = m_anchorStore->GetAllSavedAnchors();
           for each (auto const& pair in anchorMapView)
           {
               auto const& id = pair->Key;
               auto const& anchor = pair->Value;
               m_anchorMap->Insert(id, anchor);
           }
       }
   }

根据需要清除定位点存储

有时,需要清除应用状态并写入新数据。 下面介绍如何使用 SpatialAnchorStore 来这样做。

使用帮助程序类,几乎不需要包装 Clear 函数。 我们选择在示例实现中这样做,因为帮助程序类有责任拥有 SpatialAnchorStore 实例。

   // ClearAnchorStore: Clears the AnchorStore for the app.
   //
   // This function clears the AnchorStore. It has no effect on the anchors stored in memory.
   //
   void SampleSpatialAnchorHelper::ClearAnchorStore()
   {
       // If access is denied, 'anchorStore' will not be obtained.
       if (m_anchorStore != nullptr)
       {
           // Clear all anchors from the store.
           m_anchorStore->Clear();
       }
   }

示例:将定位点坐标系与静止参照系坐标系相关联

假设你有一个定位点,想要将定位点的坐标系中的内容关联到已用于其他内容的 SpatialStationaryReferenceFrame。 可以使用 TryGetTransformTo 从定位点的坐标系转换为静止参照系的坐标系:

   // In this code snippet, someAnchor is a SpatialAnchor^ that has been initialized and is valid in the current environment.
   float4x4 anchorSpaceToCurrentCoordinateSystem;
   SpatialCoordinateSystem^ anchorSpace = someAnchor->CoordinateSystem;
   const auto tryTransform = anchorSpace->TryGetTransformTo(currentCoordinateSystem);
   if (tryTransform != nullptr)
   {
       anchorSpaceToCurrentCoordinateSystem = tryTransform->Value;
   }

此过程在两个方面非常有用:

  1. 它告诉你两个参照系是否可以相互理解;
  2. 如果可以,则它会为你提供一个转换,让你直接从一个坐标系转换到另一个坐标系。

你可以根据此信息来了解两个参照系之间的对象的空间关系。

对于渲染,通常可以根据对象的原始参照系或定位点对对象进行分组,以获得更好的结果。 针对每个组执行单独的绘图轮次。 对于最初使用同一坐标系来创建模型转换的对象,视图矩阵更准确。

使用设备附加的参照系创建全息影像

有时候,你想要渲染始终附加到设备位置的全息影像,例如,有时候,设备只能确定其在空间中的方向,而不能确定其在空间中的位置,则在面板中显示调试信息或信息性消息。 为此,我们使用附加的参照系。

SpatialLocatorAttachedFrameOfReference 类定义相对于设备而不是现实世界的坐标系。 此参照系有一个相对于用户环境的固定前进方向,该前进方向指向创建参照系时用户所面向的方向。 此后,此参照系内的所有方向都是相对于该固定前进方向的,即使用户旋转设备。

对于 HoloLens,此参照系的坐标系的原点位于用户头部的旋转中心,因此其位置不受头部旋转的影响。 应用可以指定相对于此点的偏移量,以将全息影像定位在用户前面。

若要获取 SpatialLocatorAttachedFrameOfReference,请使用 SpatialLocator 类并调用 CreateAttachedFrameOfReferenceAtCurrentHeading。

这适用于全系列的 Windows Mixed Reality 设备。

使用附加到设备的参照系

以下部分讨论在 Windows Holographic 应用模板中更改的内容,更改目的是使用此 API 启用设备附加的参照系。 这个“附加的”全息影像可与静止的或锚定的全息影像一起使用。当设备暂时无法找到其在世界中的位置时,也可使用它们。

首先,我们更改了模板以存储 SpatialLocatorAttachedFrameOfReference 而不是 SpatialStationaryFrameOfReference:

在 HolographicTagAlongSampleMain.h 中

   // A reference frame attached to the holographic camera.
   Windows::Perception::Spatial::SpatialLocatorAttachedFrameOfReference^   m_referenceFrame;

在 HolographicTagAlongSampleMain.cpp 中

   // In this example, we create a reference frame attached to the device.
   m_referenceFrame = m_locator->CreateAttachedFrameOfReferenceAtCurrentHeading();

在更新期间,我们现在会通过帧预测在获取的时间戳处获取坐标系。

   // Next, we get a coordinate system from the attached frame of reference that is
   // associated with the current frame. Later, this coordinate system is used for
   // for creating the stereo view matrices when rendering the sample content.
   SpatialCoordinateSystem^ currentCoordinateSystem =
       m_referenceFrame->GetStationaryCoordinateSystemAtTimestamp(prediction->Timestamp);

获取空间指针姿势,然后跟随用户的视线

我们想要示例全息影像跟随用户的视线,类似于全息 shell 跟随用户视线的情况。 为此,我们需要从同一时间戳获取 SpatialPointerPose。

SpatialPointerPose^ pose = SpatialPointerPose::TryGetAtTimestamp(currentCoordinateSystem, prediction->Timestamp);

此 SpatialPointerPose 有根据用户的当前前进方向来定位全息影像所需的信息。

为了提高用户舒适度,我们使用线性内插法(“lerp”)来抚平一段时间内的位置变化。 对于用户来说,这比将全息影像锁定到其视线更舒适。 lerp 跟随全息影像的位置还可以通过抑制运动来稳定全息影像。 如果我们不进行这样的抑制,用户会看到全息影像抖动,因为通常认为用户头部的运动不可感知。

在 StationaryQuadRenderer::PositionHologram 中

   const float& dtime = static_cast<float>(timer.GetElapsedSeconds());

   if (pointerPose != nullptr)
   {
       // Get the gaze direction relative to the given coordinate system.
       const float3 headPosition  = pointerPose->Head->Position;
       const float3 headDirection = pointerPose->Head->ForwardDirection;

       // The tag-along hologram follows a point 2.0m in front of the user's gaze direction.
       static const float distanceFromUser = 2.0f; // meters
       const float3 gazeAtTwoMeters = headPosition + (distanceFromUser * headDirection);

       // Lerp the position, to keep the hologram comfortably stable.
       auto lerpedPosition = lerp(m_position, gazeAtTwoMeters, dtime * c_lerpRate);

       // This will be used as the translation component of the hologram's
       // model transform.
       SetPosition(lerpedPosition);
   }

注意

使用调试面板时,可以选择将全息影像重新定位到一侧,稍微偏离正中线,这样它就不会阻碍你的视线。 下面是一个展示如何执行该操作的示例。

对于 StationaryQuadRenderer::PositionHologram

       // If you're making a debug view, you might not want the tag-along to be directly in the
       // center of your field of view. Use this code to position the hologram to the right of
       // the user's gaze direction.
       /*
       const float3 offset = float3(0.13f, 0.0f, 0.f);
       static const float distanceFromUser = 2.2f; // meters
       const float3 gazeAtTwoMeters = headPosition + (distanceFromUser * (headDirection + offset));
       */

旋转全息影像,使之面向相机

定位全息影像(在此示例中为四边形)还不够,还必须旋转对象,使之面向用户。 这种旋转发生在世界空间中,因为这种类型的公告板使得全息影像始终是用户环境的一部分。 由于全息影像锁定到显示方向,因此视区公告板不够舒适;在这种情况下,还必须在左右视图矩阵之间进行内插,以进行不会中断立体渲染的视图空间公告板转换。 在这里,我们在 X 轴和 Z 轴进行旋转,使对象面向用户。

在 StationaryQuadRenderer::Update 中

   // Seconds elapsed since previous frame.
   const float& dTime = static_cast<float>(timer.GetElapsedSeconds());

   // Create a direction normal from the hologram's position to the origin of person space.
   // This is the z-axis rotation.
   XMVECTOR facingNormal = XMVector3Normalize(-XMLoadFloat3(&m_position));

   // Rotate the x-axis around the y-axis.
   // This is a 90-degree angle from the normal, in the xz-plane.
   // This is the x-axis rotation.
   XMVECTOR xAxisRotation = XMVector3Normalize(XMVectorSet(XMVectorGetZ(facingNormal), 0.f, -XMVectorGetX(facingNormal), 0.f));

   // Create a third normal to satisfy the conditions of a rotation matrix.
   // The cross product  of the other two normals is at a 90-degree angle to
   // both normals. (Normalize the cross product to avoid floating-point math
   // errors.)
   // Note how the cross product will never be a zero-matrix because the two normals
   // are always at a 90-degree angle from one another.
   XMVECTOR yAxisRotation = XMVector3Normalize(XMVector3Cross(facingNormal, xAxisRotation));

   // Construct the 4x4 rotation matrix.

   // Rotate the quad to face the user.
   XMMATRIX rotationMatrix = XMMATRIX(
       xAxisRotation,
       yAxisRotation,
       facingNormal,
       XMVectorSet(0.f, 0.f, 0.f, 1.f)
       );

   // Position the quad.
   const XMMATRIX modelTranslation = XMMatrixTranslationFromVector(XMLoadFloat3(&m_position));

   // The view and projection matrices are provided by the system; they are associated
   // with holographic cameras, and updated on a per-camera basis.
   // Here, we provide the model transform for the sample hologram. The model transform
   // matrix is transposed to prepare it for the shader.
   XMStoreFloat4x4(&m_modelConstantBufferData.model, XMMatrixTranspose(rotationMatrix * modelTranslation));

渲染附加的全息影像

对于此示例,我们还选择在 SpatialLocatorAttachedReferenceFrame 的坐标系中渲染全息影像,这是定位过全息影像的位置。 (如果已决定使用另一个坐标系进行渲染,则需从设备附加的参照系的坐标系转换为该坐标系。)

在 HolographicTagAlongSampleMain::Render 中

   // The view and projection matrices for each holographic camera will change
   // every frame. This function refreshes the data in the constant buffer for
   // the holographic camera indicated by cameraPose.
   pCameraResources->UpdateViewProjectionBuffer(
       m_deviceResources,
       cameraPose,
       m_referenceFrame->GetStationaryCoordinateSystemAtTimestamp(prediction->Timestamp)
       );

就这么简单! 全息影像现在会“追踪”用户视线方向前面 2 米的位置。

注意

此示例还加载其他内容 - 请参阅 StationaryQuadRenderer.cpp。

处理跟踪丢失

当设备无法在环境中定位自身时,应用会出现“跟踪丢失”的情况。 Windows Mixed Reality 应用应该能够处理对位置跟踪系统造成的此类中断。 通过使用默认 SpatialLocator 上的 LocatabilityChanged 事件,可以观察到这些中断并创建响应。

在 AppMain::SetHolographicSpace 中

   // Be able to respond to changes in the positional tracking state.
   m_locatabilityChangedToken =
       m_locator->LocatabilityChanged +=
           ref new Windows::Foundation::TypedEventHandler<SpatialLocator^, Object^>(
               std::bind(&HolographicApp1Main::OnLocatabilityChanged, this, _1, _2)
               );

应用在收到 LocatabilityChanged 事件时,可以根据需要更改行为。 例如,在 PositionalTrackingInhibited 状态中,应用可能会暂停正常操作,渲染一个会显示警告消息的跟随全息影像

Windows Holographic 应用模板附带一个已为你创建的 LocatabilityChanged 处理程序。 默认情况下,当位置跟踪功能不可用时,此处理程序会在调试控制台中显示警告。 可以将代码添加到此处理程序,以便根据需要从应用提供响应。

在 AppMain.cpp 中

   void HolographicApp1Main::OnLocatabilityChanged(SpatialLocator^ sender, Object^ args)
   {
       switch (sender->Locatability)
       {
       case SpatialLocatability::Unavailable:
           // Holograms cannot be rendered.
           {
               String^ message = L"Warning! Positional tracking is " +
                                           sender->Locatability.ToString() + L".\n";
               OutputDebugStringW(message->Data());
           }
           break;

       // In the following three cases, it is still possible to place holograms using a
       // SpatialLocatorAttachedFrameOfReference.
       case SpatialLocatability::PositionalTrackingActivating:
           // The system is preparing to use positional tracking.

       case SpatialLocatability::OrientationOnly:
           // Positional tracking has not been activated.

       case SpatialLocatability::PositionalTrackingInhibited:
           // Positional tracking is temporarily inhibited. User action may be required
           // in order to restore positional tracking.
           break;

       case SpatialLocatability::PositionalTrackingActive:
           // Positional tracking is active. World-locked content can be rendered.
           break;
       }
   }

空间映射

空间映射 API 使用坐标系获取曲面网格的模型转换。

另请参阅