Kurz: Integrace Remote Rendering do HoloLens Holographic App

V tomto kurzu se dozvíte:

  • Použití Visual Studio k vytvoření holografické aplikace, kterou je možné nasadit do HoloLens
  • Přidejte potřebné fragmenty kódu a nastavení projektu pro kombinování místního vykreslování se vzdáleně vykreslený obsahem.

Tento kurz se zaměřuje na přidání potřebných bitů do nativní ukázky pro kombinování místního vykreslování s Holographic App Azure Remote Rendering. Jediný typ zpětné vazby k stavu v této aplikaci je prostřednictvím panelu výstupu ladění uvnitř Visual Studio, takže doporučujeme spustit ukázku zevnitř Visual Studio. Přidání správné zpětné vazby v aplikaci je nad rámec této ukázky, protože vytvoření dynamického textového panelu od začátku zahrnuje spoustu kódování. Dobrým výchozím bodem je třída , která je součástí ukázkového projektu StatusDisplay Remoting Player na GitHub. Předem naskenovaná verze tohoto kurzu ve skutečnosti používá místní kopii této třídy.

Tip

Úložiště ukázek ARR obsahuje výsledek tohoto kurzu jako Visual Studio, který je připravený k použití. Je také obohacen o správné hlášení chyb a stavů prostřednictvím třídy uživatelského rozhraní StatusDisplay . V tomto kurzu jsou všechny přidání specifické pro SMĚROVÁNÍ směrování vymezené pomocí , takže je snadné identifikovat #ifdef USE_REMOTE_RENDERING / #endif Remote Rendering aplikace.

Požadavky

Pro tento kurz potřebujete:

  • Informace o vašem účtu (ID účtu, klíč účtu, doména účtu, ID předplatného). Pokud účet nemáte, vytvořte si účet.
  • Windows SDK 10.0.18362.0 (stáhnout).
  • Nejnovější verze nástroje Visual Studio 2019 (stáhnout).
  • Visual Studio nástroje pro Mixed Reality. Konkrétně jsou povinné následující instalace úloh:
    • Vývoj desktopových aplikací pomocí C++
    • Vývoj Windows univerzální platformou (UPW)
  • The Windows Mixed Reality App Templates for Visual Studio (download).

Vytvoření nové ukázky holografické aplikace

Jako první krok vytvoříme skladovou ukázku, která je základem integrace Remote Rendering dat. Otevřete Visual Studio vyberte Vytvořit nový projekt a vyhledejte Holographic DirectX 11 App (Universal Windows) (C++/WinRT).

Vytvoření nového projektu

Zadejte název projektu podle vašeho výběru, zvolte cestu a vyberte tlačítko Vytvořit. V novém projektu přepněte konfiguraci na Ladit / ARM64. Teď byste měli být schopni ho zkompilovat a nasadit na připojené zařízení HoloLens 2. Pokud ho spustíte na HoloLens, měla by se před váma zobrazit rotující datová krychle.

Přidání Remote Rendering závislostí prostřednictvím NuGet

Prvním krokem k Remote Rendering funkcí je přidání závislostí na straně klienta. Relevantní závislosti jsou k dispozici jako NuGet balíček. V Průzkumník řešení klikněte pravým tlačítkem na projekt a v místní nabídce vyberte Manage NuGet Packages....

V dialogovém okně s výzvou vyhledejte balíček NuGet Microsoft.Azure.RemoteRendering.Cpp:

Vyhledání NuGet balíčku

a přidejte ho do projektu tak, že vyberete balíček a stisknete tlačítko Nainstalovat.

Balíček NuGet do projektu přidá Remote Rendering závislosti. Konkrétně se jedná o tyto:

  • Odkaz na klientskou knihovnu (RemoteRenderingClient.lib).
  • Nastavte .dll závislosti.
  • Nastavte správnou cestu k adresáři include.

Project přípravy

Potřebujeme provést malé změny stávajícího projektu. Tyto změny jsou drobné, ale bez Remote Rendering by to fungovalo.

Povolení ochrany s více vlákny na zařízení DirectX

Zařízení DirectX11 musí mít povolenou ochranu s více vlákny. Pokud to chcete změnit, otevřete soubor DeviceResources.cpp ve složce Common a na konec funkce vložte následující DeviceResources::CreateDeviceResources() kód:

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

Povolení síťových schopností v manifestu aplikace

Pro nasazenou aplikaci musí být explicitně povolené síťové možnosti. Bez této konfigurace budou dotazy na připojení nakonec mít za následek časové limity. Pokud to chcete povolit, poklikejte package.appxmanifest na položku v Průzkumníku řešení. V dalším uživatelském rozhraní přejděte na kartu Schopnosti a vyberte:

  • Internet (Klient & Server)
  • Internet (klient)

Možnosti sítě

Integrace Remote Rendering

Teď, když je projekt připravený, můžeme začít kódem. Dobrým vstupním bodem do aplikace je třída (soubor HolographicAppMain.h/cpp), protože obsahuje všechny potřebné hooky pro inicializaci, deicializaci a HolographicAppMain vykreslování.

Zahrnuje

Začneme přidáním potřebných součástí. Do souboru HolographicAppMain.h přidejte následující zahrnutí:

#include <AzureRemoteRendering.h>

... a tyto další include direktivy pro soubor HolographicAppMain.cpp:

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

