共用方式為


教學課程:將 遠端轉譯 整合到 HoloLens 全像攝影應用程式中

在本教學課程中,您將了解:

  • 使用 Visual Studio 建立可部署至 HoloLens 的全像攝影應用程式
  • 新增必要的代碼段和項目設定,以結合本機轉譯與遠端轉譯的內容

本教學課程著重於將必要的位新增至原生Holographic App範例,以結合本機轉譯與 Azure 遠端轉譯。 此應用程式中唯一的狀態意見反應類型是透過Visual Studio內的偵錯輸出面板,因此建議從Visual Studio內部啟動範例。 新增適當的應用程式內意見反應超出此範例的範圍,因為從頭開始建置動態文字面板牽涉到許多程式代碼撰寫。 良好的起點是類別StatusDisplay,這是 GitHub 上遠端播放程式範例專案的一部分。 事實上,本教學課程的預裝版本會使用該類別的本地副本。

提示

ARR 範例存放庫包含本教學課程的結果,做為準備使用的Visual Studio專案。 它也會透過UI類別 StatusDisplay擴充適當的錯誤和狀態報告。 在教學課程中,所有 ARR 特定新增專案的範圍都是 #ifdef USE_REMOTE_RENDERING / #endif,因此很容易識別 遠端轉譯 新增專案。

必要條件

在本教學課程中,您需要:

建立新的全像攝影應用程式範例

在第一個步驟中,我們會建立股票範例,這是 遠端轉譯 整合的基礎。 開啟 Visual Studio 並選取 [建立新專案],然後搜尋 [全像攝影 DirectX 11 應用程式 (通用 Windows) (C++/WinRT)]

建立新專案

輸入您選擇的專案名稱,選擇路徑,然後選取 [建立] 按鈕。 在新專案中,將組態切換為 “Debug / ARM64”。 您現在應該能夠編譯並部署至連線的 HoloLens 2 裝置。 如果您在 HoloLens 上執行它,您應該會在您前面看到旋轉的立方體。

透過 NuGet 新增 遠端轉譯 相依性

新增 遠端轉譯 功能的第一個步驟是新增用戶端相依性。 相關的相依性可作為 NuGet 套件使用。 在 方案總管 中,以滑鼠右鍵按兩下專案,然後從操作功能表中選取 [管理 NuGet 套件...]。

在提示的對話框中,流覽名為 “Microsoft.Azure.RemoteRendering.Cpp” 的 NuGet 套件:

流覽 NuGet 套件

然後選取套件,然後按 [安裝] 按鈕,將它新增至專案。

NuGet 套件會將 遠端轉譯 相依性新增至專案。 具體而言:

  • 針對客戶端連結庫連結 (RemoteRenderingClient.lib)。
  • 設定.dll相依性。
  • 將正確的路徑設定為 include 目錄。

項目準備

我們需要對現有項目進行小變更。 這些變更是微妙的,但如果沒有它們,遠端轉譯 將無法運作。

在 DirectX 裝置上啟用多線程保護

裝置 DirectX11 必須啟用多線程保護。 若要變更,請在 「Common」 資料夾中開啟檔案DeviceResources.cpp,並在函式結尾 DeviceResources::CreateDeviceResources()插入下列程式代碼:

// Enable multi thread protection as now multiple threads use the immediate context.
Microsoft::WRL::ComPtr<ID3D11Multithread> contextMultithread;
if (context.As(&contextMultithread) == S_OK)
{
    contextMultithread->SetMultithreadProtected(true);
}

在應用程式指令清單中啟用網路功能

必須針對已部署的應用程式明確啟用網路功能。 若未進行此設定,連線查詢最終將會導致逾時。 若要啟用,請按兩下 package.appxmanifest 方案總管中的專案。 在下一個 UI 中,移至 [ 功能] 索引標籤,然後選取:

  • 網際網路 (用戶端和伺服器)
  • 網際網路 (用戶端)

網路功能

整合 遠端轉譯

現在已備妥專案,我們可以從程式代碼開始。 應用程式的良好進入點是 類別 HolographicAppMain(檔案 HolographicAppMain.h/cpp),因為它具有初始化、取消初始化和轉譯所需的所有連結。

Includes

我們一開始會新增必要的 include。 將下列 include 新增至 HolographicAppMain.h 檔案:

#include <AzureRemoteRendering.h>

...和這些要提出HolographicAppMain.cpp的其他 include 指示詞:

#include <AzureRemoteRendering.inl>
#include <RemoteRenderingExtensions.h>
#include <windows.perception.spatial.h>

