Zelfstudie: Remote Rendering integreren in een HoloLens Holographic-app
In deze zelfstudie leert u:
- Visual Studio gebruiken om een Holographic-app te maken die kan worden geïmplementeerd op HoloLens
- De benodigde codefragmenten en projectinstellingen toevoegen om lokale rendering te combineren met extern gerenderde inhoud
In deze zelfstudie leert u de benodigde bits aan een systeemeigen Holographic App
-voorbeeld toe te voegen om lokale rendering met Azure Remote Rendering te combineren. Het enige type statusfeedback in deze app wordt doorgegeven aan het uitvoerpaneel voor foutopsporing in Visual Studio. Het is dan ook raadzaam om het voorbeeld te starten vanuit Visual Studio. Het toevoegen van de juiste in-app-feedback valt buiten het bereik van dit voorbeeld, omdat het maken van een dynamisch tekstpaneel veel codering vergt. Een goed uitgangspunt is klasse StatusDisplay
, die deel uitmaakt van het Remoting Player-voorbeeldproject op GitHub. De standaardversie van deze zelfstudie maakt gebruik van een lokale kopie van die klasse.
Tip
De ARR-opslagplaats voor voorbeelden bevat het resultaat van deze zelfstudie als Visual Studio-project dat gereed is voor gebruik. Het wordt ook verrijkt met de juiste fout- en statusrapportage via UI-klasse StatusDisplay
. In de zelfstudie worden alle ARR-specifieke toevoegingen beperkt door #ifdef USE_REMOTE_RENDERING
/ #endif
, zodat de toevoegingen voor Remote Rendering gemakkelijk kunnen worden geïdentificeerd.
Vereisten
Voor deze zelfstudie hebt u het volgende nodig:
- Uw accountinformatie (account-id, accountsleutel, accountdomein, abonnements-id). Als u nog geen account hebt, maakt u er een.
- Windows SDK 10.0.18362.0 (download).
- De nieuwste versie van Visual Studio 2022 (download).
- Visual Studio Tools voor Mixed Reality. De volgende werkbelastinginstallaties zijn met name verplicht:
- Desktopontwikkeling met C++
- Universal Windows Platform (UWP)-ontwikkeling
- De Windows Mixed Reality-app-sjablonen voor Visual Studio (download).
Een nieuw Holographic-app-voorbeeld maken
We maken eerst een standaardvoorbeeld dat de basis vormt voor de integratie van Remote Rendering. Open Visual Studio en selecteer Een nieuw project maken en zoek naar Holographic DirectX 11-app (Universal Windows) ( C++/WinRT)
Voer een projectnaam in en kies een pad. Selecteer de knop Maken. Schakel in het nieuwe project de configuratie naar Debug / ARM64. Nu moet u de app kunnen compileren en implementeren op een verbonden HoloLens 2-apparaat. Als u deze uitvoert op HoloLens, ziet u een roterende kubus.
Remote Rendering-afhankelijkheden via NuGet toevoegen
Eerst voegt u de afhankelijkheden aan de clientzijde toe wanneer u Remote Rendering-mogelijkheden toevoegt. Relevante afhankelijkheden zijn beschikbaar als een NuGet-pakket. Klik in Solution Explorer met de rechtermuisknop op het project en selecteer NuGet-pakketten beheren... in het contextmenu.
Blader in het dialoogvenster dat wordt geopend naar het NuGet-pakket met de naam Microsoft.Azure.RemoteRendering.Cpp:
en voeg dit aan het project toe door het te selecteren en op de knop Installeren te klikken.
Het NuGet-pakket voegt de Remote Rendering-afhankelijkheden aan het project toe. Specifiek:
- Maak een koppeling voor de clientbibliotheek (RemoteRenderingClient.lib).
- Stel de .dll-afhankelijkheden in.
- Stel het juiste pad naar de map include in.
Project voorbereiden
Er moeten kleine wijzigingen in het bestaande project worden ingevoerd. Deze wijzigingen zijn subtiel, maar zijn onmisbaar voor een goede werking van Remote Rendering.
Multi-threadbeveiliging op DirectX-apparaat inschakelen
Op het DirectX11
-apparaat moet multi-threadbeveiliging zijn ingeschakeld. Als u dit wilt wijzigen, opent u het bestand DeviceResources.cpp in de map Common en voegt u de volgende code in aan het einde van de functie 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);
}
Netwerkmogelijkheden inschakelen in het app-manifest
Netwerkmogelijkheden moeten expliciet zijn ingeschakeld voor de geïmplementeerde app. Als dit niet is geconfigureerd, zullen verbindingsquery's uiteindelijk time-outs opleveren. Dubbelklik op het item package.appxmanifest
in Solution Explorer om het in te schakelen. Ga in de volgende gebruikersinterface naar het tabblad Mogelijkheden en selecteer:
- Internet (client en server)
- Internet (client)
Remote Rendering integreren
Nu het project is voorbereid, kunt u met de code beginnen. Een goed beginpunt in de toepassing is de klasse HolographicAppMain
(bestand HolographicAppMain.h/cpp), omdat het alle benodigde hooks voor initialisatie, de-initialisatie en rendering bevat.
Includes
Begin met het toevoegen van de benodigde includes. Voeg de volgende include toe aan het bestand HolographicAppMain.h:
#include <AzureRemoteRendering.h>
...en deze aanvullende include
-instructies aan het bestand HolographicAppMain.cpp:
#include <AzureRemoteRendering.inl>
#include <RemoteRenderingExtensions.h>
#include <windows.perception.spatial.h>
Ter vereenvoudiging van de code wordt de volgende naamruimtesnelkoppeling bovenaan het bestand HolographicAppMain.h na de include
-instructies gedefinieerd:
namespace RR = Microsoft::Azure::RemoteRendering;
Deze snelkoppeling is handig, omdat niet de volledige naamruimte hoeft te worden geschreven, maar ARR-specifieke gegevensstructuren nog steeds worden herkend. Uiteraard kan de using namespace...
-instructie ook worden gebruikt.
Remote Rendering-initialisatie
Er moeten enkele objecten voor de sessie enzovoort worden bewaard tijdens de levensduur van de toepassing. De levensduur valt samen met de levensduur van het HolographicAppMain
-object van de toepassing. Dit is de reden waarom objecten als leden aan klasse HolographicAppMain
moeten worden toegevoegd. In de volgende stap worden de volgende klasseleden toegevoegd aan het bestand 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
}
Een goede plaats voor de daadwerkelijke implementatie is de constructor van klasse HolographicAppMain
. Er moeten hier drie initialisatietypen worden uitgevoerd:
- De eenmalige initialisatie van het Remote Rendering-systeem
- Client maken (verificatie)
- Sessie maken
Dit wordt allemaal opeenvolgend in de constructor uitgevoerd. In echte gebruiksgevallen kan het echter beter zijn om deze stappen afzonderlijk uit te voeren.
Voeg de volgende code toe aan het begin van de constructorhoofdcode in het bestand 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:
...
}
De code roept lidfuncties SetNewSession
en SetNewState
aan, die in de volgende alinea worden geïmplementeerd, samen met de rest van de statusmachinecode.
Houd er rekening mee dat referenties in het voorbeeld zijn vastgelegd in code en moeten worden ingevuld (account-id, accountsleutel, accountdomein en Remote Rendering-domein).
De de-initialisatie wordt symmetrisch en in omgekeerde volgorde uitgevoerd aan het einde van de destructorhoofdcode:
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();
}
Statuscomputer
In Remote Rendering zijn het maken van een sessie en het laden van een model asynchrone functies. Voor dit account hebt u een eenvoudige statuscomputer nodig die in feite de volgende statussen automatisch doorloopt:
Initialisatie -> Sessie maken -> Sessie starten -> Model laden (met voortgang)
In de volgende stap wordt een beetje van de statuscomputerverwerking aan de klasse toegevoegd. Uw enum AppConnectionStatus
wordt gedeclareerd voor de verschillende statussen die uw toepassing kan hebben. Het is vergelijkbaar met RR::ConnectionStatus
, maar heeft een extra status voor een mislukte verbinding.
Voeg de volgende leden en functies toe aan de klassedeclaratie:
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;
}
Voeg de volgende functiehoofdcodes toe aan de implementatiezijde in het .cpp-bestand:
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;
}
}
Update per frame
We moeten de client eenmaal per simulatietik bijwerken en enkele aanvullende status-updates uitvoeren. Functie HolographicAppMain::Update
biedt een goede hook voor updates per frame.
Update van toestandsmachine
We moeten de status van de sessie controleren en bekijken of deze is overgegaan naar de status Ready
. Als er verbinding is gemaakt, gaan we het model laden via StartModelLoading
.
Voeg de volgende code toe aan de hoofdtekst van de functie 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:
...
}
Update coördinatensysteem
We moeten overeenstemming bereiken met de rendering-service over welk coördinatensysteem moet worden gebruikt. Om toegang te krijgen tot het coördinatensysteem dat we willen gebruiken, hebben we de m_stationaryReferenceFrame
nodig die aan het einde van de functie HolographicAppMain::OnHolographicDisplayIsAvailableChanged
is gemaakt.
Dit coördinatensysteem wordt doorgaans niet gewijzigd, dus dit is een eenmalige initialisatie. Dit moet opnieuw worden aangeroepen als uw toepassing het coördinatensysteem wijzigt.
De bovenstaande code stelt het coördinatensysteem eenmaal in binnen de functie Update
, zodra er een referentiecoördinatensysteem en een verbonden sessie is.
Camera-update
De clipvlakken van de camera moeten worden bijgewerkt, zodat de servercamera gesynchroniseerd blijft met de lokale camera. Dit kan aan het eind van de Update
functie:
...
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
De laatste stap is het aanroepen van de rendering van de externe inhoud. Deze aanroep moet worden uitgevoerd op exact de juiste positie binnen de renderingspijplijn, nadat het doel voor het renderen is gewist en de viewport is ingesteld. Voeg het volgende fragment in de UseHolographicCameraResources
-vergrendeling en in de functie HolographicAppMain::Render
in:
...
// 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();
}
...
De voorbeeldtoepassing uitvoeren
Het voorbeeld moet nu een status hebben waarin het wordt gecompileerd en uitgevoerd.
Wanneer het voorbeeld correct wordt uitgevoerd, wordt de roterende kubus weergegeven. Na het maken van een sessie en het laden van een model wordt het enginemodel op de huidige hoofdpositie weergegeven. Het maken van sessies en het laden van modellen kan enkele minuten duren. De huidige status wordt alleen naar het uitvoerpaneel van Visual Studio geschreven. Het wordt dan ook aangeraden om het voorbeeld in Visual Studio te starten.
Let op
De client verbreekt de verbinding met de server wanneer de tikfunctie een paar seconden niet wordt aangeroepen. Het activeren van onderbrekingspunten kan er zeer gemakkelijk toe leiden dat de toepassing de verbinding verbreekt.
Raadpleeg de standaardversie van deze zelfstudie op GitHub voor een juiste statusweergave met een tekstpaneel.
Volgende stappen
In deze zelfstudie hebt u alle stappen geleerd die nodig zijn om Remote Rendering toe te voegen aan een standaardvoorbeeld van Holographic-app C++/DirectX11. Raadpleeg de volgende quickstart om uw eigen model te converteren: