ARCore + Perception Neuron を試す

# ARCore + Perception Neuron を試す

ARコンテンツを作成する上で気になることの一つに、仮想オブジェクトを思うように触れない点があると思います。
光学シースルータイプのデバイスでは、手の認識を備えるものが徐々に出回り始めている感じもしますが、
複雑な動きや、両手を重ねた動き、手の甲や腕などで指が隠されてしまう動きなどは苦手なのではと思っています。

Perception Neuronはモーションキャプチャー用のデバイスで、安価（他のモーションキャプチャー製品と比較して）で全身のトラッキングが可能です。
こちら、リアルタイムでUnity側へキャプチャーデータを送信し、ボーンの入った人型を動かすことができます。

ARCoreでARしつつ、Perception Neuronで自身の体をトラッキングすることで、ARで表現できる幅が広がるはずなので、そのあたりを試してみます。

結論として、<b>実験レベルでは</b>動作を確認できました。実際のコンテンツにする上ではより詳細なテストが必要です。

こんな感じになりました。だいぶコマを飛ばしています。
![anigif.gif](https://qiita-image-store.s3.amazonaws.com/0/248057/78cf7cbf-b749-a2f4-65fe-d29b379cf7e7.gif)



## キーワード

ARCore, Unity, AR, ビデオシースルー, Perception Neuron, モーションキャプチャ


## バージョン情報など

- バージョン情報

|機能|バージョン|
|:--|:--|
|ARCore|1.2.0|
|Unity|2018.1.0f2|
|OS|Windows 10 バージョン1709|
|端末|Galaxy S8 SC-02J (docomoのやつです)|
|Perception Neuron|2018/07 購入|
|Destiny Ghost 3DPrinting setup|https://www.blendswap.com/blends/view/76025|

Perception Neuronは日本の代理店経由で購入しています。

## クレジット

Destiny Ghost 3DPrinting setupはクリエイティブ コモンズ 3.0で提供されています。
作成者はBlender-Manさんです。
モダンなゴーストです。

|||
|:--|:--|
|作品名|Destiny Ghost 3DPrinting setup|
|作品URL|https://www.blendswap.com/blends/view/76025|
|作者名|Blender-Man|
|作者URL|https://www.blendswap.com/user/Blender-Man|

## ARCore + Perception Neuron 試作

手のひらにゴーストを表示されてみます。
こちら、Destinyというゲームにおけるプレイヤーの唯一無二の相棒で、手のひらに召喚されます。<br>

次の動画のような感じです。
https://www.youtube.com/watch?v=PqBAw4weveY 2分25秒付近

### SDK導入

#### ARCore

[ダウンロードページ](https://developers.google.com/ar/develop/downloads)より、arcore-unity-sdk-v1.2.0.unitypackageをダウンロードしました。
記事作成時点では1.4.0がダウンロード可能です。ARCameraしか利用しないのでどのバージョンでも動作すると思います。
新規プロジェクトを作成し、SDKを全てインポートしました。

#### Perception Neuron

詳しくは公式などを参照ください。

一番外側のダンボールにくっついている紙に製品IDがくっついているので、それだけは捨てないようにします。
こちらのIDを用いて公式からPerception Neuronのソフトウェアをダウンロードし起動します。起動後のスタートアップページに従い作業すれば、問題なくセットアップできると思います。

追記する点があるとすれば、私のデバイスでは手袋部分のセンサーだけはカチッとはまりませんでした。動作は正常だと思われます。
不器用な人間の妄言の可能性も高いので、じつはきっちりはまるのかも……しれません……。
私の指先は差し込むだけのプラモデルを破壊するレベルで器用です。

また、Unity向けのSDKをダウンロードし、すべてインポートしました。

### 機能の確認

#### ARCore

Project > GoogleARCore > Prefabs > ARCore Device を利用します。こちらをシーンに配置します。
今回は手のひらというかなり近い場所に関心があるため、 ARCore Device > First Person Camera のClipping Planes を変更しておきます。
(Near,Far) = (0.01, 20) としました。手前1cmから奥行20mまでを範囲としています。
ARCoreの関連設定は以上です。

#### Perception Neuron

Project > Neuron > Resources > Models > NeuronRobot_SingleMesh をシーンに配置します。

Perception Neuron側を立ち上げます。Axis Neuronというアプリを起動してください。
手元のPerception NeuronとWifiで接続しておきます。
File > General のNeuron Host AddressのIPとポート番号を覚えておきます。

Unityに戻り、シーンに配置した NeuronRobot_SingleMesh のNeuron Transforms InstanceのAdress,Portに先ほどの値を書き込みます。

Perception Neuronを着用します。今回は左腕のみ装着しました。
左腕部分から肩部分に伸びる配線を、通信機につないでください。

一度、ARCore Deviceの方を非アクティブにして、Play実行します。
Unity上の人型が、正しく通信できていれば動作します。ここで、ボーンがZ軸正面を向いていない場合があります。
この時は、Axis Neuron側のModel alignmentのCCW-CWの値を微調整してあげてください。

![Neuron.png](https://qiita-image-store.s3.amazonaws.com/0/248057/de85560c-e154-a09a-5caa-56f3eb0281a1.png)


#### 両方の動作確認

ARCore Deviceの方をアクティブ状態に戻し、再度Playします。
携帯を動かすと、どこかに人型が発見されるはずです。次は、位置合わせを行います。


### 位置合わせ

位置合わせについて考えます。
何らかの方法で携帯の位置と、Perception Neuronが持ってくる位置を関連付ける必要があります。
今回は単純に、利用シーンに制限をかけることで対応します。
すなわち、必ず左手に携帯を持った状態でアプリが起動されるものとし、起動後にUnity空間上の(0,0,0)の位置に人型の左手が来るよう、人型の位置を書き換えます。

まず、Perception NeuronのSDKから得られたボーン内に位置合わせ用の空オブジェクトを追加します。
図のように、Robot_LeftHandまでノードをたどり、空のオブジェクトを二つ作成します。
一つは手のひらの位置を占めるためのもので、もう一つは物理的なカメラの位置となるよう、座標を調整します。
私の場合は人差し指の第一関節付近にスマートフォンを握ったときのカメラが来るので、そのあたりになるよう調整しました。
カメラ位置をCameraPos, 手のひら位置をPalmという名前のオブジェクトが示しています。

![Neuron2.png](https://qiita-image-store.s3.amazonaws.com/0/248057/951b6880-6567-6446-0334-32ed3c47e7f6.png)


次に、起動後に位置を合わせるためのスクリプトを記述します。適当なオブジェクトに張り付け、
physicalCameraPositionにCameraPos, root にNeuronRobot_SingleMeshを設定することで、
起動後にCameraPosが原点に来るようにNeuronRobot_SingleMeshの座標を変化させています。

```cs

   public Transform physicalCameraPosition;
   public Transform root;
   private Vector3 offset;

   private Vector3 rootPosition;

   private IEnumerator Start()
   {
       yield return null;
       rootPosition = root.position;

       yield return new WaitForSeconds(1f);
       offset = physicalCameraPosition.position;

       root.localPosition = -offset + rootPosition;
   }
```

### ゴースト召喚部分

ARに関する部分は上述した部分のみです。ここからはコンテンツ部分になります。
Blender-Manさん作のGhostをUnityに取り込み、ゴーストの形に整えました。
以降、このオブジェクトのルートをGhostとします。

ゴースト召喚のために次の二つのスクリプトを記述しました。
<br>
1.Ghostの座標を作成しておいた手のひらに同期させるスクリプトです。
targetに手のひら、sourceにGhostを設定します。

```cs

  public Transform target;
  public Transform source;


	// Update is called once per frame
	void Update () {
        target.position = source.position;
	}

```
<br>

2.ゴーストの召喚については、『2秒以上手を握った状態で、そのあと開く』という動作をキーにしています。
こちらの動作を計測するスクリプトです。なお、当たり判定を用いるので、作成しておいた空のゲームオブジェクトに
Rigidbodyとトリガーを有効にしたコライダーをアタッチしておきます。
![Neuron4.png](https://qiita-image-store.s3.amazonaws.com/0/248057/5f257ca8-b2bd-e0e0-3550-6ac1026421c7.png)
加えて、各指先にもRigidbodyとトリガーを無効にしたコライダーをアタッチしておきます。
![Neuron5.png](https://qiita-image-store.s3.amazonaws.com/0/248057/f99a9f15-16c1-2203-9b32-3a2f78945a70.png)

```cs
{
private int fingerCount;
 private float closeHandTime;

 public UnityEvent onOpenHand;

 private Renderer renderer;

 private void Awake()
 {
     renderer = GetComponent<Renderer>();
 }

 private void OnTriggerEnter(Collider other)
 {
     if (other.tag != "Finger")
         return;

     fingerCount++;
 }

 private void OnTriggerExit(Collider other)
 {
     if (other.tag != "Finger")
         return;

     fingerCount--;
 }

 private void Update()
 {
     if(fingerCount < 4)
     {
         if (closeHandTime > 2f)
             OnOpenHand();
         closeHandTime = 0f;
         return;
     }

     closeHandTime += Time.deltaTime;

     if (closeHandTime < 2f)
         return;

     OnCloseHand();
 }

 private void OnOpenHand()
 {
     onOpenHand.Invoke();
 }

 private void OnCloseHand()
 {
     if(renderer.enabled)
         renderer.enabled = false;
 }

```

<br>


Ghostの呼び出しについては、各パーツのMaterialを次のシェーダーを利用したマテリアルとし、
スクリプトから表示/非表示の境界線を変更することで徐々に出てくる感じを再現しています、

シェーダー

```glsl
Shader "Custom/FadeIn"
{
	Properties{
		originalColor ("originalColor" ,Color) =(1,1,1,1)
		fadeInOffset("fadeInOffset" , float) = 0.2
		horizontal("horizontal" , float) = 0.5
		fadeInColor("fadeInColor", Color) = (1,1,1,1)

		_Glossiness("Smoothness", Range(0,1)) = 0.5
		_Metallic("Metallic", Range(0,1)) = 0.0
	}

		SubShader{
			Tags{ "RenderType" = "Opaque" "Queue" = "Transparent" }
			LOD 200

			CGPROGRAM
			#pragma surface surf Standard //alpha:fade
			#pragma target 3.0

		half fadeInOffset;
		half horizontal;
			float4 fadeInColor;
			float4 originalColor;

			half _Glossiness;
			half _Metallic;


		struct Input {
			float3 worldPos;
		};

		void surf(Input IN, inout SurfaceOutputStandard o) {
			float dist = IN.worldPos.y;

			//水平面より小さい…普通に描画
			if (dist < horizontal) {
				o.Albedo = originalColor;
			}
			else if (dist < horizontal + fadeInOffset) {
				o.Albedo = fixed4(fadeInColor[0], fadeInColor[1], fadeInColor[2], 1 - fadeInOffset / (dist - horizontal));
			}
			else {
				discard;
				//o.Albedo = fixed4(0.5, 0.5, 0.5, 0.5);
			}

			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
		}
		ENDCG

	}

	FallBack "Diffuse"

}
```

<br>
スクリプト
Renderer[] renderers にはひたすらGhostのかくパーツを設定しています。
bottomObject, topObjectはGhostの子に生成した空のゲームオブジェクトで、オブジェクトの上下端にあたる座標を管理するためのものです。

```cs
[SerializeField]
private Renderer[] renderers;

[SerializeField]
private Transform bottomObject;
[SerializeField]
private Transform topObject;

[SerializeField]
private float transmatSpeed = 1f;

private float border;

[SerializeField]
private bool isDebug = false;

[SerializeField]
private Transform particle;

private void Start()
{
    if (isDebug)
        StartTransmat();
}

public void StartTransmat()
{
    particle.gameObject.SetActive(true);
    border = bottomObject.position.y;
    for (int i = 0; i < renderers.Length; i++)
    {
        renderers[i].gameObject.SetActive(true);
        renderers[i].material.SetFloat("horizontal", border);
    }

    StartCoroutine(transmat());
}

private IEnumerator transmat()
{
    yield return null;
    //5mまで閾値を上げて終わる
    while (border < 5f) {
        yield return null;
        border += Time.deltaTime * transmatSpeed;
        if(border * 2f > topObject.position.y)
            particle.gameObject.SetActive(false);

        for (int i = 0; i < renderers.Length; i++)
        {
            renderers[i].material.SetFloat("horizontal", border);
        }
    }
}
```

### 実働

ページ上部のGIFを参照ください。
しばらく手を握った後開くと、手のひらの位置にゴーストが出現します。


### 課題

QRコードを手の甲に張り付けて認識させ、カメラと手の位置を合わせるなど、
もう少し位置合わせ部分を自動化させる必要があります。

ただ、最終的には光学シースルーのヘッドマウントディスプレイと組み合わせたいので、
その場合はもう少し単純な方法で位置合わせできる気がしています。