ActionScript で屈折レンダリング+Molehill雑感

今回はわりと本気ネタ.まじめに屈折レンダリングをやってみました.

Clear Water with refraction rendering | Si+ (wonderfl.net)


Fork元のネタはsaharanさん3D水面 / Water 3D - wonderfl build flash onlineで,うまくパラメータ調節されてて非常に良い感じに水面の挙動が表現されていたので,反射マップだけではもったいないなと思い,屈折レンダリングと合成して水を表現してみました.
やり方自体は頭の中にあったのですが,ちゃんとした数式にしてコードまで落とし込んだのは初めてでした.トライアンドエラーでなかなか苦労したのですが,おかげでかなりリアルな水が描画できてると思います.


今回は要素技術を解説するとエラく長くなりそうなので,概要だけ簡単に説明してみます.
無色透明な液体をレンダリングする際の要素は3つ(反射,屈折,コースティック影)しかありません.今回は,箱の内面に複雑なテクスチャを採用することでコースティック影をゴマかしてしまっています.シンプル(たとえば真っ白)な箱内面や透明な入れ物の場合は,コースティックを計算して合成してやる必要があります.コースティック影の生成は,”擬似”であれば割と簡単に出来るのですが,まじめに計算しようとすると屈折以上に難しいようです(選択的フォトンマップ?まだまだ勉強不足でよく判っていません).
というわけで,今回は反射と屈折だけなのですが,幾何光学計算をする上ではほとんど同じ扱いになります.しかし,リアルタイムレンダリングする上では似て非なるものとなります.乱暴に言うと,反射はかなりゴマカシが利きますが,屈折はほとんどゴマカシが利きません.
屈折もシチュエーションによってはかなりゴマカせるのですが,今回のように箱の内面を屈折させてレンダリングさせる場合は,水面が静かな状態(箱の内面が歪まずに見えてる状態)の見た目を容易に想像する事ができます.こういった場合,近似を使って変に歪んでいると,すぐに違和感を感じてしまいます.一方で反射は,そもそも反射してる先(液体を囲んでいる風景)が見えていないため容易に想像できません.このため,レンダリング結果が適当であっても反射として見えてしまいます.(つまり,例えば複雑な形状のガラスの屈折なら,屈折した結果が容易に想像がつかないためある程度ゴマカシが利くし,鏡のように容易に結果が想像できる反射はゴマカシが利かない.メタル表面やすりガラスのように細かい凹凸による乱反射が伴う反射/屈折なら結果がボケるのでゴマカシが利く)
具体的な計算方法を見ると,どの程度ゴマカシが利くかが判ると思います.

  • 反射
    • あらかじめ,水面の中心座標から見た半球をレンダリングした(っぽい)テクスチャを用意
    • 各頂点法線ベクトルのx,y成分をそのままuv座標とする.
  • 屈折
    • あらかじめ,水面をカメラ面として内壁を(ちゃんと)レンダリングしたテクスチャを用意
    • カメラ位置から各頂点への視線ベクトルを計算.
    • 頂点法線ベクトルと視線ベクトルから入射ベクトルを計算して,入射ベクトルと屈折率から射出ベクトルを計算.
    • 射出ベクトルと内壁(4側面+1底面)のどこに当たっているかを計算(ここまでがいわゆるレイトレーシング).
    • 内壁との衝突位置から,内壁をレンダリングした際のカメラ焦点へのベクトルを計算,カメラ面との交点がuv座標.

結局,屈折レンダリングは,各頂点におけるレイトレーシングを行って,その結果を予め底面をレンダリングしておいたテクスチャ座標に変換する,という計算になります.言葉にしてしまうと,大した事してないですね...数式化とコーディングには結構苦労したんですが...

そして次世代描画エンジン molehill へ

さて,この高速化を考える場合,計算パートの中ではレイトレが比較的高負荷であるため,どの程度レイトレ計算を端折れるかというところになります.しかし所詮1頂点につき1パスですので,今回の例では高々2000パス/フレームです(単純比較は出来ませんが参考にReal Time Ray Tracingでは192x192=36864パス/フレーム).マトモに計算したところで大した負荷にはなりません.このレンダリングで圧倒的にボトルネックになっているのは,計算パートではなく,反射画像と屈折画像の合成です.
FlashPlayer10 の 3D 機能において,この合成は2回の drawTriangle() 結果をアルファブレンドする形になります.しかし,ひとつの頂点情報について,テクスチャとuv情報だけ差し替えて2回同じレンダリングを行いアルファブレンドするので,非常に効率が悪くなっています.FlashPlayer10の範囲内であれば,PixelBenderを使う事で1パスになりますので,若干の高速化が見込めますが,所詮ソフトウェアレンダリングなのでそれほど大きな改善にはなりません.
ここで大きな改善が見込めるのが FlashPlayer11 の Graphic ボードアクセラレーション機能です.最終的に PixelBender はこの機能に対応するでしょうから,現段階では PixelBender を使っておいてバージョンアップを待つのは悪い選択肢では無いと思います.しかし,せっかく目の前にお試し版 FlashPlayer11 がありますので,これを使って,2つのuv情報とテクスチャをフラグメントシェーダ内で合成する簡単なシェーダを書いて実行してみました.
Clear Water [molehill version]
このソースはFlashPlayer11のmolehill用ですのでwonderflではコンパイルは通りません.便宜的に自分のホームページのFlashコンテンツにジャンプするswfをコンパイルして,ソースだけ差し替えています.
結果,効果は非常に高く,環境によっては2倍以上の高速化になりました.シェーダプログラムはソースの最後に書いてありますが,vertex shader/fragment shader ともたった5行です.たったこれだけのプログラムでこの効果はかなり魅力的だと感じました.

