Unityで物理-FixedJoint2D

この記事のUnityのバージョン
Unity 2019.1.0f2

はじめに

UnityでHavokエンジンを使えるようになるとか
UnityPhysicsも導入されるとかUnity2019で物理エンジンも更新されるようです。

nVidiaのPhysXとFleXは何故採用され続けなかったんだろう?と調べたところ
GameWatchさんの記事で、nVidiaは”高精度だが速度の出ない方向に舵を切った”そうなので
ゲームエンジンだし速度重視だよね、となったのだと思われます。

そんなわけで将来は将来で覚えるとして、
現状の一つずつの機能をキチンと把握しようと思い、Unityで出来る
(または出来ない)物理シミュレーションを把握していこうと思います。

今回は、RigidBody2d(nVidiaのPhisX ver3.4)でのFixedJoint2Dを把握していきます。

FixedJoint2Dの使いみち

結論から書くと、「厳密に接合されているかのように動く必要があるオブジェクト」として
扱うためのジョイントシステムです。

パラメータ

Enable Collision : 接続された 2 つのオブジェクトが互いに衝突できる。

Connected Rigid Body : 接続する親の指定。無い場合はConnected Anchor 設定で定義した空間上の点。

Auto Configure Connected Anchor : これにチェックを入れると勝手にConnected Anchor位置を決められます。
もしその状態でConnected Anchorを変えたい場合は、Anchor側を調整しましょう。

Anchor : Joint 2D の端が接続する位置。

Connected Anchor : Fixed Joint 2D の端が他方のゲームオブジェクトに接続する位置。

Damping Ratio : バネの振動を抑制する値。0から1の値で、値が高いほど振動の抑制が速く行われる。

Frequency : オブジェクトが指定した分離距離へと近づいている間のバネが振動する頻度(単位はサイクル毎秒)
1から1,000,000の値を入れることが可能。

Break Force : Joint 2D を解き、削除するのに必要な力のレベル。Infinity は解除できない。

Break Torque : Joint 2D を解き、削除するのに必要な回転力 (Torque) のレベル。Infinity は解除できない。

パラメータについて

さて、なぜボンドで引っ付けたような「固定的な接合」の状態なのに接合してるのに衝突とは、
バネの値があるのかと不思議に思いました。

バネの値はマニュアルに記述されてある内容で判明しました。

“シミュレーションできるように硬く事前設定されている疑似スプリングを使用”

つまり、SpringJointのバネを硬くして固定させるように実装しているようです。

このバネ系の動きに関しては、Spring Joint 2D はもちろん、 Target Joint 2D も内部的には同じようです。

なので、バネの挙動を把握すれば、
“FixedJoint2D”も”SpringJoint2D”も”TargetJoint2D”も大体把握できるということです。
では、個々の特有のパラメータは置いておいて、同じパラメータが何を意味しているのか見ていきましょう。

Enable Collision

接続された 2 つのオブジェクトが互いに衝突できる、とありますが、
SpringなのでDistanceを短くしておいて、初期位置をそれより長くしておけば
反動でバネ挙動をして親とぶつかることも出来ます。

左がEnableCollision[Off],右がEnableCollision[On] です。

Connected Rigid Body

親の選択、ですね。特に説明の必要が無いほどシンプルです。

Auto Configure Connected Anchor

親に応じたアンカーを自動で計算してほしいときはONにします。
自身の座標とアンカー位置は別々にしたい、と思うのであればOFFがいいです。

Anchor,ConnectedAnchor

Anchor:Joint 2D の端が接続する位置
ConnectedAnchor:ジョイントの終点が他のオブジェクトに接続する場所

Damping Ratio,Frequency

物理の専門家ではないので間違ってたら修正します。
Unityの「硬く、かろうじて動くバネ」「緩く、動くバネ」の例という記述があります。

上記を見てもピンと来なかったので
それぞれのパラメータは何を意味しているのかを調べます。

