Unityで相補フィルターを使ってジョイコンの姿勢推定をした話

Unity

農工大アドベントカレンダー2025 15日目の記事です.

先日,Unityでジョイコンの姿勢推定をする機会があったので,アドカレ,知見を共有,備忘録として残しておきたいと思います.

姿勢推定のアプローチ

Joy-Conから取得できるセンサデータを用いて,Unity上でオブジェクトの姿勢(回転)を推定するには,いくつかのアプローチが考えられます.本記事では,以下の主要な3つの手法を比較検討し,最終的に採用した手法について解説してみます.

検討手法

  • 単純に回転を積算
  • 相補フィルターによる補正
  • 拡張カルマンフィルターによる補正

ジャイロセンサの単純積分

最も直感的なのは,Joy-Conのジャイロセンサから得られる角速度を時間積分して,現在の角度を算出する方法です.実装は非常にシンプルなところがいいですが,ドリフト現象という問題があります.自己位置推定とかをやったことがある人ならご存知かと思いますが,Joy-Conの通信頻度やUnityのアップデートサイクルによる離散化誤差やセンサ自体が持つ微小なバイアスノイズが積算され続けるため,時間の経過とともに少しずつ回転がズレていってしまいます.例えば静止しているのに勝手に回転するとかの問題が発生します.短時間なら問題ありませんが,数分遊ぶゲームなどでは使えませんねー.

拡張カルマンフィルター(EKF)

ロボット工学や航空宇宙分野で標準的に使われる手法です(この名前を聞くとロボコンの記憶が…).システムの状態(姿勢)の不確実性を正規分布として仮定し,ジャイロの予測と加速度センサの観測を確率的に統合して,最適な値を推定します.とは言っても,何もわからないと思うので,詳しく書いてる有用な記事に詳細はおまかせします.なんとなくで言うと以下のようなメリット・デメリットがあります.

  • メリット: 非線形なシステムに対して非常に高い精度が出せる.
  • デメリット: 行列演算が必要で計算コストが高い上,実装が複雑(ヤコビ行列の計算など).また、パラメータ(共分散行列)の調整がシビア.数学弱々な自分ではムズイ!!!.

Unityで手軽に実装するには,少し面倒いのと,実装してみたけどあんまり相補フィルターと変わらなかったです.

相補フィルター

そこで,今回採用したのがこの手法です.
この手法を端的に言うならジャイロセンサと加速度センサの良いとこ取りをするシンプルなフィルターです.ジャイロセンサーはでは単純に回転を取ります.重力加速度は真下という特性を利用して,加速度センサーで補正してやります.数式チックに書くと相補フィルターは,以下のような数式?概念?で表されます.

\(\)$$\theta_{est} = \alpha \cdot (\theta_{gyro}) + (1 – \alpha) \cdot (\theta_{accel})$$

ここで \(\alpha\) は係数(例: \(0.1\) など)です.
つまり,「積算したジャイロの値(90%)」と「加速度から求めた絶対的な傾き(10%)」を毎フレーム混ぜ合わせることで,動きの滑らかさを保ちつつ,ゆっくりと正しい重力方向へ補正し続けることが可能になります.

実装

理論なんてどうでもええんじゃ,動けばOKです,ということで早速実装を紹介していきます.

環境・ライブラリなど

使用した環境はUnity6です.また,ジョイコンはswitchの右ジョイコンを使っています.
ジョイコンの入力は同じサークルのツヨツヨの先輩が作ってくれた以下のJoyconライブラリを使用しています.

GitHub - tuatmcc/UnityJoyCon: Switch Joy-Con library for Unity.
Switch Joy-Con library for Unity. Contribute to tuatmcc/UnityJoyCon development by creating an account on GitHub.

インストールはREADMEに書いてあるのそこを参照してください.また,アドカレに記事があるのでぜひそちらも面白いので読んでみてください.

Unity向けのJoy-Conライブラリを作った話 - blog.s2n.tech

コード

まず姿勢推定の中身の部分です.詳細な処理とかはコメントとして残しておきます.

using UnityEngine;
using UnityJoycon;

public class JoyConMotion
{
    // 相補フィルターの時定数.この値を調整して補正の強さを決める
    private const float tau = 0.9f;
    
    // 現在の姿勢
    private Quaternion _orientation = Quaternion.identity;
    public Quaternion Orientation => _orientation;

    private JoyCon _joycon;

    public JoyConMotion(JoyCon joycon)
    {
        _joycon = joycon;
    }

    /// <summary>
    /// 毎フレームIMUデータを入力して姿勢を更新
    /// </summary>
    public void Update(float deltaTime)
    {
        if (_joycon == null || !_joycon.TryGetState(out var state)) return;
        
        // 最新のIMUデータを取得
        var lastSample = state.ImuSamples[^1];
        var acc = lastSample.Acceleration;
        var gyro = lastSample.Gyroscope;

        // Joy-Conの軸をUnityの座標系に合わせて入れ替えつつ更新処理へ
        // (ライブラリや持ち方によって軸のXYZマッピングは変わるのでそこら辺はいい感じにいじってください)
        UpdateOrientation(
            new Vector3(acc.Y, acc.X, acc.Z), 
            new Vector3(-gyro.Y, -gyro.X, -gyro.Z), 
            deltaTime
        );
    }

    private void UpdateOrientation(Vector3 acc, Vector3 gyro, float deltaTime)
    {
        // ジャイロスコープによる回転(積分)
        // 前フレームの姿勢に対して、角速度 * 時間 の分だけ回転させる
        Quaternion gyroDelta = Quaternion.Euler(gyro * deltaTime);
        Quaternion gyroOrientation = _orientation * gyroDelta;

        // 加速度センサーによる重力方向の計算
        Vector3 accNorm = acc.normalized;

        // 加速度の大きさが1G(重力)から大きく外れている場合は、
        // 激しく動かしている最中なので加速度センサの値をつかわないようにする
        if (accNorm.sqrMagnitude < 0.9f || accNorm.sqrMagnitude > 1.1f)
        {
            _orientation = gyroOrientation;
            return;
        }

        // 座標系の調整
        accNorm.x = -accNorm.x;
        accNorm.z = -accNorm.z;
        
        // 加速度ベクトル(重力方向)から求まる,ジョイコンが向くべき傾き
        Quaternion accQuat = Quaternion.FromToRotation(Vector3.up, accNorm);

        // 相補フィルターによる合成
        // 時定数tauとdeltaTimeを使って係数を計算
        float alpha = tau / (tau + deltaTime);
        float t = 1f - alpha;

        // ジャイロによる回転をベースに,
        // 加速度による傾きへ少しだけ(tの割合で)近づける
        _orientation = Quaternion.Slerp(gyroOrientation, accQuat, t);
        
        // 正規化
        _orientation = Quaternion.Normalize(_orientation);
    }
    // ずれちゃったとき用のリセット
    public void ResetOrientation()
    {
        _orientation = Quaternion.identity;
    }
}

お次は先程のJoyConMotionを使ったサンプルコードです.上の方で頑張ってコメント書いて若干力尽きたのでコメント適当になってるかもです.すみません.

using System;
using UnityEngine;
using UnityJoycon;
using UnityJoycon.Hidapi;

public class JoyConRight : MonoBehaviour
{
    private Hidapi _hidapi;
    private JoyCon _joycon;
    private JoyConMotion _motion; // 前述の姿勢推定クラス

    private async void Awake()
    {
        // Joy-Conの接続処理
        _hidapi = new Hidapi();
        var deviceInfos = _hidapi.GetDevices(0x057e); 
        if (deviceInfos.Count == 0)
        {
            Debug.LogError("Joy-Conが見つかりません");
            return;
        }

        var deviceInfo = deviceInfos[0];
        var device = _hidapi.OpenDevice(deviceInfo);
        
        // JoyConインスタンスの作成
        _joycon = await JoyCon.CreateAsync(device);
        
        // 姿勢推定クラスの初期化
        _motion = new JoyConMotion(_joycon);
    }

    private void FixedUpdate()
    {
        if (_joycon == null || !_joycon.TryGetState(out var state)) return;

        // 姿勢のリセット(Rボタン)
        if (state.IsButtonPressed(Button.R))
        {
            _motion.ResetOrientation();
        }

        // 姿勢推定の更新計算
        // ここでジャイロと加速度を用いた計算が行われます
        _motion.Update(Time.deltaTime);

        // 計算結果をこのオブジェクトの回転に適用します
        transform.rotation = _motion.Orientation;
    }
    
    private async void OnDestroy()
    {
        if (_joycon != null)
        {
            await _joycon.DisposeAsync();
            _joycon = null;
        }

        if (_hidapi != null)
        {
            _hidapi.Dispose();
            _hidapi = null;
        }
    }
}

後は,オブジェクトにコードをアタッチしてよしなりに,使ってみてください.

終わりに

今回はUnityで相補フィルターを使ってジョイコンの姿勢推定について書いてみました.(アドカレなので割と緩く書いてしまったのですが大丈夫かちょっと心配ですが)
最初は姿勢推定どうやるか戸惑ったので,同じように困っている方の参考になれば幸いです.

コメント

タイトルとURLをコピーしました