C++
DirectX
SteamVR
imgui
OpenVR

ダッシュボードオーバーレイ(OpenVR overlay)を作りimguiとDirectXで描いてみる


はじめに

Oculus Rift Advent Calendar 2018の17日目の記事です。

OpenVRのダッシュボードオーバーレイ機能の紹介と実際の作り方についてです。

VRの中で設定画面など2Dのスクリーンを出す方法として、OpenVRのダッシュボードのオーバーレイがあります。

これを作るサンプルとしてOpenVR SDKに付属の公式サンプル「helloworldoverlay」がありますが、そのサンプルではGUIとイベント周りにQtを使用しています。

しかしQtに慣れていなかったり、ライセンス的にQtを使いたくない場合もあると思います。

そこで、GUIにimgui、描画にDirectX11を使う方法をかんたんにまとめました。

使用言語はC++です。


  • OpenVRのOverlay機能の使い方

  • Qtを使わず、imguiを使う

  • DirectX11を使う

  • C++


OpenVRのダッシュボードのオーバーレイとは

こんなやつです。VR内のボタンを押すと2Dの画面が1mくらい先に壁のようにでてくるやつです。

IMG_8998.JPG

ダッシュボードのオーバーレイとして作成したアプリを起動しておくと、VR内でいつでも、ボタンアイコンをクリックして2Dの画面を表示することができます。

今回、この起動ボタンとGUIの乗った2D画面を作成します。


imguiとは

『OpenGLやDirectXなGUIにimguiが最強すぎる - Qiita』を御覧ください。


実装の要約


  1. ウインドウを作り(表示はさせない)、DirectXを初期化する

  2. imguiの初期化をする

  3. オーバーレイアプリとしてOpenVRを初期化をし、ダッシュボードと起動ボタンのオーバーレイを作成する

  4. イベントループを回し


    1. オーバーレイのレーザポインタのイベントを取り(今回、クリック処理の実装は省略します)

    2. imguiのフレーム処理

    3. imguiのウィジェット定義(なんとイベントループ内でやります)

    4. imguiの描画

    5. 上記の描画バッファをオーバーレイに転送



こんな感じの内容です。


下準備

では、初めてみましょう。次のような下準備をしてください。


  • OpenVRのSDKをダウンロードしておく

  • imguiをダウンロードしておく

  • VisualStudioでプロジェクトを作成し、必要なLIBやヘッダなどを追加しておく

  • imguiは基本のファイル以外に付属している「imgui_impl_win32」と「imgui_impl_dx11」のhとcppも追加しておく

  • パスなんかも通しておく

  • 起動ボタンアイコンの画像を用意しておく


手順

私の拙い説明よりも、コードを見たほうがわかりやすいと思います。

githubにプロジェクトを置いておきます


ウインドウの作成

必要な大きさで好きなように作ってください。ディスプレイに表示させてもいいですが、今回は表示させません。また、終了処理が未実装なのでプロセスを殺すときは、デバッガもしくはタスクマネージャからお願いしますm(__)m

  RECT rc = { 0, 0, 512, 512 };

AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, FALSE );
HWND hWnd = CreateWindow( L"Dashboard", L"Dashboard", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top,
NULL, NULL, hInstance, NULL );
if ( !hWnd ) {
return 0;
}


DirectXの初期化

これも一例です。

ID3D11Device

ID3D11DeviceContext

IDXGISwapChain

ID3D11RenderTargetView

を作ってしまいます。

  UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;

D3D_FEATURE_LEVEL featureLevel;
const D3D_FEATURE_LEVEL featureLevelArray[2] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, };

DXGI_SWAP_CHAIN_DESC scDesc = {};
scDesc.BufferCount = 2;
scDesc.BufferDesc.Width = 0;
scDesc.BufferDesc.Height = 0;
scDesc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
scDesc.BufferDesc.RefreshRate.Numerator = 60;
scDesc.BufferDesc.RefreshRate.Denominator = 1;
scDesc.Flags = DXGI_SWAP_CHAIN_FLAG_GDI_COMPATIBLE;
scDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
scDesc.OutputWindow = hWnd;
scDesc.SampleDesc.Count = 1;
scDesc.SampleDesc.Quality = 0;
scDesc.Windowed = TRUE;
scDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

// デバイスコンテキストとスワップチェーンを作成
HRESULT hr = D3D11CreateDeviceAndSwapChain( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags, featureLevelArray, 2,
D3D11_SDK_VERSION, &scDesc, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext );
if ( hr != S_OK ) {
return E_FAIL;
}

