GPUPPURの進捗

最近日記を全然更新してませんが、GPUPPURの開発が進んでないというわけではありません。ただ、面倒なので更新してないだけですorz
とりあえず今までのところできたのは、
レイトレーシングするGPUPPURayとラスタライジングするGPUPPURasで(いくつか制約はあるがほぼ)任意の三角形メッシュのレンダリング
・GPUPPURayはfragment shaderでピクセル単位のライティング(拡散反射光のみ)
・GPUPPURasでvertex shaderで頂点単位のライティング(拡散反射光のみ)
・カメラの位置や方法を自由に動かす
・カメラの視角の設定
・物体の位置、方法の指定
・物体のマテリアルの指定(でも拡散反射光のみ)

後、OpenGLDirect3Dの両方に対応していて、あるウィンドウでOpenGLを使ったGPUPPURayでレンダリングしながら、別のウィンドウでDirect3Dを使ったGPUPPURasでレンダリングとかできます。


まだ、GPUPPURに必要なPhysXAPI(FR0011: HW Batched Raycasts)がSDKに用意されていなくて、PPUを活用したレンダリングができてません。
今のところはPPUでやる予定の処理をPhysXAPI(NxScene::raycastClosestShape)を使ってCPU上で計算しています。
このままAPIが公開されず、PPUを使えないままってことも十分ありえます。
そのときの代案として、
XBOXPlaystation3等で動くGPUPPURを実装する。
・PPUの代わりになりそうなハードウェアを探す。
・PPUで行う予定の処理もGPUで行うようにする。
・PPUの代わりになるようなハードウェアをFPGAを使って開発する。(FPGAを使った経験は無いけど、一度は使ってみたい)
を考えているのですが、今回の未踏の期間内にこれらに手を出すのは無理そうです。

       ト、         /ヽ   /\        _ 
   /|\ | ヽ       〈三ヽ /三/|        / !    /!ヽ 
   | l  ヽ |  ヽ      !\   / _|      /  |   ,' /  ! 
   | ヘ   !   ヽ    ( )){ }( ))     ,'   !_/ /  j そんなことよりGPUPPURしようぜ! 
   ',  ヽ |    l      トイ`|i|⌒ Y=}     !    ', /   / 
   ',  ヽ!≡   l      { ヽ || r‐'リ -i    !  ≡ !     / 
    ヽ    ≡  !     | ミ )!!= 彡-ノ_   l  ≡ !   ,.' 
     \   ≡ |   ,.-ノ / ! ト、トく   `メ、_',  = / / 
      \ ≡ !  /てノしイ_人人ノ、    ヽ  / 
        \― |/ヽ ,イ- 、ヽ /   イ´ ̄ ̄ヽ_/ 
         \ ヽ/  l  ヽi / ̄`! 
            ̄     〉―く ァ―‐‐j 
                  ゝ_ノゝ-- ノ! 

GPUPPURの配布の仕方

多くのC++のライブラリは、ライブラリファイルをプログラムにリンクし、ライブラリ内の関数やクラスを使うソースコードではライブラリのヘッダファイルをインクルードするというふうになっています。GPUPPURでもお世話になっているBoost C++ libraryはインストール時にいくつかのライブラリをコンパイルする必要がありますが、ほとんどの機能はヘッダファイルをインクルードするだけで使えます。

ライブラリを使うには、ライブラリをコンパイルしてプログラムにリンクできるようなライブラリファイルにする方法と、ライブラリはソースコードのままでヘッダファイルをインクルードするだけで使えるようにする方法があります。

ライブラリを使う側から見た違い

コンパイル済みのリンクライブラリにした場合
  • コンパイル済みライブラリか自分の環境でコンパイルできるようになったパッケージからインストール
  • 自分のプログラムをコンパイルするときにライブラリまでコンパイルする必要がない
  • デバッグ用とリリース用、static/dynamicリンク等の複数のリンクライブラリを使い分けないといけない
ヘッダファイルのみのライブラリにした場合
  • ヘッダファイルを入手して、それらをインクルードパスの通ったところに置いてインストールする
  • コンパイル時間が長くなる。(プリコンパイル済みヘッダを使えば軽減できる)
  • すべての関数がインライン展開される(かもしれない)

ライブラリを作る側から見た違い

コンパイル済みのリンクライブラリにした場合
  • ライブラリを各環境用にコンパイルして配布するか、各環境でコンパイルできるようにmakefile等を書いてパッケージにする。
  • static/dynamicリンクができるようにソースコードを書かないといけない。
  • templateを使いたいときは基本的にヘッダファイルに書かないといけない。
ヘッダファイルのみのライブラリにした場合
  • ヘッダファイルをそのままパッケージに入れるだけ
  • ヘッダファイルを修正する度にすべてのヘッダファイルをコンパイルしないといけない。コンパイル時間が長くなる
  • ヘッダファイル内でusing namespaceを使うのはまずいのでnamespaceを指定した書き方をしないといけない。ソースコードが長ったらしくなる


GPUPPURは今のところ.hppファイルと.cppファイルが混ざっていてリンクするライブラリファイル方式っぽくなってますが、やはり各環境でコンパイルできるようにmakefile等を書いたりするのが面倒なのと、templateを使ったクラスが多いのでヘッダファイルのみのライブラリにしようかなと考えています。ヘッダファイル方式の一番の弱点はコンパイル時間が長くなることですが、大抵のコンパイラにプリコンパイル済みヘッダを使えるようなので(gccでも使えるらしい。使ったことないけど)それほど問題ないでしょう。むしろ、GPUPPURにboost.preprocessorを使って大量のソースコードを自動生成している行列テンプレートクラスがあってそれは必ずヘッダファイルに書かないといけないので、コンパイル済みライブラリにしようがヘッダファイルのみにしようがそれほどコンパイル時間があまり変わらないという状態になってます。

ちなみに自宅のPC
CPU:sempron 2800+
memory:1GByte
compiler:VC++2005
Debug用ビルド
でGPUPPURをコンパイルしたときの時間を計ってみると
0から全部コンパイルした場合は65秒
全部コンパイルした後に.objファイルのみを消してビルドしたときは23秒
ヘッダファイルのコンパイルに約40秒掛かってるようです。

GPUPPURをヘッダファイルのみのライブラリにするかどうか悩むな・・・

stanford bunny

GPUPPURでstanford bunnyをレンダリングしたのでそのキャプチャ画像を未踏ユースwikiの自己紹介のページに貼りました。
stanford bunnyは約70k個の三角形からなる図形です。

今のGPUPPURのGPUPPURayでレイトレーシング法でstanford bunnyをレンダリングした場合、画像のサイズが512x512のときだと0.8fpsとかなり遅いです。ライティングにはGPUを使っているのですが、PPUで行なう予定の処理をCPUを使って計算しているのでこれぐらいのfpsしかでないようです。
PPUで行なう予定のレイと物体の交差判定処理をしないとき37fpsまででるので、CPUでやっている交差判定処理が1frameをレンダリングする時間のうちの約98%を占めているわけです。PPUでこの計算を行なえるようになると、どのくらいfpsが上がるのだろか。

テンプレートとvirtual

しばらく、忙しかったり疲れてたりして更新してませんでした。GPUPPURのほうは予定よりも遅れていますが大きな問題も特に無く開発が進んでいます。


GPUPPURのようにDirectXを使った実装とOpenGLを使った実装の両方を使いたい場合は、普通以下のように書くと思いますが


class gpuppur
{
virtual void render() = 0;
};

class gpuppur_implement_with_directx : public gpuppur
{
void render()
{
}
}

class gpuppur_implement_with_opengl : public gpuppur
{
void render()
{
}
}

これだとvirtual関数だからインライン展開されなくて遅いです。

非virtualに(さらにheaderに関数定義)すればインライン展開もされ高速化できますが、virtualのような便利さ(この例では実行時にgpuppurをopengl版とdirectx版に切り替えることができる)ができません。インライン展開と同等の速さでvirtualと同じことができるということはほぼ無理でしょう。windows以外では必ずopengl版を使わないといけないというふうに、コンパイル時にどちらにするか決定することができる場合もあります。つまり、gpuppurクラスを使う人がvirtualにするか非virtualにするか決定できればいいわけです。

