この記事は、Unreal Engine (UE) Advent Calendar 2021 カレンダー 3 の22日目の記事です。
はじめに
今回作り方を解説するものは、マリオのワンワンです!
といっても、内容のメインはワンワンにつながっている鎖の動きをNiagaraでどう作っているかになります。
鎖の動きは、NiagaraのSimulation Stageという機能を使って、Position Based Dynamicsという手法でリアルタイムにシミュレーションしています。
Simulation Stageについては過去に解説を書いているので参照してみてください。
【UE4.26】Niagara Advanced 解説基礎編~Simulation Stage~
また、実は鎖のシミュレーションについても、Epicが配布しているサンプルを少し改変しているだけでして、そちらについても過去に解説を書いています。
本記事ではサンプルからどの辺りを改変したかに焦点を当てて説明していくので、先に以下の記事を読んでから、本記事を読むのが良いと思います。
【UE4.27】Niagara Advanced 解説応用編~PBDによる鎖シミュレーション~
前もって読む記事が多くて恐縮ですが、本記事の内容は、以下の三本立てでいきたいと思います!
① 鎖シミュレーションのサンプルを改変してコリジョンをとる
② ワンワンの鎖シミュレーションのセットアップ
③ (おまけ)ワンワンの動きを制御する
こちらのデータは、以下で販売しているので、実際に動くデータを見たい場合はぜひ覗いてみてください!
Chain Simulation in Niagara – Create Mario’s Chain Chomp
サンプルを改変してコリジョンをとる
まずは、サンプルのChain_SimulationStagesというNiagara Systemをコピーして、必要のないモジュールをdisableにしていきます。

Curl Noise ForceとWind Forceは、外力として鎖を動かしますが、今回はシンプルな例でコリジョンをとりたいので、disableにします。
Find Kinetic and Potential Energy と Colorize Chain Based on Kinetic Energyは、ただ力が強くかかっている所を視覚化するだけのもなので、こちらもdisableにします。
次に、鎖の始端がゆらゆら動くように設定されているので、それを動かないようにします。
Update Chain Segments After Forces の Chain Origin Offset を(0, 0, 0)に設定します。

ここまで設定すると、鎖は重力で垂れ下がるだけで動かなくなります。

次に、ブループリントを使って鎖の始端をキーボードで動かせるようにします。
まず、StartPointというVectorのUser Parameterを作り、Configure Chain の Chain Origin に設定します。

次に、適当なActorのブループリントを作成し、NiagaraとSphereを追加します。
Niagaraには、今まで編集してきたNiagara Systemを設定します。
そして、適当にノードを組んで、キーボードから左右にSphereを動かせるようにします。


最後に、Construction Script と Event Graph の Tickに、先ほど作成したNiagaraのStartPointパラメータにSphereのWorld Locationを送るようにします。

これで、キーボードで鎖の始端を動かすことができるようになりました。鎖の始端に合わせて、鎖がそれなりに自然に動いていることがわかります。

さて、いよいよコリジョンを設定します。
といってもやることは、Niagara標準のCollisionモジュールを追加するだけです。
普通に追加すると最初エラーが出ますが、fix nowボタンを押して、Solve Forces and Velocityより上に持ってくれば大丈夫です。

まず、留意しておかなけらばならないのは、Simulation Stageを使っているため、GPU Simにしなければなりません。
GPU Simの場合、Collisionの取り方のタイプは以下の3つです。
・GPU Depth Buffer:カメラからの深度値を元にコリジョン判定
・GPU Distance Fields:Global DistanceFieldを元にコリジョン判定
・Analytical Planes:設定した無限平面(2つまで)でコリジョン判定