// レンダーターゲット
ID3D11Texture2D* pBackBuffer;
g_pSwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), (LPVOID*)&pBackBuffer );
g_pd3dDevice->CreateRenderTargetView( pBackBuffer, NULL, &g_mainRenderTargetView );

pBackBuffer->Release();


imguiの準備

次に、imguiの初期化をこんな感じでやります。先程作ったウインドウのハンドルや、Direct3Dのデバイスとデバイスコンテキストを、ImGui_xx関数に渡してあげます。

  IMGUI_CHECKVERSION();

ImGui::CreateContext();

ImGui_ImplWin32_Init( hWnd ); // Windows
ImGui_ImplDX11_Init( g_pd3dDevice, g_pd3dDeviceContext ); // DirectX使うよ

ImGui::StyleColorsClassic(); // 好きなスタイルで

ImGui_ImplDX11_CreateDeviceObjects(); // DirectX


OpenVRの準備

そして今度はOpenVRの初期化をします。オーバーレイのアプリだぞ、と。

成功するとvr::VROverlay()が使えるようになります。

  vr::EVRInitError initError = vr::VRInitError_None;

vr::VR_Init( &initError, vr::VRApplication_Overlay );

if ( !vr::VROverlay() ) return false;


ダッシュボードオーバーレイの作成

CreateDashboardOverlay()でダッシュボードのオーバーレイを作成します。

ダッシュボードのオーバレイハンドル(VROverlayHandle_t m_ulOverlayHandle)と、起動ボタン用のオーバーレイハンドルが取れます。

次に、色やアルファや入力方法を設定します。

  vr::VROverlay()->ClearOverlayTexture( g_ulOverlayHandle );

vr::VROverlay()->DestroyOverlay( g_ulOverlayHandle );

g_ulOverlayHandle = vr::k_ulOverlayHandleInvalid;

// OpenVR オーバーレイダッシュボード作成
std::string name = "Hoge";
vr::VROverlayError overlayError = vr::VROverlay()->CreateDashboardOverlay( name.c_str(), name.c_str(), &g_ulOverlayHandle, &g_ulOverlayThumbnailHandle );
if ( overlayError != vr::VROverlayError_None ) {
if ( overlayError == vr::VROverlayError_KeyInUse ) {
// 起動済み
}
// エラー処理
return false;
}

vr::VROverlay()->SetOverlayAlpha( g_ulOverlayHandle, 1.0f ); /// 透明度 TODO:
vr::VROverlay()->SetOverlayColor( g_ulOverlayHandle, 1.0f, 1.0f, 1.0f );

vr::VROverlay()->SetOverlayWidthInMeters( g_ulOverlayHandle, 2.0f ); /// 拡大率
vr::VROverlay()->SetOverlayInputMethod( g_ulOverlayHandle, vr::VROverlayInputMethod_Mouse );

また、起動ボタンのオーバーレイを画像(ボタンのアイコン画像)から作成します。SetOverlayFromFile()に先程取れたハンドルを渡してください。ここでは、画像ファイルをフルパスで実行ファイルと同じ場所にある前提で指定しています。

  // ボタンオーバーレイ作成

std::string path = GetExePath();
std::string thumbIconPath = path + "\\thumbicon.png"; /// ボタンのアイコン
overlayError = vr::VROverlay()->SetOverlayFromFile( g_ulOverlayThumbnailHandle, thumbIconPath.c_str() );
if ( overlayError != vr::VROverlayError_None ) {
// エラー処理
return false;
}

以上で表示準備完了です!


イベントループ

ここも各自、好みの形で・・

  // イベントループ

MSG msg = {};
while ( msg.message != WM_QUIT ) {
if ( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );
continue;
}
RenderOverlay( hWnd );

Sleep( 10 );
}


毎フレームの処理

上記イベントループから、毎フレーム呼んでいる関数(ここではRenderOverlay)の中身です。

まず、OpenVRのイベントを処理します。次のようにマウスの移動やボタンのイベントが取れます。

今回のサンプルではここでは何もせず、取得したEventの中身をあとで使うことにします。

 if ( !vr::VROverlay() || !vr::VROverlay()->IsOverlayVisible( g_ulOverlayHandle ) )

return;

// OpenVRイベント処理
vr::VREvent_t Event;
while ( vr::VROverlay()->PollNextOverlayEvent( g_ulOverlayHandle, &Event, sizeof( Event ) ) ) {
switch ( Event.eventType ) {
case vr::VREvent_MouseMove:
// 座標変換 TODO:
break;
case vr::VREvent_MouseButtonDown:
// クリック処理 TODO:
break;
}
}