Damping Ratio :

0から1…とありますが、0から0.1でも大きく動作が変わります。
減衰振動と呼ばれる値だと思いますが、
Wikipediaの
“The effect of varying damping ratio on a second-order system.”
のグラフに近い動きです。

しかし、減衰振動が0であれば実世界では無いのでそのまま永久にバネ運動してほしいところですが
別の減衰がかかっているように思えます。

”2つのリジッドボディ上のアンカーポイント間の直線距離を0に維持します。”
と記述されているジョイントの制約なのかは分かりませんが、
常時反復運動を減衰なくさせたい、などの場合はSpringJointを使用せず、
DoTweenなどで動かしてしまうのがベターでしょう。

※簡単に減衰していないはずなのに減衰していることを試す方法
1.SpringJoint2dをSpriteRendererにアタッチ(XYZ座標は全て0にしといてください)
2.一緒にアタッチされたRigidBody2DのGravityScaleを0に指定
3.SpringJoint2dのAutoConfigureConnectedAnchorをOFF
4.SpringJoint2dのAnchorは0,0を指定、ConnectedAnchorは-3,0を指定
5.SpringJoint2dのDistanceは1を指定
6.Damping Ratioは0を指定
7.Frequencyは1を指定

一次元的に考えて、(0,0)の位置まで伸びているバネ(右の青丸)があり、
本来は親の座標(-3,0)の位置(左の青丸)から1離れた位置が正常な位置だとして、
Damping Ratio 0はWikipedia通りずっと反復してほしいのですが何かしら減衰していき
目的地の(-2,0)へ到達してしまいます。
何故減衰するのかPhysics2Dの設定項目も見てみましたが、分かりませんでした。

Frequency

バネは伸びたり縮んだり往復運動をします。
単位時間に起こる往復運動の回数をここに入れるわけです。
回数を大きくすればするほど秒間における往復は多くなり、
その分伸びたり縮んだりする力は小さくなり、すぐに伸び縮みしなくなります。
固有振動における振動数と言うものでしょう。

Break Force、Break Torque

それぞれ
Break Force : Joint 2D を解き、削除するのに必要な力のレベル
Break Torque : Joint 2D を解き、削除するのに必要な回転力 (Torque) のレベル

Jointを解除するためのパラメータであり、Infinity は解除できないのは理解できましたが
一体値をいくつ指定すればいいのかさっぱり分かりません。
DragがForceの空気抵抗値、Angular DragがTorqueの空気抵抗値なのは以下の動画から確認できます。

そこで、重力や空気抵抗はひとまずゼロにして、FixedJointをSpriteRendererにアタッチし、
ForceとTorque、Break ForceとBreak Torqueを調査しましょう。

Force

Unityでは、AddForceで力を与えられます。
高校で習った運動方程式の公式、を覚えているでしょうか。

F=ma
(F[N]:力、m[kg]:質量、a[m/s2]:加速度)

Rigidbody2Dで質量はmassで取得できます。
加速度は”速度の時間に対する変化の割合”です。
FixedUpdateで値を入れられますが、万有引力であれば地面に対して重力加速度は約9.81[m/s2]です。まずは、質量を考慮せず、FixedUpdateに Vector2(0,-9.81f) をAddForceしてみましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UserGravity : MonoBehaviour
{
    private Rigidbody2D rig2d;
    private Vector2 gravity = new Vector2(0,-9.81f);

    void Start()
    {
        rig2d = GetComponent();
    }

    void FixedUpdate()
    {
        rig2d.AddForce(gravity);
    }
}

質量が1であれば、RigidBody2dのGravityと同様の動きをしました。
では、互いに質量を3にしてみましょう。
私の用意したソース側は遅くなりましたね…。というわけで質量も加えます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UserGravity : MonoBehaviour
{
    private Rigidbody2D rig2d;
    private Vector2 gravity = new Vector2(0,-9.81f);

    void Start()
    {
        rig2d = GetComponent<Rigidbody2D>();
    }

    void FixedUpdate()
    {
        rig2d.AddForce(gravity*rig2d.mass);
    }
}

