DirectX 中的座標系統

注意

本文與舊版 WinRT 原生 API 相關。 針對新的原生應用程式專案,我們建議使用 OpenXR API

座標系統形成Windows Mixed Reality API 所提供的空間理解基礎。

現今的基座 VR 或單一房間 VR 裝置會為其追蹤空間建立一個主要座標系統。 Mixed Reality HoloLens 之類的裝置是專為大型未定義環境所設計,裝置會在使用者流覽時探索並瞭解其周圍。 裝置會適應持續改善使用者會議室的知識,但會導致協調系統在應用程式存留期內彼此變更其關聯性。 Windows Mixed Reality支援各種不同的裝置,範圍從基座沉浸式頭戴式裝置到世界附加的參考框架。

注意

本文中的程式碼片段目前示範如何使用 C++/CX,而不是 C++17 相容的 C++/WinRT,如 C++ 全像攝影專案範本中所示。 雖然您需要翻譯程式碼,但概念相當於 C++/WinRT 專案。

Windows 中的空間座標系統

用來推斷 Windows 中真實全局座標系統的核心類型是 SpatialCoordinateSystem。 此類型的實例代表任意座標系統,提供一種方法來取得轉換矩陣資料,讓您在兩個座標系統之間轉換,而不需要瞭解每個座標系統的詳細資料。

傳回空間資訊的方法會接受 SpatialCoordinateSystem 參數,讓您決定要傳回這些座標最有用的座標系統。 空間資訊會以使用者周圍點、光線或磁片區表示,而這些座標的單位一律會以公尺為單位。

SpatialCoordinateSystem 與其他座標系統具有動態關聯性,包括代表裝置位置的座標系統。 在任何時間點,裝置都可以找到一些座標系統,而不是其他座標系統。 對於大部分的座標系統,您的應用程式必須準備好處理其無法找到的期間。

您的應用程式不應該直接建立 SpatialCoordinateSystems,而是應該透過 Perception API 取用它們。 Perception API 中有三個主要座標系統來源,每個來源都對應至 [ 座標系統 ] 頁面上所述的概念:

這些物件傳回的所有座標系統都是右手,向右加上 +y、向右 +x 和 +z 向後。 您可以透過將左手或右手的手指指向正 x 方向,並將其捲動成正 Y 方向,來記住正 Z 軸指向哪個方向。 您拇指所指的方向,不論是指向您或指向您的反方向,即是該座標系統正 z 軸所指的方向。 下圖顯示這兩個座標系統。

左手和右手座標系統
左手和右手座標系統

使用 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殼層會在界限繪製柵欄,但您可能想要針對您自己的用途將可逐步執行的區域三角形化。 下列演算法可用來將階段三角形化。

空間階段三角形化的程式碼

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 全像攝影應用程式範本程式碼:

           // 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屬性會取得座標系統,可讓您在裝置調整錨點的精確位置時放置相對於錨點的內容,並套用 easing。

使用 RawCoordinateSystem 屬性和對應的 RawCoordinateSystemAdjusted 事件自行管理這些調整。

保存和共用空間錨點

您可以使用 SpatialAnchorStore 類別在本機保存 SpatialAnchorStore ,然後在相同 HoloLens 裝置上的未來應用程式會話中將其還原。

藉由使用 Azure Spatial Anchors,您可以從本機 SpatialAnchor 建立永久性雲端錨點,然後您的應用程式可以在多個 HoloLens、iOS 和 Android 裝置中找到此錨點。 藉由在多個裝置之間共用通用空間錨點,每個使用者都可以即時查看相對於相同實體位置中該錨點轉譯的內容。

您也可以使用 Azure Spatial Anchors 跨 HoloLens、iOS 和 Android 裝置進行非同步全像投影持續性。 藉由共用永久性雲端空間錨點,多個裝置可以觀察一段時間相同的保存全像投影,即使這些裝置同時不存在也一樣。

若要開始在 HoloLens 應用程式中建置共用體驗,請嘗試 5 分鐘的 Azure Spatial Anchors HoloLens 快速入門

使用 Azure Spatial Anchors 啟動並執行之後,您就可以 在 HoloLens 上建立和尋找錨點。 逐步解說也適用于 Android 和 iOS ,讓您在所有裝置上共用相同的錨點。

建立全像攝影內容的 SpatialAnchors

在此程式碼範例中,我們已修改 Windows 全像攝影應用程式範本,以在偵測到 按下 手勢時建立錨點。 然後,Cube 會在轉譯階段期間放在錨點。

由於協助程式類別支援多個錨點,因此我們可以放置想要使用此程式碼範例的 Cube 數目!

注意

錨點的識別碼是您在應用程式中控制的內容。 在此範例中,我們已根據目前儲存在應用程式錨點集合中的錨點數目,建立循序命名配置。

   // 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。

以下是如何在 SpatialAnchorStore 中儲存 SpatialAnchor物件。

類別啟動時,我們會以非同步方式要求 SpatialAnchorStore。 這牽涉到系統 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,可將字串的索引鍵值與 SpatialAnchors 的資料值產生關聯。 在我們的範例程式碼中,我們會將此儲存在可透過協助程式類別的公用函式存取的私用類別成員變數中。

   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;
   }

應用程式繼續時從錨點存放區載入內容

您可以將儲存的錨點從錨點存放區的 IMapView 傳輸至 App 繼續或隨時儲存在 SpatialAnchors 的記憶體內部資料庫,以還原錨點存放區中儲存的錨點。

若要從 SpatialAnchorStore 還原錨點,請將您感興趣的每個錨點還原至您自己的記憶體內部集合。

您需要自己的 SpatialAnchors 記憶體內部資料庫,才能將 Strings 與您建立的 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 全像攝影應用程式範本中變更的內容,以使用此 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);

取得空間指標姿勢,並遵循使用者的注視

我們希望我們的範例全像投影遵循使用者的 注視,類似于全像攝影殼層如何遵循使用者的注視。 為此,我們需要從相同的時間戳記取得 SpatialPointerPose。

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

此 SpatialPointerPose 具有根據 使用者目前標題放置全像投影所需的資訊。

為了方便使用者,我們會使用線性插補 (「lerp」) 來平滑一段時間內的位置變更。 這比將全像投影鎖定至其注視更熟悉。 Lerping 標籤與全像投影的位置也允許我們藉由抑制移動來穩定全像投影。 如果我們沒有這麼做,使用者就會看到全像投影抖動,因為通常認為使用者頭部的移動是無法察覺的移動。

StationaryQuadRenderer::P ositionHologram

   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::P ositionHologram

       // 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 會利用座標系統來取得表面網格的模型轉換。

另請參閱