Pro zjednodušení kódu definujeme následující zástupce oboru názvů v horní části souboru HolographicAppMain.h za include direktivy :

namespace RR = Microsoft::Azure::RemoteRendering;

Tato klávesová zkratka je užitečná, takže nemusíme psát celý obor názvů všude, ale stále dokážeme rozpoznat datové struktury specifické pro ARR. Samozřejmě bychom mohli použít také using namespace... direktivu .

Remote Rendering inicializace

Během životnosti aplikace musíme uchovat několik objektů pro relaci atd. Životnost se shoduje s životností objektu aplikace, takže objekty přidáme HolographicAppMain jako členy do třídy HolographicAppMain . Dalším krokem je přidání členů následující třídy do souboru 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
}

Dobrým místem, kde provést vlastní implementaci, je konstruktor třídy HolographicAppMain . Tam musíme provést tři typy inicializace:

  1. Časová inicializace systému Remote Rendering systému
  2. Vytvoření klienta (ověřování)
  3. Vytvoření relace

To všechno uděláme postupně v konstruktoru . V reálných případech však může být vhodné provést tyto kroky samostatně.

Na začátek těla konstruktoru v souboru HolographicAppMain.cpp přidejte následující kód:

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

Kód volá členské funkce a , které budeme implementovat v dalším odstavci spolu se zbytkem SetNewSession SetNewState kódu stavového stroje.

Všimněte si, že přihlašovací údaje jsou v ukázce na pevném kódu a je potřeba je vyplnit(ID účtu,klíč účtu, doména účtu a doména vzdáleného vykreslování).

Deicializační kód provádáme symetricky a v obráceném pořadí na konci těla destruktoru:

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

Stavový počítač

V Remote Rendering klíčové funkce pro vytvoření relace a načtení modelu jsou asynchronní funkce. K tomu potřebujeme jednoduchý stavový počítač, který v podstatě automaticky prochází následujícími stavy:

Inicializace –> relace –> relace se spouští –> modelu (s průběhem)

V dalším kroku přidáme do třídy trochu zpracování stavového počítače. Deklarujeme vlastní výčet AppConnectionStatus pro různé stavy, ve které může být naše aplikace. Je podobný jako RR::ConnectionStatus , ale má další stav pro neúspěšné připojení.

Do deklarace třídy přidejte následující členy a funkce:

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

Na straně implementace v souboru .cpp přidejte tato těla funkce:

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

Aktualizace na snímek

Musíme aktualizovat klienta jednou pro každou simulaci a provést nějaké další aktualizace stavu. Funkce HolographicAppMain::Update poskytuje dobrý hook pro aktualizace pro každý snímek.

Aktualizace stavového počítače

Musíme se dotazovat na stav relace a podívat se, jestli se přetála do Ready stavu . Pokud se úspěšně připojíme, můžeme načítání modelu nakonec spustit prostřednictvím StartModelLoading .

Do těla funkce přidejte následující HolographicAppMain::Update kód:

// 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:
    ...
}

Aktualizace koordinovaného systému

Musíme souhlasit s vykreslovací službou v souřadnicovém systému, který se má použít. Pro přístup k souřadnicovému systému, který chceme použít, potřebujeme objekt , který m_stationaryReferenceFrame se vytvoří na konci funkce HolographicAppMain::OnHolographicDisplayIsAvailableChanged .

Tento souřadnicový systém se obvykle nemění, takže se jedná o časovou inicializaci. Musí se zavolat znovu, pokud vaše aplikace změní souřadnicový systém.

Výše uvedený kód nastaví souřadnicový systém jednou v rámci funkce, jakmile máme referenční souřadnicový Update systém i propojenou relaci.

Aktualizace fotoaparátu

Musíme aktualizovat roviny klipů fotoaparátu tak, aby serverová kamera byla synchronizovaná s místním fotoaparátem. To můžeme udělat na konci Update funkce:

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

Vykreslování

Poslední věcí, kterou je třeba udělat, je vyvolání vykreslování vzdáleného obsahu. Toto volání musíme provést na přesné správné pozici v rámci kanálu vykreslování po vymazání cíle vykreslování a nastavení zobrazení. Vložte následující fragment kódu do UseHolographicCameraResources zámku uvnitř funkce 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();
        }

        ...

Spuštění ukázky

Ukázka by teď měla být ve stavu, ve kterém se zkompiluje a spustí.

Když se ukázka spustí správně, zobrazí obměnou datovou krychli přímo před vás. Po vytvoření relace a načtení modelu se vykreslí model motoru umístěný v aktuální pozici hlavy. Vytvoření relace a načítání modelu může trvat až několik minut. Aktuální stav je zapsán pouze Visual Studio panelu výstupu. Proto doporučujeme spustit ukázku zevnitř Visual Studio.

Upozornění

Klient se odpojí od serveru, když se na několik sekund nezavolala funkce tick. Aktivace zarážek proto může velmi snadno způsobit odpojení aplikace.

Správné zobrazení stavu s textovým panelem najdete v předem naskenované verzi tohoto kurzu na GitHub.

Další kroky

V tomto kurzu jste se naučili všechny kroky potřebné k přidání Remote Rendering do stock ukázky Holographic App C++/DirectX11. Pokud chcete převést vlastní model, projděte si následující rychlý start: