【Unity】加速度センサのみでスマホのゲーム画面を3DoFっぽくする

## 概要
UnityでARアプリのような3DoD表現をしたい！と思った時、ARFoundationを使えれば問題ないですが、低スペックのAndroid端末だとARCoreに対応しておらず、ARFoundationの機能が使えないといった状況があります。
さらにARCoreに対応していない端末だとジャイロセンサが搭載されていない場合もあり、加速度センサしか端末トラッキングに使用できるセンサがないといったことがあります。

今回はそんな場合でもできるだけ3DoFっぽい表現に近づける方法を紹介します。

## 環境
Unity 2021.3.24f1

## スクリプト(2DoF)
加速度センサに常にかかる値として重力の存在があり、重力と同じ軸であるY軸の回転は取得できませんが、X軸Z軸の回転角度は加速度センサの値のみで算出可能です。
```c#:AccelerationScript
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class AccelerationScript : MonoBehaviour
{
    private Camera mainCamera;
    private List<Vector3> accelerationList = new List<Vector3>();

    private void Start()
    {
        mainCamera = Camera.main;
    }

    void Update()
    {
        // 加速度センサ値
        var accelerate = Input.acceleration;

        // 加速度センサ値のノイズを省くために最新値を一定個数保存
        if (accelerationList.Count > 5)
        {
            accelerationList.RemoveAt(0);
        }
        accelerationList.Add(accelerate);
 
        // なまし値の算出
        var namasi = new Vector3(accelerationList.Average(x => x.x), accelerationList.Average(x => x.y), accelerationList.Average(x => x.z));

        // なました値を元に、カメラの回転を設定
        var yz = new Vector3(0, namasi.z, namasi.y);
        var xEuler = Vector3.SignedAngle(Vector3.forward, yz, Vector3.right);
        var yEuler = 0;
        var xy = new Vector3(namasi.x, namasi.y, 0);
        var zEuler = Vector3.SignedAngle(xy, Vector3.up, Vector3.forward);
        mainCamera.transform.eulerAngles = new Vector3(xEuler, yEuler, zEuler);
    }
}
```
このスクリプトをシーン上に配置した適当なオブジェクトに付与することで、スマホの傾きに応じてゲーム画面を傾けることができます。

端末の問題なのか、加速度センサのノイズがひどく最新値のみで角度を算出すると画面がブレブレになってしまいました。そのため、若干ラグは生じますがセンサ値の最新値を一定時間保存しておき、その平均値を角度の算出に使用しています。

## スクリプト(2DoF + α)
前述したとおり、Y軸の角度は加速度センサのみでは取得できませんが、それでもなんとかY軸も回転する方法はないかと検討した結果になります。
```c#:AccelerationScript
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class AccelerationScript : MonoBehaviour
{
    private Camera mainCamera;
    private List<Vector3> accelerationList = new List<Vector3>();

    private float yEuler = 0;
    private int integer = 0;
    private readonly float gain = 0.1f;
    private readonly float maxSpeed = 3f;
    
    private void Start()
    {
        mainCamera = Camera.main;
    }

    void Update()
    {
        // 加速度センサ値
        var accelerate = Input.acceleration;

        // 加速度センサ値のノイズを省くために最新値を一定個数保存
        if (accelerationList.Count > 5)
        {
            accelerationList.RemoveAt(0);
        }
        accelerationList.Add(accelerate);
 
        // なまし値の算出
        var namasi = new Vector3(accelerationList.Average(x => x.x), accelerationList.Average(x => x.y), accelerationList.Average(x => x.z));

        // Y軸回転を有効にする傾きの条件を判定(条件は任意に変更可能)
        if (Mathf.Abs(namasi.x) > 0.2f && Mathf.Abs(namasi.x) < Mathf.Abs(namasi.y))
        {
            var direct = namasi.x > 0 ? 1 : -1;
            if (integer * direct < 0) integer = direct;
            else integer += direct; // 激しくぶれないために速度は徐々に上げる

            var speed = Mathf.Clamp(integer * gain, -maxSpeed, maxSpeed);

            yEuler += speed;

            if (yEuler > 180) yEuler -= 360;
            else if (yEuler < -180) yEuler += 360;
        }
        else
        {
            integer = 0;
        }
        
        // なました値を元に、カメラの回転を設定
        var yz = new Vector3(0, namasi.z, namasi.y);
        var xEuler = Vector3.SignedAngle(Vector3.forward, yz, Vector3.right);
        var xy = new Vector3(namasi.x, namasi.y, 0);
        var zEuler = Vector3.SignedAngle(xy, Vector3.up, Vector3.forward);
        mainCamera.transform.eulerAngles = new Vector3(xEuler, yEuler, zEuler);
    }
}
```
端末を正面に持って左側に傾けたときはY軸を左側に回転し、右側に傾けたときはY軸を右側に回転します。

## まとめ
かなりなんちゃって感のある動作をしますが、加速度センサのみではこの表現が限界だと感じました。
環境の特徴点をトラッキングして位置合わせを行う方法もあるようですが、個人での実装は相当難しいので、素直に有料のライブラリを使用するのが良いと思います。