【平面検出】で、ARをより現実っぽく

#ARPlaneDetection【平面検出】

平面検出で床や壁を利用して、オブジェクトを配置できます。


![IMG_4557.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/07460428-46ad-ad40-ae9f-7e6e0e01645e.png)


###１、平面検出設定でARセッションを開始
ARSCNViewから平面のトラッキング結果を受け取るために、ARSCNViewDelegateを継承します。

```swift
class ViewController: UIViewController, ARSCNViewDelegate {

     override func viewDidLoad() {
        super.viewDidLoad()

        sceneView.delegate = self
    }

}
```
**Plane Detection**【平面検出設定】のオプションをつけて**ARWorldTrackingConfiguration**【世界検出構成】でARKitのセッションを開始します。
**ARWorldTrackingConfiguration.PlaneDetection.horizontal**を設定すると地面や床やテーブル面など水平面を検出します。
**.vertical**を設定すると、壁などの垂直面を検出します。
**.horizontal**と**.vertical**を同時に検出することもできます。

```swift
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    let configuration = ARWorldTrackingConfiguration()
    configuration.planeDetection = [.horizontal, .vertical]
    sceneView.session.run(configuration)
}
```

###２、検出結果を受け取る

####アンカーを検出したら情報を得る
ARSCNViewのデリゲートメソッドで平面検出結果を取得します。

```swift
// 新しいアンカーがみつかったときに呼ばれるデリゲートメソッド
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
    print(planeAnchor)
}
```

**床を検出してみた結果**

平面アンカーのid、向き（horizontal）、平面アンカーの中心、範囲 (1.4m,1.1m)を取得しました。


>ARPlaneAnchor: 0x1165ab6c0 identifier="C843F11D-80C0-41DB-B7F5-492BA12B52FD" transform=<translation=(0.314405 -0.878368 -0.391296) rotation=(-0.00° -15.71° 0.00°)> alignment=horizontal center=(-0.011180 0.000000 -0.055902) extent=(1.498166 0.000000 1.185117)![IMG_4539のコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/77aa93ec-f30f-c796-9e4b-74cedb32d28c.png)

わかりやすいようにPlane geometryを持ったSCNNodeを平面に自動追加された空のSCNNodeにaddChildしてみます。

```swift
var visualizedPlaneNode = SCNNode() // 平面視覚化用のノードをつくる

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
    print(planeAnchor)
    
    let planeGeometry = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z)) // アンカーと同じ大きさのPlaneGeometryをつくる
    visualizedPlaneNode.geometry = planeGeometry // ノードのgeometryにPlaneを設定
    planeGeometry.firstMaterial?.diffuse.contents = UIColor.blue.withAlphaComponent(0.7) // 青色にしてみる
    
    visualizedPlaneNode.simdPosition = planeAnchor.center // アンカーの中心に配置
    visualizedPlaneNode.eulerAngles.x = -.pi / 2  // SCNPlaneはデフォルトでは垂直なため、回転させます。

    node.addChildNode(visualizedPlaneNode) // 平面に自動追加された空ノードに入れる
}

```
![IMG_4541.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/27e2e253-1ff1-23af-362c-e80fe1deeb5e.png)

####平面情報をアップデートしていく
別のレンダラーメソッドで平面情報をアップデートしていきます。

```swift
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor, // 更新された平面アンカーを取得
          let visualizedPlaneNode = node.childNodes.first, // 自動追加された空のノードの子ノードを確認
          let planeGeometry = visualizedPlaneNode.geometry as? SCNPlane else { return } // 子ノードのplanegeometryを確認
    planeGeometry.width = CGFloat(planeAnchor.extent.x) // 幅を更新された平面アンカーの幅に
    planeGeometry.height = CGFloat(planeAnchor.extent.z) // 高さを更新された平面アンカーの高さに
    visualizedPlaneNode.simdPosition = planeAnchor.center // 中心を更新された平面アンカーの中心に
}
```
![Apr-25-2021 10-11-53.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/66b28eab-d9ff-29ca-e290-4c304bd94878.gif)

ちなみに、組み込まれている検出された平面アンカーそのものの正方形ではないgeometryの形を取ることもできます。

```swift
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor,
          let meshGeometry = ARSCNPlaneGeometry(device: sceneView.device!)
            else { fatalError("Can't create plane geometry") }
    meshGeometry.update(from: planeAnchor.geometry)
    meshNode = SCNNode(geometry: meshGeometry)
    meshNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(0.7)
    node.addChildNode(meshNode)
}

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {

    guard let planeAnchor = anchor as? ARPlaneAnchor
    if let meshGeometry = meshNode.geometry as? ARSCNPlaneGeometry {
        meshGeometry.update(from: planeAnchor.geometry)
    }
}
```
![Apr-25-2021 10-27-38.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/a299a428-16aa-af8e-1d8b-3874e9597c43.gif)

###３、検出した平面の利用

**壁紙を貼ってみる**

```swift
planeGeometry.firstMaterial?.diffuse.contents = UIImage(named:"flowerWallPaper")
```

![IMG_4548.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/7bbc78e7-1472-8dba-54b2-cacf6c2a6b62.png)

**オブジェクトをおいてみる**

画面をタップした先に平面geometry(ここではARPlaneGeometryをとってます)があれば、オブジェクトのy座標を平面ノードに合わせて、x,z軸を平面とタップの交差点に合わせます。

```swift
@objc func sceneViewTapped(recognizer:UITapGestureRecognizer) {
    let location = recognizer.location(in: sceneView)
        
    let hitResults = sceneView.hitTest(location, options: [:])
    if !hitResults.isEmpty {
        guard let planeNode = hitResults.first?.node else {return}
        
        let interiorHeight = interior.boundingBox.max.y-interior.boundingBox.min.y // オブジェクトノードの高さを計算
        let tappedCoodinates = hitResults.first?.localCoordinates // タップと平面ノードの交差点
        interior.position = SCNVector3(tappedCoodinates!.x, interiorHeight/2, tappedCoodinates!.z) // オブジェクトの原点はオブジェクトの中心にしたので、高さの半分だけ上に置く
        planeNode.addChildNode(interior)
}
```

![Apr-25-2021 12-43-24.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/d0df065f-674b-7bb9-2c76-def86bfede3e.gif)

**壁でオブジェクトを遮蔽する**

.verticalの子ノードを手前にレンダリングされるようにすることで、後ろのノードが壁に隠れている表現ができます。

```swift
if planeAnchor.alignment == .vertical {
    planeNode.renderingOrder = -1            
    planeNode.geometry!.firstMaterial!.colorBufferWriteMask = []
}
```

![IMG_4557.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/07460428-46ad-ad40-ae9f-7e6e0e01645e.png)

🐣
****
フリーランスエンジニアです。
お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

[Twitter](https://twitter.com/JackdeS11)
[Medium](https://medium.com/@rockyshikoku)