Tango Dynamic Meshでキャラクターの移動を制限する

Tangoでは3D Reconstruction（3DR）という機能で、空間を3Dメッシュとしてキャプチャし、OBJファイル形式にエクスポートすることができます。
この機能を使えば、屋内マップを作成してナビゲーションなど様々な用途で使うことができるのですが、建物内すべてのメッシュを作成しておくというのは、なかなか面倒です。

そこで、3DRでメッシュを作成しつつ、リアルタイムで障害物として利用する方法を試してみました。

<blockquote class="twitter-video" data-lang="ja"><p lang="ja" dir="ltr">階段もがんばれば上れます。 <a href="https://twitter.com/hashtag/Tango?src=hash">#Tango</a> <a href="https://t.co/FsE9BGaw9R">pic.twitter.com/FsE9BGaw9R</a></p>&mdash; jyuko (@jyuko49) <a href="https://twitter.com/jyuko49/status/841644564364513281">2017年3月14日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

まだまだロジックが不完全ではありますが、自分なりの知見が溜まってきたので、まとめておきます。


## 実現したいこと
**"3DRでリアルタイムにキャプチャしたTango Dynamic Meshを用い、キャラクターの移動に影響を与える"**

壁に衝突したら停止し、段差があれば上る・下るといった動きです。
3DR実行時のパラメータを調整すれば、移動中もほぼリアルタイムでメッシュを作成できるため、事前にデータを用意することなく、どこでも使える機能を想定しています。


## 実現方法
Unity5.5を利用しています。
Unityでキャラクターを移動させる機能として、以下の3つを試した結果、Charactor Controllerを採用することにしました。