まだそこまで深堀ってしらべたわけではないですが、結論から言うと、現状GPU Depth BufferとGPU Distance Fieldsは実際のゲームなどで使える感じではなさそうです。
簡単にどんな感じか説明します。
GPU Depth Bufferは、とりあえず初期設定だと鎖が吹っ飛びます。
おそらくMesh Particle自体のコリジョンがとられてしまっているのではないかと思っていますが、詳しくはわからないので、ちゃんと調べる必要があります。
とりあえず、Kill Occluded Particlesのチェックを外すと、鎖が吹っ飛ぶ現象は回避できますが、動かしたときの鎖の挙動が変になります。

いずれにせよ、要調査という感じです。
GPU Distance Fields の方は、まだましではありますが、コリジョンが正確ではないのと、予期せぬ所で吹っ飛ぶ問題があります。


コリジョンが正確ではないのは、Global DistanceField 自体の精度があまり高くないので、この精度を上げない限りは、正確なコリジョンはとれないのではないかと思います。

なので、こちらもまだ実用的ではないのかなと思います。
Analytical Planesは、平面を前もって決め打ちするので、かなり正確なコリジョンがとれますし、突然吹っ飛ぶということもあまり起きません。床とだけコリジョンすれば良いというような場合であれば、実用的にも使えるのではないかと思います。
次に紹介するワンワンの例でも、Analytical Planesのタイプで、床面とのみコリジョン判定を行っています。
いずれにせよ、この辺りはまだまだしっかりと調べたわけではないので、引き続き追っていければと思います。
ワンワンの鎖シミュレーションのセットアップ
いよいよワンワンのセットアップに入ります!
といってもあんまり大したことはしていません。今までと異なり始端と終端の2つを固定するだけです。
全体は以下のような感じで、前章でdisableにした部分に加えて、Constrain Chain Max Lengthもdisableにしています。これは、今回両端を固定してプログラムで動かすので、タイミングによっては鎖の長さが事前に設定した鎖の最長よりも長くなることがありえるためです。

ここからプラスして、サンプルで、始端がシミュレーションの影響を受けないようにするために、色々処理している部分があるので、それを終端にも適用していきます。
まずは、User ParameterにChainEndとChainStartを作ります。これにブループリントで、固定したい位置を後々設定していきます。(Scaleについては後述します)

前章と同様にConfigure ChainのChain OriginにChainStartを設定します。
次に、Initialize Chain ConstraintのScratch Padの中身を少し修正します。
元々ChainStartPointだけInverseMassの値が0だったのを、ChainEndPointも0になるようにします。
ちなみに、InverseMassは後にVelocityを無効化するのに使われています。

続いて、Update Chain Segments After Forcesの中身も修正します。
ここでは、固定するParticleのポジションを任意の位置に設定しています。元々ChainStartPointのみOffsetがChain Originに加えられて固定されていたのを、Offsetをなくし、ChainEndPointを追加して、先ほど作ったChainEndパラメータに位置を固定します。


最後に、Solve Chain Constraintの中身を少し修正して、終端の固定の設定はOKです。
Calculate Link ConstraintのCurrentPinnedに与えるフラグに、ChainEndPointを加えるだけです。これで、始端終端共に、シミュレーション時の計算から除外されます。

さて、コリジョンの設定ですが、前述したように、Analytical Planesにし、実際の床の位置に合うように、Analytical Collision Plane Position 1をWorld Spaceの(0, 0, 20)に設定しています。
その他Radius, Bounce, Frictionの値は適当なので、そんなに参考にしないでも大丈夫です。

あとは、最初に追加したScaleパラメータの説明をします。
鎖の大きさを自由に変えれるようにしたかったので、Scaleパラメータを追加しましたが、これを適用しなければならない所は2つあるので注意が必要です。
一つは、普通にMesh Particleの大きさに乗算します。

もう一つ、Configure ChainのChain Segment Lengthの所にも乗算して、正しい長さを保持しておかないと、シミュレーションの計算でおかしくなります。

これで、Niagaraの方の設定は終了です。
続いて、ブループリントの方で、ChainStartとChainEndに入れる値の設定をしていきます。
固定する位置は、以下の画像のようになります。このうち、ChainEndはずっと固定ですが、ChainStartはワンワンが動くのに合わせて、毎フレーム固定位置を更新します。