// Vertex shader
mov vt0.xyz, va0.xyz    // va0 (頂点座標) からvt0にコピー
mov vt0.w, vc9.z        // vt0.w に 1(vc9.z) を代入
m44 op, vt0, vc0        // vt0を座標変換して output
mov v0, va1             // va1 (uv座標1) からv0にコピー
mov v1, va2             // va2 (uv座標2) からv1にコピー
// Fragment shader
tex ft0, v0.xy, fs0 <2d,clamp,nearest>  // v0値とtexture0(反射マップ) から反射色を取得→ft0
tex ft1, v1.xy, fs1 <2d,clamp,nearest>  // v1値とtexture1(屈折マップ) から屈折色を取得→ft1
mul ft0, ft0, fc0.x                     // ft0 に fc0.x (alpha値) を掛ける
mul ft1, ft1, fc0.y                     // ft1 に fc0.y (1-alpha値) を掛ける
add oc, ft0, ft1                        // ft0 と ft1 を足して output 

おまけ

先ほど書いたように,レイトレは大した負荷ではないのであまり意味は無いのですが,Vertex Shader にレイトレ計算をさせるのは,3Dプログラマロマンです.現在のAGALでは if や for はサポートされていないので,余計な計算が増えてむしろ遅くなる危険性すらあるのですが,それでもロマンを求めて Vertex Shader のレイトレコードを書いてみました.
...が,残念ながら,if だけでなく sgn や slt コマンドもまだ実装されてないそうで,実行時エラーで動きませんでした(リファレンスにはちゃんと書いてあるのに!)...正直,相当時間を掛けて書いたコードなので,このままお蔵入りはあまりに残念すぎるということで,ここに公開供養しておきます.暇人は解析したらいいんじゃないかな.

// ----- where...
// va0 = vertex
// va1 = normal vector
// vc0-3 = mvpMatrix
// vc4 = cameraPosition
// vc8 = [xymax, ixymax, rimo, boxHeight]
// vc9 = [0, 0.5, 1, -1]
// ----- 
m44 op, va0, vc0
//----- refraction
sub vt0, va0, vc4
nrm vt0.xyz, vt0
dp3 vt3.x, vt0, va1
mul vt3.x, vt3.x, vc8.z
mul vt2, va1, vt3.x
add vt2, vt2, vt0
nrm vt2.xyz, vt2
sgn vt3, vt2                 // vt3 = sign*
mov vt4.zw, vc9.xx
mul vt4.xy, vt3.xy, vc8.xx
sub vt4.xy, vt4.xy, va0.xy
div vt4.xy, vt4.xy, vt2.xy   // vt4 = t*
mul vt5.xy, vt4.xy, vt2.xy
add vt5.xy, vt5.xy, va0.xy
mul vt5.xy, vt5.xy, vc8.yy   // vt5 = hit*
mov vt6.zw, vc9.xx
slt vt6.x, vc9.w, vt5.y
slt vt6.y, vt5.y, vc9.z
mul vt6.x, vt6.x, vt5.y
sub vt6.y, vc9.z, vt6.x      // vt6 = hitflag*
dp3 vt5.z, vt4.xyz, vt6.xyz
mul vt5.z, vt5.z, vt2.z
add vt5.z, vt5.z, va0.z      // vt5.z = hitz
slt vt6.z,  vt5.z, vc8.w
mul vt6.xy, vt6.xy, vt6.zz
sub vt6.z,  vc9.z, vt6.z     // vt6.z = hitflagz
add vt7.x,  vt5.z, vc8.w
div vt7.xy, vc8.w, vt7.x
sub vt7.z, vc8.w, va0.z
div vt7.z, vt7.z, vt2.z      // vt7 = r*
mul vt0.xy, vt2.xy, vt7.zz
add vt0.xy, vt0.xy, va0.xy
mul vt0.xy, vt0.xy, vt6.zz
mul vt0.xy, vt0.xy, vc9.yy
mul vt0.xy, vt0.xy, vc8.yy
mul vt5.xy, vt5.xy, vt6.yx
mul vt3.xy, vt3.xy, vt6.xy
add vt1.xy, vt3.xy, vt5.xy
mul vt1.xy, vt1.xy, vt7.xy
add vt0.xy, vt0.xy, vt1.xy
mul vt0.xy, vt0.xy, vc9.yy
add v0.xy, vt0.xy, vc9.yy
mov v0.zw, vc9.xx
//----- reflection
mul vt0.xy, va1.xy, vc9.yy
add v1.xy, vt0.xy, vc9.yy
mov v1.zw, vc9.xx