これで質量が変わってもRigidBody2dのGravityと同じ動きになりました。

加速後、等速運動したい場合

秒速5ユニット(Unityの単位です)で横にも動かしたい、というときは?
秒速5ユニットまで加速したら等速運動してほしいし
抵抗とかもあるわけだから微妙に力を与え続けないといけないし
重力加速度みたいにずっと加速していくわけではないのでシンプルじゃなさそう…

Kinematicにすれば直接Transformをいじればいいのでしょうが、
それではKinematicにしたオブジェクトが物理挙動を行ってくれません。
それで良いのであればKinematicにして座標を更新していっちゃうのも手です。

今回は物理挙動させたいので別の方法で移動させましょう。
要望としては以下のものになります。

・「何秒で目的速度にたどり着きたいかの指定をしたい」
・「目的速度にたどり着いたら安定してその速度で移動してほしい」

AddForceが呼ばれなくなってから停止までの時間はDrag値で調整するとして
初速は無いので、何秒で目的速度にたどり着きたいかを変数tにすると以下の形で求まります。

加速度=目標速度÷時間
(目標速度(例えば5ユニット)=加速度(v)×時間(t)のため)

とりあえず加速度は求まるので、次はDrag値が何を示してるのか調べます。

Drag値

Drag値に関しては”摩擦によるオブジェクトの減速の度合い”とマニュアルに記述されています。
水準を指し示しているわけではないためこの値がよく分かりません。
そのため、いくつかのパターンでテストしました。

■秒間10[m/s2]のForceを与えてるものにDrag値0を与える
1秒で10[m/s]の速度に至る

■秒間10[m/s2]のForceを与えてるものにDrag値5を与える
1秒で2[m/s]の速度に至る

■秒間10[m/s2]のForceを与えてるものにDrag値を10を与える
1秒で1[m/s]の速度に至る

ただ加速度を割り算してるだけ、と思いたいけど
1秒間でその速度に至るまでの時間はDrag値が高いほど早いです。
速度で割った割合をグラフ化してみました。

もう一ケーステストをしてみましょう。

■秒間1[m/s2]のForceを与えてるものにDrag値0を与える
1秒で1[m/s]の速度に至る

■秒間1[m/s2]のForceを与えてるものにDrag値0.5を与える
1秒で0.7743[m/s]の速度に至る

■秒間1[m/s2]のForceを与えてるものにDrag値を1を与える
1秒で0.6229[m/s]の速度に至る

mass値を変えても速度に変化は無い。オブジェクトや当たりのサイズを変えても速度に変化は無い。
サイズでかくなりゃ抵抗大きくなるんじゃないの?質量とかも影響しないの?物理難しい…。

まずは物理的な部分で正解を調べよう、ということで
速度 抵抗で調べると以下のものがヒット。

J03. 終端速度の求め方 – 埼玉工業大学

1.自由落下の式はうろ覚えですが覚えています。

落下速度は無限に増加し続けるのではなく、やがて重力と空気抵抗が平衡して一定速度で落下し続けることになる。その後は等速度運動をします。物体が最終的に到達できる速度は終端速度と呼ばれます。

なるほど、永久に落下速度が早くなる教えだけ受けていたわけか。
2.空気抵抗を受ける自由落下の式、これか。
とりあえず更に逆の力を足せば抵抗を表現できそうだ。

この式を見てもサッパリだけど、Wikipediaでそれぞれの値が何を意味しているのか書いてあった。
FD : 抵抗の力
p : 流体の質量密度
u : 速度
A : 表面積
D : 抗力係数。一般的にレイノルズ式というのに依存している。
というわけで質量やサイズ(表面積)は抵抗に影響がある。

Unityではどうなの

Unityのdrag値を変えずに質量や表面積をスケールしたからといって
抵抗を受けた量が変わらなかったということは
Unityでは抗力係数とか物体の面積とか算出するの色々計算必要だから
FDをそのまま入れ込んでしまえ、という値なのだろう。

これ、どうなってるのか調べた人いそうだなということでUnityForumを探した結果、
検証された方々がいました。

Bunny83氏によると以下のようです(翻訳拙いので間違ってるかも)

CallFixedUpdate();
velocity = ApplyForces(velocity);
velocity *= Mathf.Clamp01(1-drag * dt);
velocity = ApplyCollisionForces(velocity);
position += velocity * dt;

・マニュアルな場合の速度の計算:Drag計算の前にForceでの速度を出している
・Drag計算はfixedDeltaTimeとDrag値で固定の割合で速度をその後削っている
・Drag値が1/fixedDeltaTime以上の場合は自身で動くことが出来ない。
・コリジョンはDrag計算の後に計算・適用されるので、インパルススパイクによって速度と位置に補正が行われる可能性がある。
当たった場合、次のFixedUpdateでは副次的なコリジョンが再び弱いForceを適用しない限り、速度は再び0になる。
Note:コリジョンが(コリジョンのStayが呼ばれる限り)「アクティブ」である限り、衝突するRigidBodyからForceが適用される可能性があります。しかし、移行されたすべての勢いは、次のフレームで完全に消滅します

たとえば、fixedDeltaTimeを0.01に設定している場合は、100FixedUpdates/秒になります。
Drag値を50に設定すると、「百分率乗数」は(1-(0.01*50))==0.5であるため、初期速度100はfixedFrameごとに半分にカットされます。

ドラッグが100以上の場合、乗数は0(==1-(0.01*100))になります。fixedDeltaTimeの既定設定は0.02で、最大ドラッグ数は50になることに注意してください。

Jointがいつ、どのように力を発揮するのかテストしたことはありません。「マニュアル」なAddForce機構を使用している場合、DragによってJointがまったく役に立たなくなる可能性があります。Drag後に内部で適用されると、影響がある可能性があります。ただし、Dragが1/fixedDeltaTime以上の場合、次のフレームの速度は維持されません。

結論:これは単に奇妙なアプローチです。これが以前と違っていたかどうかはわかりませんが、(0から無限)の範囲が意味をなさないので、とにかくDragフィールドの「ヒント」は明らかに間違っています。

Unity 4.5.4f1を使用して決定しテストした

インパルススパイクっていうのはこれかな。
CollisionImpulse

その後、paulkopetko氏によるとUnity5.3.4f1では
velocity *= 1 / (1 + drag * Time.fixedDeltaTime);
で計算されてるような発言もあるようなので、更新によって動きが変わった可能性があります。

試しにUnity2019.1.0f2でもやってみましょう。
・fixedDeltaTimeを0.01に設定
・AddForce(Vector2(100f,0f),ForceMode2D.Impulse )を初回だけ呼ぶ
・Dragを50に指定
毎フレーム、速度が半分にカットされるはずです。
結果は以下の通りになりました。

半分じゃなくて、2/3になっていってますね。
velocity *= 1 / (1 + drag * Time.fixedDeltaTime);
であれば、 1 / ( 1 + 50 * 0.01 ) = 1 / 1.5 = 2 / 3 になるため、これで当たりです!

あとはForce値に力を更に加算して、
velocity *= 1 / (1 + drag * Time.fixedDeltaTime);
を掛けなかったことにするようにできれば、Drag値を考慮しないで速度上昇できます。
しかし、Force値じゃなくても、velocityを直接いじればいいか、ということで
以下のようにしてみました。


  var acc = new Vector2(100f,0f);
  rig2d.AddForce(acc);
  var div = 1 / (1 + rig2d.drag * Time.fixedDeltaTime);
  rig2d.velocity /= div;

上記では速度は一致するのですが、座標が一致しません…。
うーん、AddForceでもDrag分の力を増やしてみるか。


  var acc = new Vector2(100f,0f);
  var div = 1 / (1 + rig2d.drag * Time.fixedDeltaTime);
  acc /= div;
  rig2d.AddForce(acc);
  rig2d.velocity /= div;

これでdragを相殺してくれるはず…
でやってみると補正されました。
なんで?となりますが、AddForce内でdrag分ベクトル短くしちゃってるんですかね?
AddForce周りはもうちょいドキュメントあると楽だなぁ。
ひとまず、これで加速中はDrag値分も補正してくれるように出来るので、
あとは加速後の等速運動、となります。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Move : MonoBehaviour
{
    private Rigidbody2D rig2d;
    private int fixedUpdateCount;

    private float moveTime = 3f; // 移動時間
    private float accelerationTime = 0.5f; // 0.5秒で100m/sまで速度上昇してほしい
    private float accelerationDeltaTime;
// 等加速度直線運動の公式
// [0s]
// 速度(0m/s) = 初速度(0) + 加速度(100*mass(1)) * 0(経過時間) / 0.5(最高速に至る時間)
// 変位(移動距離)(0m) = 初速度(0) * time(0) + 加速度(100*mass(1)) * time(0) * time(0)
// [0.5s]
// 速度(100m/s) = 初速度(0) * 加速度(100*mass(1)) * 0.5(経過時間) / 0.5(最高速に至る時間)
// 変位(移動距離)(25m) = 初速度(0) * time(0.5) + 加速度(100*mass(1)) * time(0.5) * time(0.5)
// [1.5s]
// 速度(100m/s) = 初速度(100m/s) + 加速度(0*mass(1)) * 1.0(経過時間)
// 変位(移動距離)(125m) = 25m + 初速度(100m/s) * time(0.5) + 加速度(0*mass(1)) * time(1.0) * time(1.0)
// [3s]
// 速度(100m/s) = 初速度(100m/s) + 加速度(0*mass(1)) * 2.5(経過時間)
// 変位(移動距離)(275m) = 初速度(100m/s) * time(2.5) + 加速度(0*mass(1)) * time(2.5) * time(2.5)

    void Start()
    {
        rig2d = GetComponent();
        accelerationDeltaTime = 0f;
    }

    void FixedUpdate()
    {
        var target_velocity = new Vector2(100f, 0f); // この速度が目標
        var acc = target_velocity * rig2d.mass;
        // 一定時間で目標速度へ到達、その後runTime分まで速度を維持、runTime後はDrag値によるゆるやかな停止
        if (accelerationDeltaTime < moveTime)
        {
            float div = 1 / (1 + rig2d.drag * Time.fixedDeltaTime);
            if ( accelerationDeltaTime < accelerationTime ) {
                // 加速中は抵抗を考えずに目標速度到達を目指す
                rig2d.AddForce(acc / div / accelerationTime);
                rig2d.velocity /= div;
            } else {
                // 十分加速しているので目標速度から抵抗分を加算して速度が落ちないよう調整
                rig2d.AddForce(target_velocity / div);
                rig2d.velocity = target_velocity / div;
            }
        }
        Debug.Log(fixedUpdateCount + "f pos:" + rig2d.transform.position + " velocity:" + rig2d.velocity.magnitude );
        accelerationDeltaTime += Time.fixedDeltaTime;
        fixedUpdateCount++;
    }
}

AddForceのForceMode

AddForceで与える力に関しては、第二引数のForceModeによって変わります。
中国の方で調べてくれている方がいらっしゃいました。

ForceMode.Force:

デフォルト。AddForce呼び出しがFixedUpdate  ループ内で発生した場合、AddForce呼び出しに 
与えられた全力は1秒後にリジッドボディにのみ作用します。「毎秒与える力」と考えてください。

ForceMode.Accelaration:

