ARFoundationでヘッドトラッキング

ARFoundationを用いてユーザーの視点をトラッキングすることで、以下のような錯視空間を表現することができます。
視点と画面上の空間を連動させることで、ユーザーからはスマートフォン画面内部に箱型の空間が存在するように感じられます。

![ezgif-7-d45de45d8a2d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13147/65b3293e-4fc3-aac0-951e-c4a49ac660a1.gif)

フェイストラッキングを使用しているため、顔とデバイスカメラの位置関係によりシーンの見え方が変化します。
以下に実装サンプルのリンクも貼っていますので、サンプルをビルドして、デバイスの向きを変えたり自分の顔を移動させてみてください。

- https://github.com/draf3/ARFoundationParallaxTest

検証したUnityやパッケージのバージョン、端末は以下になります。

- Unity: 2020.1.1f1
- AR Foundation: 3.1.3
- ARKit XR Plugin: 3.1.3
- ARKit Face Tracking: 3.1.3
- 端末: iPhoneXs
- iOSシステムバージョン: 13.6.1

また、デバイスの向きはポートレートモードのみに対応しています。

# 実装について
実装に関しては、「TheParallaxView」をARFoundationに移植させていただきました。
オリジナルはARKitPluginで実装されています。

「TheParallaxView」の記事から、カメラのプロジェクションや各種パラメータなど参照しています。
実装の詳しい解説もされてますのでご興味ある方はご一読されると良いかと思います。

- [TheParallaxView](https://github.com/algomystic/TheParallaxView)
- [TheParallaxView ∙ Illusion of depth by 3D head tracking on iPhone X](https://www.anxious-bored.com/blog/2018/2/25/theparallaxview-illusion-of-depth-by-3d-head-tracking-on-iphone-x)

ソースコードはかなりシンプルになっています。
フェイストラッキングは`FindWithTag`で強引に検索しているので、もっと良いやり方があるかと思います...。

``` c#:OffAxisProjectionPortrait.cs
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

namespace Parallax
{
    [ExecuteInEditMode]
    public class OffAxisProjectionPortrait : MonoBehaviour
	{
        [SerializeField] private Camera deviceCamera, eyeCamera, arCamera;
        [SerializeField] private float left, right, bottom, top, near, far;
		[SerializeField] private Vector2 moveAmount = new Vector2(2f, 2f);

        private GameObject arFaceObj;
        private ARFace arFace;

        private void LateUpdate()
        {
            if (deviceCamera == null ||
				eyeCamera == null ||
                arCamera == null)
            {
                Debug.LogWarning("deviceCamera, eyeCamera, arCameraがセットされていません");
                return;
            }				

            Quaternion q = deviceCamera.transform.rotation * Quaternion.Euler(Vector3.up * 180f);
            eyeCamera.transform.rotation = q;

            if (arFaceObj == null || arFace == null) {
                try
                {
                    arFaceObj = GameObject.FindWithTag("ARFace");
                    arFace = arFaceObj.GetComponent<ARFace>();
                }
                catch (System.Exception e)
                {
                    Debug.LogWarning(e);
                }
            }
            else
            {
                Vector2 eyePos = arCamera.WorldToViewportPoint(arFace.leftEye.position);
                eyePos.x -= 0.5f;
                eyePos.y -= 0.5f;
                eyePos.x = Mathf.Clamp(eyePos.x, -moveAmount.x, moveAmount.x);
                eyePos.y = Mathf.Clamp(eyePos.y, -moveAmount.y, moveAmount.y);
                eyeCamera.transform.localPosition = new Vector3(-eyePos.x, eyePos.y, 0f);
            }

            Vector3 deviceCamPos = eyeCamera.transform.worldToLocalMatrix.MultiplyPoint(deviceCamera.transform.position);
			Vector3 fwd = eyeCamera.transform.worldToLocalMatrix.MultiplyVector (deviceCamera.transform.forward); 
			var devicePlane = new Plane(fwd, deviceCamPos);
			Vector3 close = devicePlane.ClosestPointOnPlane(Vector3.zero);
			near = close.magnitude;

			// iPhoneのサイズを設定する
			left = deviceCamPos.x - 0.040f;
			right = deviceCamPos.x + 0.022f;
			top = deviceCamPos.y + 0.000f;
			bottom = deviceCamPos.y - 0.135f;
			far = 10f;

			float scale_factor = 0.01f / near;
			near *= scale_factor;
			left *= scale_factor;
			right *= scale_factor;
			top *= scale_factor;
			bottom *= scale_factor;

			Matrix4x4 m = PerspectiveOffCenter(left, right, bottom, top, near, far);
			eyeCamera.projectionMatrix = m;
		}

		private static Matrix4x4 PerspectiveOffCenter(float left, float right, float bottom, float top, float near, float far)
		{
			float x = 2.0f * near / (right - left);
			float y = 2.0f * near / (top - bottom);
			float a = (right + left) / (right - left);
			float b = (top + bottom) / (top - bottom);
			float c = -(far + near) / (far - near);
			float d = -(2.0f * far * near) / (far - near);
			float e = -1.0f;
			var m = new Matrix4x4();
			m[0, 0] = x; m[0, 1] = 0; m[0, 2] = a; m[0, 3] = 0;
			m[1, 0] = 0; m[1, 1] = y; m[1, 2] = b; m[1, 3] = 0;
			m[2, 0] = 0; m[2, 1] = 0; m[2, 2] = c; m[2, 3] = d;
			m[3, 0] = 0; m[3, 1] = 0; m[3, 2] = e; m[3, 3] = 0;
			return m;
		}
	}
}
```


シーン内のボックスはシェーダーでグリッドを描画しています。
以下ソースコードです。

```glsl:Box.shader

Shader "Custom/Box"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Col ("Col", Range(0,20)) = 1
        _Row ("Row", Range(0,20)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            float grid(float2 st, float size)
            {
                size = 0.5 + size * 0.5;
                st = step(st, size) * step(1.0 - st, size);
                return st.x * st.y;
            }

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float _Col;
            float _Row;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float2 st = frac(i.uv * float2(_Col, _Row));
                return 1 - grid(st, 0.98);
            }
            ENDCG
        }
    }
}
```

# 最後に

シーンを追加していくと楽しいです。
![](https://lab.taki.co.jp/wp-content/uploads/2020/08/ar_prototype2020_main_2.jpg)
https://vimeo.com/451829511/209d7d4229