ARKit+SceneKitでオブジェクトを配置するときのカーソル表示

ARで任意のオブジェクトを配置するときなどに表示するカーソルについて。

##  やりたいこと

標準の計測アプリのこれ。
![keisoku_app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/482b4553-13d9-a9d5-bfb6-44edc3080dc0.png)


## カーソルのtransform

画面中央座標のヒットテストを行い、取得した`worldTransform`をもとに設定する。
`ARSCNView` の `hitTest(_:types:)` でヒットした場所を `ARHitTestResult` の `worldTransform` から取得できるので、これをカーソルの場所・姿勢とする。
ここで、カーソルを `SCNPlane` にした場合に、カーソルの場所・姿勢＝ヒットした場所・姿勢　としてしまうと平面のジオメトリと干渉してチラつくので、平面の上方向のベクトル `worldTransform.columns.1` を使ってカーソルの位置を調整する。

```swift
// 平面が向いている方向（UP）に0.01mずらした位置にカーソルを設定
cursorTransform.columns.3 += worldTransform.columns.1 * 0.01
self.cursorNode.simdTransform = cursorTransform
```

transform を設定するので`SCNNode`が拡大縮小(`scale` ≠ (1.0,1.0,1.0)）されている場合、拡大縮小がリセットされる点に注意。
その場合は worldTransform.columns.0 ~ 2 に拡大縮小率を掛けた値を、cursorTransform.columns.0 ~ 2 に設定する。

## 出来上がり
カーソルの上方向が分かり易いようにピラミッド形状にしている。
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/dbcee78b-ffbe-b32a-a4d1-e5fb84edeefb.gif)



ソースコード

```swift:ViewController.swift
import ARKit
import SceneKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!
    
    private let device = MTLCreateSystemDefaultDevice()!
    private let cursorNode = SCNNode()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // AR Session 開始
        self.scnView.delegate = self
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal, .vertical]
        self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking])
        // カーソルノード準備
        let pyramid = SCNPyramid(width: 0.1, height: 0.03, length: 0.1)
        pyramid.firstMaterial!.diffuse.contents = UIColor.yellow
        self.cursorNode.geometry = pyramid
        self.scnView.scene.rootNode.addChildNode(self.cursorNode)
        self.cursorNode.isHidden = true
    }
    //
    // アンカーが追加された
    //
    func renderer(_: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }

        // 平面ジオメトリノードを追加
        guard let geometory = ARSCNPlaneGeometry(device: self.device) else { return }
        geometory.update(from: planeAnchor.geometry)
        let material = SCNMaterial()
        material.lightingModel = .physicallyBased
        material.diffuse.contents = UIColor.red.withAlphaComponent(0.7)
        geometory.materials = [material]
        let planeNode = SCNNode(geometry: geometory)
        DispatchQueue.main.async {
            node.addChildNode(planeNode)
        }
    }
    //
    // アンカーが更新された
    //
    func renderer(_: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
        
        DispatchQueue.main.async {
            for childNode in node.childNodes {
                // 平面ジオメトリを更新
                guard let planeGeometry = childNode.geometry as? ARSCNPlaneGeometry else { continue }
                planeGeometry.update(from: planeAnchor.geometry)
                break
            }
        }
    }
    //
    // フレームごとに呼び出される
    //
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime _: TimeInterval) {
        DispatchQueue.main.async {
            // 画面中央でヒットテスト
            let bounds = self.scnView.bounds
            let screenCenter =  CGPoint(x: bounds.midX, y: bounds.midY)
            let results = self.scnView.hitTest(screenCenter, types: [.existingPlaneUsingGeometry])
            guard let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }),
                  let _ = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor else {
                // カーソル非表示
                self.cursorNode.isHidden = true
                return
            }
            
            // ヒットした場所のtransformをカーソルのtransformに転記
            let worldTransform = existingPlaneUsingGeometryResult.worldTransform
            var cursorTransform = worldTransform
            // 平面が向いている方向（UP）に0.01mずらした位置にカーソルを設定
            cursorTransform.columns.3 += worldTransform.columns.1 * 0.01
            self.cursorNode.simdTransform = cursorTransform
            
            self.cursorNode.isHidden = false
        }
    }
}
```