はじめに
※この記事はUnityのRaytrace機能対象のお話です。
凸でない3Dオブジェクトの衝突判定を厳密に行うには、メッシュ対メッシュの膨大な衝突計算をしてやらないといけません。
Unityのコライダー機能を使うと、任意のメッシュ同士の衝突判定はConvexをOnにしてやらないといけませんが、
元の形状から大きく乖離してしまうのと、いくつかの性能上の制限があり、あまり使い勝手のいいものではありません。
本手法はそれらを代替できるものではありませんが、シンプルにオブジェクト同士が重なっているか=衝突しているかどうかの判定だけをする仕組みとして、高速かつ汎用なものが作れないかと思って試してみたものになります。
本稿では、RTX対応レンダリングパイプラインを使ってハードウェアアクセラレーションで、GPU計算パワーを活かした衝突判定ができないかというアイディアを実装してみました。
プロジェクト全体はこちらに置きました。
https://github.com/oho-sugu/RTX_Collision
こんな感じで重なっているオブジェクトを
こんな感じで重なりを検知できます。
(青いところが重なっているところ)
なお、何かの記事で読んだ立体音響の反射計算をレイトレーシングで行うというのをヒントに、レイトレに写実画像の出力以外をやらせてみたらどうかというのが、レイトレ使えばいいんじゃね?というアイディアのきっかけになっています。
それから、ステンシルバッファとカリングの裏表を使ってマルチパスのShaderで重なり検知できないかなと考え始めた結果として、レイトレがよいのではというところまで来てしまいました。
結局、ポリゴン一枚ごとの描画順を精密にコントロールできないこととFrameBufferのピクセルカラーをとることができない(拡張はあるけど一般的でない?)ところで、視点からの距離によってすべてのポリゴンをチェックできるレイトレでしか実現できないのではないかというところになりました。
あまり先行研究などを探せていないので、同じような方向性のアプローチでもっとうまくできるよとかがあったら、教えていただけると嬉しいです。また、レイトレを使わずに普通のシェーダーでできるよ、みたいなのもあれば知りたいと思います。
※なお、本稿で扱っているレイトレは、厳密にいうとパストレーシングという手法になりますが、レイトレーシングとして記載します。警察の方は適宜スルーいただけるとありがたいと思います。
Unityのレイトレーシング
一般的なUnityのレイトレーシングについては、公式のドキュメントや各種ブログをご参照ください。本稿ではこれらの詳細は記載しません。
ベースとしたプロジェクト
こちらのスレッドで紹介されていた、INedelcu氏のUnityプロジェクトをベースに必要なところを改造していきました。
UnityのRaytraceをやってみた系の記事などは、結局RaytraceShaderをどう書くかまで踏み込んでいるものがほとんどないなか、
シンプルで理解しやすく、とてもありがたいサンプルでした。
考え方
レイトレースは、仮想的なレイ=光線をカメラから飛ばして、ぶつかったオブジェクトの色をとったり反射や屈折などを光の物理に従って計算して、最終的な絵を出力するアルゴリズムです。
ここでは、あまり詳細にまで触れませんが、非常に深い分野なので興味がある人は こちらの沼など にどうぞ。
さて、今回の考え方は、ポリゴンの裏面に当たったときと表面に当たったときに処理を変えることで、
裏→表の場合と、裏→裏→表→表の順に当たった場合を検知して、レンダリング結果に反映させるということをやっています。
つまり、オブジェクトが重なっていない場合というのは、必ず裏→表になると仮定しています。ただし、モデルの作り方によってはこの過程が成り立たないので注意です。今回想定しているのは、ゲーム用の様々なトリック(特にローポリで作る際に使われるような)を駆使したモデルではなく、CADや3Dプリンタで扱われるような閉じたモデルとなります。
この仮定のもとで考えると、オブジェクトが重なっているときは、少なくとも1回以上同じ方向の面が連続するはずです。
これをどう検知するかという方法を考えることになります。
まず、最初のレイは普通に飛ばします。
レイがポリゴンに衝突したとき、厳密に同じ方向にだけ次のレイを飛ばします。
(反射・屈折・影などはなし)
最終的に接触回数が規定を超えた場合や背景に到達した場合には、カラーとして、float4(1,1,0,0)
を返します。
ここから再帰的に来た道を戻っていきますが、その際に、
裏面に当たったときは
- R そのまま
- G もとの値を2倍する
- B Gの計算後の値を加算する
- A そのまま
表面に当たったときは
- R そのまま
- G 1を代入
- B 2を減算する
- A そのまま
という処理をしていきます。
Gを重複検知用のカウンタにしている感じですね。
こうすると、裏→表となる場合=重なっていないとみなせる場合は、Bの結果が0になります。
一方で、裏→裏→表→表となる場合=重なっている場合は、Bの結果が1より大きくなります。
と書いて、自分では納得していたのですが、重なっている状態の場合分けで見落としがあるようで、2つ以上のオブジェクトが重なっている場合や、複雑な形状の場合は、一部が変な結果になるのを確認しています。
シェーダー実装
近年のGPUによるレイトレースのハードウェアアクセラレーションでは、専用のシェーダーを実装してGPUにレイトレースの処理を委ねます。
大まかに言うと、レイとモデルの衝突検知はGPUにおまかせで、衝突したらモデルのシェーダーの衝突したときに呼ぶメソッドが呼ばれるというしくみになっています。
なので、モデル表面にレイが到達した時の処理だけにフォーカスすることで、レイトレ実装ができてしまいます。
とんでもなくイージーですね。
大学時代に友人のレイトレの課題を手伝って、どうしても反射が逆になって困ってたことを思い出して時代の進歩を実感しています。
さらに、ハードウェアアクセラレーションでリアルタイムに動くということで、いい時代になったものです。
さて、本稿のロジックの肝部分だけを抜粋すると、以下のようなコードになっています。
TraceRay(g_SceneAccelStruct, 0, 0xFF, 0, 1, 0, ray, refrRayPayload);
if (isFrontFace) {
payload.color = float4(
refrRayPayload.color.r,
1,
refrRayPayload.color.b - refrRayPayload.color.r*2,
refrRayPayload.color.a
);
}
else {
payload.color = float4(
refrRayPayload.color.r,
refrRayPayload.color.g*2,
refrRayPayload.color.b + refrRayPayload.color.g * 2,
refrRayPayload.color.a
);
}
ここで、上に書いたロジックを実装しています。
簡単ですね。なお変数名が元のをそのまま使ってる適当コードなので、もろもろ察していただければと。
最後にレイを最初に生成するRaygenerationShaderの方で、色を調整してFrameBufferに返しています。
結果
こんな感じです。オブジェクトが重なっている=衝突しているところが青く表示されます。
3つ以上のオブジェクトの重なりがうまくいってないっぽいのと、Blender Monkeyさんの端とか複雑な形状部分がおかしくなっているなというところが、気になる点です。
厳密なジオメトリの計算ではないので、後者については精度の問題などである程度起こり得るのかなと思いつつ、もう少し原因をよく調べてみたいところです。なんとなく、法線が視線方向と直角になっているときに問題が起きそうな気がしています。
前者については、重複判定ロジックに抜けがありそうなので、要検証かと思います。
最後に青いピクセルをカウントして、シーン全体での衝突の有無をBoolで出力できないかと考えていたのですが、
この精度を考えると、そこまで簡単ではなさそうで、ある程度閾値を決めた画像処理などが必要そうな雰囲気なので、
実用にするにはもっと考えることが多そうです。