C#を使ってUnityで補助骨コンポーネントを作成してみる

こんにちは、最近両親に会ったら一言目に「体系が変わってなくて面白くない」
と言われた石垣康汰です。

今回は、Unity上でC#を利用した補助骨コンポーネントの作成についてを紹介します。
紹介するのは肩によく使うねじ切れ防止や手首の捩れに使えるコンポーネントになります。


まずは、出来たものを…



スクリプトの完成形

JointAutoDriven.cs
※Unity2019.1系で作成しました。バージョンによってはエラーになるかもしれません。


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

public class JointAutoDriven : MonoBehaviour
{
    public Transform AimNode;
    public Transform BendJoint;
    public List TwistJoint;
    [Range(0f,1f)] public float TwistMagnification;
    public Vector3 DefaultPoint;
    public bool SourceParent;
    public Quaternion DefaultQuaternion;
    void Start()
    {

        DefaultPoint = AimNode.transform.localPosition;
        DefaultQuaternion = transform.localRotation;

    }

    void Update()
    {
        Quaternion SourceQuat = transform.localRotation;
        var RotateMat = Matrix4x4.Rotate(DefaultQuaternion).inverse*Matrix4x4.Rotate(SourceQuat);
        var AimPoint = AimNode.transform.localPosition;
        var InvertMatrix = RotateMat.inverse;
        Vector3 Point = RotateMat * new Vector4(AimPoint.x,AimPoint.y,AimPoint.z,1.0f);
        Quaternion AngleQuat = new Quaternion();
        AngleQuat.SetFromToRotation(DefaultPoint.normalized,Point.normalized);
        Matrix4x4 ResultMatrix = Matrix4x4.Rotate(AngleQuat);
        if (BendJoint != null)
        {
          Matrix4x4 ResultMultMatrix = InvertMatrix*ResultMatrix;
          BendJoint.transform.localRotation = ResultMultMatrix.rotation;
        }
        //twistRotate
        var InvertAngleMatrix = ResultMatrix.inverse;
        Matrix4x4 TwistMultMatrix = InvertAngleMatrix*RotateMat;
        Quaternion SlerpQuat = new Quaternion();
        if (SourceParent)
        {
          SlerpQuat = Quaternion.Inverse(TwistMultMatrix.rotation);
        }
        else
        {
          SlerpQuat = TwistMultMatrix.rotation;
        }
        if (TwistJoint != null)
        {
          foreach(Transform Joint in TwistJoint)
          {
            Quaternion ResultTwiatQuat = Quaternion.Slerp(Quaternion.identity,
                                                          SlerpQuat,
                                                          TwistMagnification);
            Joint.transform.localRotation = ResultTwiatQuat;
          }
        }
    }
}

回転の分解をする骨に対し、上記のC#スクリプトを追加してUI上の必要な設定を行い再生をするだけで、ねじ切れ防止と捩れ用の補助骨を実装可能です。


 使用方法

まずはコンポーネントの使用方法を紹介します。
使用方法では下記のようなジョイント構造を使用しています。


arm→コンポーネントを追加するジョイント(回転を分解したいジョイント)
├─armBend→ベンドを受け取るジョイント
├─armTwist→ツイストを受け取るジョイント
└─elbow→エイムノードに入力するジョイント
    └─hand

位置関係は、下記画像のようになっています。


(すべてPrimary AxisはX方向を向いています)

1.C#スクリプトの追加

アセット内にJointAutoDriven.csという名前でC#スクリプトを追加、中身は上のスクリプトをコピー&ペーストします。


2.コンポーネントを追加する

回転を分解したいジョイント(arm)にAddComponentを行い、先ほどのスクリプトを追加します。


3.各種ジョイントを設定する
  • Aim Nodeにはelbowを入力
    (ここには基本的に子階層のジョイントを入れます。)
  • Bend Jointにはベンドを受け取りたいジョイントを入れるので、armBendを入力します。
  • Twist JointのSizeに1と入力、その後ツイストを受け取るジョイントであるarmTwistをElments 0に入力します。
    ※このUIはSizeの値を増やすことで入力できるジョイントを増やすことができます。
  • TwistMagnificationにはツイストを受け取るジョイントに入る回転の倍率を入力します。
    今回は0.5と打ち込んでおきます。
    (0.5の場合、捩れの回転結果が90度分であれば45度分ツイストを受け取るジョイントに回転値が反映されるような形です。)


上記画像のように設定がされていれば、再生ボタンを押し、ジョイントを動かすだけで補助骨が挙動するようになります。

では、次にこのスクリプトの詳細について解説します。

スクリプトの詳細と補助骨の仕組み

1.クラスなどの記述

クラスなどの基本部分の記述になります。
下記画像のようにUnity上でprojectViewから作成したものから、クラス名、ファイル名以外は特に変更行っていません。

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

public class JointAutoDrivn : MonoBehaviour
{
    void Start()
    {

    }
    void Update()
    {

    }
}

・スクリプトの説明 ※一部のみ解説を入れておきます。

    void Start()
    {
    //ここに必要な処理を記述します
    }

この中には再生直後のみに実行される処理を書き込みます。

    void Update()
    {
    //ここに必要な処理を記述します
    }

この中には再生後、Unity上で画面の更新があるたびに実行される処理を書き込みます。
※両方とも内部に書き込む処理は解説含め下に記述しています。

2.コンポーネントのUIを表示

再生直後、再生後に実行される処理を書く前に、Add Componentされた際に表示されるUIを記述します。

UIは下記画像のようなものになります。

public Transform AimNode;
public Transform BendJoint;
public List<Transform> TwistJoint;
[Range(0f,1f)] public float TwistMagnification;
public Vector3 DefaultPoint;
public enum SourceObject
{
    Parent = 0,Children = 1
}
public SourceObject _sourceObject = SourceObject.Parent;

・スクリプトと表示されるUIの説明(各該当UIの画像も載せています。)

public Transform AimNode(BendJoint);

単一のトランスフォームを追加可能なUIを作成します。
処理に必要なエイムノードとベンド回転を受け取るジョイント(以降ベンドジョイント)を指定する為に使用しています。

public List<Transform> TwistJoint;

複数のトランスフォームを追加可能なUIを作成します。
ツイスト回転を受け取るジョイント(以降ツイストジョイント)を指定する為に使用しています。
Sizeを増やすと指定可能な数が増やすことができます。

[Range(0f,1f)] public float TwistMagnification;

フロートの値を持つスライダーを作成します。Range()には最小値と最大値を入力します。
今回はツイスト回転を抽出した後にツイスト回転を受け取るジョイントに流す回転の倍率を指定しています。

public Vector3 DefaultPoint;

座標などを表示するUIを作成します。
今回は初期の向きのベクトルを表示しています。


public bool SourceParent;
チェックボックスを作成します。
今回はツイストを受け取るジョイントがソースの親階層にあるか、子階層にあるかでチェックを切り替え受け取る回転を逆転させる分岐を行っています。


public Quaternion DefaultQuaternion;
クォータニオンを受け取るUIを作成します。
今回は初期のクォータニオンを表示しています。

3.再生直後に実行される処理の記述

void Start内に記述する処理になります。

再生直後とはここを押した直後のことですね。


void Start()
{

    DefaultPoint = AimNode.transform.localPosition;
    DefaultQuaternion = transform.localRotation;

}

・スクリプトの説明

DefaultPoint = AimNode.transform.localPosition

特定のトランスフォーム(AimNode)から位置情報を取得し、DefaultPointに代入します。
今回DefaultPointはUIになっていますのでそこに値が反映されます。

DefaultQuaternion = transform.localRotation;

特定のトランスフォーム(コンポーネントを追加したジョイント)から回転を取得し、DefaultQuaternionに代入します。
今回DefaultQuaternionはUIになっていますのでそこに値が反映されます。

※Unityは回転値を取得するとオイラーではなくクォータニオンで取得されます。
また、それをオイラーに変換することなく他のトランスフォームに反映できます。

それぞれ値を取得している理由ですが…
両方とも回転の計算を行う際に初期値から現在値の差分を利用しているためです。
こうすることでジョイントの向きに最初から値があっても処理が問題なく進むようになります。

4.回転分解とそれをそれぞれのノードに反映する記述

void Update内に記述する処理になります。
ここで行っていることは先述のとおりvoid Startと異なり、常に処理が実行されます。

void Update()
{
    Quaternion SourceQuat = transform.localRotation;
    var RotateMat = Matrix4x4.Rotate(DefaultQuaternion).inverse*Matrix4x4.Rotate(SourceQuat);
    var AimPoint = AimNode.transform.localPosition;
    var InvertMatrix = RotateMat.inverse;
    Vector3 Point = RotateMat * new Vector4(AimPoint.x,AimPoint.y,AimPoint.z,1.0f);
    Quaternion AngleQuat = new Quaternion();
    AngleQuat.SetFromToRotation(DefaultPoint.normalized,Point.normalized);
    Matrix4x4 ResultMatrix = Matrix4x4.Rotate(AngleQuat);
    if (BendJoint != null)
    {
        Matrix4x4 ResultMultMatrix = InvertMatrix*ResultMatrix;
        BendJoint.transform.localRotation = ResultMultMatrix.rotation;
    }
    var InvertAngleMatrix = ResultMatrix.inverse;
    Matrix4x4 TwistMultMatrix = InvertAngleMatrix*RotateMat;
    Quaternion SlerpQuat = new Quaternion();
    if (SourceParent)
    {
        SlerpQuat = Quaternion.Inverse(TwistMultMatrix.rotation);
    }
    else
    {
        SlerpQuat = TwistMultMatrix.rotation;
    }
    if (TwistJoint != null)
    {
        foreach(Transform Joint in TwistJoint)
        {
            Quaternion ResultTwiatQuat = Quaternion.Slerp(Quaternion.identity,
            SlerpQuat,
            TwistMagnification);
            Joint.transform.localRotation = ResultTwiatQuat;
        }
    }
}