次に、imguiの毎フレーム処理です。

ImGui_ImplDX11_NewFrame()は、imgui付属のimgui_impl_dx11.cppの関数です。

ImGui_ImplWin32_NewFrame_VR()のほうは、今回あたらしく作った関数です。

  // imgui毎フレーム処理

ImGui_ImplDX11_NewFrame();
ImGui_ImplWin32_NewFrame_VR( hWnd, &Event ); // マウス処理など
ImGui::NewFrame();

ImGui_ImplWin32_NewFrame_VR()の中身です。

先程OpenVRから取得したEventから、コントローラーのポインタ(レーザーポインタみたいなの)の指す座標を取り出し、それをimguiにマウス座標としてセットしてあげます。今回はやっていませんが、ボタンダウンの実装をすることでGUIのアイテムを操作できるようになります。

void ImGui_ImplWin32_NewFrame_VR( HWND hWnd, vr::VREvent_t* pEvent )

{
ImGuiIO& io = ImGui::GetIO();

RECT rect;
::GetClientRect( hWnd, &rect );
io.DisplaySize = ImVec2( (float)( rect.right - rect.left ), (float)( rect.bottom - rect.top ) );

// Setup time step
INT64 current_time;
::QueryPerformanceCounter( (LARGE_INTEGER *)&current_time );
io.DeltaTime = (float)( current_time - g_Time ) / g_TicksPerSecond;
g_Time = current_time;

// コントローラーの座標を変換
float pos_x = pEvent->data.mouse.x * io.DisplaySize.x;
float pos_y = (1.0f - pEvent->data.mouse.y) * io.DisplaySize.y;
io.MousePos = ImVec2( pos_x, pos_y );
}

次に、imguiウィジェットの定義です。自由に書いてください。ここではマウス座標の表示とチェックボックスを表示しているだけです。

  // ウィジェット定義

{
bool p_open = true;
bool foo = true;
ImGui::SetNextWindowPos( ImVec2( 10, 200 ) );
ImGui::Begin( "Simple Overlay", &p_open, ImVec2( 0, 0 ), 1.0f, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings );
ImGui::Text( "Simple Overlay\nhogehoge" );
ImGui::Separator();
ImGui::Text( "Mouse Position: (%.1f,%.1f)", ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y );
ImGui::Text( "Pointer Position: (%.1f,%.1f)", Event.data.mouse.x, Event.data.mouse.y );
ImGui::Separator();
ImGui::Checkbox( "Enable foo", &foo );
ImGui::End();
}

次は、imguiの描画です。ImGui_ImplDX11_RenderDrawData()は、imgui_impl_dx11.cpp内の関数です。詳しくはその中をみてください。本当は描画部分をコールバックにして、ImGui::Render()をするのが良いらしいですよ。

  // imgui描画

ImVec4 clear_color = ImVec4( 0.2f, 0.2f, 0.8f, 1.00f );

g_pd3dDeviceContext->OMSetRenderTargets( 1, &g_mainRenderTargetView, NULL ); /// デバイスコンテキストにバックバッファとデプスバッファを関連付け
g_pd3dDeviceContext->ClearRenderTargetView( g_mainRenderTargetView, (float*)&clear_color );

ImGui::Render(); /// 描画コールバック
ImGui_ImplDX11_RenderDrawData( ImGui::GetDrawData() ); /// 描画コールバックを使わない場合

g_pSwapChain->Present( 1, 0 );

最後に、描画されたバッファをオーバーレイのバッファに複写します。

DirectXの場合は、タイプにTextureType_DirectXを指定し、ID3D11Texture2Dを渡してやります。

このID3D11Texture2Dに描きたいものを転写します。

  // オーバーレイに複写

ID3D11Texture2D* backBuffer = NULL;
g_pSwapChain->GetBuffer( 0, IID_ID3D11Texture2D, (void**)&backBuffer );

vr::Texture_t texture = { (void *)backBuffer, vr::TextureType_DirectX, vr::ColorSpace_Auto };
vr::VROverlay()->SetOverlayTexture( g_ulOverlayHandle, &texture );

backBuffer->Release();


お疲れ様でした

以上で、VR空間内のダッシュボードに起動ボタンを表示し、それを押すことで独自のGUIの付いたオーバーレイを表示させることができました。

あまり描画が綺麗じゃない感じがするので、もっと良い方法がありましたら教えてくださいm(__)m

お疲れ様でした!