共用方式為


DirectX 中的座標系統

注意事項

本文與舊版 WinRT 原生 API 相關。 對於新的原生應用程式項目,建議您使用 OpenXR API

坐標系統是 Windows Mixed Reality API 所提供的空間理解基礎。

現今的坐式 VR 或單一會議室 VR 裝置會為其追蹤的空間建立一個主要座標系統。 Mixed Reality 像 HoloLens 的裝置是針對大型未定義的環境所設計,當使用者四處走動時,裝置會探索並瞭解其周圍環境。 裝置會調整以持續改善使用者會議室的相關知識,但會導致座標系統在應用程式存留期內變更彼此的關聯性。 Windows Mixed Reality 支援各種裝置,範圍從坐式沉浸式頭戴裝置到世界鏈接的參考畫面。

注意事項

本文中的代碼段目前示範如何使用C++/CX,而不是如C++ 全像攝影項目範本中所使用的C++17 相容C++/WinRT。 這些概念相當於C++/WinRT 專案,但您必須翻譯程序代碼。

Windows 中的空間座標系統

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

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

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

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

這些物件所傳回的所有座標系統都是右手,其中 +y 向上,右側為 +x,而 +z 為回溯。 您可以藉由將左指或右手的手指指向正 x 方向,然後將手指捲動到正 y 方向,來記住正 Z 軸指向的方向。 您的縮圖指向或離開您的方向,是該座標系統的正 Z 軸指向的方向。 下圖顯示這兩個座標系統。

左側和右側座標系統
左側和右側座標系統

使用 SpatialLocator 類別可建立附加或靜止的參考框架,以根據 HoloLens 位置啟動載入 SpatialCoordinateSystem。 請繼續進行下一節,以深入瞭解此程式。

使用空間階段將全像投影放在世界中

不透明 Windows Mixed Reality 沉浸式頭戴裝置的座標系統是使用靜態 SpatialStageFrameOfReference::Current 屬性來存取。 此 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 屬性會取得座標系統,可讓您放置相對於錨點的內容,且會在裝置調整錨點的精確位置時套用加速。

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

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

建立全像攝影內容的 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。

以下說明如何將 SpatialAnchor 物件儲存在 SpatialAnchorStore 中。

當類別啟動時,我們會以異步方式要求 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,會將 Strings 的索引鍵值與 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;
   }

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

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

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

您需要自己的 SpatialAnchors 記憶體內部資料庫,才能將 String 與您建立的 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”) 來平滑一段時間的位置變更。 這比將全像投影鎖定到用戶注視更舒適。 容許沿著全像投影卷標的位置,也可讓我們藉由抑制移動來穩定全像投影。 如果我們未執行此動作,使用者會看到全像投影抖動,因為通常會被視為使用者頭部無法察覺的移動。

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 全像攝影應用程式範本隨附已為您建立的 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 會利用座標系統來取得表面網格的模型轉換。

另請參閱