Unityの物理演算(physX)に関する諸々

正しいシミュレーションを行う上でこういうことに気を付けると良いというTipsていなやつです。

※以下は個人的な経験に基づいた古い時代の知見を含み、厳密に検証されたものでもありません。新しいバージョンのPhysXや最近のUnityでは既に解決済みであったり、仕様が変わっている物事が含まれる可能性があります。

Rigidbodyは他のRigidbodyとトランスフォーム上の親子関係を持てません(持っても意味がない)

Rigidbody同士の振る舞いにおいてインスペクターの階層(Hierarchy)上の親子関係は無視されます。

代わりに、Rigidbodyを固定したり接続関係を作る場合、ジョイント(Joint)系のコンポーネントに基づいてお互いを拘束する必要があります。普通のオブジェクトだとヒエラルキー上の親子関係が固定関係とイコールですが、リジッドボディ同士はジョイントを使わないと一切物理的な関係が発生しません

また、個人的な経験からの備考ですが、ヒエラルキー上の親子関係が無視されるとしても、リジッドボディにはヒエラルキー上の親がいないほうが無難っぽいです。UnityのPhysXのバージョンが3.xとかの古いころの話なので今は変わっているかもしれませんが、過去に複数のRigidbodyパーツを(階層ビュー上で)まとめるためだけに「ダミーのゲームオブジェクト」をフォルダー代わりに使っていたことがあります。このゲームオブジェクトのTransformはサイズは常に(1,1,1)、位置は常に(0,0,0)、回転も常に(0,0,0)という、これなら子に何も影響しないだろうと思って作ったものなのですが、Rigidbodyをこのゲームオブジェクトの子にした際、リジッドボディが親の座標(0,0,0)から離れていくにつれてオブジェクトにジッターが生じたり、親があるときとないときで細かな挙動が変わる場面がありました。(※原点から遠く離れるとジッターが生じるのはいわゆる浮動小数点精度問題(下の方の項参照)というやつでおなじみなのですが、それとは別のスケールでのジッターでした)

RigidbodyのScaleは常に(1,1,1)であるべき

(1,1,1)以外の値になってると計算量が増えるとかなんとか・・・

個人的な経験で言えば、(1,1,1)以外だと謎な場面で暴れだしたり力が変な方向に働いたりとか変な挙動になった気がします(※厳密には別の原因だった可能性はある)


物理演算パーツについては

Rigidbodyに関する処理、Rigidbodyを追う処理はFixedUpdateで行います

基本中の基本ですが、物理演算に関する計算は基本的にUpdateでなくFixedUpdate内で行います。

なお、Update()関数の方は可変タイミングのため、移動量などにTime.deltaTimeを乗算するのがセオリーになっていますが、FixedUpdate内では通常その必要はありません。AddForce()メソッドを使って力を加える場合、AddForce内でfixedDeltaTimeが乗算されるのでスクリプト側で乗算する必要はないです。

  • ちなみにTime.deltaTimeはUpdate()から呼び出すとTime.deltaTimeが呼ばれ、FixedUpdate()から呼び出すとTime.fixedDeltaTimeが呼び出されるようになっているらしい。
  • このページの一番下あたりで言及している「Simulation Mode = Update」の設定を使うと、UpdateとFixedUpdateをほぼ区別なく扱えるようになるそうです

RigidbodyでAddForceした力は質量中心(Center of Mass)に対して適用されます

らしい

それ以外の特定の位置にAddForceしたい場合は、AddForceAtPositionをつかひます

慣性テンソル(Inertia Tensor)が正しく設定されている必要があります(※通常は自動設定される)

慣性テンソルは物体の回転しやすさを決める値だそうです。これによって、例えば同じだけ力がかかっても、回転が鈍重だったり軽やかだったりするような特性が再現されます。

例えば飛行機のシミュレーションで、胴体に比べて翼が軽い曲芸用飛行機などの場合には飛行機は軽々とロールできますが、翼の一番外側に重い燃料タンクなどがついている機体の場合、同じだけの力でロールしようとしても、慣性のために「回転し始めの重さ」や「ロールをやめたときに回転が止まるまでにかかる時間」などが長くなるはずです。

このようなふるまいの違いを生み出す慣性テンソルの値の値が正しくないと、重いオブジェクトなのに変に軽く見えてしまうとかいった齟齬を引き起こすため、この値が正しく設定されていることが物理オブジェクトとしての振る舞いのリアルさに直結します。


慣性テンソルはRigidbodyを持った1ゲームオブジェクト(及び階層上の子ゲームオブジェクト)が持つ単一または複数のコライダーから自動計算されるようです。

各Rigidbodyがジョイントで接続されている場合、各物体の慣性テンソルや質量などに起因するふるまいが接続関係にあるオブジェクト全体で最終的に統合されるため、各物体のコライダー形状と質量が正しく設定されていれば慣性テンソルについては気にする必要はありません(自前で計算して設定することもできます)。

質量中心(Center Of Mass)が正しく設定されている必要があります(※通常は自動設定される

質量中心は物体の重心のことです。当たり前ですが車のオブジェクトを作っても、前に重心がある場合と後ろに重心がある場合、挙動が全然違ってきます。

単一のコライダーの場合、通常はコライダーの中心が質量中心になると思いますが、例えば飛行機の胴体で前の方に重いエンジンが載っているから前の方が重いとか、そういうのを再現するためには正しく重心が設定されていなければいけません。

ただ、こちらの値も前の「慣性テンソル」同様、各Rigidbody物体の重さ・重心位置に基づいて、接続関係にあるオブジェクト全体で自動計算されます。複数の物理パーツから成るオブジェクトでは、各物理パーツの取り付け位置と重さによって全体としての重心が決まるため、RigidbodyのCenter of Massの値を直接変更する必要は通常はないと思います。

例えば前述の「飛行機の胴体の前の方に重いエンジンが載っている」というような場合でも、そのエンジンの重さがはっきりとわかっている場合は、適切に重さを設定した不可視のパーツをあるべき位置において胴体パーツにJointで接続固定するなどした方が、自動計算の恩恵を受けるうえで適切なやり方になると思います。

広い範囲を動き回ることは問題を引き起こします(浮動小数点精度問題)

こちらは厳密には物理演算に限った話では無いのですが、浮動小数点精度(floating point precision)に関する制限があります。

intやbyteなどの整数のデータ形式は、整数を表すためにほぼ全てのビットが使えます。一方で浮動小数点の場合、少数も扱うためにもう少し複雑なやり方でビットを扱っているらしいです。その関係で、実際に整数としては扱える値の範囲はかなり小さく、整数部の桁が大きくなってくると小数部の精度が圧迫されて細かな位置情報が大雑把になり、原点(0,0,0)から離れれば離れるほど、結果としてオブジェクトカメラが振動したりするといった現象が起きます。

よく、ゲームで地面のない場所をどこまでも落下したり、原点から遠く離れた場所まで行くと、次第に主人公モデルがカクカク振動し始めたりモデルがグチャグチャに暴れ始めたりする現象がそれです。

(※広大な世界を扱うオープンワールドゲームでは、逆に世界の方を動かす「floating origin」というテクニックなどでこの問題を回避する)

これらの問題はレンダリング表示上のジッターとして現れることが多いですが、物理演算も当然浮動小数点を使って行われています。そのため、表示だけでなく物理演算上の計算においても、あまり原点から離れてはいけません。

だいたい原点から6000m(正確には6000u)あたりの範囲にとどめておくのが良いようです。

何であれ、小さすぎる/大きすぎる値を設定してはいけません

軽すぎたり重すぎたり、小さすぎたり大きすぎたりするオブジェクトを使うなどすると、物理演算は不安定になり、結果も正しくなくなります。

くらいにとどめておくのが良いようです。

Rigidbodyと補間にまつわる罠、あとカメラのジッターについて

Rigidbodyをカメラで追うなどすると、物理演算のタイミングとカメラの更新タイミングが異なるため、カメラが振動したりするジッターが起こります。

これに対する対策として、通常はRigidbodyの「補間モード」を設定することで対処します。この補間モードはオブジェクトのTransformを補間しますが、Rigidbodyの姿勢には影響しません。


以下、参考にしたフォーラムからの引用:

"補間は物理的なものではなく、レンダリングのみに使用されます。以前と現在のボディ ポーズを維持し、位置を補間して回転を滑らかにします。(中略)このため、補間時には Transform.position != Rigidbody.position を常に意識する必要があります。Rigidbody.position はシミュレーションの実行時に常に更新されますが、Transform はそれに一致せず、Interpolate / Extrapolateがオンの場合はフレームごとに更新されます。この場合、Transform は履歴的なものとなり、現在の Rigidbody.position の後ろにあり、現在の位置に向かって移動します。"

https://forum.unity.com/threads/how-to-properly-fix-camera-and-rigidbody-stutter-jitter-lag.1251702/


このことが問題を引き起こします。

例えば、補間モードがONになったRigidbodyからレイキャストを行う際、Transform.positionを引数としてレイキャストを行ってしまうと、FixedUpdateレートでの最新の位置ではなく、最後に補間された古い位置からレイをキャストしてしまうため、正しくない結果が返ってきてしまいます。

もしRigidbodyがこの正しくないレイキャストに基づいて位置を更新している場合、補間モードがONになっていても、オブジェクト自体が正しくないレイキャストに起因したジッターを起こしたりするので、結果的にオブジェクトはカメラ内でも振動して「補間モードがONなのになぜ…!?」的なことになります。

物理演算はFixedUpdateレートで行われ、補間は可変レートで後追いで起こる。Rigidbodyの最新の姿勢に、オブジェクトのTransformを後追いで合わせていくイメージですね。「補間がONの時、Transformの位置はRigidbodyの位置と基本的に一致しない」というこの原則を理解していないと、凝ったことをしようとするときに迷宮に入り込んでしまうことになります。

物理演算による最新の状態を得たい時は、Transform.position/rotationの代わりにRigidbody.position/rotationを使う必要があります。


この基本的な仕様を理解しないままに「補間モードを設定してもカメラのジッターが直らない迷宮」に彷徨いこんだ挙句、Cinemachineのフォーラムで「CinemachineカメラをFixedUpdateレートで更新できるようになりませんか?」的なアホな質問をしてしまい、そんな的を外れた質問にも開発者様が対応して下さったためにCinemachineにFixedUpdateアップデートモードが実装されてしまうという事態の混迷を招く結果となりました。ここで懺悔しておきますが、ハイ、犯人は私です。)