まず、Construction Scriptの中で、ChainEndにPile Attachのワールド座標、ChainStartにWan Wan Attachmentのワールド座標を入れます。

Pile Attachは、ワールド上の適当な位置に配置し、該当のブループリントの変数にアサインします。
Wan Wan Attachmentは、ブループリント内のワンワンのStatic Meshの子オブジェクトとしてSphereを非表示で配置したものです。

後は、Event Tickでも、Chain StartにWan Wan Attachmentのワールド座標を送ってやればOKです。

これで、ワンワンの鎖のシミュレーションのセットアップは終わりです!
(おまけ)ワンワンの動きを制御する
一応、本記事の趣旨である、PBD法による鎖シミュレーション部分の解説は終わったのですが、最後おまけとしてワンワンの動きをブループリントで制御しているあたりもざっくり解説しておきます。
まずは、ワンワンの状態を定義します。全部で以下の5つで、Enumeratorsにしておきます。
・Follow:赤色領域にいるプレイヤーを追いかけてくる
・Berserk:青色領域にプレイヤーがいるまでは追ってくるが、鎖につながっているのでそれ以上近づけない状態
・Lose:プレイヤーが青色領域から出ると、興味を失って初期位置に方向転換する
・Back:初期位置に戻る
・Stay:初期位置に待機

これらの状態の条件を、プレイヤーやワンワンの位置、鎖の長さなどで、毎フレーム評価し、状態を遷移させるのをState Managerという関数を作って処理を書きます。(中身の詳細は割愛します)


そして、各状態に対応する行動をそれぞれ関数化して、その時の状態に応じて、その関数を実行します。
それぞれの関数の詳しい解説は割愛しますが、いくつか処理上のTipsを記述しておきます。
自然な追いかける動きをつける
ワンワンが追いかけてくる動きを作るとき、距離が遠い時ほど速く近づいてきて、近くになるにつれ減速させるようにさせます。
それを最も簡単にやるには、Lerpを使うのが良いです。ワンワンの位置とターゲットの位置でLerpさせ、例えば0.2などをalphaに与えて毎フレーム処理させれば、毎フレーム両者の距離の0.2倍だけ近づくので、距離が近くなるほど、実際の移動距離は短くなります。これで、お手軽に自然な近づいてくる動きをつけれます。
跳ねる動きをつける
ワンワンが跳ねながら動く挙動を作りたくてやり方を調べたら、一番簡単なやり方は、重力とコリジョンをオンにした上で、毎フレーム上方向にAddWorldOffsetで動かしてやるといい感じにできることがわかりました。

慣性の動きを無効化する
ワンワンはコリジョンをオンにして物理挙動をさせているので、鎖の最大距離の所で止めようと思っても、最初ちゃんと止まってくれなかったりしました。
とりあえず物理的な速度を無くしてしまえば良いということで、Set Physics Linear Velocityに0を入れてあげれば、物理演算で計算されていたVelocityを上書きできるので、ぴたっと止めることができます。
回転も無効にしたい場合は、Set Physics Angular Velocity in Radiansに0を入れればOKです。

おわりに
機能別サンプルの鎖シミュレーションのNiagaraを見たときに、「コリジョンとってインタラクティブにしてー!」と思い、何か良い題材ないかと考えてワンワンが何故か頭に湧いてきました。
ただ、コリジョンは思ったよりもちゃんととれず、そう簡単に改善もしなさそうなので、実用的なインタラクティブのPBDシミュレーションは、もう少しやり方を考える必要がありそうです。
それでも、このような割と自然な鎖の挙動を簡単に作れるのは、とても便利だと思いますし、PBD法は鎖以外にも様々な使い道があるので、これからもこの可能性を追っていきたいと思います!
ちなみにワンワンはHoudiniでモデリングしました!
以外とかわいくできた(笑)