為了簡化程式代碼,我們會在指示詞後面 include ,於 HolographicAppMain.h 檔案頂端定義下列命名空間快捷方式:

namespace RR = Microsoft::Azure::RemoteRendering;

此快捷方式很有用,因此我們不需要在任何地方寫出完整的命名空間,但仍可以辨識 ARR 特定的數據結構。 當然,我們也可以使用 using namespace... 指示詞。

遠端轉譯 初始化

我們需要在應用程式的存留期期間保留一些會話的物件等等。 存留期與應用程式 HolographicAppMain 物件的存留期一致,因此我們會將物件新增為 類別 HolographicAppMain的成員。 下一個步驟是在 HolographicAppMain.h 檔案中新增下列類別成員:

class HolographicAppMain
{
    ...
    // members:
    std::string m_sessionOverride;                // if we have a valid session ID, we specify it here. Otherwise a new one is created
    RR::ApiHandle<RR::RemoteRenderingClient> m_client;  // the client instance
    RR::ApiHandle<RR::RenderingSession> m_session;    // the current remote rendering session
    RR::ApiHandle<RR::RenderingConnection> m_api;       // the API instance, that is used to perform all the actions. This is just a shortcut to m_session->Connection()
    RR::ApiHandle<RR::GraphicsBindingWmrD3d11> m_graphicsBinding; // the graphics binding instance
}

執行實際實作的好位置是 類別 HolographicAppMain的建構函式。 我們必須在那裡執行三種類型的初始化:

  1. 遠端轉譯 系統的一次性初始化
  2. 用戶端建立(驗證)
  3. 會話建立

我們會在建構函式中循序執行所有作業。 不過,在實際使用案例中,可能適合個別執行這些步驟。

將下列程式代碼新增至檔案中建構函式主體的開頭HolographicAppMain.cpp:

HolographicAppMain::HolographicAppMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) :
    m_deviceResources(deviceResources)
{
    // 1. One time initialization
    {
        RR::RemoteRenderingInitialization clientInit;
        clientInit.ConnectionType = RR::ConnectionType::General;
        clientInit.GraphicsApi = RR::GraphicsApiType::WmrD3D11;
        clientInit.ToolId = "<sample name goes here>"; // <put your sample name here>
        clientInit.UnitsPerMeter = 1.0f;
        clientInit.Forward = RR::Axis::NegativeZ;
        clientInit.Right = RR::Axis::X;
        clientInit.Up = RR::Axis::Y;
        if (RR::StartupRemoteRendering(clientInit) != RR::Result::Success)
        {
            // something fundamental went wrong with the initialization
            throw std::exception("Failed to start remote rendering. Invalid client init data.");
        }
    }


    // 2. Create Client
    {
        // Users need to fill out the following with their account data and model
        RR::SessionConfiguration init;
        init.AccountId = "00000000-0000-0000-0000-000000000000";
        init.AccountKey = "<account key>";
        init.RemoteRenderingDomain = "westus2.mixedreality.azure.com"; // <change to the region that the rendering session should be created in>
        init.AccountDomain = "westus2.mixedreality.azure.com"; // <change to the region the account was created in>
        m_modelURI = "builtin://Engine";
        m_sessionOverride = ""; // If there is a valid session ID to re-use, put it here. Otherwise a new one is created
        m_client = RR::ApiHandle(RR::RemoteRenderingClient(init));
    }

    // 3. Open/create rendering session
    {
        auto SessionHandler = [&](RR::Status status, RR::ApiHandle<RR::CreateRenderingSessionResult> result)
        {
            if (status == RR::Status::OK)
            {
                auto ctx = result->GetContext();
                if (ctx.Result == RR::Result::Success)
                {
                    SetNewSession(result->GetSession());
                }
                else
                {
                    SetNewState(AppConnectionStatus::ConnectionFailed, ctx.ErrorMessage.c_str());
                }
            }
            else
            {
                SetNewState(AppConnectionStatus::ConnectionFailed, "failed");
            }
        };

        // If we had an old (valid) session that we can recycle, we call async function m_client->OpenRenderingSessionAsync
        if (!m_sessionOverride.empty())
        {
            m_client->OpenRenderingSessionAsync(m_sessionOverride, SessionHandler);
            SetNewState(AppConnectionStatus::CreatingSession, nullptr);
        }
        else
        {
            // create a new session
            RR::RenderingSessionCreationOptions init;
            init.MaxLeaseInMinutes = 10; // session is leased for 10 minutes
            init.Size = RR::RenderingSessionVmSize::Standard;
            m_client->CreateNewRenderingSessionAsync(init, SessionHandler);
            SetNewState(AppConnectionStatus::CreatingSession, nullptr);
        }
    }

    // Rest of constructor code:
    ...
}