- [Nav Mesh Agent](https://docs.unity3d.com/ja/current/Manual/Navigation.html)
  - ~~ナビメッシュを動的にベイクできないため、リアルタイムでは利用できない~~
- [Rigid Body](https://docs.unity3d.com/ja/current/Manual/PhysicsSection.html)
  - 重力(Gravity)を利用すると、スキャンしていない地点やメッシュの隙間で落ちてしまう
- [Charactor Controller](https://docs.unity3d.com/ja/current/Manual/class-CharacterController.html)
  - 実装がやや面倒だが、移動を制御でき、移動時のみColliderによる影響を受ける

【2017/5/9 追記】
Unity5.6にて、NavMeshの動的ベイクができるようになったようです。実際に、TangoやHoloLensで利用されている方もいらっしゃるので、現在では有力な実装方法の一つになるかと思います。

## 実装方法
ExperimentalMeshBuilderWithPhysicsの構成をベースにして、変更を加えながら実装しています。

\- Tango Maneger
\- DynamicMesh
\- Directional light
\- GUI
~~\- Tango AR Camera~~
\- Tango Camera
\- MarkerBlue
\- Query-Chan


### Tango AR Screenの追加
~~Tango Delta Cameraをシーンから削除する代わりに、TangoPrefabsからTango AR Cameraをシーンに追加します。~~
Tango Cameraに"Tango AR Screen (Script)"を追加します。
Tango ManagerのInspectorを開き、"Tango Application (Script)"の設定で"Enable Video Overlay"にチェック、"Method"は"Texture(ITangoCameraTexture)"にします。

この状態でビルドすると、カメラ映像の上にメッシュが構築されていく様子がわかります。

【2017/4/8 追記】
2017年4月に行われたTango SDKのアップデート（Version 1.52：Gankino）で、カメラのPrefabがTango Cameraに統合され、Tango AR Cameraは非推奨（Deprecated）となりました。
上記の通り、Tango CameraにTango AR Screenをアタッチすることで同様の結果が得られます。


### 3DRの設定
こちらもTango Managerの"Tango Application (Script)"で設定します。
詳細は、[前回の記事](http://qiita.com/jyuko/items/4753f0c228e8db7701bf
)にまとめています。

"Resolution (meters)"を0.03-0.05の範囲で調整することで、メッシュを効率よく作成できるようにします。
また、"Space Clearing"を有効にし、Color、Normals、UVsの生成は無効にしています。

実際の設定は以下のような感じです。（Area Description関連は要調整）
![スクリーンショット 2017-03-22 2.08.02.png](https://qiita-image-store.s3.amazonaws.com/0/153460/658a243a-4daa-caf4-898b-1a983b1dfec7.png)


### Dynamic Meshの設定
"Mesh Renderer"の"Materials"を変更して、メッシュを透過にします。
UnityのAssetsにデフォルトで含まれていた"SpacialMappingOcclution"を"Element0"にセットしています。

なお、Tango Dynamic Meshはスクリプト内で"Mesh Renderer"の"Materials"を反映するようになっているため、"Mesh Renderer"を非アクティブにしただけでは非表示にはなりません。

![スクリーンショット 2017-03-22 2.12.33.png](https://qiita-image-store.s3.amazonaws.com/0/153460/e9b37600-5d54-d923-62cb-292d2f59b75f.png)


### Charactor Controllerの追加
まず、移動させるGameObjectをシーンに追加します。
クエリちゃん 通常版モデル/SD版モデルを使いました。

Inspectorの"Add Component"で"Charactor Controller"を追加し、衝突判定が正しく行われるよう"Center"、"Radius"、"Height"を設定します。

![スクリーンショット 2017-03-16 10.17.45.png](https://qiita-image-store.s3.amazonaws.com/0/153460/e82ca517-fd01-4a5f-df2f-b867dabbe888.png)

Charactor Controllerは重力や反発などの物理特性に影響しませんが、Moveによる移動時には障害物の影響を受けます。
そのため、Tangoが壁や障害物を検知し、Dynamic Meshが作成されると、メッシュがある方向には進めなくなります。
ただし、"Slope Limit"、"Step Offset"で設定した値の範囲であれば、斜面や段差を上ることができます。

もう一点、"Layer"を"Ignore RayCast"に変更しています。
後述の処理で、カメラからのRayCastにより進行方向をコントロールしており、キャラクターにRayが当たると不都合が生じるためです。
![スクリーンショット 2017-03-20 3.08.04.png](https://qiita-image-store.s3.amazonaws.com/0/153460/bbb4911c-7f4a-9fc5-7ca5-c56105bb76e6.png)


### カメラ方向によるコントロールの追加
キャラクターの移動を制御する方法はいくつかあると思いますが、両手が塞がっている状態なので、カメラを向けた方向に進む動きが自然だろうと考えました。
カメラが向いた方向に目印となるマーカーを置き、キャラクターはこのマーカーに向かって移動する形です。

実装例としては、MeshBuilderWithPhysicsGUIController.csに以下のような処理を足したスクリプトを利用しています。

```DocodemoQueryGUIController.cs
public class DocodemoQueryGUIController : MonoBehaviour
{
	public GameObject m_marker;
	private bool m_markerFixed = false;

	public void Update()
	{
		if (!m_markerFixed) {
			Vector3 target;
			RaycastHit hitInfo;
				if (Physics.Raycast (Camera.main.ScreenPointToRay (new Vector3 (Screen.width / 2.0f, Screen.height / 2.0f)), out hitInfo)) {
					Vector3 cameraBase = new Vector3 (Camera.main.transform.position.x, hitInfo.point.y, Camera.main.transform.position.z);
					target = cameraBase + Vector3.ClampMagnitude (hitInfo.point - cameraBase, Camera.main.farClipPlane * 0.9f);
				
				} else {
					target = Camera.main.transform.forward.normalized * (Camera.main.farClipPlane * 0.9f);
				}
				m_marker.transform.position = target;
			}
		}
	}

	public void OnGUI()
	{
		if (GUI.Button(new Rect(Screen.width - 280, 480, 250, 120), "<size=45>Fix Marker</size>"))
		{
			m_markerFixed = !m_markerFixed;
		}
	}
}
```

スクリーンの中心にRayを飛ばし、メッシュと衝突したらマーカーを置きます。メッシュがなければ、カメラの向いている方向でfarClipPlaneにかからない距離をマーカーの位置とします。
このあたりの処理は、FloorFindingでマーカーを置く処理を一部流用しています。
マーカーを固定するボタンも用意しましたが、単純にフラグをtoggleしているだけです。

マーカーオブジェクトはInspectorでセットします。マーカー自身もキャラクターと同様に"Layer"を"Ignore RayCast"にしています。
![スクリーンショット 2017-03-20 3.54.34.png](https://qiita-image-store.s3.amazonaws.com/0/153460/94cc9cf3-7174-97f0-b8a9-166c54e26640.png)


### 移動制御のスクリプトを追加
移動させたいGameObjectのInspectorに戻り、以下のスクリプトを新規作成で追加します。

```QueryChanController.cs
using System.Collections;
using UnityEngine;

public class QueryChanController : MonoBehaviour {

	public CharacterController m_characterController;
	public GameObject target;

	void Start () {
		m_characterController = GetComponent<CharacterController>();
	}

	void Update () {
		float speed = 0.5f;
		float jumpOffHeight = 2.0f;
		float stoppingDistance = 0.5f;

		if (m_characterController != null) {
			Vector3 moveDirection = target.transform.position - m_characterController.transform.position;
			moveDirection.y = 0;

			if (moveDirection.magnitude > stoppingDistance) {
				moveDirection = transform.TransformDirection (moveDirection.normalized * speed);
				m_characterController.transform.LookAt (target.transform);

				Quaternion tmpRotation = new Quaternion (0, m_characterController.transform.rotation.y, 0, 1);
				m_characterController.transform.rotation = tmpRotation;
				// 移動時のアニメーション(Walk)をここでセット
			} else {
				// 待機時のアニメーション(Idle)をここでセット
			}

			if (!m_characterController.isGrounded) {
			    RaycastHit hitInfo;
				if (Physics.Raycast (m_characterController.transform.position, Vector3.down, out hitInfo, jumpOffHeight)) {
					moveDirection.y -= 9.8f;
				} else {
					// 接地していないときのアニメーション(Fly_Straight)をここでセット
				}
			}

			m_characterController.Move (moveDirection * Time.deltaTime);
		}
	}
}
```

CharactorControllerのMove関数にVector3の移動ベクトルを渡すだけで、障害物を考慮した移動はできます。
それだけだと宙に浮いた状態になりますが、何も考えずに落下を実装すると、Rigid Bodyと同様にメッシュ未作成の地点で地面より下に落ちてしまいます。
対策として、地面に接地していなければキャラクターから下方向にRayを飛ばし、一定の高さにメッシュ（地面）があるときだけ落下・着地させています。

アニメーションも設定していますが、付加的な要素のため、詳しい説明は割愛します。
クエリちゃんモデルは都合の良いことに空を飛べるので、以下の3つを切り替えています。

移動中で接地している：**Walk**
待機中で接地している：**Idle**
接地していない：**Fly_Straight**


## テスト
階段を上る(冒頭の動画)、床を歩いて壁や柱で止まる、メッシュがない場所は飛んで作成されたら着地するテストを行っています。
また、屋外を含む色々な場所で利用してみました。

<blockquote class="twitter-video" data-lang="ja"><p lang="ja" dir="ltr">床を歩けるようになった。 <a href="https://twitter.com/hashtag/Tango?src=hash">#Tango</a> <a href="https://t.co/HlflW55ixm">pic.twitter.com/HlflW55ixm</a></p>&mdash; jyuko (@jyuko49) <a href="https://twitter.com/jyuko49/status/841640734201602049">2017年3月14日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-video" data-lang="ja"><p lang="ja" dir="ltr">視線の問題は、キャラクターを向かわせる先のマーカーとキャラクター自身を浮かせることで軽減できそう。<a href="https://twitter.com/hashtag/Tango?src=hash">#Tango</a> <a href="https://t.co/ZXCl3JruY3">pic.twitter.com/ZXCl3JruY3</a></p>&mdash; jyuko (@jyuko49) <a href="https://twitter.com/jyuko49/status/843080599766159360">2017年3月18日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>


## 結果
成果よりも課題の方が多いですが、課題が多く出たことも収穫のようなものです。

### 課題
#### 視線が下に向きすぎないような工夫が必要
デプス・センサが検知できる距離は、3-5ｍ程度しかありません。そのため、広い場所で使うと足元をキャプチャしようとして、視線が下を向きがちになります。

#### 人混みでの利用は難しい
3DRは壁や床だけでなく、移動体（主に通行人）もキャプチャします。通行人にぶつかる衝突判定もできる点はよいのですが、衝突すると押し出されて遠くへ消えてしまいます。
また、人が大勢いる場合、壁があるのと同じ状態なので、間を縫って進むのは困難です。

"Space Clearing"を有効にすれば、人がいなくなった場所のメッシュを除去することはできますが、カメラを再度向ける必要があります。メッシュを透過にしたこともあって、意図せず残ることもありそうです。

#### 移動した後にメッシュができると押し出される
キャラクターが移動した場所に後からメッシュができた場合、作成されたメッシュの分だけ押し出されます。
冒頭の階段を上る動画で、少し滑ったように見える動きがそれです。

極端な例では、壁のある方向に進んだ後から壁ができ、遠くまで押し出されたり、壁の向こう側に押されて閉じ込められてしまうことがありました。透過メッシュに隠れるので画面からも消えてしまい、行方不明になります。

#### カメラから離れすぎると消える
farClipPlaneに引っ掛かっていました。デフォルトは4(m)になっており、伸ばすことも可能ですが、デプス・センサの有効範囲も3-5mのため、キャラクターだけを遠くに移動させるのは避けた方がよさそうです。

#### メッシュ境界がキレイに見えない
キャラクターがDynamic Meshの裏側に回ると、透過のメッシュに隠れるため、壁や柱の影から身体の一部が見えているような表現になりますが、メッシュの表面がガタガタだと境界が目立ってしまい、逆に現実感が損なわれます。
![Screenshot_20170320-185809.png](https://qiita-image-store.s3.amazonaws.com/0/153460/18da11f2-f03a-8c9a-b4ff-fdac2d61d347.png)


#### 長時間利用時、デバイス回転時の動作が怪しい
時間が経つと、キャラクターが意図通りに動かなくなったり、トラッキングエラーが発生したりします。
ロジックの問題なのか、Motion Trackingの誤差が原因なのかといったあたりを調査中です。


### 成果
#### 目的とした基本動作は確認できた
前述の通り、課題は多くあるものの"床を歩く"、"壁にぶつかる"、"段差を上る"といった基本動作は確認できました。
課題の克服や用途の限定を行うことで、Tango Dynamic Meshをリアルタイムでキャプチャしながら使うことは可能だと思います。

#### 屋外でも案外利用できた
光の影響を受けやすいため、屋外での利用は厳しいと思っていましたが、天候や明るさ次第では利用できました。
逆光が強く眩しい場合や真っ暗で何も見えない場合には、利用できません。


## まとめ
Tango Dynamic MeshとCharactor Controllerによるキャラクターの移動および衝突判定を行いました。
結果に記述した通り、課題は多いですが、屋内外やマップのあるなしを問わず、どこでも使える点は便利です。

今はまだ、クエリちゃんを表示させて遊ぶだけのアプリですが、移動時のガイド・アシスタント機能として使えないかも模索中です。
今回得た知見から、カメラ（利用者）の比較的近くにいて、進行方向や障害物をお知らせしたり、利用者の視線をそちらに向ける役割で使うとよさそうです。

逆に、事前に屋内マップを作る方式であれば、ナビメッシュの作成を想定したモデルを構築すると用途が広がりそうなので、そちらも試してみたいところではあります。


## おわりに
本コンテンツでは、[クエリちゃん 通常版モデル/SD版モデル](http://query-chan.com/download/)を使用しています。
[![02_クエリちゃんライセンスロゴ-e1472646888241-300x256.png](https://qiita-image-store.s3.amazonaws.com/0/153460/0e428400-07c9-17ff-fe29-6be7b7e51062.png)](http://query-chan.com/download/)