オブジェクトの重なりを考慮した描画
今回のサンプルの実行結果
レイマーチングならではの排他制御
前回は、複数の異なる形状のオブジェクトを、同時にスクリーン上に描き出す方法について解説しました。
今まで登場してこなかったトーラスや平面のレンダリングについても触れましたが、同時に異なる形状のオブジェクトをレンダリングする仕組み自体は、それほど難しくはなかったのではないでしょうか。
さて、今回は前回の内容をさらに煮詰めて、オブジェクト同士が重なる場合について考えてみます。通常の WebGL の実装では少々やっかいなことでも、レイマーチングであれば容易に実装できる場合があります。今回はその典型かもしれません。
今回の内容を習得すれば、特定のオブジェクトが重なっている部分だけを抽出してレンダリングしたり、逆に、重なっている部分をくり抜いたりといったことができるようになります。概念的にもそれほど難しくないと思いますので、がんばって取り組んでみてください。
重なりを考慮したレンダリング
さて、まずは前回のおさらいから。
同時に異なる形状のオブジェクトをレンダリングするためには、それぞれの distance function の結果を比較して、よりレイの先端から近い結果を用いればよかったのでしたね。
distance function の比較
float distFunc(vec3 p){
float d1 = distFuncTorus(p);
float d2 = distFuncFloor(p);
return min(d1, d2);
}
上記の場合、トーラスの distance function と平面(床)の distance function の結果を一度変数に確保してから、ビルトイン関数の min
を用いてより値の小さなものを返すようにしています。
このような実装では、オブジェクトが重なる場合でも、あるいはそうでない場合でも、呼び出し元に返される値が一意に定まることで結果的に複数のオブジェクトが正しくレンダリングされます。通常の WebGL のプログラムで言うなら深度テストと同じような効果が生まれます。
このように distance function の結果をうまく比較して返す方法を応用すれば、レンダリング結果を特殊な方法で調整することができるようになります。
どのようなことができるのかは、以下の画像を見てみれば一目瞭然だと思います。
様々なレンダリング結果
まず、一番目のレンダリング結果は、前回と同様に min
関数を用いた場合のものです。
トーラスと、薄く成形したボックスモデルとが重なり合って描かれていますね。隠れるべきところはきちんと隠れているのが見て取れます。
二番目は一番目と比較するとレンダリングされている領域が非常に少ないですが、よく見てみると、それぞれのオブジェクトが重なり合っている部分のみがレンダリングされているのがわかると思います。
三番目と四番目は、逆に重なり合う部分だけがすっぽりと抜き取られたような状態になっていますね。
これらのレンダリング結果を見ると、いわゆる論理演算のような効果が表れていることがわかります。複雑な形状を描くことが難しいレイマーチングですが、このような論理演算方式を用いれば、思いもよらない複雑な形状を作り出すこともできるようになります。次項よりコードを踏まえて見ていきましょう。
重なり合う部分だけをレンダリング
それではまずはオブジェクト同士が重なり合っている部分だけを抽出する方法です。
とはいえ、実装自体は非常に簡単です。コードへの変更箇所もそれほど多くありません。
重なり合う部分だけを描く
float distFunc(vec3 p){
float d1 = distFuncTorus(p);
float d2 = distFuncBox(p);
return max(d1, d2);
}
前回と比較すると min
関数を使っていたところが max
に変わっていますね。
どうして最小値を取るか最大値を取るかを変更しただけで、これほどまでに描画結果が変化するのか、想像がつくでしょうか。
これは落ち着いて考えてみれば簡単でしょう。
ビルトイン関数の max
を用いるケースでは、そもそもレイがマーチングループのなかで複数のオブジェクトに衝突している必要があります。そうでない場合は、少なくともいずれかの distance function が、衝突しているとはみなせない大きい値を返してきているはずなので、結果的に何も描かれません。
いずれのオブジェクトにも衝突するレイであっても、 max
関数によって返される値が大きな数値のほうに切り替えられてしまいます。結果、本来なら手前でレイとオブジェクトが衝突していた場合でも、マーチングループが続行されて奥にあるオブジェクトに衝突してしまうのですね。言葉で説明するのが難しいなあ……
論理演算風に言うと、AND が成立する場合のようになるわけですね。
重なる部分を排他する
先の画像の三番目と四番目のように、重なり合う部分をレンダリングしないようにするにはどうしたらいいでしょうか。
実は、これにも先ほどと同様に max
を使います。
重なり合う場所は描かない
float distFunc(vec3 p){
float d1 = distFuncTorus(p);
float d2 = distFuncBox(p);
return max(-d1, d2); // d1が重なっていないd2部分を描く
return max(d1, -d2); // d2が重なっていないd1部分を描く
}
これは書いて説明するより、コードを見れば一発ですね。
いずれかの distance function の結果を、正負反転させればいいわけです。どちらの結果を反転させたのかによって、描き出される部分が変わってきます。
原理は先ほどまでの理屈がわかっていれば理解できるでしょう。
こうして見てみると、やっていることは distance function の結果に対して正負を反転してみたり、あるいは戻り値として返す結果の判断基準を変えているだけなのがわかりますね。このようにお手軽な変更で、様々なレンダリング結果が容易に描き出せる点はレイマーチングの優れた点だと言えるのではないでしょうか。
重なり合っている部分だけを抽出する方法を工夫すれば、以下のようなレンダリング結果を得ることも可能です。
重なり合う部分だけをレンダリングした例
なにやらサッカーボールのような不思議な物体が描かれていますが……これをどうやって実現したかわかるでしょうか。
仕組みは簡単で、球体と箱、このふたつの形状用の distance function を用います。箱のほうは、repetition で繰り返し複製されるようにしておき、球体のほうは中心にひとつだけレンダリングされるようにするのです。
レンダリング結果の画像をよく見ると、ひとつの球体と、複製されたたくさんのボックスとで、論理演算で AND を取ったときのように重なり合う部分だけがレンダリングされているのがわかると思います。
まとめ
いかがでしたか。レイマーチングでは、はじめのうちは特にそうですが、シンプルな形状のモデルしかレンダリングできずどうも見た目的に面白味がない状態になってしまいがちです。
しかし、今回のようにちょっとした工夫をするだけで、いとも簡単に複雑なレンダリング結果を得ることも可能なのです。これは通常の WebGL の実装にはない独特な面白さです。
概念をつかむことさえできれば、様々な応用が利くテクニックだと思います。ぜひ、がんばって挑戦してみてください。
実際に動作するサンプルはいつものように以下のリンクから。