Rigidbodyをワープ(テレポート)させる

前の項で述べた理由から、Rigidbodyをテレポートさせたい場合にはtransform.positionではなくrigidbody.positionを書き換えます(回転についても同様)。



なお、rigidBody.positionやrotationを直接変更しても、Rigidbody.velocityやangulerVelocityの値は残っていますので注意してください。そのままだとワープした先でもワープする前の加速度が残っているため、ワープ後に静止するためにはそれらをリセットする必要があります。


// ワープrigidbody.position = newPosition;rigidbody.rotation = newRotation;
// 加速度をリセットrigidbody.velocity = Vector3.zero;rigidbody.angulerVelocity = Vector3.zero;

逆にPortalみたいにワープした先でも加速度を残したいゲームとか、floating originなどで位置だけを一瞬ですり替えるような用途では、加速度リセットはせずにrigidbody.position/rotationだけを書き換えればいいことになります。


中間位置のスムーズな変遷なしでRigidbodyをテレポートさせたい場合はMovePositionを使う代わりにrigidbody.positionを直接書き換えるようにとマニュアルでもゆってました

↑の2つをいずれも解決する新しいUnity設定

前の2つの項で述べた「UpdateとFixedUpdateのタイミングの不一致」からくる様々な問題を、一気に解決できるのが2022.2から搭載された"Update"Simulationモードだそうです。

これは今までFixedUpdateタイミングで行われていた物理演算を、Updateのタイミングで行うようにするモードだそうで、それなら何も問題はなくなりますね!

ただ動画では「これは物理演算の更新タイミングが可変になることを意味するので、Deterministic(決定論的)性質が損なわれ、挙動に再現性がなくなる。物理演算を演出として使うゲームなら良いが、物理演算が重要な役割を果たしているゲームでは避けた方が良い」との注意もあり、ちゃんとしたシミュレーション前提のゲームでは依然としてSimulationモード=FixedUpdateの状態で運用する方が良いかもしれません。

参考サイト

https://digitalopus.ca/site/using-rigid-bodies-in-unity-everything-that-is-not-in-the-manual/

ここで記述した内容以外にも物理演算でしてはいけないことなど、いろいろ参考になる情報がのってます