以下のようにテンプレートを使って基底クラスを指定できるようにすれば、virtualか非virtualかを切り替えることができます。


template
class gpuppur_implement_with_directx : public Base
{
public:
void render();
};

class virtual_gpuppur
{
public:
virtual virtual_gpuppur() = 0;
virtual void render() = 0;
};

class void_
{
};

typedef gpuppur_implement_with_directx gpr_dx_v; //virtual
typedef gpuppur_implement_with_directx gpr_dx; //非virtual

virtual_gpuppur* phoge = new gpr_dx_v;

gpuppurにはdirectx実装とopengl実装があるわけですが、それぞれの実装クラスのrender関数でデバッグ時はrenderを呼び出す前

と後に不変条件をチェックしたいとします。その不変条件のチェックは各実装で共通なのでgpuppurクラスのrender関数から各実

装のrenderをにまとめておけばいいのですが、gpuppurクラスを基底クラスにするとvirtualを使わない限り基底クラスから派生ク

ラスの関数を呼べません。また、directx実装とopengl実装の各クラスからgpuppurの関数を呼び出したいとします。もちろん、先

ほど書いたようにvirtual/非virtualを切り替えることができるようにしたいのです。


template
class gpuppur_implement_with_opengl : public Base
{
public:
void render()
{
vector3 = Base::get_camera_position();
...
}
};

template
class gpuppur_implement_with_dx : public Base
{
public:
void render()
{
vector3 = Base::get_camera_position();
...
}
};

template
class gpuppur_base : public Base
{
public:
vector3 get_camera_position();
};

template class Implement>
class gpuppur
{
public:
template
class c : public Implement >

typedef Implement > base;

void render()
{
check_render_ready();
base::render();
check_any_err();
}
};

//仮想関数を持つgpuppurのdirectx実装とopengl実装
typedef gpuppur::c gpuppur_gl;
typedef gpuppur::c gpuppur_dx;

virtual_gpuppur* pgpuppur = is_gl() ? (virtual_gpuppur*)new gpuppur_gl() : new gpuppur_dx();

//仮想関数を持たないgpuppurのdirectx実装とopengl実装
gpuppur::c static_gpuppur_gl;
gpuppur::c static_gpuppur_dx;

これで、gpuppurをvirtual版、非virtual版と切り替えることができ、gpuppurのopengl実装とdirectx実装に共通な部分はきちん

と一箇所に集まってます。
もしgpuppurの3dfx Glide実装を作る場合でもGlide依存のrender()関数とかだけを実装し、後はgpuppurテンプレート引数にGlide実装のテンプレートクラスを指定すれば、gpuppurのGlide実装が出来上ります。

浮動小数点型でビット演算

人間には波があり、調子のいい日、悪い日があると思いますが、どうやら今日は調子の悪い日のようでした。それでもGPUPPURを開発し続けなければならないので、できるだけ自分の好きなことをやることにしました。そこで最適化をやることにしました。もちろんタイミング的に早過ぎますが。

GPUPPURではPPUで計算した結果を受け取り、GPUへそのデータを送る部分があります。データの量が数MB〜数十MBとなることがあり、GPUへデータを送る部分がボトルネックとなります。ですのでデータの量を減らしたり、データのフォーマットを工夫するなどして最適化する必要があります。

PPUから受け取るデータは基本的にfloat型で、そのうち法線ベクトルは値が-1.0f〜1.0fの範囲となります。また、GPUへ物体の色を送るとき、その値は0.0f~1.0fの範囲となります。float型をそのまま送るとデータがでかいし、そもそもそんなに精度が必要だとは思えないので16bitのhalf float型にするといいかもしれません。half float型では指数部に5bitぐらい使うのですが、値の範囲がほぼ程度決まっているのに指数部に5bitも使うのは勿体ない気がしたので、unsigned short型のデータに変換してGPUへ送るとよさそうです。OpenGLではテクスチャの色としてデータを送るとき、符号無し整数型の場合だと入力値iをi/(2^n-1)(nは整数型のビット数)として0~1の間の値を持つ固定小数点へと変換します。

