DirectX11でOCulus SDK for Windowsを使う


はじめに

 VRをやりたいと思い、折角だからDirectX11Oculus SDK for Windowsを使ってやろうと思った方に向けて、私が得た経験が参考になってもらえればいいなと思い、自分用のメモがてら、初めてQiitaに投稿させてもらいます。
 こういったものを書くのは初めてなので、お目汚し等ございましたらご容赦ください。また、私自身そこまでプログラミングに詳しいわけでもないので間違ったこともあると存じます。

使用しているツール
・Visual Studio 2017
・DirectX SDK(June 2010)
・Oculus SDK for Windows 1.43.0

1.目標

この記事内では、Oculus SDK内のSamples\OculusRoomTiny\OculusRoomTiny (DX11)を元に、この空間内に自作の3Dオブジェクトを描画することがが第一目標です。ファイル形式は一旦objファイルとしましょう。
 将来的にはゲームとしてプログラミングができるようにしたいので見やすくなるようにちょっとしたリファクタリングもしておきたいところです。
 基本的にはサンプルコードに加筆・修正を加えていくことになっていきます。

2.下準備

 というわけで、早速なのでサンプルコードを実行できるか試してみてください。
Samples\Projects\Windows\VS2017フォルダ内にあるslnファイルを実行すれば、
Samples\OculusRoomTiny\OculusRoomTiny (DX11)\Bin\Windows\Win32\Debug\VS2017フォルダ内にexeファイルが作られるのでそれを実行すればサンプルコードのVR空間が見れると思います。
 このままこのコードを改造していくのでは中々骨が折れます。というのも、目的のコードの他に別のプログラムも入っているでVS2017の検索機能が使いにくいからです。なのでVS2017を起動し新たなファイルを作って、自作のファイルにソースコードを移してから改造しましょう。移すべきファイルは、
Samples\OculusRoomTiny\OculusRoomTiny (DX11)\main.cppSamples\OculusRoomTiny_Advanced\Common\Win32_DirectXAppUtil.hの2つです。
 ここまでできたら、VS2017内のプロパティの設定でインクルードディレクトリと、ライブラリディレクトリに適切なパスを渡して、それぞれのコードに必要なインクルードファイルとライブラリのファイルを指定してください。ここまでくれば、実行すれば先程の景色が見られると思います。
 ここからはコードを上手いこと改造していきましょう!

3.立方体を描画する

 objファイルの読み込みに関してはここでは触れません。ここで必要なのは、初期化、描画、解放処理に関してだと思いますのでそれらに関して触れていきたいと思います。立方体モデルはBlenderなりMayaなりで適当に作ってもらえればいいです。

3-1.初期化

 初期化の内容は基本的にモデルのロードだと思います。それができるという前提で、どこにそのメソッドを入れればいいのかというのは、下記の通りです。

// Create the room model
roomScene = new Scene(false);

// Create camera
mainCam = new Camera(XMVectorSet(0.0f, 0.0f, 5.0f, 0), XMQuaternionIdentity());

 ここのあたりですね。元のサンプルコードでの235~241行目に当たる部分です。私はroomSceneの直後に入れてますが基本的にはこのあたりならどこでも大丈夫かと。

3-2.解放処理

 続いて解放処理に関してです。

delete mainCam;
delete roomScene;
if (mirrorTexture)
    ovr_DestroyMirrorTexture(session, mirrorTexture);
for (int eye = 0; eye < 2; ++eye)
{
    delete pEyeRenderTexture[eye];
}
DIRECTX.ReleaseDevice();
ovr_Destroy(session);

 ここの部分ですね。元のコードでの361~371行目部分です。
 ついでに更新処理を入れる場合についても、その場所を書いておきます。

