2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Unity] 2Dキャラの髪や衣装の自然な揺れを簡単にする拡張

Last updated at Posted at 2024-12-08

概要

Unity Animation2D パッケージに含まれる SpriteSkin をつかったキャラクターアニメーションにおいて、髪や衣装などの自然な揺れを手軽に設定できる揺れコンポーネントを作成しました。

compare-dragger.gif

ただし現状ではまだまだ発展途上で改善の余地があります。いい感じに改修してくれる人大歓迎。
GitHubにて公開しますので改善のアドバイスやプルリクをお待ちしています!

この記事ではその背景、設計思想、実装方法を紹介します。

課題と背景

Kaia.gif

UnityのSpriteSkinを使った2Dキャラクターアニメーションを使って自作ゲームを開発してた時の話。SpriteSkinというのはレイヤーでパーツ分けした2Dキャラクター画像にボーンを設定してボーンの動きに合わせてパーツのメッシュを変形させる仕組です(Live2Dみたいな感じ)。
SkeletalAnimationなどとも呼ぶようです。

髪や衣装を自然な感じに揺らす動きを付けるのは、なかなか手間のかかる作業。
それなりの数のキャラを登場させたいので、なんとか簡略化したいと思いました。

最初はRigidbodyとかジョイント組み合わせて、既存の物理エンジンでやれないかと思って試してみたけれどこれが予想以上に難しい。

  • どう設定したら望み通りの動きになるかわかりにくい
  • ボーンごとにコンポーネント設定するのが面倒くさい
  • ちょっとしたことで荒ぶったり、画面外にふっ飛んでったりする
  • しばらく動かしてるとボーンの連結が外れてどんどんずれていく
  • そもそもAnimatorでボーンを動かした場合、物理エンジンが機能しない【重要】

これらの問題に直面して、結局自作してみることにしました。

コンポーネントの設計思想

Veradonna.gif

Animatorで動かした場合でも、ルートオブジェクトのTransformを変更して動いた場合でも、同じように揺らせるようにしたい。

空気抵抗と慣性力を表現したい。一方で「正しい」物理演算かどうかは追究せず、見た目にそれっぽければよい。ボーンの位置を弄るとRidgidbodyを使った時のようにジョイントが外れたり画面外にふっとぶ恐れが出てくるので、調整するのは回転だけとする。

設定はなるべく簡単に、パラメーター変更の結果が視覚的に分かりやすいことを目指しました。

使用方法

実装内容は後回しにして、まず使用方法を説明しましょう。

ここでは SpriteSkin を使った2Dアニメーション作成の基本については説明を省きます。
もしそのあたりから知りたいという方は以下のリンクを参考にしてください。

SpriteSkinの設定

まずは揺らしたい部位にボーンを付けます。
(ふさ:揺らしたいの髪や布の部位に割り当てた一連のボーン)は最低限1つ目だけでも区別しやすい名前にするとよいです。そして、同じパラメーターで揺らしたい房が複数ある場合は、名前に共通のキーワードを含めるようにしてください。

そして、重要なのが末端に短いボーンを付けること。
この末端のボーンは揺らしコンポーネントでは制御しないので、スプライト側にウェイトはつけないほうがよい。ただ、まったくつけないと不要ボーンということで消えてしまうので、目立たない部分にちょっとだけウェイトを混ぜてください。

末端に短いボーンを付ける理由

揺らしコンポーネントでは各ボーンの付け根と先端の座標を取得したいのですが、自分の調べた範囲ではボーンの先端の座標を取得する方法がわかりませんでした。

そのため、子ボーンの付け根を親ボーンの先端と見做しています。房の末端ボーンは連なる子ボーンがないと先端位置がわからないため、もう一つ余分に揺らさなくてもよいボーンを付ける必要がありました。

image.png

揺らしコンポーネントが制御するのはボーンだけであり、各ボーンをどのようにジオメトリにウェイトづけるかは完全に自由です。一つのスプライトを複数の房で動かしてもよいし(例:前髪)、一つの房のボーンそれぞれに異なるスプライトを付けてもよいです(例:おさげ)。

image.png

IK,Animatorの設定

揺らしコンポーネントとは関係なく、SpriteSkin を使うために必要なプレハブを構築します。私は親となるEmptyオブジェクトを作成して、その子供に PSDインポーターで読み込んだスキンを追加しています。
image.png

この親オブジェクトに IK Manager 2D をつけて、手足に Limb Solver 2D を付け、 Aniamtor でそれらを動かすモーションを設定したりします。

image.png

動的にキャラの左右を反転させる場合は、Y軸回転させると本コンポーネントの挙動がおかしくなるので、X座標のスケール反転を使ってください。

この辺は本記事で紹介する揺らしコンポーネントとはあまり関係ないことなので、詳しく知りたい場合はこちらの記事などを参考にしてください。

揺らしたいボーンはモーションを付ける必要はありません。そうしなくてすむようにするのがコンポーネントの目的ですから。

集中管理コントローラー

image.png

親オブジェクトに集中管理コントローラー(BoneDraggerManager)を追加。
Keyword for bone に房ボーンの名前の一部を入れて、 [Create Object And Auto Setup] ボタンを押すと、新しい子オブジェクトが作成されて、一つのBoneDraggerPrameters と、キーワードを名前に含むボーンとそれに連なるボーンが房として設定された BoneDragger コンポーネントが自動的に設定されます(キーワードに一致する房が複数あればそれぞれ個別にコンポーネントが作られます)。

この自動検索で「房」とみなされるボーンには条件があります。
「子供を持たないボーンが、子供を1つだけ持つボーンを1つ以上介して連なっていること」です。
もう少しわかりやすく言い換えると、「枝分かれのない3つ以上のボーンの連なり」です。

例: fh_ というキーワードで設定がうまくいったときのログ。 fh_n1, fh_m1, fh_f1 が房の根本のボーンとして登録されました。
image.png

一つのボーンが複数の BoneDragger に登録されることがないように注意してください。異常な動きの原因となります。
BoneDraggerManager のインスペクターに表示される [Check duplicate associations] ボタンを押すと、重複登録されたボーンがないか調べることができます。

BoneDraggerParameter と BoneDragger は BoneDraggerManager があるオブジェクトの子オブジェクトとして作られます。(画像の Dragger xxx というのがそれ)
image.png

【応用】異なるキーワードの房たちで一つの BoneDraggerParameter を共有したい

既にある BoneDraggerParameter のついたオブジェクトを GameObject to setup にセットしてから、新しいキーワードとともに [Create Object And Auto Setup] ボタンを押すと、新しいキーワードで見つかった房の BoneDragger コンポーネントが既存のオブジェクトに追加されます。
またこのとき、 Clear current draggers on the object をチェックしておくと、既存のBoneDragger コンポーネントを削除して、新しい房のコンポーネントを追加することができます。

【応用】キーワードが重複するので、第二キーワードで絞り込みたい

[Create Object And Auto Setup] は既にいずれかの BoneDragger に登録されているボーンは除外するようになっています。
先により絞り込めるキーワードで自動設定を行い、あとでより緩いキーワードで自動設定してください。

【応用】自動設定時に検索にヒットしてくれないとき!

自動設定ではどうしても設定できないとき、例えば枝分かれした房を登録したいとき、 BoneDraggerManager に頼らず手動で BoneDragger を追加することもできます。

枝分かれしたボーンを登録できないのは、自動設定の検索ロジックの都合であって、BoneDragger に重複登録されなければ、枝分かれした房を動かすことは可能です。
望みどおりに揺れてくれるかどうかはまた別問題ですが

AddComponent で BoneDragger を追加し、Nodesの最初の要素を追加して、 Target に始まり(根本)のボーンを追加します。 Inartia Generator にも同じ始まりのボーンを設定します。

image.png

[Auto add decendant nodes] ボタンを押せば、連なるボーンを自動で設定してくれますが、万一うまくいかない場合は画像のように Nodes を追加して Target と Next Bone を設定していきます。先端は Next Bone は None のままで構いません。
設定できたら、必要に応じてボーンの初期位置を整えて [Save Current Bone Rotation] ボタンを押してください。

必ず Prameters に適切な BoneDraggerParameter コンポーネントが設定されているか確認してください

パラメーター調整

各子オブジェクトの BoneDraggerParameter を開いて、パラメーターを調整します。

image.png

各パラメーターの意味と効果はこんな感じです。

  • Air Drag Time Scale: 空気抵抗の効きやすさ(deltaTimeに倍率)
  • Inartia Time Scale: 慣性力の効きやすさ(deltaTimeに倍率)
  • Inartia: 慣性力を決めるパラメーター(※ MoveScale以外は弄ってもあまり変化なし)
    • Move Scale: 慣性力の効きやすさ(移動距離に倍率)
  • Air Vs Mass: 空気抵抗と慣性力のブレンド割合。0だと空気抵抗のみ、1だと慣性力のみ
  • Softness: Angular Limit を超えたときどれぐらい曲がるか
  • Angular Limit: 抑制なく回転できる角度、元の位置から両側に同じ角度幅
  • Restore Spring: 元の位置に戻ろうとする力、効き方がおかしい。0にすると特に変わる
  • Apply Physics: 計算をFixedUpdateで行う(非チェック時は Updateで行う)

