教學課程:將 遠端轉譯 整合到 HoloLens 全像攝影應用程式中
在本教學課程中,您將了解:
- 使用 Visual Studio 建立可部署至 HoloLens 的全像攝影應用程式
- 新增必要的代碼段和項目設定,以結合本機轉譯與遠端轉譯的內容
本教學課程著重於將必要的位新增至原生Holographic App
範例,以結合本機轉譯與 Azure 遠端轉譯。 此應用程式中唯一的狀態意見反應類型是透過Visual Studio內的偵錯輸出面板,因此建議從Visual Studio內部啟動範例。 新增適當的應用程式內意見反應超出此範例的範圍,因為從頭開始建置動態文字面板牽涉到許多程式代碼撰寫。 良好的起點是類別StatusDisplay
,這是 GitHub 上遠端播放程式範例專案的一部分。 事實上,本教學課程的預裝版本會使用該類別的本地副本。
提示
ARR 範例存放庫包含本教學課程的結果,做為準備使用的Visual Studio專案。 它也會透過UI類別 StatusDisplay
擴充適當的錯誤和狀態報告。 在教學課程中,所有 ARR 特定新增專案的範圍都是 #ifdef USE_REMOTE_RENDERING
/ #endif
,因此很容易識別 遠端轉譯 新增專案。
必要條件
在本教學課程中,您需要:
- 您的帳戶資訊(帳戶標識碼、帳戶密鑰、帳戶網域、訂用帳戶標識碼)。 如果您沒有帳戶, 請建立帳戶。
- Windows SDK 10.0.18362.0 (下載) 。
- 最新版的 Visual Studio 2022 (下載) 。
- 適用於混合實境的Visual Studio工具。 具體而言,必須安裝下列 工作負載 :
- 使用 C++ 的傳統型開發
- 通用 Windows 平台 (UWP) 開發
- 適用於 Visual Studio 的 Windows Mixed Reality 應用程式範本 (下載) 。
建立新的全像攝影應用程式範例
在第一個步驟中,我們會建立股票範例,這是 遠端轉譯 整合的基礎。 開啟 Visual Studio 並選取 [建立新專案],然後搜尋 [全像攝影 DirectX 11 應用程式 (通用 Windows) (C++/WinRT)]
輸入您選擇的專案名稱,選擇路徑,然後選取 [建立] 按鈕。 在新專案中,將組態切換為 “Debug / ARM64”。 您現在應該能夠編譯並部署至連線的 HoloLens 2 裝置。 如果您在 HoloLens 上執行它,您應該會在您前面看到旋轉的立方體。
透過 NuGet 新增 遠端轉譯 相依性
新增 遠端轉譯 功能的第一個步驟是新增用戶端相依性。 相關的相依性可作為 NuGet 套件使用。 在 方案總管 中,以滑鼠右鍵按兩下專案,然後從操作功能表中選取 [管理 NuGet 套件...]。
在提示的對話框中,流覽名為 “Microsoft.Azure.RemoteRendering.Cpp” 的 NuGet 套件:
然後選取套件,然後按 [安裝] 按鈕,將它新增至專案。
NuGet 套件會將 遠端轉譯 相依性新增至專案。 具體而言:
- 針對客戶端連結庫連結 (RemoteRenderingClient.lib)。
- 設定.dll相依性。
- 將正確的路徑設定為 include 目錄。
項目準備
我們需要對現有項目進行小變更。 這些變更是微妙的,但如果沒有它們,遠端轉譯 將無法運作。
在 DirectX 裝置上啟用多線程保護
裝置 DirectX11
必須啟用多線程保護。 若要變更,請在 「Common」 資料夾中開啟檔案DeviceResources.cpp,並在函式結尾 DeviceResources::CreateDeviceResources()
插入下列程式代碼:
// Enable multi thread protection as now multiple threads use the immediate context.
Microsoft::WRL::ComPtr<ID3D11Multithread> contextMultithread;
if (context.As(&contextMultithread) == S_OK)
{
contextMultithread->SetMultithreadProtected(true);
}
在應用程式指令清單中啟用網路功能
必須針對已部署的應用程式明確啟用網路功能。 若未進行此設定,連線查詢最終將會導致逾時。 若要啟用,請按兩下 package.appxmanifest
方案總管中的專案。 在下一個 UI 中,移至 [ 功能] 索引標籤,然後選取:
- 網際網路 (用戶端和伺服器)
- 網際網路 (用戶端)
整合 遠端轉譯
現在已備妥專案,我們可以從程式代碼開始。 應用程式的良好進入點是 類別 HolographicAppMain
(檔案 HolographicAppMain.h/cpp),因為它具有初始化、取消初始化和轉譯所需的所有連結。
Includes
我們一開始會新增必要的 include。 將下列 include 新增至 HolographicAppMain.h 檔案:
#include <AzureRemoteRendering.h>
...和這些要提出HolographicAppMain.cpp的其他 include
指示詞:
#include <AzureRemoteRendering.inl>
#include <RemoteRenderingExtensions.h>
#include <windows.perception.spatial.h>
為了簡化程式代碼,我們會在指示詞後面 include
,於 HolographicAppMain.h 檔案頂端定義下列命名空間快捷方式:
namespace RR = Microsoft::Azure::RemoteRendering;
此快捷方式很有用,因此我們不需要在任何地方寫出完整的命名空間,但仍可以辨識 ARR 特定的數據結構。 當然,我們也可以使用 using namespace...
指示詞。
遠端轉譯 初始化
我們需要在應用程式的存留期期間保留一些會話的物件等等。 存留期與應用程式 HolographicAppMain
物件的存留期一致,因此我們會將物件新增為 類別 HolographicAppMain
的成員。 下一個步驟是在 HolographicAppMain.h 檔案中新增下列類別成員:
class HolographicAppMain
{
...
// members:
std::string m_sessionOverride; // if we have a valid session ID, we specify it here. Otherwise a new one is created
RR::ApiHandle<RR::RemoteRenderingClient> m_client; // the client instance
RR::ApiHandle<RR::RenderingSession> m_session; // the current remote rendering session
RR::ApiHandle<RR::RenderingConnection> m_api; // the API instance, that is used to perform all the actions. This is just a shortcut to m_session->Connection()
RR::ApiHandle<RR::GraphicsBindingWmrD3d11> m_graphicsBinding; // the graphics binding instance
}
執行實際實作的好位置是 類別 HolographicAppMain
的建構函式。 我們必須在那裡執行三種類型的初始化:
- 遠端轉譯 系統的一次性初始化
- 用戶端建立(驗證)
- 會話建立
我們會在建構函式中循序執行所有作業。 不過,在實際使用案例中,可能適合個別執行這些步驟。
將下列程式代碼新增至檔案中建構函式主體的開頭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:
...
}
程式代碼會呼叫成員函式 SetNewSession
和 SetNewState
,我們將在下一個段落中實作,以及其餘的狀態機器程序代碼。
請注意,認證會在範例中硬式編碼,而且必須就地填寫 (帳戶標識元、帳戶密鑰、帳戶網域和 遠端轉譯網域)。
我們會以對稱方式和反向順序在解構函式主體結尾執行還原初始化:
HolographicAppMain::~HolographicAppMain()
{
// Existing destructor code:
...
// Destroy session:
if (m_session != nullptr)
{
m_session->Disconnect();
m_session = nullptr;
}
// Destroy front end:
m_client = nullptr;
// One-time de-initialization:
RR::ShutdownRemoteRendering();
}
狀態機器
在 遠端轉譯 中,用來建立會話和載入模型的主要函式是異步函式。 為了說明這一點,我們需要一個簡單狀態機器,基本上會自動透過下列狀態轉換:
初始化 - 工作階段建立 -> 工作階段開始 ->> 模型載入 (進度)
因此,在下一個步驟中,我們會將一些狀態機器處理新增至 類別。 我們針對應用程式可以處於的各種狀態宣告自己的列舉 AppConnectionStatus
。 它類似於 RR::ConnectionStatus
,但有失敗連線的額外狀態。
將下列成員和函式新增至類別宣告:
namespace HolographicApp
{
// Our application's possible states:
enum class AppConnectionStatus
{
Disconnected,
CreatingSession,
StartingSession,
Connecting,
Connected,
// error state:
ConnectionFailed,
};
class HolographicAppMain
{
...
// Member functions for state transition handling
void OnConnectionStatusChanged(RR::ConnectionStatus status, RR::Result error);
void SetNewState(AppConnectionStatus state, const char* statusMsg);
void SetNewSession(RR::ApiHandle<RR::RenderingSession> newSession);
void StartModelLoading();
// Members for state handling:
// Model loading:
std::string m_modelURI;
RR::ApiHandle<RR::LoadModelAsync> m_loadModelAsync;
// Connection state machine:
AppConnectionStatus m_currentStatus = AppConnectionStatus::Disconnected;
std::string m_statusMsg;
RR::Result m_connectionResult = RR::Result::Success;
RR::Result m_modelLoadResult = RR::Result::Success;
bool m_isConnected = false;
bool m_sessionStarted = false;
RR::ApiHandle<RR::SessionPropertiesAsync> m_sessionPropertiesAsync;
bool m_modelLoadTriggered = false;
float m_modelLoadingProgress = 0.f;
bool m_modelLoadFinished = false;
double m_timeAtLastRESTCall = 0;
bool m_needsCoordinateSystemUpdate = true;
}
在 .cpp 檔案中的實作端,新增下列函式主體:
void HolographicAppMain::StartModelLoading()
{
m_modelLoadingProgress = 0.f;
RR::LoadModelFromSasOptions options;
options.ModelUri = m_modelURI.c_str();
options.Parent = nullptr;
// start the async model loading
m_api->LoadModelFromSasAsync(options,
// completed callback
[this](RR::Status status, RR::ApiHandle<RR::LoadModelResult> result)
{
m_modelLoadResult = RR::StatusToResult(status);
m_modelLoadFinished = true;
if (m_modelLoadResult == RR::Result::Success)
{
RR::Double3 pos = { 0.0, 0.0, -2.0 };
result->GetRoot()->SetPosition(pos);
}
},
// progress update callback
[this](float progress)
{
// progress callback
m_modelLoadingProgress = progress;
m_needsStatusUpdate = true;
});
}
void HolographicAppMain::SetNewState(AppConnectionStatus state, const char* statusMsg)
{
m_currentStatus = state;
m_statusMsg = statusMsg ? statusMsg : "";
// Some log for the VS output panel:
const char* appStatus = nullptr;
switch (state)
{
case AppConnectionStatus::Disconnected: appStatus = "Disconnected"; break;
case AppConnectionStatus::CreatingSession: appStatus = "CreatingSession"; break;
case AppConnectionStatus::StartingSession: appStatus = "StartingSession"; break;
case AppConnectionStatus::Connecting: appStatus = "Connecting"; break;
case AppConnectionStatus::Connected: appStatus = "Connected"; break;
case AppConnectionStatus::ConnectionFailed: appStatus = "ConnectionFailed"; break;
}
char buffer[1024];
sprintf_s(buffer, "Remote Rendering: New status: %s, result: %s\n", appStatus, m_statusMsg.c_str());
OutputDebugStringA(buffer);
}
void HolographicAppMain::SetNewSession(RR::ApiHandle<RR::RenderingSession> newSession)
{
SetNewState(AppConnectionStatus::StartingSession, nullptr);
m_sessionStartingTime = m_timeAtLastRESTCall = m_timer.GetTotalSeconds();
m_session = newSession;
m_api = m_session->Connection();
m_graphicsBinding = m_session->GetGraphicsBinding().as<RR::GraphicsBindingWmrD3d11>();
m_session->ConnectionStatusChanged([this](auto status, auto error)
{
OnConnectionStatusChanged(status, error);
});
};
void HolographicAppMain::OnConnectionStatusChanged(RR::ConnectionStatus status, RR::Result error)
{
const char* asString = RR::ResultToString(error);
m_connectionResult = error;
switch (status)
{
case RR::ConnectionStatus::Connecting:
SetNewState(AppConnectionStatus::Connecting, asString);
break;
case RR::ConnectionStatus::Connected:
if (error == RR::Result::Success)
{
SetNewState(AppConnectionStatus::Connected, asString);
}
else
{
SetNewState(AppConnectionStatus::ConnectionFailed, asString);
}
m_modelLoadTriggered = m_modelLoadFinished = false;
m_isConnected = error == RR::Result::Success;
break;
case RR::ConnectionStatus::Disconnected:
if (error == RR::Result::Success)
{
SetNewState(AppConnectionStatus::Disconnected, asString);
}
else
{
SetNewState(AppConnectionStatus::ConnectionFailed, asString);
}
m_modelLoadTriggered = m_modelLoadFinished = false;
m_isConnected = false;
break;
default:
break;
}
}
每個畫面更新
我們必須在每個模擬刻度更新用戶端一次,並執行一些額外的狀態更新。 函 HolographicAppMain::Update
式提供每個畫面格更新的良好勾點。
狀態機器更新
我們需要輪詢會話的狀態,並查看它是否已轉換為 Ready
狀態。 如果已成功連線,我們最後會透過 StartModelLoading
開始載入模型。
將下列程式代碼新增至函式主體 HolographicAppMain::Update
:
// Updates the application state once per frame.
HolographicFrame HolographicAppMain::Update()
{
if (m_session != nullptr)
{
// Tick the client to receive messages
m_api->Update();
if (!m_sessionStarted)
{
// Important: To avoid server-side throttling of the requests, we should call GetPropertiesAsync very infrequently:
const double delayBetweenRESTCalls = 10.0;
// query session status periodically until we reach 'session started'
if (m_sessionPropertiesAsync == nullptr && m_timer.GetTotalSeconds() - m_timeAtLastRESTCall > delayBetweenRESTCalls)
{
m_timeAtLastRESTCall = m_timer.GetTotalSeconds();
m_session->GetPropertiesAsync([this](RR::Status status, RR::ApiHandle<RR::RenderingSessionPropertiesResult> propertiesResult)
{
if (status == RR::Status::OK)
{
auto ctx = propertiesResult->GetContext();
if (ctx.Result == RR::Result::Success)
{
auto res = propertiesResult->GetSessionProperties();
switch (res.Status)
{
case RR::RenderingSessionStatus::Ready:
{
// The following ConnectAsync is async, but we'll get notifications via OnConnectionStatusChanged
m_sessionStarted = true;
SetNewState(AppConnectionStatus::Connecting, nullptr);
RR::RendererInitOptions init;
init.IgnoreCertificateValidation = false;
init.RenderMode = RR::ServiceRenderMode::Default;
m_session->ConnectAsync(init, [](RR::Status, RR::ConnectionStatus) {});
}
break;
case RR::RenderingSessionStatus::Error:
SetNewState(AppConnectionStatus::ConnectionFailed, "Session error");
break;
case RR::RenderingSessionStatus::Stopped:
SetNewState(AppConnectionStatus::ConnectionFailed, "Session stopped");
break;
case RR::RenderingSessionStatus::Expired:
SetNewState(AppConnectionStatus::ConnectionFailed, "Session expired");
break;
}
}
else
{
SetNewState(AppConnectionStatus::ConnectionFailed, ctx.ErrorMessage.c_str());
}
}
else
{
SetNewState(AppConnectionStatus::ConnectionFailed, "Failed to retrieve session status");
}
m_sessionPropertiesQueryInProgress = false; // next try
}); }
}
}
if (m_isConnected && !m_modelLoadTriggered)
{
m_modelLoadTriggered = true;
StartModelLoading();
}
}
if (m_needsCoordinateSystemUpdate && m_stationaryReferenceFrame && m_graphicsBinding)
{
// Set the coordinate system once. This must be called again whenever the coordinate system changes.
winrt::com_ptr<ABI::Windows::Perception::Spatial::ISpatialCoordinateSystem> ptr{ m_stationaryReferenceFrame.CoordinateSystem().as<ABI::Windows::Perception::Spatial::ISpatialCoordinateSystem>() };
m_graphicsBinding->UpdateUserCoordinateSystem(ptr.get());
m_needsCoordinateSystemUpdate = false;
}
// Rest of the body:
...
}
座標系統更新
我們需要同意座標系統上要使用的轉譯服務。 若要存取我們想要使用的座標系統,我們需要 m_stationaryReferenceFrame
在函式 結尾建立的 HolographicAppMain::OnHolographicDisplayIsAvailableChanged
。
此座標系統通常不會變更,因此這是一次初始化。 如果您的應用程式變更座標系統,則必須再次呼叫它。
上述程式代碼會在函式內 Update
設定座標系統一次,只要我們有參考座標系統和連接的會話。
相機 更新
我們需要更新相機剪輯平面,讓伺服器相機與本機相機保持同步。 我們可以在函式 Update
的結尾執行此動作:
...
if (m_isConnected)
{
// Any near/far plane values of your choosing.
constexpr float fNear = 0.1f;
constexpr float fFar = 10.0f;
for (HolographicCameraPose const& cameraPose : prediction.CameraPoses())
{
// Set near and far to the holographic camera as normal
cameraPose.HolographicCamera().SetNearPlaneDistance(fNear);
cameraPose.HolographicCamera().SetFarPlaneDistance(fFar);
}
// The API to inform the server always requires near < far. Depth buffer data will be converted locally to match what is set on the HolographicCamera.
auto settings = m_api->GetCameraSettings();
settings->SetNearAndFarPlane(std::min(fNear, fFar), std::max(fNear, fFar));
settings->SetEnableDepth(true);
}
// The holographic frame will be used to get up-to-date view and projection matrices and
// to present the swap chain.
return holographicFrame;
}
轉譯
最後一件事就是叫用遠端內容的轉譯。 在轉譯目標清除並設定檢視區之後,我們必須在轉譯管線內的確切正確位置執行此呼叫。 將下列代碼段插入函式內的UseHolographicCameraResources
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();
}
...
執行範例
範例現在應該處於編譯和執行的狀態。
當範例正確執行時,它會在前面顯示旋轉的 Cube,並在一些會話建立和模型載入之後,轉譯位於目前前端位置的引擎模型。 會話建立和模型載入最多可能需要幾分鐘的時間。 目前的狀態只會寫入 Visual Studio 的輸出面板。 因此,建議從Visual Studio內部啟動範例。
警告
當未呼叫刻度函式幾秒鐘時,用戶端會中斷與伺服器的連線。 因此,觸發斷點很容易導致應用程式中斷連線。
如需使用文字面板的適當狀態顯示,請參閱 GitHub 上本教學課程的預先設定版本。
下一步
在本教學課程中,您已瞭解將 遠端轉譯 新增至庫存全像攝影應用程式 C++/DirectX11 範例所需的所有步驟。 若要轉換您自己的模型,請參閱下列快速入門: