OGRE – D3D11の複数ビューポートの問題

前回の記事でOculus RiftをOGREで使うデモを作ってみたのですが,そのときにぶち当たった問題です.

Oculus Riftでは一つのレンダーターゲットの左右半分々々に,両眼から見たシーンをそれぞれ描画する必要があります. このためには一つのレンダーターゲットに,複数のビューポートを設定します.

OGREでは通常以下のようになります.OGREのビューポート(Ogre::Viewport)は「レンダーターゲットにカメラを 渡してビューポートを生成する」という形で作成します.

  Ogre::SceneManager *sceneManager = /* ... */;
  Ogre::RenderTarget *renderTarget = /* ... */;

  auto camera1 = sceneManager->createCamera("camera1");
  auto vp1 = renderTarget->addViewport(camra1, 0, 0.0f, 0.0f, 0.5f, 1.0f); // 左半分のビューポート

  auto camera2 = sceneManager->createCamera("camera2");
  auto vp2 = renderTarget->addViewport(camra2, 1, 0.5f, 0.0f, 0.5f, 1.0f); // 右半分のビューポート

  ...

Ogre::RenderTarget::addViewport()がビューポートを生成するメソッドです。引数は、カメラ、Zオーダー、 ビューポートの左上の位置,幅高さ,となります.

参考:Basic Tutorial 8 – Ogre Wiki

通常はこれで事足りるわけですが,Direct3D11レンダーシステムを使う場合これはうまく動作しません
フォーラムにも報告があったのですが, D3D11レンダーシステムでこの方法を使うと,最後に描かれたビューポートのみが有効になります

screenshot_d3d9 screenshot_d3d11
↑左がD3D9レンダーシステムを使った描画結果,右は同じプログラムをD3D11レンダーシステムで動作させた描画結果

何故こういうことが起きるかというと,D3D11のフレームバッファのクリア操作がビューポートやシザーの設定を無視するためです.

Differences between Direct3D 9 and Direct3D 11/10:

Unlike Direct3D 9, the full extent of the resource view is always cleared. Viewport and scissor settings are not applied.

要するに左半分のビューポートを描いた後,右半分のビューポートを描くときに左半分を含めたレンダーターゲット全域をクリアしてしまっているわけですね.
これはD3D11と,DirectX9やOpenGLとの大きな相違点の一つかも知れません. おそらくD3D11からは「フレームバッファのクリアは一回で済ませろ」ってことなのでしょう.

OGREはそのようなバックエンドAPIを想定していなかったようで,ビューポートのクリアとは独立にレンダーターゲットを クリアするような手段が用意されていません.

回避策1: レンダーターゲットに空のシーンを描く

若干無駄が多いですが,レンダーターゲットに,空のシーンと関連付いたレンダーターゲット全体を覆うダミーのビューポートを設定する という回避策が考えられます.

Ogre::ViewportにはsetClearEveryFrame(bool)というメソッドがあります.これは,ビューポートの更新毎に フレームバッファをクリアするかどうかの設定です.「最初の一回だけのクリア」を実現するには, 上述のダミーのビューポート以外はこれをオフに設定します.またダミーのビューポートのZオーダーは最も小さいものにしておきます.

たとえば以下のようにします.

  Ogre::SceneManager *mainSceneManager = /* メインのシーンのためのシーンマネージャ */; 
  Ogre::SceneManager *dummySceneManager = /* ダミーのシーンマネージャ(シーングラフは空にしておく) */; 

  Ogre::RenderTarget *renderTarget = /* ... */;

  // ダミーのカメラを設定(このカメラのビュープロジェクション変換は使わないので何も設定しなくていい).
  auto dummyCamera = dummySceneManager->createCamera("dummyCamera"); 
  auto dummyViewport = renderTarget->addViewport(dummyCamera, 0); // 最も下側のビューポートとして設定(Z=0)

  auto camera1 = sceneManager->createCamera("camera1");
  auto vp1 = renderTarget->addViewport(camra1, 1, 0.0f, 0.0f, 0.5f, 1.0f); // 左半分のビューポート(Z=1)
  vp1->setClearEveryFrame(false); // ビューポートごとにクリアしない.

  auto camera2 = sceneManager->createCamera("camera2");
  auto vp2 = renderTarget->addViewport(camra2, 2, 0.5f, 0.0f, 0.5f, 1.0f); // 右半分のビューポート(Z=2)
  vp2->setClearEveryFrame(false); // ビューポートごとにクリアしない.

  ...  

この方法はD3D9/OpenGLレンダーシステムでも動きました.先日のOculus Riftのデモではこの回避策を使っています.

回避策2: 無理矢理クリアする

上記の回避策は空のシーンのためのシーンマネージャを生成する必要があり,シーングラフの為のメモリが無駄になります(空なので大したこと無いとは思いますケド).
一応もう少し軽い回避策があります.ただし,将来のバージョンに渡って安全に使える方法かどうかは分かりません.
OGREでは以下のように更新処理が行われます.

  1. すべてのRenderTargetを反復して必要なものを更新する.
  2. RenderTargetについて,割り当てられたViewportのうち必要なものを更新する.
    • 通常このときにViewportごとのクリアが行われる.

Ogre::RenderTargetListenerを 使って2の処理の前後に追加の処理を挟み込むことが可能です.
なので、2の処理の前にフレームバッファのクリア処理を挟み込みます.

このためにはOgre::RenderTargetListenerを何らかの形で継承します.

class Hoge : public Ogre::RenderTargetListener 
{
// ..
   void Init()
   {
     Ogre::RenderTarget *renderTarget = /* ... */;

     renderTarget->addListener(this); // 初期化時にリスナーを登録しておく.
   }

Ogre::RenderTargetListener::preRenderTargetUpdateメソッドを,以下のようにオーバーライドして実装しておきます.

  Ogre::Root *mRoot; 
  // ...
  virtual void preRenderTargetUpdate (const Ogre::RenderTargetEvent &evt)/*override*/
  {
      auto rs = mRoot->getRenderSystem();
      std::unique_ptr<Ogre::Viewport> vp(new Ogre::Viewport(nullptr, evt.source, 0.0f, 0.0f, 1.0f, 1.0f, 0));
      rs->_setRenderTarget(evt.source);
      rs->_setViewport(vp.get());
      rs->clearFrameBuffer(FBT_COLOUR | FBT_DEPTH | FBT_STENCIL, Ogre::ColourValue::Black);
  }
// ..
};

事前にメインとなるビューポート全てにsetClearEveryFrame(false)を呼び出しておく必要がある点は回避策1と同様です.

Ogre::RenderSystem::clearFrameBufferを呼び出して レンダーターゲットをクリアしています.

その前に,カメラと紐付いていないViewportを生成して,_setRenderTarget_setViewportを呼び出しています.
少なくともD3D9/11/OpenGLレンダラでの動作を確認しました.

ただアンダースコアで始まるAPIは,publicなメソッドではあるもののOGREのユーザーコードから 直接呼び出されることを意図されたものではないので,本当に安全なのかどうかは分かりません (軽くライブラリのコードを追った限りは害のある挙動はしないようですが).

Leave a Reply