というわけで、値が0.0f~1.0fの範囲となるfloat型を値が0~65535の範囲となる符号無しshort型へ変換する処理と、-1.0f〜1.0fの範囲のfloat型の値を0~65535の範囲の符号無しshort型へ変換する処理について考えてみます。ここで前者の場合は0.0fが0へ、1.0fが65535へ必ず変換され、後者の場合は-1.0fが必ず0へ、1.0fは必ず65535へ変換されるという条件を付けます。

まず0.0f~1.0fを0~65535へ変換するほうですが、単純にやると以下のようなコードになります。


typedef unsigned short ui16;

ui16 to_ui16_from_0_1(float v)
{
return v*0xffff;
}

次にビット演算を駆使して少しでも高速化できるようにもがきます。

typedef unsigned short ui16;
typedef unsigned int ui32;

union f_i
{
float f;
ui32 i;
};

int get_exp(float v)
{
return 127 - ((*(union f_i*)&v).i >> 23);

}

ui32 get_frac(float v)
{
return (*(union f_i*)&v).i&0x7fffff | 0x800000;
}

ui16 to_ui16_from_0_1(float v)
{
int exp = get_exp(v);

ui32 frac = get_frac(v);

return (frac-1) >> exp >> 7;
}

自分のPCでテストした結果、後者のコードで正しい結果が得られました。また、VisualC++2005のコンパイラで/Ox /Ot /fp:fastのオプション付きでコンパイルした場合、ビット演算を使ったほうが1.3倍ほど高速でした。

  • 1.0f~1.0fを0~65535へ変換するほうですが、単純なやり方は


ui16 to_ui16_from_n1_1(float v)
{
return (v+1.0f)/2.0f*0xffff;
}
ビット演算を使って高速化しようとすると

union f_i
{
float f;
ui32 i;
};

int get_exp(float v)
{
return 127 - ((*(union f_i*)&v).i >> 23);
}

ui32 get_frac(float v)
{
return (*(union f_i*)&v).i&0x7fffff | 0x800000;
}

ui16 to_ui16_from_n1_1(float v)
{
int exp = get_exp(v);
ui32 frac = get_frac(v);

ui32 tmp = (frac-1) >> (exp&0xff) >> 8;
exp = exp>>8;
return (0x7fff - tmp) >> ((16 << exp)&16) |
(tmp | 0x8000) >> (exp&16);
}

先ほどと同様の条件ではビット演算を使ったほうのコードが約1.07倍速かったです。苦労したわりにはあまり速くなってない。

bit演算を使って符号無しshortへ変換しようとすると、思ったり複雑な処理になったのでもしかするとhalf floatへ変換するほうが高速かもしれません。そもそも8bitでも精度が十分かもしれません。

もし、上記の方法より高速に変換する方法を見つけた方は是非教えて下さい。

他人の書いたコードの警告を出力しない

VisualC++2005を使っているとコンパイル時に標準ライブラリ、boost library, PhysXSDK等のヘッダファイルから大量の警告が発生します。少しでもバグを無くす為に常に警告レベルを最大にしてコンパイルするようにしているのですが、他人の書いた、あまり自分で書き換えるのはよくなさそうなファイルから大量に警告が出ると自分のコードの警告を探すのが面倒になります。

そこで、警告を出力しないように以下のように#pragma warning(disable: xxxx)を使います。


//たぶんVisual C++でしか効果はありません。
#ifdef _MSC_VER

#pragma warning(push)
//移植性の為、C標準ライブラリを使う。
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable: 4127) //warning about constant condition
#pragma warning(disable: 4819) //warning about encoding of charactor in source code.
#pragma warning(disable: 4512) //warning about impossible to generate operator=
#pragma warning(disable: 4100) //warning about argument is not referenced.
#pragma warning(disable: 4305) //warning about narrowing value from double to float.
#pragma warning(disable: 4996) //warning about deprecated standard C function.
#endif

//ここで他人の書いたヘッダファイルをインクルード

#ifdef _MSC_VER
#pragma warning(pop)
#endif

