
みなさん、こんにちは!多分ctfの時のブログ振り、こまさんです!
今日はなんか面白い事したいな~と思って、多少得意分野の3D系からちょっとお話してみようかな、と思います!
実際にWebGL2でぐりぐりうごかせるデモもあるので、ぜひ触ってみて下さいね!
そもそもレイマーチングとは何ぞや
一言でいうと、3Dのレンダリング(シーンを描画すること)の方法の一つです。
とまぁこれだけじゃ「はぁ。そうですか。。」としかならないと思うので、ちょっとしっかり説明していきますね!「そんなの知っとるわ!」という方はガンガン飛ばしてもらって大丈夫です!
まぁそんなわけで、「レイマーチング」について語る前に、ここ5年強ぐらいで(特にゲームを触る方なら)聞くことが多くなったであろう「レイトレーシング」というものから話そうかな、と思います。
こちらは、「我々が見ている視野は、目に入ってきた光の色や強さで決まるよね。じゃあ逆に目の方から光線を追跡してその線が光源の方に近づけば明るい。みたいな感じで計算できるんじゃね?」といった感じの方法です。
読んで字の如く、レイ(=Ray、光線)をトレース(=Trace、追跡)するんですね。
ただこれってかなり処理の量が多いんです。というのも、光を追跡する、と言っても反射の計算が必要になります。壁に当たったら反射させて、また当たったら反射させて、、とやって光源にある程度まで近づけば光に衝突した、という感じで、色を計算したりするのですが、、、
もちろん計算する反射の回数を制限すればいいよね、となるのはその通りなのですが、あまり少なくしすぎると今度画質が苦しいものになってしまいます。。
でも昨今のGPUの進化によってある程度リアルタイムでもできるようになってきていて、実際ゲームでもレイトレーシングを使って画質の向上をできるものが結構あります。
まぁちょっと脇道に逸れてしまいましたが、今回話そうと思っていた「レイマーチング」はこのレイトレーシングの一種です。
ですが、大きな違いがあります。まず2つ、
- 基本的に反射は計算しません!(これは計算することもできます)
- そして次に、光線を追跡する際にレイトレーシングは、オブジェクトに当たるところまでを1ステップで進行させます。それに対して、レイマーチングは少しずつ進めていく(レイ(=Ray、光線)がマーチ(=March、行進)するということですね!)という感じ。そして一度衝突したらそこで終わりです。
ここから分かるように、よりきれいでリアルに近いような描写をするときにはレイトレーシングの方が向いています。
というか基本的にはレイトレーシングの方が優秀です。
ですが、今回レイマーチングに的を絞った理由は、3つ目の違いがとても重要で、限られた場面ではレイマーチングがとても優秀なんです。 その違いは、
- オブジェクトの形状の定義が「関数」によって行われる
というところです。
正確に言うと、
- 「ある点が与えられたときにその点から描画したいオブジェクトの表面までの最短距離」を求める関数
を定めると、そのオブジェクトが描ける、という感じです。
正直多分ほとんどの人は「なんでそれで描けるんだ???」と思ったと思いますが、今回は細かい事は省略して、実際にどんな感じのものが描けるか、というデモを通しながらレイマーチングの魅力を見せていきたいかな、と思います。
詳しいことが知りたい人は色々調べてみてくださいね!とっても奥が深いです
まずは手始めに、立方体!
ここから先のデモはWebGL2を使用して描画しています!比較的古い端末だと、重かったりそもそも見れなかったりするので、そこはご留意ください!
まずはとてもシンプルに立方体を1つです。
回転やズームもできます! (PCならドラッグで回転、ホイールでズームイン/アウト。モバイルならタッチで回転、ピンチ操作でズームイン/アウトできます)
フルスクリーン表示に対応している環境では、キャンバスの右上にボタンがあると思うので、スペックに自信のある方はぜひ大画面で楽しんでください。(描画するピクセル数が上がるので結構重いです!ご注意を!)
ブーリアン演算
次にとても強力なレイマーチングのメリットを一つ紹介してみます。
見出しで「ブーリアン演算」といったのでCADやモデリングなどを触ったことのある人はピンときたかもしれませんが、要は2つのオブジェクトの「和・差・積」です。
- 和(Union): 二つのオブジェクトの合体ですね。
- 差(Subtract): 片方のオブジェクトからもう一方のオブジェクトの重なっている部分をくり抜く感じです。
- 積(Intersect): 二つのオブジェクトの重なっている部分だけを残す感じです。
これを形状に対して行うのって難しそう、と思ったかもしれませんが、レイマーチングでシーンを描画する場合驚くほど簡単になります。
ここで、先程言った「オブジェクトの形状を決める関数」を距離関数(以下ではSDFと呼称)と呼びます。
ここで立方体と球があるとして、さっきの3つの操作をしてみます。
立方体のSDF(最初のデモで使った「立方体を表す関数」)をf、球のSDF(同じように「球を表す関数」)をgとします。 そして先程の3操作を行った形状もまたSDFで表現されます。
- 和(Union): U(x) = min(f(x), g(x))
- 差(Subtract): S(x) = max(f(x), -g(x))
- 積(Intersect): I(x) = max(f(x), g(x))
これだけです。二つのオブジェクトを表す関数で距離を計算して、それの小さいほうや大きい方をとる(差は符号を反転させる必要がありますが)。 これだけで「それぞれ3つの操作を行った形状を表す関数」になっちゃうんです。
そしたらその関数を使って描画すると、合体してたり、くり抜かれたり、重なってる部分だけになったり、と面白いです。
そんなわけで次のデモでは、立方体と球体を同じ配置でそれぞれ3つの演算をしてみています。
左から順に「和」「差」「積」です。
フラクタル図形を描画できる。
これがもう一つの強力なレイマーチングのメリットというか強みです。
フラクタル図形、というのは「自己相似図形」、つまり図形の一部分が全体と同じような構造になっている。というようなものです。
今回はその中から「メンガーのスポンジ」と「マンデルバルブ」というものを描画してみました。
もしかしたら集合体恐怖症の方は苦手かもしれないので注意してくださいね。
メンガーのスポンジはメッシュを細かくしていったら描画できそうだと思うかもしれません。まぁもちろんできなくはないのですが、再帰的に穴をあけていく関係上、指数的にメッシュの数が増加してしまうのですぐに限界が来てしまいます。
レイマーチングはこういうものを効率的に描画することができます。
(今回のデモでは8階層まで穴をあけています。メッシュデータに起こそうとしたらとんでもない頂点数になるはずです)
終わりに
いかがでしたか? 多分初めて知った人なら、なかなか新しい感覚を得られたんじゃないかな、と思います!
使う場面が来るかは分かりませんが、もしこれを見た人の創作などに新しいインスピレーションを提供できればうれしく思います。
あと最後に、ソースコードはscriptタグで埋め込んであるので、興味のある方はDevtoolsとかで取り出してもらって自由に研究したりこねこねしたりしてみてくださいね~(Gistとかに上げてる感じじゃなくてすみません😅)
それでは!