チュートリアル:Remote Rendering を HoloLens Holographic アプリに統合する

このチュートリアルでは、次の事項について説明します。

  • Visual Studio を使用して、HoloLens にデプロイできるホログラフィック アプリを作成する
  • ローカルでのレンダリングとリモートでレンダリングされるコンテンツとを組み合わせるために必要なコード スニペットとプロジェクト設定を追加する

このチュートリアルでは、ネイティブな Holographic App サンプルに必要なビットを追加して、ローカルでのレンダリングと Azure Remote Rendering を組み合わせる方法について主に取り上げます。 このアプリの状態のフィードバックは、Visual Studio 内のデバッグ出力パネルを通じてのみ得られます。そのため、このサンプルは Visual Studio 内から開始することをお勧めします。 動的なテキスト パネルをゼロから作成するには多くのコーディングが必要になるため、適切なアプリ内フィードバックの追加は、このサンプルの対象範囲外になります。 最初は、GitHub の Remoting Player サンプル プロジェクトに含まれているクラス StatusDisplay から開始することをお勧めします。 実際、このチュートリアルの既定のバージョンでは、このクラスのローカル コピーを使用しています。

ヒント

ARR サンプルのリポジトリには、このチュートリアルの結果が、使用できる状態の Visual Studio プロジェクトとして含まれています。 これには、UI クラス StatusDisplay を通じた適切なエラーおよび状態レポートによる強化も施されています。 チュートリアル内では、ARR 固有の追加はすべて #ifdef USE_REMOTE_RENDERING / #endif によってスコープ指定されています。そのため、Remote Rendering の追加を特定するのは容易です。

前提条件

このチュートリアルには、次のものが必要です。

  • アカウント情報 (アカウント ID、アカウント キー、アカウント ドメイン、サブスクリプション ID)。 アカウントをお持ちでない場合は、アカウントを作成してください。
  • Windows SDK 10.0.18362.0 (ダウンロード)
  • 最新バージョンの Visual Studio 2022 (ダウンロード)
  • Visual Studio tools for Mixed Reality。 特に、次のワークロードのインストールは必須です。
    • C++ によるデスクトップ開発
    • ユニバーサル Windows プラットフォーム (UWP) の開発
  • Visual Studio 用の Windows Mixed Reality アプリ テンプレート (ダウンロード)

新しいホログラフィック アプリ サンプルの作成

最初の手順では、Remote Rendering 統合の基礎となるストック サンプルを作成します。 Visual Studio を開いて [新しいプロジェクトの作成] を選択し、"ホログラフィック DirectX 11 アプリ (ユニバーサル Windows) (C++/WinRT)" を検索します。

Create new project

目的のプロジェクト名を入力し、パスを選択して [作成] ボタンを選択します。 新しいプロジェクトで、構成を "デバッグ/ARM64" に切り替えます。 これで、コンパイルして接続済み HoloLens 2 デバイスにデプロイできるようになりました。 これを HoloLens で実行すると、回転するキューブが目前に表示されます。

NuGet を通じた Remote Rendering 依存関係の追加

Remote Rendering 機能を追加する最初の手順は、クライアント側の依存関係の追加です。 関連する依存関係は、NuGet パッケージとして入手できます。 ソリューション エクスプローラーでプロジェクトを右クリックし、コンテキスト メニューから [NuGet パッケージの管理] を選択します。

表示されるダイアログで、 "Microsoft.Azure.RemoteRendering.Cpp" という名前の NuGet パッケージを参照します。

Browse for NuGet package

パッケージを選択してプロジェクトに追加し、[インストール] ボタンを押します。

NuGet パッケージによって、Remote Rendering の依存関係がプロジェクトに追加されます。 具体的な内容は次のとおりです。

  • クライアント ライブラリ (RemoteRenderingClient.lib) に対してリンクします。
  • .dll 依存関係を設定します。
  • 正しいパスをインクルード ディレクトリに設定します。

プロジェクトの準備

既存のプロジェクトにわずかに変更を加える必要があります。 小さな変更ではありますが、これらを行わないと Remote Rendering は機能しません。

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 で、 [機能] タブに移動して以下を選択します。

  • インターネット (クライアントとサーバー)
  • インターネット (クライアント)

