ARKitで「非」矩形な平面を検出する

iOS 11.3（いわゆるARKit 1.5）より、**任意の２次元形状のジオメトリで平面を検出**できるようになりました。たとえば次のgifのように、丸テーブルの平面を円形（に近い）ジオメトリとして検出できます。

![irregular.gif](https://qiita-image-store.s3.amazonaws.com/0/3180/004b8d84-f316-cd64-8fa2-8b3b758badc1.gif)

なお、これから解説するAPIを見るとわかりますが、水平面だけでなく、ARKit 1.5で検出可能になった**垂直平面にも本機能は有効**です。

## ARSCNPlaneGeometry

本機能において鍵となるのは`ARSCNPlaneGeometry`というiOS 11.3（ARKit 1.5）の新クラスです。

- https://developer.apple.com/documentation/arkit/arscnplanegeometry

`SCNGeometry`を継承しており、ARKitが検出した平面の２次元形状を表すためのクラスです。

このクラスのイニシャライザはひとつしかなく、

```swift
init?(device: MTLDevice)
```

このように、`MTLDevice`オブジェクトを渡すようになっています。つまり、**Metalサポート必須**ということです。

<img width="757" alt="Screen Shot 2018-05-20 at 16.27.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/3180/b85a5a6a-e08f-d7e8-bbb2-41df0aa12c52.png">

> ARSCNPlaneGeometry is available only in SceneKit views or renderers that use Metal. This class is not supported for OpenGL-based SceneKit rendering.

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ARKit 1.5から使えるようになった「非」矩形なジオメトリとして平面検出できるやつ、Metalベースでレンダリングしてるときだけ使える。OpenGLベースでレンダリングしてる場合は使えない。ちょっとずつMetalしかサポートされない機能が増えてきた。 / ARSCNPlaneGeometry <a href="https://t.co/gUC4IA6K3i">https://t.co/gUC4IA6K3i</a></p>&mdash; Shuichi Tsutsumi (@shu223) <a href="https://twitter.com/shu223/status/982553862044139520?ref_src=twsrc%5Etfw">2018年4月7日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

個人的には、こういうAPIで明示的にMetalのクラス（MTLDeviceはプロトコルですが）が必要になるというのも珍しいなと思いました。たとえばCore MLは内部的にMetalを用いていても一切Core MLフレームワークのAPIには出てきませんし、SceneKitやCore Imageも明示的にMetalを利用する部分以外では出てきません。


## ARPlaneAnchorのgeometryプロパティ

iOS 11.3で`ARPlaneAnchor`に`geometry`というプロパティが追加されました。型は`ARPlaneGeometry`です。

`ARPlaneGeometry`はARKitが検出した平面の形状の**メッシュ情報を保持**するクラスです。プロパティから、頂点座標や頂点インデックスにアクセスできます。

```objc
@property(nonatomic, readonly) const vector_float3 *vertices;
```

```objc
@property(nonatomic, readonly) const int16_t *triangleIndices;
```

なお、こちらの親クラスは`NSObject`であり、`SCNGeometry`ではありません。つまりこれはSceneKitのノードにジオメトリとしてそのまま割り当てられるものではないということです。`ARAnchor`は`SCNNode`ではない、というのと同様に考えればいいでしょう。

このオブジェクトを`ARSCNPlaneGeometry`の`update(from:)`メソッドに渡すことでジオメトリを更新できます。

```swift
func update(from: ARPlaneGeometry)
```

## 実装方法

### `ARSCNPlaneGeometry`を初期化する

`MTLDevice`オブジェクトを作成し、`ARSCNPlaneGeometry`を初期化します。

```swift
let device = MTLCreateSystemDefaultDevice()!
planeGeometry = ARSCNPlaneGeometry(device: device)!
```

`MTLDevice`やそのデフォルトデバイスについては「[Metal入門](https://booth.pm/ja/items/826318)」で解説しています。


### 平面検出時／更新時に`ARSCNPlaneGeometry`を更新する

平面検出時／更新時に、`ARPlaneAnchor`オブジェクトの`geometry`プロパティから取得した`ARPlaneGeometry`オブジェクトを渡して`ARSCNPlaneGeometry`オブジェクトを更新します。

```swift
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}
    planeGeometry.update(from: planeAnchor.geometry)
    ...
}

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}
    planeGeometry.update(from: planeAnchor.geometry)
}
```

ARKit-Samplerにサンプルコード入ってます＆すぐにビルドして試せます。

- https://github.com/shu223/ARKit-Sampler
- https://qiita.com/shu223/items/e55e1426c9e036254761

以上。