【Unity】スマホ端末の傾き角を知る方法【決定版】【加速度センサー】【ジャイロスコープ】

本記事は Advent Calendar 2022 「Unity #2」 2日目の記事です。

【1日】 [Unity WebGL で <canvas> で文字を描画する](https://qiita.com/gtk2k/items/18575dcc2b5d7ad65368) (@gtk2k さん)
【2日】 本記事
【3日】 [ARスタンプラリーを作ったよ、ていう記事](https://qiita.com/SanQ/items/35917d1e28a9872a179b)　（ @SanQ ）


# TL;DR
- 端末の向きを知るなら「Input.gyro.attitude」の値をうまく使ってやろう！
- 一応加速度センサーを使う手法もあるけど、X軸、Z軸のみ有効でY軸回転(Yaw) は正しくとれないぞ！
- 制作物の3Dモデルでは右手系・左手系の違いがあるので注意しよう

# 検証環境
- Unity2021 LTS
- MacOSX 12.4


# Unityで利用可能なセンサー
スマートフォンの状態取得のために利用可能なセンサーは以下の通りです

| センサー | 説明 | Unity 公式Document |
|--- |---- | ---- |
|加速度センサー | 重力加速度センサーです。下記図の端末のXYZ各軸にかかる重力加速度の大きさを取得可能です | https://docs.unity3d.com/ja/2021.3/ScriptReference/Input-acceleration.html |
|ジャイロセンサー | 角速度 / 端末の傾き検知用センサーです. | https://docs.unity3d.com/ja/2021.3/ScriptReference/Gyroscope.html|

## 加速度センサー
よく `端末を動かした時の(加)速度` を取得するためのセンサーと誤解されますが、実際には
`端末にかかる重力加速度ベクトルを端末のローカル各軸に分解したときの重力の大きさ` を取得するためのセンサーです。

ちなみに加速度センサーは `右手系` のためZ軸は`手前が正` であることに注意しましょう。
(Unity は左手系なのでZ軸奥が正で、手前が負です)
![スクリーンショット 2022-11-18 14.29.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/1c3eeacc-7608-6af5-13bd-2bece62a55bb.png)


Unityでは `Input.acceleration` で各軸にかかる重力加速度の大きさがわかります。
基本的にはどの値も `|x| ≦ 1.0 ` なことが多いですが、端末をぶん回したり運動させると |x|が1を超える場合もあります。
1.0 の意味ですが  `1.0G` という意味になります。
仮に2.0 となったら2倍の重力がかかっていることになります。（日常生活レベルでは基本1.0以下になることがほとんどです）

## ジャイロセンサー

姿勢（角度）や、角速度を取得するのに用いられるセンサーです。
`Input.gyro.rotationRateUnbiased` または`Input.gyro.rotationRate` で各軸周りの各速度(X軸:pitch, Y軸: Yaw, Z軸:Roll)を取得できます(※1)

また、 `Input.gyro.attitude` で四元数(Quaternion) として姿勢状態を取得可能です。

※1: 今回紹介している軸はUnity準拠です。外部サイト等では上方向がZ軸だったり右手系だったり色々違いがあるのでご注意ください。


# 実際の使い方と注意点
## 加速度センサー
加速度センサーの値から、各軸における重力加速度の大きさがわかります。
それを元に端末の傾きを計算することが可能です。

加速度センサーから角度を取得するには
``` sample.cs

Vector3 angle = Vector3.zero;
angle.x = Mathf.Asin(Mathf.Clamp(Input.acceleration.x, -1, 1)) * Mathf.Rad2Deg;
angle.y = Mathf.Asin(Mathf.Clamp(Input.acceleration.y, -1, 1)) * Mathf.Rad2Deg;
angle.z = Mathf.Asin(Mathf.Clamp(Input.acceleration.z, -1, 1)) * Mathf.Rad2Deg;
```
のような形で オイラー角を取得可能です

![スクリーンショット 2022-11-18 14.29.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/1c3eeacc-7608-6af5-13bd-2bece62a55bb.png)

先ほどの図では 加速度としてはおおよそ( 0, -1.0, 0 )が返ってきます。

もし、この図で液晶画面を段々と下に向けるとZ軸の加速度が大きくなります→X軸回転
また、スマホ画面を時計回りに回転して横画面にすると、X軸の加速度が大きくなります→Z軸回転

この特性から Pitch(X軸回転）とRoll（Z軸回転）は加速度センサーから計算ができそうです。

しかし、この図でYaw（Y軸回転）を考えてみると、常に重力加速度は ( 0, -1.0, 0 ) 一定になります。
以上からわかることとしては

`加速度センサー` では `Pitch(X軸回転）とRoll（Z軸回転）の推定は可能` ですが `Yaw の回転角の推定には加速度センサーは利用しない方が良い` ことがわかります。よって、基本的には「ジャイロ」を利用することがオススメです。

## ジャイロセンサー
### 利用の準備
利用前に `Input.gyro.enabled = true;` を事前に実行しましょう。
もし [UniRx](https://github.com/neuecc/UniRx) が利用可能であれば、初期化用MonoBehaviour 継承クラスで以下のようなサンプルクラスを生成することでしっかりジャイロを有効化しつつ、ReactiveProgrammingの形で入力値を取り扱えるようになるのでおすすめです。

```sampleSensorStream.cs

using System;
using UniRx;
using UnityEngine;

public class SensorStream : IDisposable, IAccelerationStream, IGyroStream
{
    private readonly CompositeDisposable m_disposable = new CompositeDisposable();
    private readonly Vector3ReactiveProperty m_accelProperty = new Vector3ReactiveProperty(Vector3.zero);
    private readonly Vector3ReactiveProperty m_accelAngleProperty = new Vector3ReactiveProperty(Vector3.zero);
    private readonly Vector3ReactiveProperty m_gyroProperty = new Vector3ReactiveProperty(Vector3.zero);
    private readonly QuaternionReactiveProperty m_gyroAttitudeProperty = new QuaternionReactiveProperty(Quaternion.identity);

    public SensorStream()
    {
        Input.gyro.enabled = true;
        
        m_accelProperty.AddTo(m_disposable);
        m_gyroProperty.AddTo(m_disposable);
        Vector3 angle = Vector3.zero;
        
        Observable.EveryGameObjectUpdate().Subscribe(_ =>
        {
            m_accelProperty.Value = Input.acceleration;
            angle.x = Mathf.Asin(Mathf.Clamp(Input.acceleration.x, -1, 1)) * Mathf.Rad2Deg;
            angle.y = Mathf.Asin(Mathf.Clamp(Input.acceleration.y, -1, 1)) * Mathf.Rad2Deg;
            angle.z = Mathf.Asin(Mathf.Clamp(Input.acceleration.z, -1, 1)) * Mathf.Rad2Deg;
            m_accelAngleProperty.Value = angle;
            m_gyroProperty.Value = Input.gyro.rotationRateUnbiased * Mathf.Rad2Deg;
            m_gyroAttitudeProperty.Value = Input.gyro.attitude;

        }).AddTo(m_disposable);
    }

    public void Dispose()
    {
        if (m_disposable.IsDisposed) return;
        m_disposable.Dispose();
    }



    /// <summary>
    /// 値取得用ストリーム発行
    /// </summary>
    /// <returns></returns>
    IObservable<Vector3> IAccelerationStream.GetStream() { return m_accelProperty;}

    /// <summary>
    /// 加速度センサーから角度変換を行なった時のストリーム
    /// </summary>
    /// <returns></returns>
    IObservable<Vector3> IAccelerationStream.GetAccel2AngleStream()
    {
        return m_accelAngleProperty;
    }
    
    /// <summary>
    /// 値取得用ストリーム発行
    /// </summary>
    /// <returns></returns>
    IObservable<Vector3> IGyroStream.GetStream(){ return m_gyroProperty;}

    /// <summary>
    /// 傾き取得用ストリーム発行
    /// </summary>
    /// <returns></returns>
    IObservable<Quaternion> IGyroStream.GetRotationStream() => m_gyroAttitudeProperty;
    
    /// <summary>
    /// 指定フレーム数平均を行なったストリームの発行
    /// </summary>
    /// <param name="averageFrame"></param>
    /// <returns></returns>
    IObservable<Vector3> IGyroStream.GetAveragedStream(int averageFrame)
    {
        var newProperty = new Vector3ReactiveProperty().AddTo(m_disposable);

        m_gyroProperty.Buffer(averageFrame, 1)
            .Subscribe(list =>
            {
                Vector3 ret = Vector3.zero;
                foreach (var item in list)
                {
                    ret = ret + item;
                }

                ret /= averageFrame;

                newProperty.Value = ret;
            }).AddTo(m_disposable);

        return newProperty;
    }
```

### 値の利用の注意点

https://qiita.com/fuqunaga/items/b1a3e38af71f062f0781 
上記記事でも解説がされていますが、`右手系` ・ `左手系` の違いは注意しましょう。
基本的にはAndroid/iOS 双方とも以下のように値の加工が必要です。
1. xyzの値を反転させる
2. ZとYの値を入れ替える

```ModelRotator.cs
using System;

using UniRx;
using UnityEngine;

public class ModelRotator : MonoBehaviour, IDisposable
{
    private readonly CompositeDisposable m_disposable = new CompositeDisposable();
    public void Subscribe(IGyroStream gyroStream)
    {
        var tr = this.transform;

        Quaternion cache = Quaternion.identity;
        gyroStream.GetRotationStream().Subscribe(rot =>
        {
            cache.x = -rot.x;
            cache.y = -rot.z;
            cache.z = -rot.y;
            cache.w = rot.w;
            tr.localRotation = cache;
        }).AddTo(m_disposable);
    }

    private void OnDestroy()
    {
        Dispose();
    }

    public void Dispose()
    {
        if (m_disposable.IsDisposed) return;
        m_disposable.Dispose();
    }
}
```



また、Z軸周りの回転ですが、OSごとに挙動が変わります。
| OS | 挙動 |
|--- |--- |
|iOS| アプリ起動したタイミングの方向をZ軸回転0°としています。<br>つまりアプリ起動タイミングによってはZ軸の方向が異なるため、全ユーザー同じ挙動にするためにはアプリ内部で調整が必要です。 |
|Android| 他の地磁気センサー等を利用していて `北方向が0°` となっています。<br>また、他のセンサーを扱っている関係上、端末によっては初期化にしばらく時間がかかるため初動が安定しないケースがあります。|


# 実装結果
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/9dd6134b-a2dc-47a4-d523-7ddcc308f343.gif">

検証で用いたスマートフォンモデルは以下のAssetを一部検証用に改変しております。
https://assetstore.unity.com/packages/3d/props/electronics/free-smartphone-90324


# まとめ
- 加速度センサーで傾き取得をする方法は、よほどのことでない限り選択する必要はない
- ジャイロを扱う場合は座標系の違いに気を付ける
- プラットフォームによる挙動の違いを念頭においておく

# 参考

https://github.com/neuecc/UniRx

https://qiita.com/fuqunaga/items/b1a3e38af71f062f0781