第3回「趣味ナレジ」 ~UnityでRigidBodyを使わずにtransform.rotationで物体を制御してみた編~

おはようございます!育良です!
またしてもUnityの記事です!本当に好きですねこいつ!

今回は、RigidBody.angularVelocityを使わずに、transform.rotationから角速度を計算できないか、ということに挑戦したので、その話をします。

え、なにがしたいんそれ

ごもっともな疑問だと思います。必要な局面あるのかなコレ……

私は、以前記事にしたとおりVtuberのようなものを運営しています(気まぐれ更新)。

最近その撮影環境をUnity 2018.3 に移行したのですが、以前使っていた VRTK という便利なアセットが使えなくなってしまい、ならばと物を掴む機能やプレーヤー移動の機能などを、Unity-C# で自作するなどしていました。

【Unity】2018.3でVRTK無しの撮影環境(進捗動画)(1分30秒) #ikr_neko_vt
https://youtu.be/9NwxITFmm1w

「RigidBody.angularVelocityを使わずに~」というのは、その中で必要になったものです。

掴む機能

掴む機能は「対象となるオブジェクトをコントローラーの子オブジェクトにする(大雑把に言うと)」という仕組みで実装していました。脳筋です。

このとき、スクリプトさえアタッチすれば、RigidBodyがついているオブジェクトも掴むことはできるのですが、掴んだまま移動するとオブジェクトが手についてこず、その場に置いてけぼりになるという問題が発生しました。

これはもちろん、一旦物理演算を無効化 (= RigidBody.isKinematic を有効化) して、手から放す瞬間に戻せばよいのですが、この実装の場合掴んだオブジェクトを投げようとしても手の運動量が伝わらず、その場にストンと落ちるだけになってしまいます。

そこで、「だったら手で動かしている最中のKinematicなオブジェクトの運動量を計算しておいて、投げる瞬間にRigidBodyに渡してあげればいいんじゃないか?」と考えました。脳筋です。

速度は簡単だったけど回転がやばい

RigidBodyからではなく、持ったオブジェクトの位置や回転から運動量を求めるには、フレーム毎の移動距離を計算してやればよいはずです。

速度に関してはかなりあっさり実装できました。下記のような感じにすれば実装できます。

private Rigidbody rb = null;
private float speed;
private Vector3 latestPos;

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

void FixedUpdate()
{
        speed = ((transform.position - latestPos) / Time.deltaTime).magnitude;
        latestPos = transform.position;
}

void ReleaseItem()
{
        rb.Velocity = speed;
}

厄介なのは回転です。

Unityにおける回転は、まずローカル座標・ワールド座標で回転軸がわかりにくくなりがちだった上、オイラー角やらクォータニオンやらなにやらいろいろあり、「回転の差分」を出して「角速度」を出すのに手間取りました。

ここではサクッと「たぶんこんなかんじでいける」というサンプルを貼ります。

using UnityEngine;
using System.Collections;

namespace Ikr.Test
{
    public class GetAngularSpeedTest : MonoBehaviour
    {
        private Rigidbody rb = null;
        private Vector3 v3;

        private Quaternion latestRotation;
        private Vector3 angularSpeed;

        // Use this for initialization
        void Start()
        {
            rb = GetComponent();

            // 斜めに回転
            v3 = new Vector3(10, 5, 3);
        }

        void FixedUpdate()
        {
            // 現在の角速度をRotationから計算
            if (!rb.isKinematic)
            {
                GetAngularSpeed();
                Debug.Log(angularSpeed);
            }

            // 回転開始
            if (Input.GetMouseButtonDown(0))
            {
                Debug.Log("GetMouseButtonDown 0");
                rb.angularVelocity = v3;
            }

            // 物理演算を無効化、回転を停止
            if (Input.GetMouseButtonDown(1))
            {
                Debug.Log("GetMouseButtonDown 1");
                rb.isKinematic = true;
                rb.angularVelocity = Vector3.zero;
            }

            // 物理演算を有効化、計算した角速度を入れ直す
            if (Input.GetMouseButtonDown(2))
            {
                Debug.Log("GetMouseButtonDown 2");
                rb.isKinematic = false;
                rb.angularVelocity = angularSpeed;
            }
        }

        void GetAngularSpeed()
        {
            // 回転量の前フレームとの差を取得
            Quaternion quaternionDiff = transform.rotation * Quaternion.Inverse(latestRotation);
            latestRotation = transform.rotation;

            // Quaternionを角度と回転軸に変換
            quaternionDiff.ToAngleAxis(out float angle, out Vector3 axis);

            // 角度と回転軸から角速度を計算
            Vector3 angularDiff = axis * angle * Mathf.Deg2Rad;
            angularSpeed = transform.rotation * (angularDiff / Time.deltaTime);
        }
    }
}

GetAngularSpeedメソッドが、transform.rotationから角速度を割り出している部分です。FixedUpdateメソッド内での使用を想定しています。

処理としては、大まかに下記の流れです。

① 現在のrotationと一つ前のフレームのrotation(の逆Quaternionとの積)でフレーム間の回転をとる

② フレーム間の回転(Quaternion)を角度・回転軸に分解する(Quaternion.ToAngleAxisでできる)

③ 角度(ラジアン)と回転軸をベクトル化し、秒間の回転速度を出す

サンプルなので諸々作りが甘いですが、大まかにはこれでisKinematic有効の条件下での回転をとることができます。

現状の課題として、これをこのまま先程のVR撮影環境に放り込んでも、回転方向が想定したとおりの方向にならない(投げた瞬間逆回転したり想定通りに回転したりバラバラ)という問題があります。

コレに関しては、正確な表現を行うためには座標系の扱いをもろもろ実装してやる必要がありそうですが、ぶっちゃけこの状態でも小道具をぶん投げたときに派手に吹っ飛んでくれるので、撮影環境としては必要十分です。

取り敢えずこれはこのままにして、他のもっと必要なものをつくっていきたいなーと思います。