Network capabilities

Remote Rendering の統合

プロジェクトの準備ができたところで、コーディングを開始できます。 アプリケーションへのエントリ ポイントは、クラス HolographicAppMain (HolographicAppMain.h/cpp ファイル) にすることをお勧めします。ここには、初期化、初期化解除、レンダリングに必要なすべてのフックが含まれているためです。

Includes

まず、必要なインクルードを追加します。 HolographicAppMain.h ファイルに次のインクルードを追加します。

#include <AzureRemoteRendering.h>

また、これらの追加の include ディレクティブを HolographicAppMain.cpp ファイルに追加します。

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

コードをわかりやすくするために、HolographicAppMain.h ファイルの冒頭 (include ディレクティブの後ろ) で次の名前空間ショートカットを定義します。

namespace RR = Microsoft::Azure::RemoteRendering;

このショートカットは便利なので、すべての場所で完全な名前空間を記述する必要はありませんが、ARR 固有のデータ構造を認識することができます。 もちろん、using namespace... ディレクティブを使用することもできます。

Remote Rendering の初期化

アプリケーションの有効期間中に、セッションなどのためにいくつかのオブジェクトを保持する必要があります。 この有効期間はアプリケーションの 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 のコンストラクターです。 ここで 3 種類の初期化を行う必要があります。

  1. Remote Rendering システムの 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 が呼び出されます。これらは、次の段落で、残りのステート マシン コード部分と共に実装されます。

資格情報はサンプル内にハードコーディングされており、所定の場所に入力する必要があることに注意してください (アカウント ID、アカウント キー、アカウント ドメインリモート レンダリング ドメイン)。

初期化解除は対称的に、デストラクター本体の末尾で逆の順番で実行します。

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

ステート マシン

Remote Rendering では、セッションの作成とモデルの読み込みを行う主要な関数は、非同期関数です。 これに対応するには、基本的に以下の状態が自動で切り替わる単純なステート マシンが必要です。

"初期化 -> セッションの作成 -> セッションの開始 -> モデルの読み込み (および進行状況)"

それに応じて、次の手順では、ちょっとしたステート マシン処理をクラスに追加します。 アプリケーションが取れるさまざまな状態に対して、独自の列挙型 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:
    ...
}

座標系の更新

使用する座標系については、レンダリング サービスに同意する必要があります。 使用する座標系にアクセスするには、関数 HolographicAppMain::OnHolographicDisplayIsAvailableChanged の末尾で作成される m_stationaryReferenceFrame が必要です。

通常、この座標系は変更されないため、これは 1 回限りの初期化となります。 アプリケーションによって座標系が変更された場合は、再度呼び出す必要があります。

双方が参照座標系および接続済みセッションを取得するとすぐに、上記のコードにより 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;
}

表示

最後の手順では、リモート コンテンツのレンダリングを呼び出します。 この呼び出しは、レンダー ターゲット クリアおよびビューポートの設定の後で、レンダリング パイプライン内の厳密に適切な位置で行う必要があります。 次のスニペットを HolographicAppMain::Render 関数内の UseHolographicCameraResources ロックに挿入します。

        ...
        // 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();
        }

        ...

サンプルを実行する

これでサンプルは、コンパイルと実行を行える状態になりました。

サンプルが正常に動作すると、回転するキューブが目前に表示され、セッションの作成とモデルの読み込みが何回か行われた後に、現在のヘッド位置にあるエンジン モデルがレンダリングされます。 セッションの作成とモデルの読み込みには、最大で数分かかる場合があります。 現在の状態は、Visual Studio の出力パネルにのみ書き込まれます。 そのため、Visual Studio 内からサンプルを開始することをお勧めします。

注意事項

ティック関数が数秒間呼び出されないと、クライアントはサーバーから切断されます。 そのため、ブレークポイントをトリガーすると、アプリケーションを非常に簡単に切断させることができます。

適切な状態をテキスト パネルに表示させるには、GitHub にある、このチュートリアルの既存のバージョンを参照してください。

次のステップ

このチュートリアルでは、Remote Rendering をストック ホログラフィック アプリ C++/DirectX11 サンプルに追加するために必要なすべての手順を説明しました。 ご自分のモデルを変換するには、次のクイックスタートを参照してください。