Virtual Chara開発 - その4

店舗接客などに使える仮想のキャラクターが扱えるアプリ開発をしました。そちらの説明はこちらになります。
https://qiita.com/NestVisual/items/fe4dd168178e27156682

環境：
Windows 10
Unity 2018.4.1
RealSense D435i

###Nuitrackの関節の回転制限:
キャラクターの動きに大きな問題があったのは、トラッキングデータが不正確だと手足が不気味な位置に回転してしまいました。これはかなりの頻度で起きて、精度を上げるためにやることはほとんどありませんでした。このような事態が発生した場合に対処するために、2つの解決策を思いつきました。最初の方はかなりわかりやすく、簡単に実装できました。利用者がカメラに対して相対的に移動できる最小距離と最大距離を設定しました。現状利用者とカメラの距離はその２つの値の範囲内にない場合、骨格トラッキングはキャラに反映されません。

```C#:sample2.cs
private void Update()
{
    ProcessSkeleton();
}

private void ProcessSkeleton()
{
    nuitrack.Skeleton skeleton = CurrentUserTracker.CurrentSkeleton;
    UserIsWithinRange = (!(userDistanceCm < _minCamDistance) && !(userDistanceCm > _maxCamDistance));

    if (skeleton != null && UserIsWithinRange)
    {
        IsTrackingReflected = true;
        AnimateSkeleton(skeleton);
    }
    else    {IsTrackingReflected = false;}
}
```

2つ目の解決策の方が難しいでした。関節の回転が不自然の限定に拘束設定すれば、手足はあまり不自然な位置に動かないと思いました。まず、Unityエディタでモデルの関節を手動で回転させて限界を見つけました。最大値と最小値で、不自然に見える限界の回転を記しました。しかし、これらの回転は各関節の局所的なものであったのに対し、Nuitrackからの回転はすべてグローバルなものです。 このため、トラッキングされた回転をモデルの関節に設定する前に、ローカル回転に変換必要があります。トラッキングされた関節のグローバル回転に親関節の回転の逆数を乗算した値をローカル回転に反映させれば良いと考えました。

```C#:
Quaternion localRotation = worldRotation * Quaternion.Inverse(parentRotation);
```

しかし、常に手足があらゆる種類の回転で回転していたので、結果は思った通りに完全違いました。色々のテストの後で、ついにNuitrackフォーラムで問題を投稿しました。Nuitrackのスタッフが、掛け算の順番が関係しているのを教えてくれました。それは考えなかったのですが、掛け算の順番を逆にしたら、回転が正しく設定されました。Quaternion は行列なので、掛け算の順番が重要です。これで関節の回転を制限する事に一歩近づきました。

次の問題は、前述の関節回転制限を使用してみたときに発生しました。これらの回転値は、Unity インスペクタでいじって見つけました。Unity インスペクタでは、負と正の両方の回転値を使用ができます。しかし、Unityは実行時に 0～360 の角度しか使用しません。言い換えれば、0～180度のインスペクタ回転値は実行時の値に正しく対応しますが、-180～0度のインスペクタ値は180～360度の実行時の値に対応します。これを考慮するために、eulerAnglesでローカル関節の回転が180を超えるかどうかに応じて、各軸から360を差し引きました。そしたら、最小回転と最大回転の同じ軸値の間で各軸の値をクランプができました　Mathf.Clamp(回転ｘ, 最小ｘ, 最大ｘ)。 これは以下のGetConstrictedRotation()メソッドで行います。

```C#:
private void RotateJointLocal(ModelJoint modelJoint, Quaternion worldRotation, bool setConstraint)
{
    Quaternion parentRotation = Quaternion.identity;
    if (_newRotations.ContainsKey(modelJoint.parentJointType)) {
        parentRotation = _newRotations[modelJoint.parentJointType];
    }
    Quaternion localRotation = Quaternion.Inverse(parentRotation) * worldRotation;
    Vector3 localEuler = localRotation.eulerAngles;

    if (modelJoint.jointType == nuitrack.JointType.Torso)   localEuler.x = 0;
    if (modelJoint.jointType == nuitrack.JointType.Waist)   localEuler.x = 0;

    if (setConstraint)  localEuler = GetConstrictedRotation(localEuler, modelJoint.jointType);

    Vector3 currentEuler = modelJoint.bone.localEulerAngles;

    modelJoint.bone.localEulerAngles = new Vector3(
        Mathf.LerpAngle(currentEuler.x, localEuler.x, _moveSpeed * Time.deltaTime),
        Mathf.LerpAngle(currentEuler.y, localEuler.y, _moveSpeed * Time.deltaTime),
        Mathf.LerpAngle(currentEuler.z, localEuler.z, _moveSpeed * Time.deltaTime));
}

private Vector3 GetConstrictedRotation(Vector3 euler, nuitrack.JointType jointType)
{
    var minRot = _jointMinRotations[jointType];
    var maxRot = _jointMaxRotations[jointType];

    float x = (euler.x > 180) ? euler.x - 360 : euler.x;
    float y = (euler.y > 180) ? euler.y - 360 : euler.y;
    float z = (euler.z > 180) ? euler.z - 360 : euler.z;

    x = Mathf.Clamp(x, minRot.x, maxRot.x);
    y = Mathf.Clamp(y, minRot.y, maxRot.y);
    z = Mathf.Clamp(z, minRot.z, maxRot.z);

    return new Vector3(x, y, z);
}
```

このように、モデル四肢の回転を制限する事が出来た為、不気味な動きを軽減させる事が出来ました。
次回はキャラのアニメーションに関して投稿します。それではまた