Bounding Volume Hierarchy (BVH) の実装 - 交差判定編

レイトレアドベントカレンダー5週目の記事として、「Bounding Volume Hierarchy (BVH) の実装 - 交差判定編」を書きました。

http://qiita.com/omochi64/items/c2bbe92d707b280896fd

BVH の基本の記事を完結させるだけで半年以上の間が空いたという…
レイトレでかっこいい3Dモデルを描画したいけどそのままじゃ重くて話にならない!という人は是非使ってみてください。

Bounding Volume Hierarchy (BVH) の実装 - 構築編

前回記事を書いたレンダラ (eduptベースでレンダラ書いた) ですが、 obj ファイルで定義されたモデルを読み込み、レンダリングする機能も実装しています。
その際、レイと多量のポリゴンを高速に交差判定行うために、BVH と呼ばれる領域分割処理を行っています。
それについて、Qiita というサービスで記事を書いてみました。詳細に興味があればこちらをご覧ください。
http://qiita.com/omochi64/items/9336f57118ba918f82ec

最近このQiitaというサービスが若干の流行を見せているようなので使ってみましたが、月間でアップロード可能な画像サイズが合計2MBというのはかなりきついですね…

edupt ベースでレンダラ書いた

@h013 氏作の edupt (http://kagamin.net/hole/edupt/index.htm) をベースにパストレーサでレンダラを書いてみました。

ソースはこちら: https://github.com/omochi64/simple_pathtracer (Visual Studio 2013 のみビルドを確認)
edupt の機能に加えてobjファイルの読み込みや領域分割も実装しましたが、とりあえず edupt デフォルトの機能でコーネルボックスレンダリングした結果がこちら (クリックして拡大)

レンダリング時間は、Core i7-4770 3.4GHz で7並列実行したときの時間です
トーンマッピングは edupt と同じく、ガンマ補正して8ビットの値にマッピングしてます (rgb_8bit = (int)(255.0*pow(rgb_float, 1.0/2.2) + 0.5))。

このシーンは結構ライトが大きいですが、それでもシンプルなパストレだと収束するの結構時間かかりますね。
(下3つは変わってないように見えるかもしれないですが、拡大してオリジナルサイズで見ると変化がわかると思います)


まだまだ実装していきたいものがいろいろあるのですが、とりあえず自分が今のところ何を実装したのかを文章でまとめる目的で記事にしてみました
気が向けば実装した他の機能についても書いていこうかと思います

githubアカウントを作ってみた

時代に乗り遅れてるというレベルじゃないですが、イマドキ外部に公開しているコードが1つも無いのはヤバイらしいのでgithubアカウント作って1つコードを公開してみました。

Midpoint Displacement Algorithm

プロシージャルにフラクタルな地形を作る手法の1つである、中点変位法というものを実装しました。
詳しい解説はググるなりここらへんを見てください (中点変位法でググるとトップに出てくるページ) http://nis-lab.is.s.u-tokyo.ac.jp/nis/CG/cgtxt/cg5/cg59.htm

左上の画面が最初の状態で、分割数を増やしていくと段々と細かくなって右下のようになっていきます。





メモリプールのインターフェース

某所で書いたメモリプールに関しての実装をメモがてら。
これに関して色々考えてみて、C++についての知識が足りないなー、と実感したり。


お話としては、普通にnewしていくのは重いのでメモリプール使おうぜー、って話です。その実装方法とか。
とは言ってもメモリプール自体はboost::poolに任せて、そのインターフェースについて考えていました。
メモリプールというのは、以下のように先にメモリをどかっと確保しておいて必要に応じてその一部を使おうぜ、ということです。

struct Pool{
  bool is_used;
  Hoge *hoge;
};
Pool pool[128];
void init() {
  for(int i=0;i<128;i++) {
    pool[i].is_used = false;
    pool[i].hoge = new Hoge;
  }
}
Hoge *my_malloc() {
  for(int i=0;i<128;i++) {
    if(!pool[i].is_used) {
      pool[i].is_used = true;
      return pool[i].hoge;
    }
  }
  return NULL;
}
void my_free(Hoge *ptr) {
  for(int i=0;i<128;i++) {
    if(ptr == pool[i].hoge) {
      pool[i].is_used = false;
      return;
    }
  } 
}

くそコードなのでこれを真似たりしないでください。一応、Hogeクラスをプールしておくコードです。
boost::poolというのは、このメモリプールをいい感じにやってくれるboostライブラリ中のクラスです。


以下そのboost::poolを使ったメモリ管理のコード。

最初やってた方法

class Hoge {
  static boost::pool<> memoryPool;

  Hoge(args);
public:
  static Hoge *create(args) {
    void *ptr = memoryPool.malloc();
    return new(ptr) Hoge(args);
  }
  static void destroy(Hoge *ptr) {
    ptr->~Hoge();
    memoryPool.free(ptr);
  }
};

boost::pool<> Hoge::memoryPool(sizeof(Hoge));

クラスHoge内にboost::pool::mallocとplacement newの処理をラッピングするクラスメソッドを準備し、それらを介してインスタンスを生成できるようにしていました。
使い方は以下のような感じです。

Hoge *hoge = Hoge::create(args);
hoge->fugafuga();
Hoge::destroy(hoge);

ですが、この方法だとメモリプールが必要なクラスごとにいちいちcreateやらdestroyやら定義しなきゃいけなくてめんどくさい!
でも引数付きのコンストラクタ使いたい!という事で、某所で聞いた意見を合わせて以下のようにしました。

今やってる方法

template <class TYPE>
class ObjectPool {
  static boost::pool<> &getPool() {
    static boost::pool<> pool(sizeof(TYPE));
    return pool;
  }

public:
  static TYPE *create() {
    void *ptr = getPool().malloc();
    return new(ptr) TYPE;
  }
  template <class ARG1_TYPE> static TYPE *create(ARG1_TYPE arg1) {
    void *ptr = getPool().malloc();
    return new(ptr) TYPE(arg1);
  }
  template <class ARG1_TYPE, class ARG2_TYPE> static TYPE *create(ARG1_TYPE arg1, ARG2_TYPE arg2) {
    void *ptr = getPool().malloc();
    return new(ptr) TYPE(arg1, arg2);
  }

  static void destroy(TYPE *ptr) {
    ptr->~TYPE();
    getPool().free(ptr);
  }  
};

すごい泥臭いです。
使い方はこんな感じ。

Hoge *hoge = ObjectPool<Hoge>::create<Fuga &, int>(fuga, 10);
hoge->fugafuga();
ObjectPool<Hoge>::destroy(hoge);

こうすると、引数付きコンストラクタを実行してメモリプールからメモリを引っ張ってこれます。
が、引数の数が増えるたびにメソッドを増やさないといけないのが非常にアレです。
あと、個人的にTYPEで指定したクラスはObjectPoolを通じてのみインスタンスを生成できるようにしたかったので、

class Hoge {
  friend template <class TYPE> class ObjectPool;
  Hoge(args);
  ~Hoge();
public:
  void func();
};

------

Hoge *hoge = ObjectPool<Hoge>::create();

としたかったのですが、これじゃ「ObjectPoolはHoge::Hogeにアクセス権無いよ!」と怒られました。
createがクラスメソッドなので、ObjectPoolクラスだけをfriend指定するだけじゃダメ、ということでしょうか。
アクセス権限に関してあんまり知らないのでどうすればいいのかさっぱりです…。

それとも、ここらへんはどこかしら妥協するしか無いんものなんでしょうか。

LuaとLuaJITと浮動小数点数演算

更新があまりに久しぶり過ぎる。
19時過ぎに寝て2,3時頃に起き、そのまま勉強するぜー、とか思って寝たら23時頃には目が覚めてその上腹が痛くてたまらないって状況です。

今回は、こないだLuaJITいじっててハマった点をちょっと書いときます。
小ネタなので多分短い。

Luaについて

説明略。
このブログ見ている人なら分かるでしょう。

LuaJITについて

LuaJIT(Just In Time)処理系です。
はい、何の説明にもなってないです。

とりあえず、Luaの超速いバージョンだと思ってくれて差し支え無いです。
でも、実際使うならそれだけじゃ不味いと思うので、詳しくはググってください。

LuaとLuaJITと浮動小数点数演算

本題です。
ここで予防線。自分は最新のLua、LuaJITの実装を知らないので、以下に書かれていることが変わっている可能性は十二分にあります。
ご注意ください。

Lua浮動小数点数演算

さて、ご存じの方もおられるでしょうが、Luaは内部での数値管理を全て倍精度の浮動小数点数(C言語で言うdouble)で管理しています。intみたいなのがありません。
通常の使用範囲であればそれで問題ないのですが、DirectXなどど絡んできたときに少々問題が発生します。
何が不味いのかというと、DirectXでは、FPU(浮動小数点数演算ユニット)の精度を落として速度を向上させる、といったことをしています(詳細はこちら http://hammm.blog21.fc2.com/blog-entry-52.html)
つまり、今まで「doubleの範囲なら整数表現も問題ないぜー」とやっていたのが、DirectXによって「あれ?」となることがあるかもしれないって事です。
上記URLにあるように、D3DCREATE_FPU_PRESERVEを指定してデバイスを作ればとりあえずこの問題は解決します。指定しなくても、Luaの精度が落ちるだけでプログラムの実行は可能です。

LuaJITと浮動小数点数演算

JITが入ってないLuaの場合、D3DCREATE_FPU_PRESERVEは付けてても付けてなくてもOKでした。
が、LuaJITの場合(LuaJIT 2.0.0-beta8で確認)はこれを付けないとluaL_openlibsを呼び出したときにプログラムが強制終了してしまいます。
この時、「D3DCREATE_FPU_PRESERVEが指定されてないよ!」ってエラーメッセージを吐いているので、LuaJITを使う場合は、必ず「D3DCREATE_FPU_PRESERVE」を付けてデバイスを作成する必要があるみたいです。


自分はこれになかなか気づきませんでした。
標準出力だったか標準エラーだったかに「D3DCREATE_FPU_PRESERVEが指定されてないよ!」ってエラーを出すのですが、その直後にはプログラムが終了してしまうので、コマンドラインからプログラムを実行させるまで気づきませんでした。
LuaJITのビルドオプション当たりをいじればここらへんは変わるのかな?
該当箇所のソースコードを自分でいじればD3DCREATE_FPU_PRESERVEを指定しなくても動くように出来るんでしょうが、精度が落ちて整数値の演算が上手くいかなくなる可能性が高まるなら、そこまでしてやりたいとは思いませんですね。



LuaJITではD3DCREATE_FPU_PRESERVEを指定しないといけない、という情報がパッと調べた感じでは出てこなかったのですが、実は僕が見逃しているだけでどっかに転がってるもんなんでしょうか?
それとも僕が無知なだけで実は常識だったりするのだろうか。
そこら辺分かる方が居たら是非とも教えていただきたいです。

デスクトップコンポジションの無効化

プログラミング関連のことは、とりわけ自分が書くことが殆ど無くて放置気味になっているこのはてダ。
今回、Windows Vistaから導入されたAeroの描画に用いられている、「デスクトップコンポジション」の無効化について書きます。

デスクトップコンポジションとは

そもそも何それ?という方もおられると思います。そもそもWindows XPをまだ使っている方には関係ない話ですからね。
ゲームなどを起動したとき、画面のスタイルが変更されて、以下のようなポップアップが出たことがないでしょうか。

これが、デスクトップコンポジションが無効になったことの合図です。
これが出たとき、Windows Aeroが無効になり、描画スタイルがベーシックになっているかと思います。

何故無効にするのか

Windows Aeroの描画にはDirectX9が利用されています。
たいていのゲームにも、描画にDirectXが使われます。
これらの両方が同時にDirectXを利用すると、場合によっては不具合が発生することがあります。
とはいっても、そんなに頻発するものではありません。これが大きく影響するのはマルチディスプレイ環境の時です。
マルチディスプレイ環境でゲーム中、デスクトップコンポジションを有効にしたまま「ウィンドウモード→フルスクリーンモード」としたりすると、画面に何も映らなくなったりします。
これはデスクトップコンポジション絡みの不具合であることが多いです。
デスクトップコンポジションはユーザが有効/無効をアプリケーションごとに切り替えられますが、ほぼ大半のユーザはそんなこと知りませんし、それ任せにするのはあまりに不親切です。
そこで、プログラム側から無効化させてやることを考えます。

無効化

デスクトップコンポジションの無効化には、 DwmEnableComposition 関数を呼び出します。
DwmEnableComposition( DWM_EC_DISABLECOMPOSITION ); とすれば無効化されます。
しかし、デスクトップコンポジションWindows Vista以降で導入されたものなので、Windows XPでこの関数を呼び出すことは出来ません。
つまり、OSのバージョン判定が必要になります。
OSのバージョン判定については色々ネット上に資料があるので、ここでは割愛します(自分で調べてね)。
コード例は以下のようになります。

#include <Dwmapi.h>

  OSVERSIONINFO os_info;
  os_info.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
  GetVersionEx(&os_info);
  if( os_info.dwPlatformId == 2 && os_info.dwMajorVersion >= 6 ) {
    HMODULE hLibrary = LoadLibrary( "Dwmapi.dll" );
    if( hLibrary ) {
      DwmProc lpfnDwmEnableComposition;    // Function pointer
      lpfnDwmEnableComposition = reinterpret_cast<DwmProc>(::GetProcAddress( hLibrary, "DwmEnableComposition" ));
      if( lpfnDwmEnableComposition ) {
        lpfnDwmEnableComposition( DWM_EC_DISABLECOMPOSITION );
      }
    }
    FreeLibrary(hLibrary);
  }

ちなみに、このコードはメインのウィンドウが表示されてからじゃないとうまく動かないことがあります。
ウィンドウ生成のコードを先に書いてからこのコードを書きましょう。

上記のプログラムは、if( os_info.dwPlatformId == 2 && os_info.dwMajorVersion >= 6 ) の部分が、Windows Vista以降であるかどうかをチェックしています。
そしてWindows Vista以降であることが分かったらDwmapi.dllを遅延ロードしています。このdllの中にDwmEnableComposition関数が入っています。
(これ書いている間に、そもそもWindows XP以前であったらDLLの遅延ロードが失敗して関数が実行出来ないので、OSのバージョン判定がいらない気がしてきました…)
以上のコードでデスクトップコンポジションが無効化できます。やったね!

有効/無効

デスクトップコンポジションの無効化がこれでできるようになりましたが、問答無用でデスクトップコンポジションを無効化されるのが嫌な人もいます。
ですが、何も知らない人にとっては、無効化されずに不具合が発生するのも嫌でしょう。
これに対する正確な解はないですが、プログラム内の設定項目にデスクトップコンポジションの無効化、という項目を入れるなどの対処方法があります。
ここらへんは、作るものの種類によって考えていくといいのではないでしょうか。