・スクリプトと行っている事の説明

  • Step1
    Quaternion SourceQuat = transform.localRotation;
    var RotateMat = Matrix4x4.Rotate(DefaultQuaternion).inverse*Matrix4x4.Rotate(SourceQuat);

    コンポーネントを追加したジョイントの回転を取得しクォータニオンに代入
    その後、そのクォータニオンをマトリクスに変換します。


  • Step2
    var AimPoint = AimNode.transform.localPosition;

    エイムノードの位置情報をAimPointに代入します。


  • Step3
    var InvertMatrix = RotateMat.inverse;

    Step1 で取得した回転マトリクスを反転します。


  • Step4
    Vector3 Point = RotateMat * new Vector4(AimPoint.x,AimPoint.y,AimPoint.z,1.0f);
    Quaternion AngleQuat = new Quaternion();
    AngleQuat.SetFromToRotation(DefaultPoint.normalized,Point.normalized);
    Matrix4x4 ResultMatrix = Matrix4x4.Rotate(AngleQuat);

    Vector3 Pointでは Step1 で取得した現在の回転マトリクスと、Step2 で取得した位置情報を乗算して現在の向きをベクトルに変換します。
    その後、新たにクォータニオンを作成(Quaternion AngleQuat)、それに最初に取得した初期の位置情報(DefaultPoint)とVector3 Pointを比較して間の角度を求めてクォータニオンに代入します。
    最後にそれをマトリクスに変換しています。

※DefaultPoint(Point).normalizedとありますが、normalizedは
ベクトルの正規化を行っています。


  • Step5
    if (BendJoint != null)
    {
    Matrix4x4 ResultMultMatrix = InvertMatrix*ResultMatrix;
    BendJoint.transform.localRotation = ResultMultMatrix.rotation;
    }

    Step4 で求めたマトリクスと Step3 で求めた逆マトリクスを乗算して、ベンドジョイントに代入する回転マトリクスを求めます。
    求めた回転マトリクスの回転成分のみを利用してベンドジョイントに代入します。
    この処理はベンドジョイントに入力がない時にスキップするのでツイストのみを使用する事も可能となっています。


  • Step6
    var InvertAngleMatrix = ResultMatrix.inverse;
    Matrix4x4 TwistMultMatrix = InvertAngleMatrix*RotateMat;

    Step4 で求めたマトリクスを逆にしています。
    そしてそのマトリクスと Step1 で求めたマトリクスを乗算しています。


  • Step7
    Quaternion SlerpQuat = new Quaternion();
    最終的にツイストジョイントに代入するクォータニオンを宣言します。

  • Step8
    if (SourceParent)
    {
    SlerpQuat = Quaternion.Inverse(TwistMultMatrix.rotation);
    }
    else
    {
    SlerpQuat = TwistMultMatrix.rotation;
    }

    UIにあったチェックボックス(SourceParent)の状態によって Step6 にて作成したマトリクスの回転成分を反転させて Step7 で作成したSourceParentに代入するかを判別します。
    (下記画像のUIになります)


  • Step9
    if (TwistJoint != null)
    {
    foreach(Transform Joint in TwistJoint)
    {
        Quaternion ResultTwiatQuat = Quaternion.Slerp(Quaternion.identity,
        SlerpQuat,
        TwistMagnification);
        Joint.transform.localRotation = ResultTwiatQuat;
    }
    }

    Quaternion.Slerpを利用して回転0から Step8 で求めたSlerpQuatまでの回転に、UIで設定したTwistMagnificationを掛けた値を新しいクォータニオンに代入します。
    そしてそのクォータニオンをツイストジョイント全てに代入します。
    こちらもベンドの時同様ツイストジョイントが無ければ処理をスキップするのでベンドのみの実装を可能にしています。

実はこれ…

この処理、maya上ではほぼ同一のものを、ユーティリティノードを利用して構築することが可能です。
むしろ、今回これを作成した時はmayaにて先にノードで処理を作ってそれを参考に作りました。
同じような処理が意外とあるものなんですね。
※Unityの方がオイラーに変換する必要が無いので多少mayaよりも楽に感じました。


最後に

今回はC#を利用した補助骨コンポーネント作成を紹介しました。

作成したコンポーネントの活用方法ですがmayaなどとの連携でしょうか。
補助骨の動きをアニメーションのフルベイクでゲームエンジンに持ち込むのではなく、ゲームエンジン上で同じ補助骨処理の実装が出来るようになります。
メリットとしては補助骨のアニメーションがいらないような場合に補助骨部分を切り離したりできる事で挙げられます。
コンポーネント設定が手間だと感じるようでしたら、maya上で補助骨の情報をjsonファイルで出力、Unityでそのjsonを読み込む…なんてこともできます。
試してもいいかもしれませんね。(少しjsonの読み込みには少しクセがありますが…)

それでは、今回はこの辺りで…!

Author: ishigakikota