ARCoreのCloud AnchorsをSwift, iOSで試してみた

[Google I/O 2018](https://events.google.com/io/)で発表された [Cloud Anchors|ARCore](https://blog.google/products/google-vr/experience-augmented-reality-together-new-updates-arcore/)はiOSでも使えるということで、ARKitを使ったアプリを作っている身として早速触ってみました。

[公式サンプル](https://github.com/google-ar/arcore-ios-sdk)がobjcだったので、今回Swiftでオリジナルサンプルを作ってみました。以下にアップしたので参考にしてみてください。

https://github.com/kboy-silvergym/CloudAnchorsSwiftSampler

本記事では、Cloud Anchorsの概要とサンプルの解説をしたいと思います。

# Cloud Anchorsとは

![alt](https://storage.googleapis.com/gweb-uniblog-publish-prod/original_images/google_justALine_GIF6_180506a.gif)
(by [Experience augmented reality together with new updates to ARCore](https://www.blog.google/products/google-vr/experience-augmented-reality-together-new-updates-arcore/))

現在、ARKit, ARCoreを使うと、カメラに写した世界の平面を認識し、空間を座標で表すことができます。しかし、他の端末で同じ場所を見た時に、**そこが同じ場所かどうかということはわかりません。**空間を座標で表し、平面を認識しただけだからです。

しかし、様々な企業が研究開発しているARCloudがあると、違う端末どうしが同じ場所を写した時に、**そこが同じ場所であると認識できる**ようになります。

Cloud AnchorsはそのARCloudの簡易版と言えるでしょう。

空間から認識した特徴点を元にアンカーを生成し、その情報をGoogleのCloudを使って共有することができるフレームワークです。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">With ARCore&#39;s Cloud Anchors, we&#39;ve made it easy to build shared AR experiences. Works across Android and iOS. More here: <a href="https://t.co/mPmKzmPpcp">https://t.co/mPmKzmPpcp</a> <a href="https://t.co/iafB1Rewzq">pic.twitter.com/iafB1Rewzq</a></p>&mdash; Clay Bavor (@claybavor) <a href="https://twitter.com/claybavor/status/993957213726101504?ref_src=twsrc%5Etfw">2018年5月8日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

公式で公開されているサンプルは、ドロイドくんを平面上に配置すると、同じAPIKeyを参照するアプリで、同じ場所にドロイドくんを現すことができるというものです。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">とりあえずiOSのARCoreのサンプル動かすマン <a href="https://twitter.com/hashtag/io18?src=hash&amp;ref_src=twsrc%5Etfw">#io18</a> <a href="https://twitter.com/hashtag/ARCore?src=hash&amp;ref_src=twsrc%5Etfw">#ARCore</a> <a href="https://t.co/YNYKl59IKM">pic.twitter.com/YNYKl59IKM</a></p>&mdash; Daiki Matsudate@io18 (@d_date) <a href="https://twitter.com/d_date/status/994337784151265280?ref_src=twsrc%5Etfw">2018年5月9日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# Original Sample

僕が簡単に作ったオリジナルサンプルを紹介します。

|飛行機をおく|同じ場所に飛行機が出現|
|:--:|:--:|
|![GIF image-B938C504D902-1.gif](https://qiita-image-store.s3.amazonaws.com/0/111366/df1f8119-042a-3b77-c4c9-0ec6a8271feb.gif)|![GIF image-8D03C122CEC6-1.gif](https://qiita-image-store.s3.amazonaws.com/0/111366/cff02bb5-33dd-3c96-53b2-1b59e7584595.gif)|

ちょっと向き変わっちゃってますけど(笑)、こんな感じで同じ場所に物体を出現させることができます。

手順はこんな感じです。

- 前提
 - アンカーが追加されたら飛行機出すようにする

- 送る側
 - アンカーを追加する(飛行機を置く) 
 - アンカー情報をGoogleのCloudに送る
 - GoogleのCloudから帰ってきたcloudIdentifierをdatabaseに入れる

- 表示する側
 - databaseに入ったcloudIdentifierを受け取る
 - cloudIdentifierを使ってGoogleのCloudを検索する
 - アンカー情報を受け取ってアンカーを追加
 - アンカーが追加された場所に飛行機出てくる

解説していきます。

## PlaneAnchor以外のアンカーだったら飛行機出すようにしとく

```swift
// MARK: - <#ARSCNViewDelegate#>
extension ViewController: ARSCNViewDelegate {
    
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        // Basicaly, you can get only ARPlaneAnchor,
        // but this case, you can also get another anchor which is created by ARCore.
        if !(anchor is ARPlaneAnchor) {
            let scene = SCNScene(named: "art.scnassets/ship.scn")!
            return scene.rootNode.childNode(withName: "ship", recursively: false)
        }
        return nil
    }
}
```

基本的に`ARAnchor` って現状`ARPlaneAnchor`しか配置されないので、`ARPlaneAnchor` じゃなかったら飛行機を配置するようにしちゃいます。[^1]

https://developer.apple.com/documentation/arkit/aranchor

## アンカー情報を送る

### 1.タップした平面にアンカーを追加

```swift:ViewController.swift

    // Tap Event
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else {
            return
        }
        let touchLocation = touch.location(in: sceneView)
        let hitTestResults = sceneView.hitTest(touchLocation, types: [.existingPlane, .existingPlaneUsingExtent, .estimatedHorizontalPlane])
        
        if let result = hitTestResults.first {
            addAnchor(transform: result.worldTransform)
        }
    }

    // add an anchor to AR Space and share the anchor data to Google Cloud
    private func addAnchor(transform: matrix_float4x4) {
        let arAnchor = ARAnchor(transform: transform)
        sceneView.session.add(anchor: arAnchor)
        
        do {
            _ = try gSession.hostCloudAnchor(arAnchor)
        } catch {
            print(error)
        }
    }
```

タップした平面にアンカーを追加し、その情報を `try gSession.hostCloudAnchor(arAnchor)` でGoogleのCloudに共有します。

### 2.`session:didHostAnchor`が呼ばれるので、databaseにcloudIdentifierを共有

```swift:GARSessionDelegate
    func session(_ session: GARSession, didHostAnchor anchor: GARAnchor) {
        let id = anchor.cloudIdentifier
        firebaseReference.childByAutoId().child("hosted_anchor_id").setValue(id)
    }
```

anchorのhostが終わると、GARSessionDelegateの`session:didHostAnchor` が呼ばれて、`cloudIdentifier`を得ることができます。

ここではFirebaseRealtimeDatabaseで`cloudIdentifier`を共有していますが、何を使ってもいいです。idが別の端末と共有できればいいので。

## 記録されたアンカー情報を受け取って表示

### 1.cloudIdentifierを受け取る

```swift:ViewController.swift
    // fetch anchors from Firebase Database
    private func observeAnchors() {
        firebaseReference.observe(.value) { (snapshot) in
            guard let value = snapshot.value as? [String : Any] else {
                return
            }
            let list = value.values
            list.forEach { value in
                if let dic = value as? [String: Any],
                    let anchorId = dic["hosted_anchor_id"] as? String {
                    self.fetchedAnchorIds.append(anchorId)
                }
            }
        }
    }
```

サンプルでは、firebaseから受け取った情報を１回`fetchedAnchorIds `に`cloudIdentifier`に入れています。。この後ボタンを押した時にそのidentifierを使って同期することを試みます。

### 2.cloudIdentifierからGoogle Cloudを検索

```swift
    private func resolveAnchors(){
        fetchedAnchorIds.forEach { id in
            _ = try! gSession?.resolveCloudAnchor(withIdentifier: id)
        }
        fetchedAnchorIds.removeAll()
    }
```

ボタンのタップをトリガーに、`resolveAnchors()`メソッドを呼び、`try! gSession?.resolveCloudAnchor(withIdentifier: id)` で `fetchedAnchorIds` を一個ずつGoogleのCloudに投げています。

### 3.発見するとdidResolveが呼ばれるので、アンカーを配置する

```swift:GARSessionDelegate
    func session(_ session: GARSession, didResolve anchor: GARAnchor) {
        let arAnchor = ARAnchor(transform: anchor.transform)
        sceneView.session.add(anchor: arAnchor)
    }
```

空間を見回していると、発見できます。

これで、アンカーをクラウドで共有することができました。

|飛行機をおく|同じ場所に飛行機が出現|
|:--:|:--:|
|![GIF image-B938C504D902-1.gif](https://qiita-image-store.s3.amazonaws.com/0/111366/df1f8119-042a-3b77-c4c9-0ec6a8271feb.gif)|![GIF image-8D03C122CEC6-1.gif](https://qiita-image-store.s3.amazonaws.com/0/111366/cff02bb5-33dd-3c96-53b2-1b59e7584595.gif)|

## 注意点

```swift:ARSCNViewDelegate
// MARK: - <#ARSCNViewDelegate#>
extension ViewController: ARSessionDelegate {
    
    func session(_ session: ARSession, didUpdate frame: ARFrame) {
        do {
            try gSession.update(frame)
        } catch {
            print(error)
        }
    }
}
```

上のように、フレーム情報を常にARCoreに共有してください。そうじゃないとARCoreが空間を把握できないので。

# まとめ

コードは https://github.com/kboy-silvergym/CloudAnchorsSwiftSampler に公開しました。Cloud AnchorsをSwiftで使う方の手助けになれば幸いです。

本家のサンプルよりもより無駄を削ぎ落として、CloudAnchorsが何をしているのか分かりやすく整理したつもりです。

では！

# 参考
* https://www.blog.google/products/google-vr/experience-augmented-reality-together-new-updates-arcore/
* https://developers.google.com/ar/develop/ios/overview
* https://github.com/d-date/arcore-ios-sdk/tree/swift-sample/Examples/CloudAnchorExampleSwift

[^1]: ...`ARImageAnchor`と `ARFaceAnchor`もあります