【ARCore】HelloARを試す

# HelloARを試す

他の方も記事として上げていらっしゃると思いますが、私もARCoreを触ってみましたので内容をまとめてみました。
何か参考になれば。

ARCoreのExsampleシーンであるHelloARを触ってみました。
Environmental understandingによって平面、または特徴点を検知し、ドロイド君を表示するサンプルです。

ARCoreの各スクリプトは[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)により提供されます。

## キーワード

ARCore, Unity, AR, ビデオシースルー, 平面検知, Environmental understanding

## Environmental understandingとは

[ARCore公式](https://developers.google.com/ar/discover/concepts#environmental_understanding)を参照ください。
現実世界の特徴点を認識するほか、特徴点群から水平面を探し出します。

## バージョン情報など

- バージョン情報

|機能|バージョン|
|:--|:--|
|ARCore|1.1.0|
|Unity|2017.3.0f3|
|OS|Windows 10 バージョン1709|
|端末|Galaxy S8 SC-02J (docomoのやつです)|

- 作業日

    2017年4月上旬


## SDK導入

[ダウンロードページ](https://developers.google.com/ar/develop/downloads)より、arcore-unity-sdk-v1.1.0.unitypackageをダウンロードしました。
新規プロジェクトを作成し、SDKを全てインポートしました。


## HelloARで用いられている関数

HelloARシーン内にあるスクリプトのうち、主要そうなものを表にしました。
順に中身を簡単に確認しました。

|スクリプト名|アタッチされているオブジェクト|
|:--|:--|
|Hello AR Controller |Exsample Controller|
|Ponintcloud Visualizer|Point Cloud|
|Environment Light |Environmental Light|
|AR Core Session |ARCore Device|
|Tracked Pose Driver |First Person Camera|
|AR Core Background Renderer |First Person Camera|

## Hello AR Controller

サンプルシーンにおけるコントローラースクリプトです。
Update内では次の操作を行っていました。

- ESCキー押下時のアプリ終了処理
- パーミッション不許可時のアプリ終了処理
- 平面検知を勧めるUIの表示切替処理
- 今フレームで検出された平面の生成処理
- 画面タッチの検出処理
- 画面タッチ時に検出した平面に向けてRayを飛ばし、その位置にオブジェクトを配置する処理

### 今フレームで検出された平面の生成処理

平面の取得は```Session.GetTrackables```関数にて行っていました。
第一引数にTrackedPlane型のリスト、第二引数に取得する平面の種類を取ります。第二引数を省略すると、すべての平面が取得されるようです。
また、第一引数のリストは呼び出す関数内でクリアされます。

新しく平面が作成されるたびに、TrackedPlanePrefabを複製して配置しています。
TrackedPlanePrefabにはTrackedPlaneVisualizerがアタッチされています。

<!--TrackedPlaneVisualizerについては別項にまとめました。-->

### 平面に向けてRayを飛ばす処理

```
TrackableHit, TrackableHitFlags, Frame.Raycast()
```
上記を用いてRayの設定とRayを飛ばす処理を行っていました。UnityのRaycast処理と同様に記述できるようです。
[リファレンス](https://developers.google.com/ar/reference/unity/class/GoogleARCore/Frame)も参照ください。

TrackableHitFlagsについては、複数のフラグでフィルターをかけられるようです。それぞれのフィルターで検出できる項目は[リファレンスのTrackableHitFlags](https://developers.google.com/ar/reference/unity/namespace/GoogleARCore#namespaceGoogleARCore_1a8da5a2395bdef6ffe411700df5a713ff)を参照ください。ざっくりですが、次のようなものがとれるようです。

単なる特徴点なのか、垂直方向を考慮した特徴点なのか
検出されている箇所のみを対象とするのか、無限の広さを持つ平面とみて検出をかけるのか


## Ponintcloud Visualizer

`Frame.PointCloud.PointCount`および```Frame.PointCloud.GetPoint```を用いて、検出された特徴点を表示するスクリプトです。

特徴点すべてを含むメッシュを作成し、```MeshTopology.Points```により表示しています。
検出された点の順序は整理されていないため、```MeshTopology.Points```を```MeshTopology.Lines```等に変えると無数の線が引かれてしまいます。
非常に見にくいですが、水色の線がMeshTopology.Linesを指定した際の結果です。
![bandicam 2018-04-14 20-55-26-450.jpg](https://qiita-image-store.s3.amazonaws.com/0/248057/29e03b54-7726-e9fb-1e5f-f4ed64db8c9c.jpeg)




## Environment Light

\_GlobalColorCorrectionの色合いを、カメラ映像をもとに調整する機能を持つスクリプトです。
自作のシェーダにも_GlobalColorCorrectionを用意しておけば、色が変わります。

## AR Core Session

UnityにおけるStart、OnDestroy、OnEnable、OnDisableのタイミングで、
ARCoreの機能を開始させたり中断させたりするスクリプトです。
このスクリプトの有効/無効を切り替えることでAR機能のOn/Offが切り替えられます。

## Tracked Pose Driver

良くわかりませんでした。
[Unityのリファレンス](https://docs.unity3d.com/ScriptReference/SpatialTracking.TrackedPoseDriver.html)によれば、位置を追跡できるデバイスのPoseをGameObjectのtransformに適用するもののようです。
ARCoreではこちらのスクリプトを目印として検索に用い、別に用意してあるInstantPreviewTrackedPoseDriverをアタッチしているようでした。
InstantPreviewTrackedPoseDriverでは毎フレームトラッキングの結果である位置と回転をオブジェクトに反映しています。

## AR Core Background Renderer

デバイスのカメラが取得している映像を、Unityのカメラの背景として描画するためのスクリプトのようでした。
詳しくは公式の関数リファレンスを参照ください。
内部でGetComponent関数を用いて描画対象のカメラを選択しています。

デバイスのカメラの映像については、```Texture backgroundTexture = Frame.CameraImage.Texture;```として取得しているようです。
Frame.CameraImageの詳細は[リファレンス](https://developers.google.com/ar/reference/unity/class/GoogleARCore/Frame/CameraImage)にて確認ください。
Frame.CameraImage.Textureの他には、Frame.CameraImage.AcquireCameraImageBytes()でYUV-420-888形式でカメラ映像にアクセスできるようですが、まず使わないと思います。


## TrackedPlaneVisualizer

Hello AR Controllerスクリプト内で、新に平面が検出されるたびに生成されるオブジェクトにアタッチされているスクリプトでした。
内部を確認してみました。主にUpdateで毎フレーム動作するほか、生成時に外部からInitialize関数が呼ばれます。

### Initialize関数

Hello AR Controllerスクリプト内で検出された平面を引数として呼び出されている初期化関数でした。
与えられた平面をメンバ変数として取得するほか、描画時に区別するため、描画色と画像の回転を調整しています。

### Update内

参照している平面について、存在しているのか、トラッキング状態にあるのかなどを確認し、表示状態を切り替えるなどしていました。<br>
サンプルアプリを起動すると、机の左端と右端に異なる平面が割り当てられる場合があります。認識を広げていき、二つの平面が結合される段になると、いずれか一つの平面が消えるのですが、この部分の処理もこちらで行われています。
`m_TrackedPlane.SubsumedBy != null` とすることで、自平面を包含する別平面があるかを確認しています。別平面がある場合は自オブジェクトを破棄しています。<br>
最後に_UpdateMeshIfNeeded()関数を呼び出しています。

### _UpdateMeshIfNeeded()関数

処理を順に追っていきました。

- `m_TrackedPlane.GetBoundaryPolygon(m_MeshVertices);`にて平面の境界部分の座標を求めています。
GetBoundaryPolygonについてはページ下部でもう少し詳しく掘り下げました。<br>
- `m_PlaneCenter = m_TrackedPlane.CenterPose.position;`にて平面の中心を取得しています。<br>
- `m_MeshColors.Add(Color.clear);`部分で境界部分のメッシュの色を透明にしています。<br>
- ` // Add vertex 4 to 7.`の次のfor文内では、境界部分の座標より少し中心寄りの箇所に新しく座標を設定しています。
この部分の色を白とすることで、描画される平面は境界に近づくにつれ透明になっていきます。<

参考：境界部分の色も白にしたとき、境界部分の色を白、内側に生成した座標の色を透明にしたとき。

|(境界,内側)=(clear,white)|(境界,内側)=(white,white)|(境界,内側)=(white,clear)|
|:--|:--|:--|
|![color1.jpg](https://qiita-image-store.s3.amazonaws.com/0/248057/e282be21-e878-90f3-8098-8924387aae3d.jpeg)|![color2.jpg](https://qiita-image-store.s3.amazonaws.com/0/248057/8be497a4-61f0-7658-050d-e0b685b0da81.jpeg)|![Screenshot_20180415-184633.png](https://qiita-image-store.s3.amazonaws.com/0/248057/46808150-96a4-91f4-b84a-64cfbc9593d6.png)

細長い奴はGetBoundaryPolygonで取得した座標位置を示すためのオブジェクトです。
<br>

---------------- ここまでで必要な座標の取得、追加は終了していました。------------------------------<br>

- `// Generate triangle (4, 5, 6) and (4, 6, 7).`部分のfor文にて、内側を結ぶメッシュを作成しています。
少し前置インクリメントに戸惑いましたが、C++だと前置インクリメントの方が推奨されるのですね。
ここではi++の後置インクリメントでも差異はありません。知らなかった……
<br>
- ` Generate triangle (0, 1, 4), (4, 1, 5), (5, 1, 2),...`部分のfor文にて、境界部分の座標と内側部分の座標の間でメッシュを作成しています。<br>


<!--処理の順を追ったイメージ図です。多分こんな感じのはず。-->


取得した境界部分の座標に応じてメッシュを作り直すことで、徐々に平面が広がっていくように見せています。


## GetBoundaryPolygonの確認

_UpdateMeshIfNeeded内で初めに呼ばれている関数です。
こちらで取得した点をもとにメッシュを作成している様子ですので、
この関数で取得できた座標を確認します。

### 方法

与えられた座標群に対してオブジェクトを配置する関数をTrackedPlaneVisualizerに追加しました。
こちらの関数を_UpdateMeshIfNeeded内で更新したm_MeshVerticesを引数として呼び出します。
また、visualizeObjectには(x,y,z)=(0.1,0.1,0.1)のCylinderを設定しました。

```

public GameObject visualizeObject;
private List<GameObject> visualizeObjects;

//Awake内に次の一行を追加
//visualizeObject = new GameObject();

public void Visualize(Vector3[] coordinates)
{
    CreateShortageObjects(coordinates);
    for (int i = 0; i < coordinates.Length; i++)
    {
        visualizeObjects[i].transform.position = coordinates[i];
        visualizeObjects[i].SetActive(true);
    }

    //余分なオブジェクトがある場合は隠す
    for (int i = coordinates.Length; i < visualizeObjects.Count; i++)
    {
        visualizeObjects[i].SetActive(false);
    }
}

private void CreateShortageObjects(Vector3[] coordinates)
{
    //不足分の可視化用オブジェクトの生成
    for (int i = visualizeObjects.Count; i < coordinates.Length; i++)
    {
        var o = Instantiate(visualizeObject);
        visualizeObjects.Add(o);
    }
}
```


### 結果

m_MeshVerticesで検出された平面の端点が円柱の部分になります。
このような点に対してメッシュを張っているようです。

|認識開始|少し左へ拡張|さらに左へ拡張|
|:--|:--|:--|
|![planeDetect1.jpg](https://qiita-image-store.s3.amazonaws.com/0/248057/a562d308-c59b-3639-92b7-6e47dee2e637.jpeg)|![planeDetect2.jpg](https://qiita-image-store.s3.amazonaws.com/0/248057/349c9a1e-c2cf-893b-f033-937eee78cbc1.jpeg)|![planeDetect3.jpg](https://qiita-image-store.s3.amazonaws.com/0/248057/246d8880-39ca-1e5c-cdf6-87d550adace9.jpeg)
### 感想

端点が検出できているので、この座標に沿ってプレイエリアを区切るオブジェクトを表示すると親切かもしれません。