程式代碼會呼叫成員函式 SetNewSessionSetNewState,我們將在下一個段落中實作,以及其餘的狀態機器程序代碼。

請注意,認證會在範例中硬式編碼,而且必須就地填寫 (帳戶標識元、帳戶密鑰、帳戶網域遠端轉譯網域)。

我們會以對稱方式和反向順序在解構函式主體結尾執行還原初始化:

HolographicAppMain::~HolographicAppMain()
{
    // Existing destructor code:
    ...
    
    // Destroy session:
    if (m_session != nullptr)
    {
        m_session->Disconnect();
        m_session = nullptr;
    }

    // Destroy front end:
    m_client = nullptr;

    // One-time de-initialization:
    RR::ShutdownRemoteRendering();
}

狀態機器

在 遠端轉譯 中,用來建立會話和載入模型的主要函式是異步函式。 為了說明這一點,我們需要一個簡單狀態機器,基本上會自動透過下列狀態轉換:

初始化 - 工作階段建立 -> 工作階段開始 ->> 模型載入 (進度)

因此,在下一個步驟中,我們會將一些狀態機器處理新增至 類別。 我們針對應用程式可以處於的各種狀態宣告自己的列舉 AppConnectionStatus 。 它類似於 RR::ConnectionStatus,但有失敗連線的額外狀態。

將下列成員和函式新增至類別宣告:

namespace HolographicApp
{
    // Our application's possible states:
    enum class AppConnectionStatus
    {
        Disconnected,

        CreatingSession,
        StartingSession,
        Connecting,
        Connected,

        // error state:
        ConnectionFailed,
    };

    class HolographicAppMain
    {
        ...
        // Member functions for state transition handling
        void OnConnectionStatusChanged(RR::ConnectionStatus status, RR::Result error);
        void SetNewState(AppConnectionStatus state, const char* statusMsg);
        void SetNewSession(RR::ApiHandle<RR::RenderingSession> newSession);
        void StartModelLoading();

        // Members for state handling:

        // Model loading:
        std::string m_modelURI;
        RR::ApiHandle<RR::LoadModelAsync> m_loadModelAsync;

