RayMarching

前から気になってたんだけどようやくできた。

レイマーチングはレイトレーシングの一種で、ポリゴンを使わない3D描画である。コードはこちら。CustomRenderTargetは使っておらず、通常のShaderで描画している。

レイマーチングの仕掛け

まあ、難しい話は適当に検索してくれれば出てくるとは思うが、例えばシェーダだけで世界を創る!three.jsによるレイマーチングなどを見てもらうと丁寧に説明されている。
最も簡単な例としては、球体を(0,0,0)を中心に配置して、カメラ(レイの初期位置)からの距離を算出して、半径を引くと、カメラから物体表面までの距離になる。この距離のぶんだけレイを前に進める。レイの向きが球体の中心ぴったりだとこれで表面まで到達して距離0になるが、ちょっとでもズレていると表面に届かないことになるので、それを何度か繰り返すことによって、限りなく表面に近い位置まで進める。これは例えば16回ループとかいう感じで繰り返す。最終的に表面までの距離と非常に近い数値になったら物体とレイが衝突したということでそこに色を置く。衝突しない向きのレイの場合、どうしたって表面の近くにならないので色は置かれない。
表面までの距離だけ進む、というアルゴリズムのため、球体の表面スレスレだけど当たらない方向のレイはぜんぜん進まなくて向こう側に物体があっても途中でループが終わる。これにより輪郭付近は色が置かれないような描画になる。

ハマりポイント

検索してみた人はわかると思うが、出てくるのはGLSLの例ばっかりである。これをHLSLに書き直したとき、非常によくハマると思われるのがmodとfmodの違いだろう。
サンプルコードでは球体がたくさん並んでいるが、これを実現するにはレイの座標のmodを取る。GLSLのコードではmod関数を使っていて、これと似たような計算をするHLSLの関数がfmodなのだが、この2つは値がマイナスの時の計算結果が異なっていて、ちゃんとした描画ができない。よって今回はGLSLのmodと同じ計算をする関数を自前で定義してある。

DXRuby的事情

Shaderクラスを使ってはいるが、頂点シェーダも書いてある。これには深いワケがある。
そもそもDXRubyのシェーダを使うサンプルは基本的に全てピクセルシェーダのバージョン、PS2.0を使う。なぜかと言うと3.0にするとうちのPCで動かないからである。DirectX9.0でサポートされるのがPS2.0であり、PS3.0はDirectX9.0cでのサポートとなる。DXRubyはDirectX9.0cを使っているのでPS3.0は動くはずで、実際グラボを差していた頃は動いていたのだが、壊れたので外したら動かなくなった。ので2.0にしていた。
うちのPCのチップセット内蔵GPUがしょぼいのかな〜ってぼんやり思っていたのだが、よくよく考えてみるとDirectX9.0初期のPS2.0しかサポートしないGPUってどんだけ古いんだよって話で、いくらなんでもそこまで古くない。じゃあ何で動かないの?ってことで少し調べてみた。
DXRubyの通常描画ではシェーダは使っていなくて固定機能パイプラインを利用している。そこに強引にピクセルシェーダだけ使うようにするのがShaderクラスの仕組みである。頂点シェーダも書けば使えるけど、とりあえずそういう使い方はDXRubyの仕様上は必要がない。ここにPS2.0のピクセルシェーダを書くと動くが、PS3.0を書くと動かない。CustomRenderTargetでVS3.0/PS3.0と指定すると動く。VS2.0/PS3.0は動かない。VS3.0/PS2.0も動かない。VS2.0/PS2.0は動く。つまり・・・?
とりあえずこの結果から想定できるのはVSとPSのバージョンが食い違うと動かないということで、固定機能パイプラインはVS2.0相当なのではないか、という仮説。じゃあなんで前は動いていたのかというと、おそらくGPUによってバージョン混在でも動くものがあるのではないか。そうでなければ固定機能パイプラインがVS3.0相当だったかだが、それだと現状のサンプルが動かない人が大量に発生することになるので、ちょっと考えにくい。いや、動いてないけど誰も報告してくれてないだけかもしれない。
ともあれ、レイマーチングのコードはロジックが複雑なのでPS2.0では動かず、PS3.0にしたいんだけど従来の書き方ではできず、自前で頂点シェーダも記述して両方3.0にするために今回はこのようなコードになっているわけだ。なのでGPUによっては頂点シェーダのコードをばつーんと消してもちゃんと動くんじゃないかな。

DXRubyの頂点指定

頂点シェーダを書くためにはDXRubyからどのような頂点情報が渡されるのかを知る必要がある。しかしこれはきわめて内部的な情報のため、リファレンスには書いていない。今回のサンプルは作者ならではの内部情報利用というひどいものと言える。
せっかくなので書いておくと、基本的なImageの描画の場合、頂点数は3角形2つで6個、スクリーン座標のxとy、0固定のz、それからテクスチャ座標のtu、tv(通常は0〜1)と、頂点色、となる。これをサンプルのようなビュー行列、プロジェクション行列で変換する形である。画面のピクセルとテクスチャのテクセルがドットバイドットで一致するように調整してバッファに格納する。回転やスケーリング時はC側で座標を計算して渡す。頂点色は基本255,255,255,255の白で、alphaが指定されたときにaにそれをセットする。

おしまい

などと妙なところで苦労したので力尽きた感じ。ほんとはもっと色々試してスクリーンショットとか置きたかったのだけども。
まあ、できてよかった。理解できたかと言うと結構怪しいのだが、とりあえず動いた。続きは気が向いたときにでも。