XMVECTOR forward = XMVector3Rotate(XMVectorSet(0, 0, -0.05f, 0), mainCam->Rot);
XMVECTOR right   = XMVector3Rotate(XMVectorSet(0.05f, 0, 0, 0),  mainCam->Rot);
if (DIRECTX.Key['W'] || DIRECTX.Key[VK_UP])      mainCam->Pos = XMVectorAdd(mainCam->Pos, forward);
if (DIRECTX.Key['S'] || DIRECTX.Key[VK_DOWN])    mainCam->Pos = XMVectorSubtract(mainCam->Pos, forward);
if (DIRECTX.Key['D'])                            mainCam->Pos = XMVectorAdd(mainCam->Pos, right);
if (DIRECTX.Key['A'])                            mainCam->Pos = XMVectorSubtract(mainCam->Pos, right);
static float Yaw = 0;
if (DIRECTX.Key[VK_LEFT])  mainCam->Rot = XMQuaternionRotationRollPitchYaw(0, Yaw += 0.02f, 0);
if (DIRECTX.Key[VK_RIGHT]) mainCam->Rot = XMQuaternionRotationRollPitchYaw(0, Yaw -= 0.02f, 0);

// Animate the cube
static float cubePositionClock = 0;
if (sessionStatus.HasInputFocus) // Pause the application if we are not supposed to have input.
roomScene->Models[0]->Pos = XMFLOAT3(9 * sin(cubePositionClock), 3, 9 * cos(cubePositionClock += 0.015f));

 ここの部分でしょうか。264~277行目です。ここは事実上roomSceneの更新処理の部分なので、この後や前に書いておくといいと思います。

3-3.描画

 さて、ここからが一番大変なところです。まずは今まで通りどこに描画メソッドを入れるかに関してです。

XMMATRIX prod = XMMatrixMultiply(view, proj);
roomScene->Render(&prod, 1, 1, 1, 1, true);

 この直後です。318行目です。ここに描画メソッドを入れて立方体が描画されればその人はそれで大丈夫です。

 さて、大半の人が描画されないと思いますが、どうすればいいのか書いていこうと思います。まずは、描画メソッドの中に以下のようなコードを入れてくれればいいかと思います。

XMMATRIX modelMat = XMMatrixMultiply(XMMatrixRotationQuaternion(XMLoadFloat4(&m_Rotation)), XMMatrixTranslationFromVector(XMLoadFloat3(&m_Position)));
XMMATRIX mat = XMMatrixMultiply(modelMat, *projView);
float col[] = { 1, 1, 1, 1 };
memcpy(DIRECTX.UniformData + 0, &mat, 64); // ProjView
memcpy(DIRECTX.UniformData + 64, &col, 16); // MasterCol
D3D11_MAPPED_SUBRESOURCE map;
DIRECTX.Context->Map(DIRECTX.UniformBufferGen->D3DBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &map);
memcpy(map.pData, &DIRECTX.UniformData, DIRECTX.UNIFORM_DATA_SIZE);
DIRECTX.Context->Unmap(DIRECTX.UniformBufferGen->D3DBuffer, 0);

// サンプラーステート
D3D11_SAMPLER_DESC samplerDesc;
ZeroMemory(&samplerDesc, sizeof(samplerDesc));
samplerDesc.Filter = D3D11_FILTER_ANISOTROPIC;
samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.MipLODBias = 0;
samplerDesc.MaxAnisotropy = 8;//16;
samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = 15;//D3D11_FLOAT32_MAX;

ID3D11SamplerState* samplerState = NULL;
DIRECTX.Device->CreateSamplerState(&samplerDesc, &samplerState);

DIRECTX.Context->PSSetSamplers(0, 1, &samplerState);

// ブレンドステート設定
D3D11_BLEND_DESC blendDesc;
ZeroMemory(&blendDesc, sizeof(blendDesc));
blendDesc.AlphaToCoverageEnable = FALSE;
blendDesc.IndependentBlendEnable = FALSE;
blendDesc.RenderTarget[0].BlendEnable = TRUE;
blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_SRC_ALPHA;//D3D11_BLEND_ONE;
blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; //D3D11_BLEND_ZERO;
blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;

float blendFactor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
ID3D11BlendState* blendState = NULL;
DIRECTX.Device->CreateBlendState(&blendDesc, &blendState);
DIRECTX.Context->OMSetBlendState(blendState, blendFactor, 0xffffffff);

 こんな感じのコードを入れれば描画されるかと思います。もしテクスチャがなかったり、
ComparisonFunc = D3D11_COMPARISON_ALWAYSをする必要がなければサンプラーステートのコードは必要ないと思います。その上のコードに関してですが、Win32_DirectXAppUtil.h内の747~755行目をまんま移しただけです。違うところはというと、XMMatrixMultiplyメソッドで引数を立方体用のメンバ変数にしてるというところですね。また、私はこれ自体が描画メソッドの中身なので、引数にDIRECTX*型の引数を用いてアロー演算子でContextなどにアクセスしています。
 さて、一先ずは立方体が描画されたかと思います。人によってはマテリアルやテクスチャなんかが変だという方がいるかも知れません。その方は、Win32_DirectXAppUtil.h内503~506行目部分の

D3D11_INPUT_ELEMENT_DESC defaultVertexDesc[] = {
    { "Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,  D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "Color",    0, DXGI_FORMAT_B8G8R8A8_UNORM,  0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "TexCoord", 0, DXGI_FORMAT_R32G32_FLOAT,    0, 16, D3D11_INPUT_PER_VERTEX_DATA, 0 }, };

 この部分を変えればいいかもしれません。私は、ColorのDXGI_FORMAT_B8G8R8A8_UNORMをDXGI_FORMAT_R32G32B32A32_FLOATにすると立方体の描画が上手くいきました。もちろん、以前にあった他の物体の色はおかしくなりましたが、本来の目的は立方体の描画なので問題ないと思います。
 さらに、表裏が反転したという方。同ヘッダー内の587行目直後に

    rs.FrontCounterClockwise = TRUE;

こちらを入れれば大丈夫かと思います。これは面の裏表を決めている部分です。もちろん、他の物体の表裏は反転してしまいます。

4.描画の補足説明

 さて、描画することに成功したからこれでいいという方は大丈夫ですが、一応補足説明をつけておきます。今回のコードでは、頂点/ピクセルシェーダやその他色んな描画に関するステートの設定をほとんど行っておりません。ここに関して説明させてもらいます。
 まずは、roomSceneの中身を追ってもらえれば分かりますが、Scene構造体のInit()の中(837行目)でScene内のモデルを生成しています。このModelなのですが、それぞれ一つ一つに先程述べたステートの設定を保存しています。イメージとしてはこんな感じです。

おそらく分かりやすいようにそうしているのですが、全て同じ設定を適用しているので、リファクタリングの際に初期化部分で一回適用するだけで終わらせておきたいところです。
 今回立方体の描画の際には、roomSceneで最後に描画されたモデルのステートの設定をほぼ流用しているというものになっています。

最後に

 これでSampleのVR空間内に自分で用意した物体(立方体)を描画できたと思います。今後としては、今のコードでは非常に使いづらく、特にVRでゲームを作りたいというような方が大半だと思いますので、このコードのままでは扱いづらいと思います(gotoが使われているあたりとか)。なのでこれが使いやすくなるようにリファクタリングしていきたいと思うので、時間があるときにまた書きたいと思います。
 最初にも述べましたが、このようなものを書くのは初めてですので、不備等ございますでしょうが、優しく指摘くださると幸いです。また、まずい部分などがあれば、速やかに削除などの対応をしたいと思います。

参考文献

[PC SDK]https://developer.oculus.com/documentation/native/pc/pcsdk-intro/
[アトリエ鬱 -準備室- ひとりでできるもん]http://www.os.rim.or.jp/~pekochan/develop/DK2Tuto/