質量を無視する以外はForceMode.Forceと一緒です。結果として生じる動きは、質量1と変わらないです。以下の計算では一緒の結果になります。
rigidbody.AddForce((Vector3.forward * 10)、ForceMode.Force);
rigidbody.AddForce((Vector3.forward * 10)/rigidbody.mass,ForceMode.Acceleration);

ForceMode.Impulse:

AddForce呼び出しに供給された瞬発的に加わる力は、すぐに一度に適用されます。

ForceMode.VelocityChange
オブジェクトの質量が無視されること以外はForceMode.Impulseと同じです。以下の計算では一緒の結果になります。

rigidbody.AddForce((Vector3.forward * 10)、ForceMode.Impulse);
rigidbody.AddForce((Vector3.forward * 10)/rigidbody.mass,ForceMode.VelocityChange);

Impulseがよく意味わからなかったので検証したところ、
速度変化が10m/sから始まる、という認識でいい。
押すようなゆっくりした力の影響を与える場合はForceかAccelarationを、
爆発に巻き込まれて吹っ飛ぶなどのいきなり大きなForceが加わるようなものはImpulseかVelocityChangeを指定すればいい。(こちらは毎フレーム呼ぶと毎フレーム速度が加算されていくので、最初だけ呼べば吹っ飛び動作はしてくれる)
しかし、ForceMode2DはForceとImpulseのみです。2Dはなんで質量無視しちゃいけないんですかねぇ。まあ、式にmass入れればいいだけですけど…。

Torque

Unity公式で動画などがあり、なんとなく理解はできます。

で、このサンプルのamount=50は何を意味しているの…?という。
その辺りは、回転の運動方程式というものがあるらしい。

(2)回転の運動エネルギーと角運動量により、
移動と回転運動は似たような考え方でいいようだ。
力(force=質量(mass)*加速度(acceleration)) → トルク(torque=慣性モーメント(Inertia)*角加速度)
質量(mass) → 慣性モーメント(Inertia(イナー(ル)シャ、と呼ぶらしい))
距離(x) → 回転角(θ(シータ))
速度(velocity=経過時間の移動距離(dx)÷経過時間(dt)) → 角速度ω(オメガ)
加速度(acceleration=経過時間の速度(dv)÷経過時間(dt)) → 角加速度(経過時間の角速度(dω)÷経過時間(dt))
運動量(p=質量(mass)*速度(velocity)) → 角運動量(L=慣性モーメント(Inertia)*角速度(ω))

重要なのはそのトルクでどれだけ回転するかです。
以下のソースをFixedUpdateが1/60秒で行われる場合において、60回実行されたあとの回転角を調べます。もちろん、減衰をさせないためにAnglurDragは0にしています。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Rotate : MonoBehaviour
{
    private Rigidbody2D rig2d;

    void Start()
    {
        rig2d = GetComponent();
    }

    void FixedUpdate()
    {
        rig2d.AddTorque(1f);
    }
}

結果はZ軸に+29.1度回転しました。(RigidBody2DのInfo値内のRotation値(回転量)とAngularVelocity(速度)で確認できます)
位置で言えば、等加速度直線運動方程式でいうと以下の計算になるはずです。

回転角(θ) = 角初速度?(v0) * 時間(t) + 1/2 * 角加速度(ω) * 時間(t)^2

角初速度?でいいんですかね…は0で、トルクの慣性モーメント(Inertia)は1なので角加速度をそのままAddTorqueしていたことになります。で、結果として回転角は1/2が与えられたはずなので、回転角が29度くらい、ということはラジアン換算(0.5radは28.648度くらい)されています。つまり2秒後には2rad(114.6度くらい)まで回転している、ということですね。

とりあえずここまで考えて調整する場合はこういう感じで考えれば大体どのくらいのForceかTorqueを与えれば良いかわかりました。
半分以上が力と抵抗のことで費やされてしまいましたがスッキリしました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください