📑

Unity揺れもの設定メモ (VRMSpringBone, MagicaCloth2)

2023/12/25に公開
1
  • 2021/12/27 : BoneSpring のメモを追加

VRMSpringBone

https://github.com/vrm-c/UniVRM

個人的にはボーンが一本の線で繋がってる長めの揺れ物 (ポニーテールや振り袖など) では、最適な物理コンポーネント。回転時に形を維持したまま重力感のある綺麗な弧を描いてくれる。MagicaCloth2 でも頑張ってみたが再現できなかった。設定が少なくて楽なのも利点

ただ設定が少ないことは不利な点でもあり、MagicaCloth2 のような細かい制限を加える設定がなく CapsuleCollider もないため、シンプルな構造の揺れ物に適用するのがベスト。名前から勘違いしていたが、VRM 形式以外でも利用できる汎用的なコンポーネント

項目 内容 デフォルト値
Stiffness Force 剛性。さげると柔らかくなり、あげると硬くなる 1
Gravity Power 重力。さげると重力が弱くなり、あげると強くなる 0
Gravity Dir 重力の向き 0, -1, 0
Drag Force 空気抵抗 0.4
Center 移動量を計算する起点 None

Stiffness Force は柔らかい布のような素材なら小さい値を指定する。 例えば、重力に従って形状が大きく変化する薄手の布なら 0.3、セルルックモデルの長い髪なら形状は維持したいため 1.0 ~ 1.2あたりがおすすめ。

Gravity Power は重力に従ってだらんとさせたい揺れ物で大きい値を指定する。 振り袖のような腕を振り回しても常に下向きに垂れ下がる揺れ物なら 1.0、セルルックモデルの長い髪なら 0あたりがおすすめ。

Center は中心に位置する Transform を指定する。 位置や回転が瞬間的に変化する(テレポート)時に、揺れ物が暴れる問題を回避できる。詳しい理屈はわからないが Center に指定する Transform は下記ツリーの Prefab Root がおすすめ。理由は表を参照

## 例
Prefab Root
 └── FBX + Animator
       └── VRM Spring Bone
Center Transform 使用目的 結果
指定しない 揺れ多め、テレポート時に暴れる
Prefab Root 位置指定に利用。テレポートあり 揺れ多め、暴れない
FBX + Animator アニメーションを適用するルート 揺れ少な目、暴れない

Note: Gravity Dir / Drag Force は初期値から変更したことナシ

Note: Culling Mode について。VRMSpringBone Component は Animator 以下に配置し、Animator の Culling Mode を適宜設定する。高負荷上等で面倒な問題を避けたいなら Always Animate がおすすめ。物理を設定したのに動かない時は大体 Culling が原因

Note: Collider について。CapsuleCollider がないので細かい衝突設定が大変。大きめの SphereCollider で対処するのが楽。髪なら Spine2 あたりにデカイ SphereCollider を置くと良い。複雑な Collider を構築しても制御できる設定がないため大抵失敗する

MagicaCloth2

https://assetstore.unity.com/packages/tools/physics/magica-cloth-2-242307

MagicaCloth2 があるので Unity を使っていると言っても過言ではないほど優れた物理システム。難点はパラメータ設定に関するキツめの学習曲線。ただ品質は他のゲームエンジンの揺れ物システムを含めて比較しても突出して高い。特にスカートに設定したときの MeshCloth と、胸に設定したときの BoneSpring が高品質で、他の物理システムでは再現できなかった

フォーラムを見ていると 公式ドキュメント をよりコンパクトにした資料の需要がありそうだったので、経験則も含めてまとめる

Note: 必読の公式資料。特にこれらは事前に目を通しておいた方が捗る

トラブルシューティング

  • 揺れものが動かない
    Camera Culling Mode を Off にして Animator を Always Animate にする。高負荷上等の設定なのでパフォーマンスが重要な場合は、設定を 適宜調整 する
  • 揺れものが急に飛び跳ねる
    テレポートかカリングのどちらかが原因。前述の FAQ. "揺れものが動かない" と、Inertia のテレポート設定 (Teleport Mode / Teleport Distance / Teleport Rotation) を調整する
  • 衝突判定を設定しても貫通する
    大変だけど高品質な対策と、簡単だけど限界のある対策がある。前者から順に列挙すると、(1) アニメーション姿勢の重要性 に従いモデルを編集して布部分にウェイトを設定してから MagicaCloth を適用する。(2) Unity の Constraints または MagicaCloth のカスタムスキニング機能 を利用し、物理エンジンの外で貫通を緩和してから MagicaCloth を適用する。(3) バックストップを使った衝突制御 を利用する。ただしバックストップが貫通の原因になるケースもあるため注意。(4) Inertia の設定 (World or Local Inertia / Movement Speed Limit / Rotation Speed Limit) を見直す。(5) 頂点の Reduction Setting と、Collider Collision の設定 (Mode / Radius / Collider の Transform) を見直す。(6) その他設定を見直す。(1) から順に大変だけど高品質な対策で、(5) (6) は簡単だけど限界のある対策
  • 揺れものがガタガタ振動する
    TBD
  • フワッとした形状にならずポリゴンが目立つ
    TBD

Reduction Setting

https://magicasoft.jp/mc2_magicacloth_reduction/
Simple Distance 0.3 ~ 0.8、Shape Distance 0.0 あたりが多い

項目 内容
Simple Distance 頂点間の距離で削減
Shape Distance 接続されている頂点間の距離削減

Force

https://magicasoft.jp/mc2_magicacloth_force/

項目 内容
Gravity 重力
さげると重力方向への影響が減り、あげると重力方向への影響が増える
Damping 抵抗力
さげると静止しにくくなり、あげると静止しやすくなる

Spring (BoneSpring のみ)

https://magicasoft.jp/mc2_magicacloth_spring/
BoneSpring の貫通対策は Inertia より Limit Distance の方が良さそう

項目 内容
Spring Power バネの強さ
さげると柔らかいバネに、あげると硬いバネになる
Limit Distance 頂点が移動できる最大距離
Spring Noise 複数の固定属性 (赤色) があるとき、各バネに不規則性を持たせる
さげると不規則性が減り、あげると不規則性が高まる

Angle Restoration

https://magicasoft.jp/mc2_magicacloth_anglerestoration/

項目 内容
Stiffness 1 回あたりの角度復元量
さげると角度の復元が遅くなり、あげると角度の復元が速くなる
Velocity Attenuation 角度復元の減衰量
さげると角度の復元が速くなり、あげると角度の復元が遅くなる

Angle Limit

https://magicasoft.jp/mc2_magicacloth_anglelimit/

項目 内容
Limit Angle 曲がることができる角度の制限
Stiffness 制限にひっかかった時の反発力
さげると制限時に柔らかく戻る、あげると制限時に固く戻る

Shape Restoration

https://magicasoft.jp/mc2_magicacloth_shaperestoration/

項目 内容
Distance Stiffness 各頂点が接続する頂点との距離を保つ復元力
さげるとメッシュが伸びやすくなり、あげると伸びにくくなる
Tether Compression 頂点がどの程度始点に近づけるかを指定する
さげると形状を維持し、あげると形状を維持しない
0.8 なら 80% の可動範囲、0.2 なら 20% の可動範囲
Triangle Bending Stiffness 隣接する Triangle が元に戻る力を加える
さげると形状を維持しない、あげると形状を維持する

Inertia

https://magicasoft.jp/mc2_magicacloth_inertia
個人的な重要パラメータ。World / Local Movement Speed Limit0.3 ~ 1.0Rotation Speed Limit100 程度にすることが多い。Teleport 関連は Keep / 0.03 / 45 を常に設定

項目 内容
World Inertia キャラクターのワールド空間の動きが与える影響度
0.1 なら移動量の 10% が布に加わる
World Movement Speed Limit ワールド移動量が与える影響を指定した速度でカットする
0.1 なら 0.1m/sec 以上の移動を無視する
World Rotation Speed Limit ワールド回転量が与える影響を指定した速度でカットする
100 なら 100°/sec 以上の回転を無視する
項目 内容
Teleport Mode None: 自動テレポート判定を無効
Reset: テレポート後にシミュレーションをリセット
Keep: テレポート後にシミュレーションを継続
Teleport Distance テレポートとして検出する 1 フレームの移動距離 (m)
Teleport Rotation テレポートとして検出する 1 フレームの回転角度 (degree)
項目 内容
Local Inertia キャラクターのローカル空間の動きが与える影響度
0.1 なら移動量の 10% が布に加わる
Local Movement Speed Limit ローカル移動量が与える影響を指定した速度でカットする
0.1 なら 0.1m/sec 以上の移動を無視する
Local Rotation Speed Limit ローカル回転量が与える影響を指定した速度でカットする
100 なら 100°/sec 以上の回転を無視する
Local Depth Inertia 頂点の深さに応じたローカル慣性の削減
さげると慣性の削減なし、あげると始点に近いほど動きが減る
Centrifual Acceleration 遠心力の増強値
さげると遠心力の増強なし、あげると遠心力を増強する
Partice Speed Limit 頂点毎の最大速度制限 (m/sec)
さげると遠心力で長い布が過剰に外側に膨らむ現象を抑えられる
1.0 以下にさげると衝突判定の精度が下がるため注意

Movement Limit

https://magicasoft.jp/mc2_magicacloth_movementlimit/

項目 内容
Use Max Distance 最大距離制限の有効・無効化
Max Distance 頂点から移動できる最大距離
項目 内容
Use Backstop バックストップ制限の有効・無効化
Backstop Radius バックストップ衝突球 (頂点の法線の逆方向に置く) の半径
Backstop Distance 頂点とバックストップ衝突球の初期距離
Stiffness バックストップにひっかかった時の反発力
さげると制限時に柔らかく戻る、あげると制限時に固く戻る

Collider Collision

https://magicasoft.jp/mc2_magicacloth_collidercollision/
Collider は体の形状にキッチリ合わせる必要はない。過剰に大きくして貫通を防いだり、意図的に小さくしてある程度めり込ませた方が良いケースもある

項目 内容
Mode コライダーとの衝突判定の方法
Point: 頂点球のみ
Edge: 頂点球を結ぶ線
Radius 頂点球の大きさ
Friction 摩擦係数
さげると頂点がコライダーの上をよく滑り、あげると滑らなくなる

Appendix

Collider の自動生成

フォーラムにあった MagicaCloth の Collider 自動生成案を書いた
https://assetstore.unity.com/packages/tools/sacolliderbuilder-15058

VRMSpringBone の ColliderGroup も自動生成できるが CapsuleCollider がないのであまり役に立たない。幸い VRMSpringBone では複雑な Collider を設定しないので、手動作成で問題なし。MagicaCloth は複雑な Collider を設定してもうまく機能する

SAColliderConverter2.cs
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class SAColliderConverter2 : MonoBehaviour {
  [Header("MagicaClothCollider を生成するか")]
  [SerializeField] public bool createMC2Collider = false;
  [Header("MagicaCloth に生成した MagicaClothCollider を登録するか")]
  [SerializeField] public bool registerMC2Collider = false;
  [Header("自動生成された MagicaClothCollider を削除するか")]
  [SerializeField] public bool clearMC2Collider = false;

  [Space(20)]
  [SerializeField] public List<MagicaCloth2.MagicaCloth> magicaClothes = new List<MagicaCloth2.MagicaCloth>();
}

#if UNITY_EDITOR
[CustomEditor(typeof(SAColliderConverter2)), CanEditMultipleObjects]
public class SAColliderConverter2CustomEditor : Editor {
  SAColliderConverter2 Instance;

  public override void OnInspectorGUI() {
    base.OnInspectorGUI();
    Instance = target as SAColliderConverter2;

    EditorGUILayout.Space(20);
    if (GUILayout.Button("\n1. SAColliderBuilder を追加\n")) {
      Add();
    }

    EditorGUILayout.Space(20);
    EditorGUILayout.HelpBox("事前に SAColliderBuilder で Process をクリックして Collider を生成してください", MessageType.Info);
    if (GUILayout.Button("\n2. Collider を MagicaClothCollider に変換\n")) {
      Apply();
    }

    if (GUILayout.Button("\n3. 変換された MagicaClothCollider を削除\n")) {
      Clear();
    }
  }

  private void Add() {
    var builder = Instance.GetComponent<SABoneColliderBuilder>();
    if (builder == null) builder = Instance.gameObject.AddComponent<SABoneColliderBuilder>();
    builder.reducerProperty.shapeType = SAColliderBuilderCommon.ShapeType.Capsule;
    builder.reducerProperty.optimizeRotation = new SAColliderBuilderCommon.Bool3(false, false, false);
  }