        // Connection state machine:
        AppConnectionStatus m_currentStatus = AppConnectionStatus::Disconnected;
        std::string m_statusMsg;
        RR::Result m_connectionResult = RR::Result::Success;
        RR::Result m_modelLoadResult = RR::Result::Success;
        bool m_isConnected = false;
        bool m_sessionStarted = false;
        RR::ApiHandle<RR::SessionPropertiesAsync> m_sessionPropertiesAsync;
        bool m_modelLoadTriggered = false;
        float m_modelLoadingProgress = 0.f;
        bool m_modelLoadFinished = false;
        double m_timeAtLastRESTCall = 0;
        bool m_needsCoordinateSystemUpdate = true;
    }

在 .cpp 檔案中的實作端,新增下列函式主體:

void HolographicAppMain::StartModelLoading()
{
    m_modelLoadingProgress = 0.f;

    RR::LoadModelFromSasOptions options;
    options.ModelUri = m_modelURI.c_str();
    options.Parent = nullptr;

    // start the async model loading
    m_api->LoadModelFromSasAsync(options,
        // completed callback
        [this](RR::Status status, RR::ApiHandle<RR::LoadModelResult> result)
        {
            m_modelLoadResult = RR::StatusToResult(status);
            m_modelLoadFinished = true;

            if (m_modelLoadResult == RR::Result::Success)
            {
                RR::Double3 pos = { 0.0, 0.0, -2.0 };
                result->GetRoot()->SetPosition(pos);
            }
        },
        // progress update callback
            [this](float progress)
        {
            // progress callback
            m_modelLoadingProgress = progress;
            m_needsStatusUpdate = true;
        });
}



void HolographicAppMain::SetNewState(AppConnectionStatus state, const char* statusMsg)
{
    m_currentStatus = state;
    m_statusMsg = statusMsg ? statusMsg : "";

    // Some log for the VS output panel:
    const char* appStatus = nullptr;

    switch (state)
    {
        case AppConnectionStatus::Disconnected: appStatus = "Disconnected"; break;
        case AppConnectionStatus::CreatingSession: appStatus = "CreatingSession"; break;
        case AppConnectionStatus::StartingSession: appStatus = "StartingSession"; break;
        case AppConnectionStatus::Connecting: appStatus = "Connecting"; break;
        case AppConnectionStatus::Connected: appStatus = "Connected"; break;
        case AppConnectionStatus::ConnectionFailed: appStatus = "ConnectionFailed"; break;
    }

    char buffer[1024];
    sprintf_s(buffer, "Remote Rendering: New status: %s, result: %s\n", appStatus, m_statusMsg.c_str());
    OutputDebugStringA(buffer);
}

void HolographicAppMain::SetNewSession(RR::ApiHandle<RR::RenderingSession> newSession)
{
    SetNewState(AppConnectionStatus::StartingSession, nullptr);

    m_sessionStartingTime = m_timeAtLastRESTCall = m_timer.GetTotalSeconds();
    m_session = newSession;
    m_api = m_session->Connection();
    m_graphicsBinding = m_session->GetGraphicsBinding().as<RR::GraphicsBindingWmrD3d11>();
    m_session->ConnectionStatusChanged([this](auto status, auto error)
        {
            OnConnectionStatusChanged(status, error);
        });

};

void HolographicAppMain::OnConnectionStatusChanged(RR::ConnectionStatus status, RR::Result error)
{
    const char* asString = RR::ResultToString(error);
    m_connectionResult = error;

    switch (status)
    {
    case RR::ConnectionStatus::Connecting:
        SetNewState(AppConnectionStatus::Connecting, asString);
        break;
    case RR::ConnectionStatus::Connected:
        if (error == RR::Result::Success)
        {
            SetNewState(AppConnectionStatus::Connected, asString);
        }
        else
        {
            SetNewState(AppConnectionStatus::ConnectionFailed, asString);
        }
        m_modelLoadTriggered = m_modelLoadFinished = false;
        m_isConnected = error == RR::Result::Success;
        break;
    case RR::ConnectionStatus::Disconnected:
        if (error == RR::Result::Success)
        {
            SetNewState(AppConnectionStatus::Disconnected, asString);
        }
        else
        {
            SetNewState(AppConnectionStatus::ConnectionFailed, asString);
        }
        m_modelLoadTriggered = m_modelLoadFinished = false;
        m_isConnected = false;
        break;
    default:
        break;
    }
    
}

每個畫面更新

我們必須在每個模擬刻度更新用戶端一次,並執行一些額外的狀態更新。 函 HolographicAppMain::Update 式提供每個畫面格更新的良好勾點。

狀態機器更新

我們需要輪詢會話的狀態,並查看它是否已轉換為 Ready 狀態。 如果已成功連線,我們最後會透過 StartModelLoading開始載入模型。

將下列程式代碼新增至函式主體 HolographicAppMain::Update

// Updates the application state once per frame.
HolographicFrame HolographicAppMain::Update()
{
    if (m_session != nullptr)
    {
        // Tick the client to receive messages
        m_api->Update();

        if (!m_sessionStarted)
        {
            // Important: To avoid server-side throttling of the requests, we should call GetPropertiesAsync very infrequently:
            const double delayBetweenRESTCalls = 10.0;

            // query session status periodically until we reach 'session started'
            if (m_sessionPropertiesAsync == nullptr && m_timer.GetTotalSeconds() - m_timeAtLastRESTCall > delayBetweenRESTCalls)
            {
                m_timeAtLastRESTCall = m_timer.GetTotalSeconds();
                m_session->GetPropertiesAsync([this](RR::Status status, RR::ApiHandle<RR::RenderingSessionPropertiesResult> propertiesResult)
                    {
                        if (status == RR::Status::OK)
                        {
                            auto ctx = propertiesResult->GetContext();
                            if (ctx.Result == RR::Result::Success)
                            {
                                auto res = propertiesResult->GetSessionProperties();
                                switch (res.Status)
                                {
                                case RR::RenderingSessionStatus::Ready:
                                {
                                    // The following ConnectAsync is async, but we'll get notifications via OnConnectionStatusChanged
                                    m_sessionStarted = true;
                                    SetNewState(AppConnectionStatus::Connecting, nullptr);
                                    RR::RendererInitOptions init;
                                    init.IgnoreCertificateValidation = false;
                                    init.RenderMode = RR::ServiceRenderMode::Default;
                                    m_session->ConnectAsync(init, [](RR::Status, RR::ConnectionStatus) {});
                                }
                                break;
                                case RR::RenderingSessionStatus::Error:
                                    SetNewState(AppConnectionStatus::ConnectionFailed, "Session error");
                                    break;
                                case RR::RenderingSessionStatus::Stopped:
                                    SetNewState(AppConnectionStatus::ConnectionFailed, "Session stopped");
                                    break;
                                case RR::RenderingSessionStatus::Expired:
                                    SetNewState(AppConnectionStatus::ConnectionFailed, "Session expired");
                                    break;
                                }
                            }
                            else
                            {
                                SetNewState(AppConnectionStatus::ConnectionFailed, ctx.ErrorMessage.c_str());
                            }
                        }
                        else
                        {
                            SetNewState(AppConnectionStatus::ConnectionFailed, "Failed to retrieve session status");
                        }
                        m_sessionPropertiesQueryInProgress = false; // next try
                    });                }
            }
        }
        if (m_isConnected && !m_modelLoadTriggered)
        {
            m_modelLoadTriggered = true;
            StartModelLoading();
        }
    }

