Dela via


Självstudie: Integrera fjärråtergivning i en HoloLens Holographic App

I den här självstudien lär du dig:

  • Använda Visual Studio för att skapa en holografisk app som kan distribueras till HoloLens
  • Lägg till nödvändiga kodfragment och projektinställningar för att kombinera lokal rendering med fjärråtergivet innehåll

Den här självstudien fokuserar på att lägga till nödvändiga bitar i ett internt Holographic App exempel för att kombinera lokal rendering med Azure Remote Rendering. Den enda typen av statusfeedback i den här appen är via panelen för felsökningsutdata i Visual Studio, så vi rekommenderar att du startar exemplet inifrån Visual Studio. Att lägga till korrekt feedback i appen ligger utanför omfånget för det här exemplet, eftersom det krävs mycket kodning för att skapa en dynamisk textpanel från grunden. En bra utgångspunkt är klassen StatusDisplay, som är en del av exempelprojektet Remoting Player på GitHub. I själva verket använder den förkonserverade versionen av den här självstudien en lokal kopia av den klassen.

Dricks

ARR-exempellagringsplatsen innehåller resultatet av den här självstudien som ett Visual Studio-projekt som är redo att användas. Den utökas också med korrekt fel- och statusrapportering via UI-klassen StatusDisplay. I självstudien begränsas alla ARR-specifika tillägg av #ifdef USE_REMOTE_RENDERING / #endif, så det är enkelt att identifiera tilläggen för fjärrrendering.

Förutsättningar

För den här självstudien behöver du:

  • Din kontoinformation (konto-ID, kontonyckel, kontodomän, prenumerations-ID). Om du inte har något konto skapar du ett konto.
  • Windows SDK 10.0.18362.0 (ladda ned).
  • Den senaste versionen av Visual Studio 2022 (nedladdning).
  • Visual Studio-verktyg för Mixed Reality. Mer specifikt är följande arbetsbelastningsinstallationer obligatoriska:
    • Skrivbordsutveckling med C++
    • utveckling av Universell Windows-plattform (UWP)
  • Windows Mixed Reality-appmallar för Visual Studio (ladda ned).

Skapa ett nytt Holographic App-exempel

Som ett första steg skapar vi ett lagerexempel som ligger till grund för fjärrrenderingsintegrering. Öppna Visual Studio och välj "Skapa ett nytt projekt" och sök efter "Holographic DirectX 11 App (Universal Windows) (C++/WinRT)"

Skapa nytt projekt

Skriv ett valfritt projektnamn, välj en sökväg och välj knappen Skapa. I det nya projektet växlar du konfigurationen till "Felsök/ARM64". Nu bör du kunna kompilera och distribuera den till en ansluten HoloLens 2-enhet. Om du kör den på HoloLens bör du se en roterande kub framför dig.

Lägga till fjärrrenderingsberoenden via NuGet

Det första steget för att lägga till funktioner för fjärrrendering är att lägga till beroenden på klientsidan. Relevanta beroenden är tillgängliga som ett NuGet-paket. Högerklicka på projektet i Solution Explorer och välj "Hantera NuGet-paket..." på snabbmenyn.

I den efterfrågade dialogrutan bläddrar du efter NuGet-paketet med namnet "Microsoft.Azure.RemoteRendering.Cpp":

Bläddra efter NuGet-paket

och lägg till det i projektet genom att välja paketet och sedan trycka på knappen "Installera".

NuGet-paketet lägger till fjärrrenderingsberoenden i projektet. Specifikt:

  • Länk till klientbiblioteket (RemoteRenderingClient.lib).
  • Konfigurera .dll beroenden.
  • Ange rätt sökväg till inkluderingskatalogen.

Projektförberedelse

Vi behöver göra små ändringar i det befintliga projektet. Dessa ändringar är subtila, men utan dem skulle fjärrrendering inte fungera.

Aktivera flertrådsskydd på DirectX-enhet

Enheten DirectX11 måste ha flertrådsskydd aktiverat. Om du vill ändra det öppnar du filen DeviceResources.cpp i mappen "Common" och infogar följande kod i slutet av funktionen 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);
}

Aktivera nätverksfunktioner i appmanifestet

Nätverksfunktioner måste uttryckligen aktiveras för den distribuerade appen. Utan att detta har konfigurerats resulterar anslutningsfrågor i timeouter så småningom. Om du vill aktivera dubbelklickar du på objektet package.appxmanifest i Lösningsutforskaren. I nästa användargränssnitt går du till fliken Funktioner och väljer:

  • Internet (klient och server)
  • Internet (Klient)

Nätverksfunktioner

Integrera fjärråtergivning

Nu när projektet har förberetts kan vi börja med koden. En bra startpunkt i programmet är klassen HolographicAppMain(filen HolographicAppMain.h/cpp) eftersom den har alla nödvändiga krokar för initiering, avinitiering och återgivning.

Innehåller

Vi börjar med att lägga till nödvändiga inkluderingar. Lägg till följande inkludera i filen HolographicAppMain.h:

#include <AzureRemoteRendering.h>

... och dessa ytterligare include direktiv för att arkivera HolographicAppMain.cpp:

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

För enkelhetens skull definierar vi följande genväg till namnområdet överst i filen HolographicAppMain.h, efter direktiven include :

namespace RR = Microsoft::Azure::RemoteRendering;

Den här genvägen är användbar så att vi inte behöver skriva ut hela namnområdet överallt, men ändå kan känna igen ARR-specifika datastrukturer. Naturligtvis skulle vi också kunna använda direktivet using namespace... .

Initiering av fjärråtergivning

Vi måste lagra några objekt för sessionen osv. under programmets livslängd. Livslängden sammanfaller med livslängden för programmets HolographicAppMain objekt, så vi lägger till våra objekt som medlemmar i klassen HolographicAppMain. Nästa steg är att lägga till följande klassmedlemmar i filen 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
}

En bra plats att göra den faktiska implementeringen på är konstruktorn för klassen HolographicAppMain. Vi måste göra tre typer av initiering där:

  1. Engångsinitiering av fjärrrenderingssystemet
  2. Skapande av klient (autentisering)
  3. Skapa session

Vi gör allt detta sekventiellt i konstruktorn. I verkliga användningsfall kan det dock vara lämpligt att utföra dessa steg separat.

Lägg till följande kod i början av konstruktorns brödtext i filen 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:
    ...
}

Koden anropar medlemsfunktioner SetNewSession och SetNewState, som vi implementerar i nästa stycke tillsammans med resten av tillståndsdatorkoden.

Observera att autentiseringsuppgifterna är hårdkodade i exemplet och måste fyllas i på plats (konto-ID, kontonyckel, kontodomän och fjärrrenderingsdomän).

Vi gör avinitiering symmetriskt och i omvänd ordning i slutet av destructor kroppen:

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

Tillståndsdator

I Fjärrrendering är nyckelfunktioner för att skapa en session och läsa in en modell asynkrona funktioner. För att ta hänsyn till detta behöver vi en enkel tillståndsdator som i princip övergår genom följande tillstånd automatiskt:

Initiering –> Sessionsskapande –> Sessionsstart –> Modellinläsning (med förlopp)

Som nästa steg lägger vi därför till lite tillståndsdatorhantering i klassen. Vi förklarar vår egen uppräkning AppConnectionStatus för de olika tillstånd som vårt program kan finnas i. Det liknar RR::ConnectionStatus, men har ytterligare ett tillstånd för misslyckad anslutning.

Lägg till följande medlemmar och funktioner i klassdeklarationen:

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

Lägg till följande funktionsorgan på implementeringssidan i filen .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;
    }
    
}

Uppdatering per bildruta

Vi måste uppdatera klienten en gång per simuleringsfäste och göra ytterligare tillståndsuppdateringar. Funktionen HolographicAppMain::Update ger en bra krok för uppdateringar per bildruta.

Uppdatering av tillståndsdator

Vi måste avsöka sessionens status och se om den har övergått till Ready tillstånd. Om vi har anslutit startar vi slutligen modellinläsningen via StartModelLoading.

Lägg till följande kod i brödtexten i funktionen 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:
    ...
}

Samordna systemuppdatering

Vi måste komma överens med renderingstjänsten på ett koordinatsystem som ska användas. För att få åtkomst till det koordinatsystem som vi vill använda behöver m_stationaryReferenceFrame vi det som skapas i slutet av funktionen HolographicAppMain::OnHolographicDisplayIsAvailableChanged.

Det här koordinatsystemet ändras vanligtvis inte, så det här är en engångsinitiering. Det måste anropas igen om programmet ändrar koordinatsystemet.

Koden ovan anger koordinatsystemet en gång i Update funktionen så snart vi båda har ett referenskoordinatsystem och en ansluten session.

Kamera uppdatering

Vi måste uppdatera kameraklippsplan så att serverkameran hålls synkroniserad med den lokala kameran. Vi kan göra det i slutet av Update funktionen:

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

Rendering

Det sista du behöver göra är att anropa återgivningen av fjärrinnehållet. Vi måste göra det här anropet i exakt rätt position i återgivningspipelinen, efter att återgivningsmålet har rensats och inställningen av visningsplatsen. Infoga följande kodfragment i låset UseHolographicCameraResources inuti funktionen HolographicAppMain::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();
        }

        ...

Kör exemplet

Exemplet bör nu vara i ett tillstånd där det kompileras och körs.

När exemplet körs korrekt visar det den roterande kuben precis framför dig, och efter att en del sessioner har skapats och modellen har lästs in återges motormodellen som finns i aktuellt huvudläge. Det kan ta upp till några minuter att skapa session och modellinläsning. Den aktuella statusen skrivs bara till Visual Studio-utdatapanelen. Vi rekommenderar därför att du startar exemplet inifrån Visual Studio.

Varning

Klienten kopplar från servern när tick-funktionen inte anropas på några sekunder. Att utlösa brytpunkter kan därför mycket enkelt göra att programmet kopplas från.

Korrekt statusvisning med en textpanel finns i den förkonserverade versionen av den här självstudien på GitHub.

Nästa steg

I den här självstudien har du lärt dig alla steg som krävs för att lägga till fjärråtergivning i ett lager av Holographic App C++/DirectX11-exempel. Om du vill konvertera din egen modell läser du följande snabbstart: