CustomRenderTargetその4

初歩的な3Dサンプルということで追加したspheretest.rbだが、実行するとこんな画面が出る。マウスの方向にライトの光源があるっぽい感じにライティング処理が入っている。

今回はこれの話。つってもだらだら書くだけなのでこのへんの記事で3Dが理解できるかというと怪しい。こういう要素があって、こういうことができて、だいたいこんな感じ、みたいなのが把握できればよい、かな。詳しい話はそういうのをきちんと解説する記事がWebにはたくさんあるのでそちらで勉強していただく感じで。

光の表現

球体を作る方法については適当すぎるのでコード見てもらうかその手のサイトを見てもらうとして、ポリゴンで構成された物体に対して光を当てる話。
太陽のような遠い光源から発せられる光は、だいたいどの場所においても平行だと捉えることができる。これを利用して、光の向きを1個の3次元ベクトルで表現し、後はポリゴンの面との内積を取れば光の向きと面の角度というか正面から当たり度合いみたいな値が取れるので、これを使って色を暗くする。実際には光の逆向きベクトルと面の法線の内積
この手法は非常に簡単に物体の表面の色をそれっぽくできるが、表面の明るさを計算しているだけで、光を遮蔽しているわけではない。言い換えると光が遮蔽されて影の部分が暗くなっているわけではない。ので、物体の後ろに物体を置いても光のある側は明るくなってしまうし、地面に影が落ちることも無い。
これはよく考えてみると非常に面白い話で、結果的にそれっぽくなるが、それっぽくなっているだけで、ぜんぜん光の計算では無い。3D描画の世界は基本的に物理を頑張って計算する世界ではなく、負荷の低い処理でどうやって見た目をそれっぽくするかの世界なのである。これはもう全面的にそうで、ここに端的に現れている。

法線について

今回のサンプルはポリゴン単位で光の計算をしている。でもポリゴンは頂点3個の集合で、頂点シェーダの入力は頂点単位なので、面を表現するには情報が足りない。そこでポリゴンを構成する3個の頂点に、それらが表現しようとしている面の向きをそれぞれ持たせる。これを面の法線と言い、面の向きを表す3次元ベクトルとなる。3個とも同じ値を持つ。法線の計算は3点の座標を使って外積を取る。
頂点シェーダでは法線ベクトルをワールド変換行列で回転させてピクセルシェーダに渡し、ワールド座標系で表現される光のベクトルと内積を取って、色を減衰させる。

面の向きとカリングとZバッファ

ところで3Dのポリゴンには向きがある。そりゃ法線を算出するぐらいだからあるに決まっているのだが、ポリゴンを構成する3つの頂点の並び方を一定方向に揃えてやることで、画面上に描画される座標で右回りか左回りかを判定して表裏がわかるようになる。例えば右回りを表(こっち向いてる)と表現すれば、描画時に左回りになったポリゴンは裏を向いていると言える。正しく構成された3Dの物体は裏向きのポリゴンは必ず表向きのポリゴンの影に隠れるので、裏向きポリゴンを描画する必要が無い。こうやってGPUの描画負荷を削ることをカリングと言う。今回のサンプルでは

    # レンダーステート設定
    o.set_render_state(D3DRS_CULLMODE, D3DCULL_CW)

とレンダーステートを設定していて、これが右回りを表として裏ポリゴンは描画しない指定となる。左右間違ってるかもしれないけど。D3DCULL_NONEを指定するとカリング処理をしなくなる。そうなるとどうなるかと言うと、Zバッファを使って奥行きの処理をするので処理の負荷は増えるが結局正しく描画される。Zバッファというのはピクセル単位でZ座標を保持しておいて、次に同じピクセルに描画する際に、より奥なら描画せず、手前なら上書きするような処理である。当然メモリを食う。DXRubyではZバッファはバックバッファを生成するときに一緒に作っているのでもともと無駄にメモリを食っている。ということはCustomRenderTargetをWindowより大きく作るとZバッファのサイズが足りなくなる気がするが、これはそのうち試してみる。ともあれ

    o.set_render_state(D3DRS_ZENABLE, D3DZB_TRUE)
    o.set_render_state(D3DRS_ZWRITEENABLE, 1) # bool値の場合とりあえずTRUEは1、FALSEは0

がZバッファを使う指定である。つまりZバッファとカリングのどっちもONにしているが、どっちかを消してもちゃんと描画できる。よく考えたら無駄だ。
Zバッファは3D描画には非常によい性能を出してくれるが、半透明色を持つ物体があると困ることになる。後から奥の物体を描画する際に正しい色を算出することができないからだが、そういう場合はZソートと言ってZ座標で並び替えて奥から順に描画する。DXRubyは2D描画でもGPUを使って描画するので描画優先順をZ座標に入れてZバッファを使えば高速に描画できるが、半透明色を扱えるようにするためにZソートして描画するようになっている。

グローシェーディング

ポリゴンを構成する頂点情報を返すメソッドは

# 三角を構成する頂点を返すメソッド
def make_polygon(v1, v2, v3)
  cross = Vector.cross_product(v3-v1, v2-v1)
  cross = cross.normalize if cross.norm != 0
  cross = cross.to_a
  [v1.to_a, cross, v2.to_a, cross, v3.to_a, cross].flatten
#  [v1.to_a, v1.to_a, v2.to_a, v2.to_a, v3.to_a, v3.to_a].flatten
end

と定義されているが、これの最後のコメントを外すとこんな描画になる。

本来ポリゴンを構成する3点はそれぞれ違う方向を向いていて、元のコードだとそれらが構成する面の法線を頂点に設定していたのだが、このコメントの部分では頂点3つの法線にそれぞれ自分の向きを設定する。頂点シェーダからピクセルシェーダに渡す頂点はTEXCORD2で、TEXCORDで渡す値は点の点の間で補間され、法線がなめらかに回転する。法線は光の反射率に使われるので、面の中でも色が少しずつ変わっていき、これをグローシェーディングと言う。古典的な手法である。表面はそれっぽく見えるが結局は大きな面であるので物体の輪郭はカクカクになる。
こういうのもなかなか興味深く、3D座標系としてみると平面なのに、光の計算に使う向きだけを変更することで見た目だけをそれっぽくする。なんだかメタマテリアル的な不思議さである。古い手法だが例えば複雑な形状にアニメ的な表現をするためにシンプルな法線を設定して影をシンプルに付けるなどといった応用がされたりする。

おしまい

今回は球体を描画してみたわけだが、これだけでも3DとGPUがいかに強引にそれっぽい画像を作り出しているのかが伺える。まあ3D描画はそういうものの塊である。
そしてそういったテクニックをDXRubyで実際に使ってみることができる、というのもなかなか楽しい。DirectX9世代のライブラリだしいろいろ削っているので世の中の全てのテクニックが使えるわけでもないが、まだまだしばらくは遊べそうである。