    if (m_needsCoordinateSystemUpdate && m_stationaryReferenceFrame && m_graphicsBinding)
    {
        // Set the coordinate system once. This must be called again whenever the coordinate system changes.
        winrt::com_ptr<ABI::Windows::Perception::Spatial::ISpatialCoordinateSystem> ptr{ m_stationaryReferenceFrame.CoordinateSystem().as<ABI::Windows::Perception::Spatial::ISpatialCoordinateSystem>() };
        m_graphicsBinding->UpdateUserCoordinateSystem(ptr.get());
        m_needsCoordinateSystemUpdate = false;
    }

    // Rest of the body:
    ...
}

座標系統更新

我們需要同意座標系統上要使用的轉譯服務。 若要存取我們想要使用的座標系統,我們需要 m_stationaryReferenceFrame 在函式 結尾建立的 HolographicAppMain::OnHolographicDisplayIsAvailableChanged

此座標系統通常不會變更,因此這是一次初始化。 如果您的應用程式變更座標系統,則必須再次呼叫它。

上述程式代碼會在函式內 Update 設定座標系統一次,只要我們有參考座標系統和連接的會話。

相機 更新

我們需要更新相機剪輯平面,讓伺服器相機與本機相機保持同步。 我們可以在函式 Update 的結尾執行此動作:

    ...
    if (m_isConnected)
    {
        // Any near/far plane values of your choosing.
        constexpr float fNear = 0.1f;
        constexpr float fFar = 10.0f;
        for (HolographicCameraPose const& cameraPose : prediction.CameraPoses())
        {
            // Set near and far to the holographic camera as normal
            cameraPose.HolographicCamera().SetNearPlaneDistance(fNear);
            cameraPose.HolographicCamera().SetFarPlaneDistance(fFar);
        }

        // The API to inform the server always requires near < far. Depth buffer data will be converted locally to match what is set on the HolographicCamera.
        auto settings = m_api->GetCameraSettings();
        settings->SetNearAndFarPlane(std::min(fNear, fFar), std::max(fNear, fFar));
        settings->SetEnableDepth(true);
    }

    // The holographic frame will be used to get up-to-date view and projection matrices and
    // to present the swap chain.
    return holographicFrame;
}

轉譯

最後一件事就是叫用遠端內容的轉譯。 在轉譯目標清除並設定檢視區之後,我們必須在轉譯管線內的確切正確位置執行此呼叫。 將下列代碼段插入函式內的UseHolographicCameraResourcesHolographicAppMain::Render鎖定中:

        ...
        // Existing clear function:
        context->ClearDepthStencilView(depthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
        
        // ...

        // Existing check to test for valid camera:
        bool cameraActive = pCameraResources->AttachViewProjectionBuffer(m_deviceResources);


        // Inject remote rendering: as soon as we are connected, start blitting the remote frame.
        // We do the blitting after the Clear, and before cube rendering.
        if (m_isConnected && cameraActive)
        {
            m_graphicsBinding->BlitRemoteFrame();
        }

        ...

執行範例

範例現在應該處於編譯和執行的狀態。

當範例正確執行時,它會在前面顯示旋轉的 Cube,並在一些會話建立和模型載入之後,轉譯位於目前前端位置的引擎模型。 會話建立和模型載入最多可能需要幾分鐘的時間。 目前的狀態只會寫入 Visual Studio 的輸出面板。 因此,建議從Visual Studio內部啟動範例。

警告

當未呼叫刻度函式幾秒鐘時,用戶端會中斷與伺服器的連線。 因此,觸發斷點很容易導致應用程式中斷連線。

如需使用文字面板的適當狀態顯示,請參閱 GitHub 上本教學課程的預先設定版本。

下一步

在本教學課程中,您已瞭解將 遠端轉譯 新增至庫存全像攝影應用程式 C++/DirectX11 範例所需的所有步驟。 若要轉換您自己的模型,請參閱下列快速入門: