LibOVRの作るメッシュ

Oculus Riftのあのレンズ歪みにあわせた画像は,以前にも述べたように おおまかには以下のような手順で作られています.

  1. レンダーターゲットテクスチャに一つのシーンを二つのカメラで描く(オフスクリーンで描く).
  2. LibOVRが提供するディストーション用のメッシュに↑のテクスチャを貼り付けて表示する.
  3. この表示には専用のシェーダを使う(SDKのソースに埋め込まれてる).
  4. HMDから両眼の位置と向きを取得してビュー変換に反映させる.

今回は2.についてまとめておこうと思います.

*  *  *

LibOVRの以下の関数が返すメッシュがあの歪んだ画像の正体です.

OVR_CAPI.h:700行目付近
OVR_EXPORT ovrBool  ovrHmd_CreateDistortionMesh( ovrHmd hmd,
                                                 ovrEyeType eyeType, ovrFovPort fov,
                                                 unsigned int distortionCaps,
                                                 ovrDistortionMesh *meshData);

meshData引数にメッシュが作られて帰ってきます.ovrDistortionMeshは以下のような構造体です.

OVR_CAPI.h:682行目付近
typedef struct ovrDistortionMesh_
{
    ovrDistortionVertex* pVertexData;
    unsigned short*      pIndexData;
    unsigned int         VertexCount;
    unsigned int         IndexCount;
} ovrDistortionMesh;

普通の頂点バッファとインデックスバッファの組ですね.これを環境固有の頂点/インデックスバッファにコピーして 使えばいいわけですね.

頂点データはどうなっているかというと以下のようになっています.

OVR_CAPI.h:682行目付近
typedef struct ovrDistortionVertex_
{
    ovrVector2f ScreenPosNDC;    // [-1,+1],[-1,+1] over the entire framebuffer.
    float       TimeWarpFactor;  // Lerp factor between time-warp matrices. Can be encoded in Pos.z.
    float       VignetteFactor;  // Vignette fade factor. Can be encoded in Pos.w.
    ovrVector2f TanEyeAnglesR;
    ovrVector2f TanEyeAnglesG;
    ovrVector2f TanEyeAnglesB;    
} ovrDistortionVertex;

二つの係数,三つのUV座標をもつ二次元の座標データです.プロジェクション空間の座標を直接持っています.
このメッシュに,二つのカメラで描いたテクスチャを貼り付けて表示するわけです.

ためしにこの二次元座標をダンプしてプロットしてみました.こんな風になっていました.

distotionMesh

まぁ,見たままですね.

OGREでのメッシュの作り方

先日のデモではOGREで表示していたのですが,OGREでプログラム内でメッシュを作るには Ogre::MeshManager::createManual()を 使います.

auto mesh = Ogre::MeshManager::getSingleton().createManual(meshName,
                Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);

この”メッシュ”に,Ogre::HardwareBufferManager::createVertexBuffer()Ogre::HardwareBufferManager::createIndexBuffer()で生成した頂点バッファとインデックスバッファを 割り当てます.
このあたりはOGREでなくても似たようなものなので詳細は省略します.完全なウォークスルーが欲しい方は以下が参考になるかも知れません.

参考: Generating A Mesh – Ogre Wiki

上述のディストーションメッシュは,floatが2,1,1,2,2,2の順序ですが,私は以下の頂点宣言を使いました.

VertexDeclaration* decl = mesh->sharedVertexData->vertexDeclaration;
size_t offset = 0;

// ScreenPosNDC(float2) + TimeWarpFactor(float) + VignetteFactor(float)
decl->addElement(0, offset, VET_FLOAT4, VES_POSITION);
offset += VertexElement::getTypeSize(VET_FLOAT4);

decl->addElement(0, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 0); // TanEyeAnglesR
offset += VertexElement::getTypeSize(VET_FLOAT2);

decl->addElement(0, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 1); // TanEyeAnglesG
offset += VertexElement::getTypeSize(VET_FLOAT2);

decl->addElement(0, offset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 2); // TanEyeAnglesB
offset += VertexElement::getTypeSize(VET_FLOAT2);

二つの係数をfloat4にまとめました(これはovrDistortionVertexのコメントにあるとおり).

OGREでの二次元メッシュの表示

さて,本題はここからです.

先ほどのディストーションメッシュを表示するには,Direct3DやOpenGLを使っている場合は, 単にシェーダを設定して頂点を流し込めばいいのですが,OGREで表示するには少し工夫する必要があります.

OGREでは,シーンマネージャ(シーングラフ)に登録したものが勝手に描画されます(なので所謂Draw部分を実装する必要が無い). 大雑把に言うと「シーングラフの中の可視属性に設定されているオブジェクトのうち,描画に使うカメラから 可視であるオブジェクトがレンダリング処理に送られる」というロジックです.
「カメラから可視であるかどうか」はシーンマネージャの実装に任せられています.典型的には八分木構造を持っている Ogre::OctreeSceneManager が使われているでしょう.

件のメッシュはプロジェクション空間での座標で構成されておりワールド空間での座標を持っていません. これはこのメッシュをシーングラフに登録できないことを意味しています.そもそも,ディストーションメッシュは 必ず表示されなければならないのでシーングラフの中にある必要は無いわけです.

シーングラフの可視/不可視の判断ロジックとは無関係に,メッシュをレンダリング処理に送り込むには, Ogre::SceneManager::Listenerを使います.これはSceneManagerの挙動の様々な部分で呼ばれる イベントリスナーですが,先ほどの「カメラから可視であるかどうか」をチェックしたに呼ばれる postFindVisibleObjectsというメソッドがあります.

このメソッドを実装してメッシュをレンダリング処理に強制的に送り込めばよいわけです.

virtual void postFindVisibleObjects(Ogre::SceneManager* source,
    Ogre::SceneManager::IlluminationRenderStage irs, Ogre::Viewport* v)/*override*/
{
    auto ent1 = SceneManager()->getEntity("distEnt0"); // 右目用
    source->getRenderQueue()->addRenderable(ent1->getSubEntity(0), 0); // レンダーキューに強制登録
    auto ent2 = SceneManager()->getEntity("distEnt1"); // 左目用
    source->getRenderQueue()->addRenderable(ent2->getSubEntity(0), 0); // レンダーキューに強制登録
}

もちろん,このメッシュ(正確にいうとOgre::Meshから作ったOgre::Entity)に事前にマテリアルの設定を しておく必要はあります.あとシェーダの定数の更新なども必要です.

Leave a Reply