■ 2017年05月29日 [OpenGL][メモ] GLFW 3 で Oculus Rift を使う (1)
忙しい (こればっかり)
でも本当に忙しいんです.意味わからんくらいに忙しいんです.たぶん,自分は仕事が遅いというか,一つの仕事に時間をかけすぎるのが問題なんだと思います.できんもんはできんと言わんと結局人に迷惑をかけることになるので,ここんとこ安請け合いをしないよう気を付けていたつもりなんですけど,結局いろんな仕事を引き受けてしまいました.そのせいで学生さんの面倒を十分見れてないんですけど,学生さんの方から「自分たちでやります」と言ってくれるので助かります.なんもかんも自分がやらんといかんと思いこむのは,自分の驕りなんでしょうな.そのうちラーメンおごったるからな (と安請け合い).
ゲームエンジンを使わずに VR
VR コンテンツ開発には,ゲームエンジンを使うべき(VR開発の基本はゲームエンジン。ゲームエンジンとは何か、UnityとUnreal Engineの違いは?←ここで紹介されている本はおススメ)だと重々承知してますし,ミドルウェアを使わずに API 直叩きすることに何のメリットもなく,むしろ自分の仕事が遅い最大の原因なんでしょう,だから API の変更や新しいデバイスへの対応のたびに「ゲームエンジンに移行してやる!」と思いますし,Unity や UE4 の「さわり」の演習だってやってたりします.でも,もう 20 年以上そういうものを使わずにやってきたのだし,「床井もやられたようだな…」「フフフ…奴は四天王の中でも最弱…」みたいな扱いになるのも癪なので*1,とりあえずやったことだけメモしておこうと思います*2.なお,次回はこのメモの内容を使ったラッパーを使い,OpenGL 直叩きで Oculus Rift にレンダリングするサンプルプログラムを説明する予定です.ニーズはないと思いますけどっ.
でも GLFW は使う
とはいっても,GLFW くらいは使います.これくらいは使わせてください.
#include <GLFW/glfw3.h>
このほか,当然ながら Oculus SDK for Windows に含まれる LibOVR を使用します*3 .SDK のバージョンは Oculus Rift DK1 / DK2 で使われる 0.8 と CV1 で使われる 1.x (これを書いている時点では 1.14) に対応しています.この切り替えは OVR_PRODUCT_VERSION で判断しています.また,GLFW で LibOVR を使う場合,ヘッダファイルの読み込みに若干工夫が必要です.このほか,これより前に USE_OCULUS_RIFT が #define されているものとします.
// Oculus Rift SDK ライブラリ (LibOVR) の組み込み #if defined(USE_OCULUS_RIFT) # if defined(_WIN32) # define GLFW_EXPOSE_NATIVE_WIN32 # define GLFW_EXPOSE_NATIVE_WGL # include "glfw3native.h" # define OVR_OS_WIN32 # undef APIENTRY # pragma comment(lib, "LibOVR.lib") # endif # include <OVR_CAPI_GL.h> # include <Extras/OVR_Math.h> # if OVR_PRODUCT_VERSION > 0 # include <dxgi.h> // GetDefaultAdapterLuid のため # pragma comment(lib, "dxgi.lib") inline ovrGraphicsLuid GetDefaultAdapterLuid() { ovrGraphicsLuid luid = ovrGraphicsLuid(); # if defined(_WIN32) IDXGIFactory *factory(nullptr); if (SUCCEEDED(CreateDXGIFactory(IID_PPV_ARGS(&factory)))) { IDXGIAdapter *adapter(nullptr); if (SUCCEEDED(factory->EnumAdapters(0, &adapter))) { DXGI_ADAPTER_DESC desc; adapter->GetDesc(&desc); memcpy(&luid, &desc.AdapterLuid, sizeof luid); adapter->Release(); } factory->Release(); } # endif return luid; } inline int Compare(const ovrGraphicsLuid& lhs, const ovrGraphicsLuid& rhs) { return memcmp(&lhs, &rhs, sizeof(ovrGraphicsLuid)); } # endif #endif
Oculus SDK 1.x では,デフォルトのビデオカードがプライマリになっているかどうかを確認するために,ここで GetDefaultAdapterLuid() および Compare() を定義しています.この部分は Direct3D 10 の DXGI を使っています.この確認は Oculus SDK の 0.8 では行いません.確認したってどうなるもんでもないと思うんですけどね.
Oculus Rift を使うためのデータ
Oculus Rift を使用するために,Oculus Rift の設定や状態の取得のための変数をいくつか用意しておきます.
// Oculus Rift のセッション ovrSession session; // Oculus Rift の情報 ovrHmdDesc hmdDesc;
Oculus Rift に表示する際は,GLFW で開いたウィンドウとは別に,Oculus Rift 自体への表示(レンダリング)を行います.そのために FBO を使用します.また,GLFW で開いたウィンドウは,Oculus Rift へのレンダリング結果をディスプレイ上で確認するためのミラー表示に使います.それにも FBO を用意します.
// Oculus Rift 表示用の FBO GLuint oculusFbo[ovrEye_Count]; // ミラー表示用の FBO GLuint mirrorFbo;
Oculus Rift に表示するには,Oculus Rift の構造に合わせた投影変換を行う必要があります.また,ヘッドトラッキングを行いますから,それにより計測された視点の位置を取得して,レンダリングの際のカメラの位置として使います.
// Oculus Rift のスクリーンのサイズ GLfloat screen[ovrEye_Count][4]; // Oculus Rift のスクリーンのヘッドトラッキング位置からの変位 GLfloat offset[ovrEye_Count][3];
Oculus SDK 1.x では ovrLayerEyeFov 構造体の変数 layerData が保持するレンダーターゲット (FBO のカラーテクスチャ) にレンダリングを行います.これには姿勢情報 (ヘッドトラッキング情報) なども含みますが,デプスバッファ用のテクスチャは別に保持します.レンダリングするフレームの番号 frameIndex は,Oculus Rift CV1 の特徴でもある Asynchronous TimeWarp (ATW) 処理で使用します.たぶん.
# if OVR_PRODUCT_VERSION > 0 // Oculus Rift への描画情報 ovrLayerEyeFov layerData; // Oculus Rift 表示用の FBO のデプステクスチャ GLuint oculusDepth[ovrEye_Count]; // Oculus Rift にレンダリングするフレームの番号 long long frameIndex; // ミラー表示用の FBO のサイズ int mirrorWidth, mirrorHeight; // ミラー表示用の FBO のカラーテクスチャ ovrMirrorTexture mirrorTexture;
これに対して Oculus SDK 0.8 では,ovrLayer_Union 共用体の変数 layerData にレンダーターゲットとデプスバッファ用のテクスチャを保存する一方,姿勢情報などは別 (eyeRenderDesc, eyePose) に保持します.
# else // Oculus Rift に転送する描画データ ovrLayer_Union layerData; // Oculus Rift のレンダリング情報 ovrEyeRenderDesc eyeRenderDesc[ovrEye_Count]; // Oculus Rift の視点情報 ovrPosef eyePose[ovrEye_Count]; // ミラー表示用の FBO のカラーテクスチャ ovrGLTexture *mirrorTexture; # endif
初期化
まず,Oculus Rift 用に用意した変数に初期値を設定しておきます.session = nullptr; oculusFbo[0] = oculusFbo[1] = 0; mirrorFbo = 0; mirrorTexture = nullptr; # if OVR_PRODUCT_VERSION > 0 frameIndex = 0LL; oculusDepth[0] = oculusDepth[1] = 0; # endif
最初に ovr_Initialize() を呼び出して,LibOVR を初期化します.これに失敗したときは,ここでは単に return していますが,例外を投げるなどエラー処理を適切に行ってください.成功したら,プログラムの終了時に呼び出す必要のある ovr_Shutdown() を atexit() で登録しておきます.
// Oculus Rift (LibOVR) を初期化する if (OVR_FAILURE(ovr_Initialize(nullptr))) return; // プログラム終了時には LibOVR を終了する atexit(ovr_Shutdown);その後,ovr_Create() を呼び出して Oculus Rift のセッションを作成します.その際,ovr_Create() から得られた luid を冒頭で作成した GetDefaultAdapterLuid() の値と比較して,一致していなければ (Compare() で呼び出している memcmp() の値が非 0) ばエラーとします.
// LUID は Oculus SDK 0.8 では使っていないらしい ovrGraphicsLuid luid; // Oculus Rift のセッションを作成する if (OVR_FAILURE(ovr_Create(&session, &luid))) return; # if OVR_PRODUCT_VERSION > 0 // デフォルトのグラフィックスアダプタが使われているか確かめる if (Compare(luid, GetDefaultAdapterLuid())) return; # endif
あと,Oculus Rift に表示するときは,ダブルバッファリングは行いません.ダブルバッファリングはディスプレイ上のウィンドウに対して行われるので,これを有効にすると表示がディスプレイのリフレッシュレートに律速されてしまいます.また,Oculus Rift へのレンダリングは SRGB 色空間で行います.
// Oculus Rift ではダブルバッファリングしない glfwWindowHint(GLFW_DOUBLEBUFFER, GL_FALSE); // Oculus Rift では SRGB でレンダリングする glfwWindowHint(GLFW_SRGB_CAPABLE, GL_TRUE);
OpenGL のウィンドウの作成
LibOVR の初期化が完了したら,GLFW の初期化を行って,ディスプレイ上にウィンドウを開きます (たぶん,どっちが先でも構わないと思います).このウィンドウはミラー表示に使います.
Oculus Rift の設定
OpenGL で描画するウィンドウを開くことに成功したら (たぶん開く必要はないんですけど OpenGL 自体の初期化には必要なので…),Oculus Rift 自体の設定を行います.まず,ovr_GetHmdDesc() を使って使用する Oculus Rift の情報を取り出します.一応,デバッグ用に,取り出した情報を表示するようにしておきます.
// Oculus Rift の情報を取り出す hmdDesc = ovr_GetHmdDesc(session); # if defined(_DEBUG) // Oculus Rift の情報を表示する std::cout << "¥nProduct name: " << hmdDesc.ProductName << "¥nResolution: " << hmdDesc.Resolution.w << " x " << hmdDesc.Resolution.h << "¥nDefault Fov: (" << hmdDesc.DefaultEyeFov[ovrEye_Left].LeftTan << "," << hmdDesc.DefaultEyeFov[ovrEye_Left].DownTan << ") - (" << hmdDesc.DefaultEyeFov[ovrEye_Left].RightTan << "," << hmdDesc.DefaultEyeFov[ovrEye_Left].UpTan << ")¥n (" << hmdDesc.DefaultEyeFov[ovrEye_Right].LeftTan << "," << hmdDesc.DefaultEyeFov[ovrEye_Right].DownTan << ") - (" << hmdDesc.DefaultEyeFov[ovrEye_Right].RightTan << "," << hmdDesc.DefaultEyeFov[ovrEye_Right].UpTan << ")¥nMaximum Fov: (" << hmdDesc.MaxEyeFov[ovrEye_Left].LeftTan << "," << hmdDesc.MaxEyeFov[ovrEye_Left].DownTan << ") - (" << hmdDesc.MaxEyeFov[ovrEye_Left].RightTan << "," << hmdDesc.MaxEyeFov[ovrEye_Left].UpTan << ")¥n (" << hmdDesc.MaxEyeFov[ovrEye_Right].LeftTan << "," << hmdDesc.MaxEyeFov[ovrEye_Right].DownTan << ") - (" << hmdDesc.MaxEyeFov[ovrEye_Right].RightTan << "," << hmdDesc.MaxEyeFov[ovrEye_Right].UpTan << ")¥n" << std::endl; # endif
そして,layerData に値を設定します.Oculus Rift へのレンダリングは,この layerData 変数を介して行います.この Header の Type メンバに,Oculus SDK 1.x では (デプスバッファを保持しないことを示す) ovrLayerType_EyeFov を設定し,Oculus SDK 0.8 では (デプスバッファを保持することを示す) ovrLayerType_EyeFovDepth を設定します.
// Oculus Rift に転送する描画データを作成する # if OVR_PRODUCT_VERSION > 0 layerData.Header.Type = ovrLayerType_EyeFov; # else layerData.Header.Type = ovrLayerType_EyeFovDepth; # endif layerData.Header.Flags = ovrLayerFlag_TextureOriginAtBottomLeft; // OpenGL なので左下が原点
左右の目ごとに Oculus Rift の視野を求め,それをもとに Oculus Rift へのレンダリングに使う FBO のレンダーターゲットのサイズを求めます.アスペクト比はこの FBO のレンダーターゲットのサイズから決定しますが,アプリケーションの組み方によっては必要ないかもしれません.スクリーンのサイズ screen は前方面の位置 zNear = 1 の時のスクリーンの領域で,glFrustum() に設定する left, right, bottom, top に相当します.
// Oculus Rift 表示用の FBO を作成する for (int eye = 0; eye < ovrEye_Count; ++eye) { // Oculus Rift の視野を取得する const auto &fov(hmdDesc.DefaultEyeFov[ovrEyeType(eye)]); // Oculus Rift 表示用の FBO のサイズを求める const auto textureSize(ovr_GetFovTextureSize(session, ovrEyeType(eye), fov, 1.0f)); // Oculus Rift 表示用の FBO のアスペクト比を求める aspect = static_cast<GLfloat>(textureSize.w) / static_cast<GLfloat>(textureSize.h); // Oculus Rift のスクリーンのサイズを保存する screen[eye][0] = -fov.LeftTan; screen[eye][1] = fov.RightTan; screen[eye][2] = -fov.DownTan; screen[eye][3] = fov.UpTan;
FBO のレンダーターゲットの作成
FBO のレンダーターゲットを作成します.Oculus SDK 1.x では,まず ovr_CreateTextureSwapChainGL() でテクスチャのリストみたいなものを作るようです.その際に ovrTextureSwapChainDesc 構造体によってテクスチャの特性を指定します.ここでミップマップレベルヤマルチサンプリングなんかも指定できるようです (やってません).そして,ovr_GetTextureSwapChainLength() でそのリストの長さを求め,その数だけ ovr_GetTextureSwapChainBufferGL() でテクスチャを作成します.ovr_GetTextureSwapChainBufferGL() はテクスチャ名を texId に返すので,それを glBindTexture() して glTexParameteri() でテクスチャのパラメータを設定します.
# if OVR_PRODUCT_VERSION > 0 // 描画データに視野を設定する layerData.Fov[eye] = fov; // 描画データにビューポートを設定する layerData.Viewport[eye].Pos = OVR::Vector2i(0, 0); layerData.Viewport[eye].Size = textureSize; // Oculus Rift 表示用の FBO のカラーバッファとして使うテクスチャセットの特性 const ovrTextureSwapChainDesc colorDesc = { ovrTexture_2D, // Type OVR_FORMAT_R8G8B8A8_UNORM_SRGB, // Format 1, // ArraySize textureSize.w, // Width textureSize.h, // Height 1, // MipLevels 1, // SampleCount ovrFalse, // StaticImage 0, 0 }; // Oculus Rift 表示用の FBO のレンダーターゲットとして使うテクスチャチェインを作成する layerData.ColorTexture[eye] = nullptr; if (OVR_SUCCESS(ovr_CreateTextureSwapChainGL(session, &colorDesc, &layerData.ColorTexture[eye]))) { // 作成したテクスチャチェインの長さを取得する int length(0); if (OVR_SUCCESS(ovr_GetTextureSwapChainLength(session, layerData.ColorTexture[eye], &length))) { // テクスチャチェインの個々の要素について for (int i = 0; i < length; ++i) { // テクスチャを作成してパラメータを設定する GLuint texId; ovr_GetTextureSwapChainBufferGL(session, layerData.ColorTexture[eye], i, &texId); glBindTexture(GL_TEXTURE_2D, texId); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } }
デプスバファについては,Oculus SDK 1.x では普通にデプステクスチャを目ごとに一枚作成します.
// Oculus Rift 表示用の FBO のデプスバッファとして使うテクスチャを作成する glGenTextures(1, &oculusDepth[eye]); glBindTexture(GL_TEXTURE_2D, oculusDepth[eye]); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, textureSize.w, textureSize.h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); }
Oculus SDK 0.8 では,ovr_CreateSwapTextureSetGL() を使って FBO のカラーバッファ (レンダーターゲット) とデプスバッファに使うテクスチャセットをそれぞれ作成し,layerData に登録します.
# else // 描画データに視野を設定する layerData.EyeFov.Fov[eye] = fov; // 描画データにビューポートを設定する layerData.EyeFov.Viewport[eye].Pos = OVR::Vector2i(0, 0); layerData.EyeFov.Viewport[eye].Size = textureSize; // Oculus Rift 表示用の FBO のカラーバッファとして使うテクスチャセットを作成する ovrSwapTextureSet *colorTexture; ovr_CreateSwapTextureSetGL(session, GL_SRGB8_ALPHA8, textureSize.w, textureSize.h, &colorTexture); layerData.EyeFov.ColorTexture[eye] = colorTexture; // Oculus Rift 表示用の FBO のデプスバッファとして使うテクスチャセットを作成する ovrSwapTextureSet *depthTexture; ovr_CreateSwapTextureSetGL(session, GL_DEPTH_COMPONENT32F, textureSize.w, textureSize.h, &depthTexture); layerData.EyeFovDepth.DepthTexture[eye] = depthTexture;
あと,Oculus SDK 0.8 (Oculus Rift DK1 / DK2) では,ヘッドトラッキングにより取得した頭 (HMD) の位置に対する左右の目の位置が固定なので (Oculus SDK 1.x では Asynchronous TimeWarp (ATW) 処理のためにフレームごとに変化するらしい),ここで求めておきます.
// Oculus Rift のレンズ補正等の設定値を取得する eyeRenderDesc[eye] = ovr_GetRenderDesc(session, ovrEyeType(eye), fov); // Oculus Rift のスクリーンのヘッドトラッキング位置からの変位を保存する offset[eye][0] = eyeRenderDesc[eye].HmdToEyeViewOffset.x; offset[eye][1] = eyeRenderDesc[eye].HmdToEyeViewOffset.y; offset[eye][2] = eyeRenderDesc[eye].HmdToEyeViewOffset.z; # endif }
ミラー表示用の FBO の作成
ミラー表示用の FBO を作ります.Oculus SDK 1.x では ovr_CreateMirrorTextureGL() で作成したテクスチャを FBO に組み込みます.ミラー表示用の FBO には Oculus Rift 表示用の FBO から画像を転送 (BitBlt) するだけなので,デプスバッファは使いません.したがって,FBO からデプスバッファとして組み込まれていたレンダーバッファは削除します.mirrorWidth と mirrorHeight はミラー表示用の FBO のレンダーターゲットのサイズです.
# if OVR_PRODUCT_VERSION > 0 // ミラー表示用の FBO を作成する const ovrMirrorTextureDesc mirrorDesc = { OVR_FORMAT_R8G8B8A8_UNORM_SRGB, // Format mirrorWidth, // Width mirrorHeight, // Height 0 // Flags }; // ミラー表示用の FBO のカラーバッファとして使うテクスチャを作成する if (OVR_SUCCESS(ovr_CreateMirrorTextureGL(session, &mirrorDesc, &mirrorTexture))) { // 作成したテクスチャのテクスチャ名を得る GLuint texId; if (OVR_SUCCESS(ovr_GetMirrorTextureBufferGL(session, mirrorTexture, &texId))) { // 作成したテクスチャをミラー表示用の FBO にカラーバッファとして組み込む glGenFramebuffers(1, &mirrorFbo); glBindFramebuffer(GL_READ_FRAMEBUFFER, mirrorFbo); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId, 0); glFramebufferRenderbuffer(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0); glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); } }
あと,Oculus SDK 1.x ではヘッドトラッキングを行う空間の原点の高さを設定できます.ここでは原点の高さを床の高さとします.
// 姿勢のトラッキングにおける床の高さを 0 に設定する ovr_SetTrackingOriginType(session, ovrTrackingOrigin_FloorLevel);
Oculus SDK 0.8 では ovr_CreateMirrorTextureGL() で作成したテクスチャをミラー表示用の FBO に組み込みます.
# else // ミラー表示用の FBO のカラーバッファとして使うテクスチャを作成する if (OVR_SUCCESS(ovr_CreateMirrorTextureGL(session, GL_SRGB8_ALPHA8, mirrorWidth, mirrorHeight, reinterpret_cast<ovrTexture **>(&mirrorTexture)))) { // 作成したテクスチャをミラー表示用の FBO にカラーバッファとして組み込む glGenFramebuffers(1, &mirrorFbo); glBindFramebuffer(GL_READ_FRAMEBUFFER, mirrorFbo); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mirrorTexture->OGL.TexId, 0); glFramebufferRenderbuffer(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0); glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); } # endif
Oculus SDK 0.8, Oculus SDK 1.x ともに Oculus Rift のレンダリング用の FBO を作成し,SRGB 色空間でのレンダリングを有効にします.あと,ダブルバッファリングは行わないので glfwSwapInterval() の設定は影響しないのですが,気分的に 0 にしておきます.
// Oculus Rift のレンダリング用の FBO を作成する glGenFramebuffers(ovrEye_Count, oculusFbo); // Oculus Rift にレンダリングするときは sRGB カラースペースを使う glEnable(GL_FRAMEBUFFER_SRGB); // Oculus Rift への表示ではスワップ間隔を待たない glfwSwapInterval(0);
フレームの描画開始前の処理
Oculus Rift に描画する際は,それに先立ってフレームごとに Oculus Rift の状態を取得し,それに対応した処理をします.Oculus SDK 1.x では,HMD を介してアプリケーションの終了要求を受け取った時に,アプリケーションを終了します.ここでは GLFW を使っているので,lfwSetWindowShouldClose() を使って描画ループを終了させています.また,Oculus Rift CV1 ではユーザが HMD を外したことを検知するので,その場合は Oculus Rift にレンダリングを行わないようにします.ここでは false を返してそのことを呼び出し側に伝えています.あと,Oculus SDK 1.x では Asynchronous TimeWarp 処理のために,フレームごとに視点の位置を調整するので,ここで視点の位置を取得しておきます.
# if OVR_PRODUCT_VERSION > 0 // セッションの状態を取得する ovrSessionStatus sessionStatus; ovr_GetSessionStatus(session, &sessionStatus); // アプリケーションが終了を要求しているときはウィンドウのクローズフラグを立てる if (sessionStatus.ShouldQuit) glfwSetWindowShouldClose(window, GL_TRUE); // Oculus Rift に表示されていないときは戻る if (!sessionStatus.IsVisible) return false; // Oculus Rift の原点を再設定する if (sessionStatus.ShouldRecenter) ovr_RecenterTrackingOrigin(session); // HmdToEyeOffset などは実行時に変化するので毎フレーム ovr_GetRenderDesc() で ovrEyeRenderDesc を取得する const ovrEyeRenderDesc eyeRenderDesc[] = { ovr_GetRenderDesc(session, ovrEyeType(0), hmdDesc.DefaultEyeFov[0]), ovr_GetRenderDesc(session, ovrEyeType(1), hmdDesc.DefaultEyeFov[1]) }; // Oculus Rift のスクリーンのヘッドトラッキング位置からの変位を取得する const ovrVector3f hmdToEyeOffset[] = { eyeRenderDesc[0].HmdToEyeOffset, eyeRenderDesc[1].HmdToEyeOffset }; // Oculus Rift のスクリーンのヘッドトラッキング位置からの変位を保存する for (int eye = 0; eye < ovrEye_Count; ++eye) { offset[eye][0] = hmdToEyeOffset[eye].x; offset[eye][1] = hmdToEyeOffset[eye].y; offset[eye][2] = hmdToEyeOffset[eye].z; } // 視点の姿勢情報を取得する ovr_GetEyePoses(session, ++frameIndex, ovrTrue, hmdToEyeOffset, layerData.RenderPose, &layerData.SensorSampleTime);
Oculus SDK 0.8 では,ここで (Asynchronous ではない) TimeWarp 処理のために,フレームの描画開始時刻を保存しておきます.また,ヘッドトラッキングの情報も取得しておきます.
# else // フレームのタイミング計測開始 const auto ftiming(ovr_GetPredictedDisplayTime(session, 0)); // sensorSampleTime の取得は可能な限り ovr_GetTrackingState() の近くで行う layerData.EyeFov.SensorSampleTime = ovr_GetTimeInSeconds(); // ヘッドトラッキングの状態を取得する const auto hmdState(ovr_GetTrackingState(session, ftiming, ovrTrue)); // Oculus Rift のスクリーンのヘッドトラッキング位置からの変位を取得する const ovrVector3f hmdToEyeViewOffset[] = { eyeRenderDesc[0].HmdToEyeViewOffset, eyeRenderDesc[1].HmdToEyeViewOffset }; // 視点の姿勢情報を求める ovr_CalcEyePoses(hmdState.HeadPose.ThePose, hmdToEyeViewOffset, eyePose); # endif
Oculus Rift へのレンダリングが可能であることを知らせるために,true を返しておきます.
return true;
片目の描画開始前の処理
次に,左右の目のそれぞれについて,描画開始前に FBO の設定やビューポートの設定などを行います.変数 eye は Oculus Rift の目の識別子で,0 が左目,1 が右目です.
# if OVR_PRODUCT_VERSION > 0 // Oculus Rift にレンダリングする FBO に切り替える if (layerData.ColorTexture[eye]) { // FBO のカラーバッファに使う現在のテクスチャのインデックスを取得する int curIndex; ovr_GetTextureSwapChainCurrentIndex(session, layerData.ColorTexture[eye], &curIndex); // FBO のカラーバッファに使うテクスチャを取得する GLuint curTexId; ovr_GetTextureSwapChainBufferGL(session, layerData.ColorTexture[eye], curIndex, &curTexId); // FBO を設定する glBindFramebuffer(GL_FRAMEBUFFER, oculusFbo[eye]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, curTexId, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, oculusDepth[eye], 0); // ビューポートを設定する const auto &vp(layerData.Viewport[eye]); glViewport(vp.Pos.x, vp.Pos.y, vp.Size.w, vp.Size.h); } // Oculus Rift の片目の位置と回転を取得する const auto &p(layerData.RenderPose[eye].Position); const auto &o(layerData.RenderPose[eye].Orientation); # else // レンダーターゲットに描画する前にレンダーターゲットのインデックスをインクリメントする auto *const colorTexture(layerData.EyeFov.ColorTexture[eye]); colorTexture->CurrentIndex = (colorTexture->CurrentIndex + 1) % colorTexture->TextureCount; auto *const depthTexture(layerData.EyeFovDepth.DepthTexture[eye]); depthTexture->CurrentIndex = (depthTexture->CurrentIndex + 1) % depthTexture->TextureCount; // レンダーターゲットを切り替える glBindFramebuffer(GL_FRAMEBUFFER, oculusFbo[eye]); const auto &ctex(reinterpret_cast<ovrGLTexture *>(&colorTexture->Textures[colorTexture->CurrentIndex])); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ctex->OGL.TexId, 0); const auto &dtex(reinterpret_cast<ovrGLTexture *>(&depthTexture->Textures[depthTexture->CurrentIndex])); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, dtex->OGL.TexId, 0); // ビューポートを設定する const auto &vp(layerData.EyeFov.Viewport[eye]); glViewport(vp.Pos.x, vp.Pos.y, vp.Size.w, vp.Size.h); // Oculus Rift の片目の位置と回転を取得する const auto &p(eyePose[eye].Position); const auto &o(eyePose[eye].Orientation); # endif
また,スクリーンの情報や,ヘッドトラッキングの情報もここで取り出しておきます.
// Oculus Rift のスクリーンの大きさを返す screen[0] = this->screen[eye][0]; screen[1] = this->screen[eye][1]; screen[2] = this->screen[eye][2]; screen[3] = this->screen[eye][3]; // Oculus Rift の位置を返す position[0] = offset[eye][0] + p.x; position[1] = offset[eye][1] + p.y; position[2] = offset[eye][2] + p.z; position[3] = 1.0f; // Oculus Rift の方向を返す orientation[0] = o.x; orientation[1] = o.y; orientation[2] = o.z; orientation[3] = o.w;なお,Oculus SDK 0.8 では TimeWarp 処理のために,ここでスクリーンの大きさ screen と前方面・後方面の位置 (zNear, zFar) で求めた投資投影変換行列 (glFrustum() で求められるもの) の要素を取り出しておく必要があります.この投資投影変換行列を projection[4][4] とするとき,これを参照して以下の処理を行います.
// TimeWarp に使う変換行列の成分を設定する # if OVR_PRODUCT_VERSION < 1 auto &posTimewarpProjectionDesc(layerData.EyeFovDepth.ProjectionDesc); posTimewarpProjectionDesc.Projection22 = (projection[2][2] + projection[3][2]) * 0.5f; posTimewarpProjectionDesc.Projection23 = projection[2][3] * 0.5f; posTimewarpProjectionDesc.Projection32 = projection[3][2]; # endif
片目の描画完了後の処理
一方,Oculus SDK 1.x では,片目の描画が完了した後に,以下の処理を行います.
# if OVR_PRODUCT_VERSION > 0 // GL_COLOR_ATTACHMENT0 に割り当てられたテクスチャが wglDXUnlockObjectsNV() によって // アンロックされるために次のフレームの処理において無効な GL_COLOR_ATTACHMENT0 が // FBO に結合されるのを避ける glBindFramebuffer(GL_FRAMEBUFFER, oculusFbo[eye]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0); // 保留中の変更を layerData.ColorTexture[eye] に反映しインデックスを更新する ovr_CommitTextureSwapChain(session, layerData.ColorTexture[eye]); # endif両目の描画完了後の処理 左右の目両方の FBO へのレンダリングが完了したら,それを Oculus Rift に送出します.ここで送出に失敗したときは,Oculus Rift の設定を最初からやり直します.これは Oculus Rift のケーブルが抜けたときなどに発生します.ですので,ここではめんどくさいので,そういうときはプログラムを終わらせてしまうことにします.
# if OVR_PRODUCT_VERSION > 0 // 描画データを Oculus Rift に送出する const auto *const layers(&layerData.Header); if (OVR_FAILURE(ovr_SubmitFrame(session, frameIndex, nullptr, &layers, 1))) # else // Oculus Rift 上の描画位置と拡大率を求める ovrViewScaleDesc viewScaleDesc; viewScaleDesc.HmdSpaceToWorldScaleInMeters = 1.0f; viewScaleDesc.HmdToEyeViewOffset[0] = eyeRenderDesc[0].HmdToEyeViewOffset; viewScaleDesc.HmdToEyeViewOffset[1] = eyeRenderDesc[1].HmdToEyeViewOffset; // 描画データを更新する layerData.EyeFov.RenderPose[0] = eyePose[0]; layerData.EyeFov.RenderPose[1] = eyePose[1]; // 描画データを Oculus Rift に送出する const auto *const layers(&layerData.Header); if (OVR_FAILURE(ovr_SubmitFrame(session, 0, &viewScaleDesc, &layers, 1))) # endif { // 送出に失敗したら Oculus Rift の設定を最初からやり直す必要があるらしい // けどめんどくさいのでウィンドウを閉じてしまう glfwSetWindowShouldClose(window, GLFW_TRUE); }
Oculus Rift へのレンダリング結果は,glBlitFramebuffer() によりミラー表示用の FBO にも転送します.width と height はミラー表示用のウィンドウの幅と高さです.
// レンダリング結果をミラー表示用のフレームバッファにも転送する glBindFramebuffer(GL_READ_FRAMEBUFFER, mirrorFbo); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); # if OVR_PRODUCT_VERSION > 0 const auto w(mirrorWidth), h(mirrorHeight); # else const auto w(mirrorTexture->OGL.Header.TextureSize.w); const auto h(mirrorTexture->OGL.Header.TextureSize.h); # endif glBlitFramebuffer(0, h, w, 0, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); // 残っている OpenGL コマンドを実行する glFlush();
プログラム終了時の処理
プログラムの終了時には,ミラー表示用の FBO や,それらにカラーバッファとして組み込んでいたテクスチャを削除します.
#if defined(USE_OCULUS_RIFT) // ミラー表示用の FBO を削除する if (mirrorFbo) glDeleteFramebuffers(1, &mirrorFbo); // ミラー表示に使ったテクスチャを開放する if (mirrorTexture) { # if OVR_PRODUCT_VERSION > 0 ovr_DestroyMirrorTexture(session, mirrorTexture); # else glDeleteTextures(1, &mirrorTexture->OGL.TexId); ovr_DestroyMirrorTexture(session, reinterpret_cast<ovrTexture *>(mirrorTexture)); # endif }
同様に,Oculus Rift 表示用の FBO や,それらに組み込んでいたレンダリングターゲットのテクスチャ,デプスバッファとして使っていたテクスチャを削除します.
// Oculus Rift のレンダリング用の FBO を削除する glDeleteFramebuffers(ovrEye_Count, oculusFbo); // Oculus Rift 表示用の FBO を削除する for (int eye = 0; eye < ovrEye_Count; ++eye) { # if OVR_PRODUCT_VERSION > 0 // レンダリングターゲットに使ったテクスチャを開放する if (layerData.ColorTexture[eye]) { ovr_DestroyTextureSwapChain(session, layerData.ColorTexture[eye]); layerData.ColorTexture[eye] = nullptr; } // デプスバッファとして使ったテクスチャを開放する glDeleteTextures(1, &oculusDepth[eye]); oculusDepth[eye] = 0; # else // レンダリングターゲットに使ったテクスチャを開放する auto *const colorTexture(layerData.EyeFov.ColorTexture[eye]); for (int i = 0; i < colorTexture->TextureCount; ++i) { const auto *const ctex(reinterpret_cast<ovrGLTexture *>(&colorTexture->Textures[i])); glDeleteTextures(1, &ctex->OGL.TexId); } ovr_DestroySwapTextureSet(session, colorTexture); // デプスバッファとして使ったテクスチャを開放する auto *const depthTexture(layerData.EyeFovDepth.DepthTexture[eye]); for (int i = 0; i < depthTexture->TextureCount; ++i) { const auto *const dtex(reinterpret_cast<ovrGLTexture *>(&depthTexture->Textures[i])); glDeleteTextures(1, &dtex->OGL.TexId); } ovr_DestroySwapTextureSet(session, depthTexture); # endif }
この後,Oculus Rift のセッションを破棄します.
// Oculus Rift のセッションを破棄する ovr_Destroy(session); session = nullptr;
これらが完了した後,ミラー表示を行なっていた OpenGL (GLFW) のウィンドウを削除します.
サンプルプログラム
やっぱりこれじゃめっちゃわかりにくいので,次回,これを組み込んだラッパーを使ったサンプルプログラムで解説します.
*1 冗談です.単に年を取って頭が固くなってるだけです.弘法は筆を選ばんのです.
*2 実は前に Qiita に書いた http://qiita.com/tokoik/items/61ae07d0f8f448959deb http://qiita.com/tokoik/items/1b4bc645786632513773 のですが,改めて自分のブログにも書きました.
*3 これに含まれているバイナリは「プロパティ」の「コード生成」の「ランタイムライブラリ」の設定が「マルチスレッド (/MT)」でビルドされているので,GLFW もこれに合わせてビルドするか,libOVR の方を「マルチスレッド DLL (/MD)」でビルドしなおす必要があります.