なお、残念ながら現バージョンでは、エディタモードで揺らしを再現することができないため、プレイモードで調整して、 BoneDraggerParameter をコピーし、プレイモード停止後に Paste Component Values で値を再更新する必要があります。

モーションにあわせて個別ノード調整

必要に応じて、 Dragger Node を個別に調整します。
この揺らしコンポーネントは、他のボーンなどとの衝突で揺れたりしてくれないので、
例えば、走ってる時の脚でスカートの端が跳ね上がる動きは、揺らしコンポーネントではなく Animator で制御します。
(※画像でオレンジの縁取りがついてるボーン)
skirt.gif

BoneDraggerManager の自動設定ではオレンジのボーンが始まり(根本)として登録されるので、あとで手動で BoneDragger の Nodes から削除しました。

image.png

結果

冒頭にも貼ったサンプルです。左が揺らしコンポーネントあり、右がなしです。
髪の毛、スカート、背後のリボンなどに注目してください。
compare-dragger.gif

本当は、アホ毛はもう少しばねのように弾力感ある感じを出したいなとか、おさげはもう少し慣性で激しく揺らしたいなと思うのですが、今のコードではあまりうまくいってません。
とはいえ、いちいちモーションを手動で設定しなくてもこれぐらい揺らせることができたのはそれなりの成果かなと思っています。

それぞれ別々にキャプチャして、動画編集ツールで並べたので微妙にタイミングずれてるところがありますが、コンポーネントのせいではないので気にしないでください。

GitHub に当該シーンも含まれていて、概ねこのキャプチャのように動きますが、コード改良&パラメーター調整を続けた結果、動画とは若干異なる挙動になっていること悪しからずご了承ください。

Leira.gif

自作ゲームでは16体のキャラx6モーションを作成しました。基本学生服なので服装はほぼ同じですが、髪形と杖はそれぞれ異なるデザインです。
これに曲がりなりにも、それぞれの髪形に合った動きをつけようと思えば、かなり大変な作業になるところですが、本コンポーネントを使うことで、アニメーションで動かすのは四肢のIKターゲットと、腰から頭にかけてのボーン、あとは最低限のボーンに動きを付けるだけで済みました。

それだけではなく、アニメーションクリップ側には髪形による相違がないため、同じクリップを複数のキャラに使いまわすことができて、大幅に工数が短縮できました。
cards.jpg

本コンポーネントで作られた動きは、プロのアニメーターの目からみたら、拙くてお粗末な出来かもしれませんが、素人が頑張ってできるライン程度にはなってるのではないでしょうか。

そもそもプロは SpriteSkin じゃなくて SpriteStudio とか Spine とかの専用ソフトで動きを付けるでしょうから、 私のようにプロの手を借りずにやりたい人の助けになればと思い記事にしてみました。

実装

主に3つのコンポーネントで構成されています

  • パラメーター保持コンポーネント: 設定の共有を容易に
  • 揺らしコントロールコンポーネント: 空気抵抗や慣性を計算
  • 集中管理コンポーネント: 半自動でボーンを登録・設定

パラメーター保持コンポーネント

まずは調整用パラメーターを保持するコンポーネントです。なぜメインの揺らしコンポーネントとわけたかというと、同じパラメーターを複数のボーン列(一本の髪や布、以下「房(ふさ)」と呼びます)に適用したいケースが多そうなこと、別のキャラや再生中のシーンで調整したコンポーネントから複製するとき、パラメーターだけコピーできるほうが何かと使い勝手がよかったからです。

揺らしコントロールコンポーネント

次に本拡張の中核となる揺らしコンポーネント。もっともこのクラス自体は、一本の髪や布に相当する連なったボーンのリストや、慣性計算用おもりの管理を行うもので、揺らしのロジックは持っていません。

ボーン一つ一つに対応し、揺らしのロジックや状態管理を行う中心的役割を持つのが BoneDragNode です。

なぜボーンに直接つけるコンポーネントじゃないのか?

最初はそういう設計してましたが、SpriteSkinって目的のボーン選択するの結構面倒くさいですよね。
シーンビューにボーンのギズモ出してれば幾分マシですが、私の作ったスキンはボーンが多くてごちゃごちゃしてるので大変でした。
なので、一つのインスペクタ上で関連するボーンの設定全部見れる方が便利だなと気づいて、今の設計になりました。

空気抵抗の実装と計算ロジック

空気抵抗の計算部分はこのメソッド

BoneDragNode.cs
        private Quaternion RotateByAirDrag(BoneDragger dragger)
        {
            var timeScale = dragger.AirDragTimeScale * dragger.DeltaTime;
            var nowPos = target.position;
            var moveDir = prevPos - nowPos - dragger.Wind;
            var weightRatio = WeightInLength == 0 ? 0 : (WeightInLength + PrecedingLength) / WeightInLength;
            var ratio = BoneDraggerSettings.AIR_DRAG_MULTIPLIER * timeScale * moveDir.magnitude * weightRatio;
            ratio = Mathf.Min(BoneDraggerSettings.MAX_AIR_DRAG_ANGLE, ratio);
            //Debug.Log($"r2={r2}, ratio={ratio}, mag={moveDir.magnitude}, tscl={timeScale}");
            var baseDir = flipX ? Vector3.left : Vector3.right;
            var rot = Quaternion.FromToRotation(baseDir, moveDir);
            //DebugLog($"movDir={moveDir}, rot={rot}");
            var q = Quaternion.RotateTowards(prevWorldRot, rot, ratio);
            return limitter.LimitAsWorld(q, dragger.Softness, flipX);
        }

空気抵抗の表現と言えばDumperみたいに動きを遅くする係数なんかは従来の物理演算でもありますが、ここで目指すのは、移動量と空気抵抗の大きさに応じてボーンを移動方向に平行になるように回転させる仕組です。新体操のリボンみたいに、空気の流れに沿うような形での揺らしを目指しました。

慣性力を計算するメカニズム

一方、慣性力の計算部分はこちらのメソッド

BoneDragNode.cs
        private Quaternion RotateByInertia(BoneDragger dragger)
        {
            var inartia = dragger.InartiaForce;
            var ratio = dragger.InartiaTimeScale * dragger.DeltaTime;
            var nowPos = target.position;
            var endPos = nextBone.transform.position;
            var boneDir = endPos - nowPos;
            var look = boneDir.normalized + inartia + dragger.Gravity;
            look.Normalize();
            if (look.magnitude <= Mathf.Epsilon) return prevWorldRot;
            var mag = BoneDraggerSettings.INARTIA_SCALE * inartia.magnitude;
            mag *= WeightInLength == 0 ? 10 : (WeightInLength + FollowingLength) / WeightInLength;
            //Debug.Log($"inamag={inartia.magnitude}, Wfol={FollowingLength}, Win={WeightInLength}, Wpre={PrecedingLength}");
            //Debug.Log($"inartia={inartia}, look={look}, ratio={ratio}, mag={mag}");

            var baseDir = flipX ? Vector3.left : Vector3.right;
            var rot = Quaternion.FromToRotation(baseDir, look.normalized);
            var q = Quaternion.RotateTowards(prevWorldRot, rot, ratio * mag);
            return limitter.LimitAsWorld(q, dragger.Softness, flipX);
        }

慣性力は基準となるポイントを仮想の重りに設定して、そのワールド座標の移動から計算するようにしました。

そのおもりの計算は、一つの房に対して一つでいいだろうということで、別クラスにしてBoneDraggerに持たせています。

一度、それぞれのボーンの位置をそのまま使ってみたのですが、アニメで動いたのか揺らしコンポーネントの結果動いたのか区別がつかなくなり、物理エンジンみたいに荒ぶって手が付けられなくなってしまったのでやめました。

仮想のおもりにしたところで、結局ボーンごとにおもりをつけてはうまく計算できそうになかったので、おもりは揺らす根本に一個だけつけることにしました。

/InartiaGenerator.cs
        public Vector3 Force
        {
            get
            {
                if (Freeze || Parameters.moveScale == 0) return Vector3.zero;
                var vtmp = forceQueue.Aggregate(Vector3.zero, (sum,v) => sum+v);
                var vmag = vtmp.magnitude * Parameters.moveScale / forceQueue.Count;
                var vsum = vmag + gravity;
                // 混合比率としての gravity, ベクトル強度としての gravity で二回掛ける
                var f = -vtmp.normalized * vmag + Vector3.down * gravity;
                return f;
            }
        }

        public void Update()
        {
            // 移動量を力とする
            var curPos = target.transform.position;
            prevVelocity = velocity;
            velocity = curPos - prevPosition;
            prevPosition = curPos;

            if (this.Freeze)
            {
                return;
            }
            force = velocity - prevVelocity;
            force = force.normalized * adjustMagnitude(force.magnitude, expSquare);
            forceQueue.Enqueue(force);
            forceQueue.Dequeue();
        }

正直、一番「うまくいってない」と思ってるところです。やはり、もう少しちゃんとした物理演算をしないとだめなのかもしれません。

なお、調整をしやすくするため慣性力の様子をエディタ上で視認できるようなギズモを作りました。
(※緑の円と点線は IK のギズモで、揺らしと関係ありません)
gizmo.gif

復元力の実装

そして、上では述べてませんがもう一つ主要な要素として、Springによる回転を実装してます。これはJoint2Dなどで使われるSpringと同じ意味で、元の形に戻ろうとする力を表現したものです。
髪や衣服が動いて乱れるのは狙い通りですが、ずっとそのままというのも見栄えが悪いことが多いので、時間がたてば元の形に戻るようにと考えました。

BoneDragNode.cs
        private Quaternion RotateByRestoreSpring(BoneDragger dragger)
        {
            var amove = Mathf.Max((1 - airDrag) * (1 - softness), Mathf.Epsilon);
            var mag = BoneDraggerSettings.RESTORE_SPRING_MULTIPLIER * Mathf.Pow(dragger.RestoreSpring, 2f) * dragger.DeltaTime;
            var overLimitMultiplier = Mathf.Max(1f, limitter.AngleRatioLocal(prevLocalRot));
            mag = Mathf.Min(BoneDraggerSettings.MAX_RESTORE_ANGLE, mag * overLimitMultiplier);
            var newRot = Quaternion.RotateTowards(prevLocalRot, initailRotation, mag);
            //DebugLog($"mag={mag}, prevLocalRot={prevLocalRot}, initailRotation ={initailRotation}");

            var saved = target.rotation;
            target.localRotation = newRot;
            newRot = target.rotation;
            target.rotation = saved;
            return newRot;
        }

仕組みは単純で、初期のボーン回転(Quaternion)を保持しておいて、パラメーターに応じて徐々に初期の回転に近づけていくだけです。

集中管理コンポーネント

最後にスキンに付けたすべての揺らしコンポーネントを集中管理するコントローラーです。これは揺らしを実現するために必須のパーツではなく、あくまで設定・管理を楽にしてくれるユーティリティのようなものです。
スキンの親オブジェクトに付ける想定です。

image.png

上記のスクリーンショットにあるインスペクター用ボタンは、エディタ拡張を書いて実装してます。

課題とまとめ

現時点での課題はありますが、モーション設定を簡略化できた点は大きな成果です。

Animatorでもtransform操作でも揺れるという最低限は達成できました。モーション設定をだいぶ楽にできたという自負があります。

しかし、結局各パラメーター変更による影響がわかりやすくできてないし、調整の難しさは残ってしまいました。
単体で動かしていい感じにできたと思っても、シーンヒエラルキー中の別オブジェクトの下に持っていくと挙動が違ったり、あるモーションでいい感じにできたと思っても、別のモーションでおかしな動きになったりして、自分でも期待したクオリティに仕上がってないなと思いながらも、ゲームの完成を優先して妥協して進めることになりました。

他にも以下のような課題・問題があります。

  • 慣性力・復元力が効きすぎたり、あやしい挙動をすることがある
  • X軸回転で左右反転すると乱れる(スケールをマイナス値にして代替可能)
  • 効果のはっきりしない無駄なパラメーターが多い
  • プレイモード再生しながらでないとパラメーター変更しながらの動作確認が難しい

また、次のステップとしては、左右反転させたときにスムーズに補完できるようにしたり、コライダーで揺れる範囲を制限したり、衝突で揺れるようにできたらいいなと考えています。

揺らしコンポーネントとサンプルシーンは GitHubにてMITライセンスで公開しますので、使ってみたいという方はもちろんご自由に、さらなる改善に向けてみなさんの意見やプルリクをお待ちしています!

Rosseta.gif

おまけの宣伝

今回ご紹介した揺らしコンポーネントを使ったミニゲームをSteamで公開中です。
日本語でもプレイできますので、よかったら遊んでみてください。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?