土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity レイマーチング入門(1) レイマーチングの原理

 レイマーチング(Ray Marching)という描画手法があります。ポリゴンを空間に配置するのではなく、個々の物体を「距離関数(Signed Distance Function)」として定義し、その関数を介して物体がどのように空間に配置されているかを計算し、描画します。

 レイマーチング自体は汎用的な描画手法ですが、Unity界隈でレイマーチングと言ったら、「シェーダー芸」を実現するエフェクト表現を指すと考えて良いと思います。ここでは「シェーダー芸で用いるレイマーチング手法」くらいの意味で、レイマーチングという言葉を使います。

 土屋はシェーダー芸をやらないので、レイマーチング手法を知らず、解説記事やコードを読んでもどういう挙動によって描画が実現されているのかさっぱり理解できずにいまして、今回記事にまとめてみようと思い、改めて勉強してみました。いつも通りですが、とても1回では終わらない分量になったので、ちょっとずつアップしていきます。レイマーチングはシェーダー本4(次回技術書典で頒布予定)の掲載範囲じゃないんだけどな……。

最終的に表示したい絵

f:id:t_tutiya:20191029221937p:plain

 これです。これが描画されるまでの道のりを追っていきます。

レイマーチングの考え方・特徴

 長方形の板ポリを用意し、これにレイマーチングで立体を描画するとします。板ポリをスクリーンに相当する物だと考えるとわかりやすいかもです。

 次の図を見てください(レイマーチングの記事では、模式図としてシーンに球体が複数並んでいる物を使うことが多いのですが、より単純化するために球を1個だけ配置しています)。
f:id:t_tutiya:20191029221942p:plain

 カメラ(視点)が板ポリに向いています。視点から見て、板ポリの向こう側に球体が配置されていると「みなして」、それを板ポリに書き込むことにします。

 レイマーチングでは、視点から板ポリ上の各ピクセルまでのベクトルを用意し(これをレイといいます)、そのレイにたいし以下のAとBの処理を繰り返し実行することで、そのピクセルになにを描画するかを決定します。

  • A:レイの先端から、物体表面までの距離を算出する(距離ゼロなら衝突として終了)。
  • B:算出した距離の分だけレイを前に伸ばす(設定回数伸ばしたら非衝突として終了)。

 この処理を板ポリ上の全てのピクセルについて実行すると、必要な描画結果を得られます。

 これだけ聞いても、「……え、なんで?」って感じかと思います。以下、実際にレイを伸ばしていく処理を3パターン確認していきます。

①レイが真正面から球体に衝突する場合

 まずは、レイが真正面から球体に衝突する場合を見ていきます。
f:id:t_tutiya:20191030085546p:plain
 視点から板ポリのピクセルまでレイが伸びています(一般的にはレイがまだ作られていない状態から始めるようですが、これもわかりやすさのために、視点から板ポリのピクセルまでのレイは既にある物として説明しています。)。
f:id:t_tutiya:20191030085121p:plain
1-A:レイの先端から球体の表面までの最短距離を計算します。この計算は、立体ごとに実装した「距離関数」を実行して取得します。ある座標Xから球の表面までの距離は「(Xから球の中心座標までの距離)-球の半径」という、極めて簡単な式で計算でき、これがそのまま距離関数になります。これが、レイマーチングの威力の一つです。
f:id:t_tutiya:20191030085124p:plain
1-B:1-Aで算出した距離分だけ、レイを延長します。すると、レイは球の表面に到達します。これによって、「レイの先端は球体に衝突した」とわかるので(正確には、それが判明するのは次に距離関数が実行された時です)。このピクセルには球を描画すると決定されます。ここでは灰色の板ポリ部に白を書き込みました。

②レイが真正面ではないが球体に衝突する場合

 次に、レイの方向が少しずれている場合を見ていきます。
f:id:t_tutiya:20191030085127p:plain
1-A/1-B:①と同じようにレイの先端から球体表面までの最短距離を算出し、その距離の分だけレイを延長しました。
f:id:t_tutiya:20191030085131p:plain
2-A:延長したレイの先端から、球体表面までの最短距離を計算します。距離関数は同じ物が使えます(座標Xだけが変化しています)。距離ゼロならレイが物体表面に到達したとみなしますが、見てわかる通りそうではないので、非ゼロの値が返ってきます。

2-B:2-Aで算出した距離分だけレイを延長します。これを繰り返していくと、最終的にはレイが物体表面に到達します(緑色の矢印で示しています)。「何回繰り返すか」は任意で、処理負荷とのトレードオフで設定します。このピクセルも白を書き込みます。

④レイが球体に衝突しない場合

 最後に、レイが大きくずれて、球体に衝突しない場合を見ていきます。
f:id:t_tutiya:20191030085135p:plain
1-A/1-B:これまでと同じように最短距離を算出し、レイを延長しました。
f:id:t_tutiya:20191030085139p:plain
2-A/2-B:さらにレイを伸ばします。球には向かっていません。以下何回繰り返しても急に到達しないことはお分かりかと思います(先述した通り「何回繰り返すか」は任意です)。このピクセルには黒を書き込み、球に到達しなかったことを示します。

 以上のような処理を、板ポリ上の全てのピクセルについて行うと、レイが球体に到達したピクセルに白、到達しなかったピクセルに黒が書き込まれ、それによって板ポリ上に白い球が描かれます。これが、レイマーチングの原理です。

続く

 次は実際にコードを書いていきます(というか、まさかコード書く前にこんな書くことになるとは思わなかった……)。