  private void Apply() {
    var magicaColliders = new List<MagicaCloth2.MagicaCapsuleCollider>();

    var colliders = Instance.GetComponentsInChildren<CapsuleCollider>();
    foreach (var col in colliders) {
      if (!col.gameObject.name.StartsWith("Coll.")) continue;

      if (Instance.createMC2Collider) {
        var magicaCol = col.gameObject.GetComponent<MagicaCloth2.MagicaCapsuleCollider>();
        if (magicaCol == null) magicaCol = col.gameObject.AddComponent<MagicaCloth2.MagicaCapsuleCollider>();
        magicaCol.direction = (MagicaCloth2.MagicaCapsuleCollider.Direction) col.direction;
        magicaCol.center = col.center;
        magicaCol.SetSize(col.radius, col.radius, col.height);
        magicaColliders.Add(magicaCol);
      }
    }

    if (Instance.registerMC2Collider) {
      foreach (var magicaCloth in Instance.magicaClothes) {
        var sdata = magicaCloth.SerializeData;
        foreach (var magicaCol in magicaColliders) {
          if (sdata.colliderCollisionConstraint.colliderList.IndexOf(magicaCol) == -1) {
            sdata.colliderCollisionConstraint.colliderList.Add(magicaCol);
          }
        }
      }
    }

    var prefab = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage()?.assetPath;
    if (prefab != null) PrefabUtility.SaveAsPrefabAsset(Instance.transform.root.gameObject, prefab);
  }

  private void Clear() {
    if (Instance.clearMC2Collider) {
      var magicaColliders = Instance.GetComponentsInChildren<MagicaCloth2.MagicaCapsuleCollider>();
      foreach (var magicaCol in magicaColliders) {
        if (magicaCol.gameObject.name.StartsWith("Coll.")) {
          DestroyImmediate(magicaCol);
        }
      }

      foreach (var magicaCloth in Instance.magicaClothes) {
        var sdata = magicaCloth.SerializeData;
        sdata.colliderCollisionConstraint.colliderList = new List<MagicaCloth2.ColliderComponent>();
      }
    }

    var prefab = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage()?.assetPath;
    if (prefab != null) PrefabUtility.SaveAsPrefabAsset(Instance.transform.root.gameObject, prefab);
  }
}
#endif

条件付き RotationConstraint

ウェイトのないスカートに MagicaCloth で物理を設定するとき、脚とスカートに RotationConstraint を設定しておくと、足がスカートを貫通しない堅牢な物理が設定できる。このとき右の脚にはスカートの右ボーンを、左の脚にはスカートの左ボーンを連動させるが、スカートの中央ボーンでは、"前に出した方の脚" の回転をスカートに連動させる、という条件付きの Constraint が必要になる。しかし Unity の RotationConstraint にそのような機能はないため書いた

RotationConstraintForCenterSkirtBone.cs
using UnityEngine;

public class RotationConstraintForCenterSkirtBone : MonoBehaviour {
  [SerializeField] public Animator animator;
  [SerializeField] public float weight = 0.5f;

  private HumanPoseHandler humanPoseHandler;
  private HumanPose humanPose;
  private Quaternion initRotation;

  private float initLeftFrontBackValue;
  private float initRightFrontBackValue;

  void Awake() {
    humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
    humanPoseHandler.GetHumanPose(ref humanPose);
    initLeftFrontBackValue = humanPose.muscles[21]; // LeftUpperLegFrontBack
    initRightFrontBackValue = humanPose.muscles[29]; // RightUpperLegFrontBack

    initRotation = this.transform.localRotation;
  }

  void Update() {
    humanPoseHandler.GetHumanPose(ref humanPose);
    var leftFrontBackValue = humanPose.muscles[21];
    var rightFrontBackValue = humanPose.muscles[29];

    if (leftFrontBackValue < rightFrontBackValue && leftFrontBackValue < initLeftFrontBackValue) {
      var value = Mathf.Abs(leftFrontBackValue - initLeftFrontBackValue);
      this.transform.localRotation = Quaternion.AngleAxis(value * 100 * weight, Vector3.forward) * initRotation;
    }

    if (rightFrontBackValue < leftFrontBackValue && rightFrontBackValue < initRightFrontBackValue) {
      var value = Mathf.Abs(rightFrontBackValue - initRightFrontBackValue);
      this.transform.localRotation = Quaternion.AngleAxis(value * 100 * weight, Vector3.forward) * initRotation;
    }
  }
}