//ここで自分の書いたヘッダファイルをインクルード
//もちろん警告レベル最大で警告が出力される。

上記の方法だと全ての警告を無効にしているわけではないのですが、自分のインクルードしているファイルでは警告が全滅しました。


上記の方法は少し長いので、ソースコードのあちこちにコピペせずにマクロにしようとしたのですが・・・


#define SUPPRESS_WARNINGS \
#if _MSC_VER \
...

マクロの中にプリプロセッサの命令を含めることは(少なくともVisualC++では)できません。今までプリプロセッサの命令をマクロに含めたりすることがなかったせいか、初めてこのことを知りました。
##ifと書いたり、\#とか書いてみたのですがエラーとなりました。移植性のことを考えるとマクロの中に無理矢理プリプロセッサの命令を含めようとするのはまずいような気がします。そこでプリプロセッサの命令の塊をインクルードファイルにまとめ、使いたいときはファイルをインクルードして使うことにしました。

本体
begin_suppress_warnings_from_others_code.hpp:


#ifdef _MSC_VER

#pragma warning(push)
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable: 4127) //warning about constant condition
#pragma warning(disable: 4819) //warning about encoding of charactor in source code.
#pragma warning(disable: 4512) //warning about impossible to generate operator=
#pragma warning(disable: 4100) //warning about argument is not referenced.
#pragma warning(disable: 4305) //warning about narrowing value from double to float.
#pragma warning(disable: 4996) //warning about deprecated standard C function.
#endif

end_suppress_warnings.hpp


#ifdef _MSC_VER
#pragma warning(pop)
#endif

使うとき


#include

//ここで他人の書いたヘッダファイルをインクルード

#include

//ここで自分の書いたヘッダファイルをインクルード
//もちろん警告レベル最大で警告が出力される。

自分の書いたコードの警告を消すためにプリプロセッサ命令が使われることがないよう、少し長めのファイル名にしてみました。

openglの仕様書

GPUPPURでOpenGLDirect3Dのテクスチャを同じように扱えるようなクラスを作ろうと思いどのように設計するか迷ったので、OpenGLのSpecificationsを読んでいました。ちなみに仕様書では関数や定数名の頭にあるgl*やGL_*は省略されて書いてあります。ですので、glHogehogeの文字列を検索しようとしてもダメでHogehogeで検索しないといけません。
OpenGL2.0を使っているのですが、最新版はOpenGL2.1なので一応OpenGL2.1のほうを読むことにしました。ただ、OpenGL2.1に対応したグラフィックスカードが今あるかどうかは知りませんが。
OpenGL2.1だとPixel Buffer Objectが標準機能になっているので、今回参照した3.6 Pixel Rectanglesでは所々Pixel Buffer Object関連の記述が追加されているようです。今回はTextureあたりのことを調べたかったのですが、TexImage3Dでの処理はDrawPixelsと一部同じ処理をするので先に説明されているDrawPixelsでの説明を参考にしないといけません。TexImage3Dの説明の後にTexImage2Dが説明してあったりするので最初みたときは変な感じがしましたが、できるだけ一般的に説明しようとしているのでTexImage2DはTexImage3Dを特殊化したようなものだという感じになってます。
OpenGLにRGBA modeとcolor index modeがあるとは仕様書を読むまで知りませんでした。ただ、これからcolor index modeを使うことはないような気がしますが。OpenGLだと、テクスチャにRの値だけを整数で指定したとしても後で0.0~1.0の値を持つ固定小数点型の数値になり、指定されていないGBは0.0に、Aは1.0になります。
OpenGLではテクスチャ座標は(s,t,r,q)の4つの数字で表すので、TexCoord2fでs,tを指定した場合はrに0、qに1が設定されます。s,t,r,qは最後にs/q, t/q, r/qになります。ここらへんは頂点座標と似ていますね。普通はuv座標とかって言いますがOpenGLの世界ではstrq座標って言ったほうがいいんでしょうか。
それにしてもSpecificationsを読むのは結構疲れます。でもこれを読んで確実なOpenGLの知識を身に付けたほうがいいですし、読みなれればwebで調べなくてもすぐに必要な情報を見つけられるようになると思います。