先日の記事で紹介したARKitで絵を描くデモと同じく有名なのがARメジャーアプリ。
※ARKitで絵を描いてみた前回記事はこちら
https://techpartner.jp/blog/drawing
今回は、メジャーアプリを作る上での基礎となる、ARkitを用いた距離計測を実装してみます。
目次
実装する内容
ARKitを用いた距離計測を実現する流れは下記となります。
- 始点と終点を照準するための画像を画面の中心に設置
- 始点と終点の座標をhitTest(_:types:)を利用して取得
- 始点、終点の2点間の距離を計算
また、始点と終点に球体のノードを設置し、線のノードでつなぐ事で
メジャー風のビジュアルを実現します。
※ARKitの基本的な実装は、今回の説明からは省略しています。
画面の中心に照準画像を設置
画面の中心にスコープ画像を設置し、始点、終点を指定できる様にします。
※利用させていただいた画像
https://www.flaticon.com/free-icon/target_149231#term=target&page=1&position=2
hit Testで始点、終点座標を取得
ARSCNViewのhitTestを利用して、ヒットした箇所のワールド座標を取得します。
hit Test(_: types:)について
1 2 | func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult] |
- パラメータ
- 第1引数はCGPoint(2D座標軸)
- 第2引数はARHitTestResult.ResultType(検出タイプ)
- 戻り値:
- ARHitTestResult型の配列。※カメラから近い順にソートされる
- .worldTransform: simd_float4x4型 (4×4の変換行列)
- ARHitTestResult型の配列。※カメラから近い順にソートされる
※参考
https://developer.apple.com/documentation/arkit/arscnview/2875544-hittest
ARHit Test Result .Result Type
ARHitTestResult.ResultTypeの種類は下記
- featurePoint (特徴点)
- estimatedHorizontalPlane (推定の平面)
- estimatedVerticalPlane (推定の垂直の平面)
- existingPlane (検出した平面 + 平面サイズを考慮しない)
- existingPlaneUsingExtent (検出した平面 + 平面サイズを考慮する)
- existingPlaneUsingGeometry (検出したジオメトリ + 平面サイズとカタチを考慮する)
今回は、平面に限らず距離を計測したいので、featurePointを指定します。
※参考
https://developer.apple.com/documentation/arkit/arhittestresult/resulttype
中心点を取得するメソッド
1 2 3 4 5 6 7 8 9 10 | func getCenter() -> SCNVector3? { let center = sceneView.center let results = sceneView.hitTest(center, types: .featurePoint) if results.isEmpty == false { if let result = results.first { return SCNVector3(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z) } } return nil } |
測定ボタンのタップで計測開始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @IBAction func measure(_ sender: Any) { if let centerPosition = getCenter() { //既存の始点、終点ノードを削除 if startNode != nil { startNode!.removeFromParentNode() } if endNode != nil { endNode!.removeFromParentNode() } //測定中ステータスに変更 isMeasuring = true //中心点を始点座標として格納 startPosition = centerPosition //始点座標に球体ノードを追加 startNode = createBallNode(position: startPosition, color: UIColor.red) sceneView.scene.rootNode.addChildNode(startNode!) } } |
測定ボタンのタップが終了したら計測終了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @IBAction func touchUpInside(_ sender: Any) { endMeasure() } @IBAction func touchUpOutside(_ sender: Any) { endMeasure() } func endMeasure() { if !isMeasuring { return } isMeasuring = false if let endPosition = getCenter() { endNode = createBallNode(position: endPosition, color: UIColor.green) sceneView.scene.rootNode.addChildNode(endNode!) } } |
始点、終点の2点間の距離を計算
3次元の始点、終点の2点間の距離を計算する公式は下記です。
始点と終点の座標から距離を計算します。
1 2 3 4 5 | func getDistance(endPosition: SCNVector3) -> Float { let position = SCNVector3Make(endPosition.x - self.startPosition.x, endPosition.y - self.startPosition.y, endPosition.z - self.startPosition.z) return sqrt(position.x*position.x + position.y*position.y + position.z*position.z) } |
renderer(_:updateAtTime:)で始点、終点をつなぐ線ノードを更新
renderer(_:updateAtTime:)は、フレーム更新毎する度に呼ばれるので、測定中のステータスの時は、線ノードをアップデートし続けます。
線ノードの作成
1 2 3 4 5 6 7 8 9 10 | func createLineNode(from: SCNVector3, to: SCNVector3, color: UIColor) -> SCNNode { let source = SCNGeometrySource(vertices: [from, to]) let indices: [Int32] = [0, 1] let element = SCNGeometryElement(indices: indices, primitiveType: .line) let line = SCNGeometry(sources: [source], elements: [element]) line.firstMaterial?.lightingModel = SCNMaterial.LightingModel.blinn let lineNode = SCNNode(geometry: line) lineNode.geometry?.firstMaterial?.diffuse.contents = color return lineNode } |
測定中のステータスの時は、線ノードをアップデートし続ける
1 2 3 4 5 6 7 8 9 10 11 12 13 | func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { DispatchQueue.main.async { if self.isMeasuring { if let endPosition = self.getCenter() { self.updateLineNode(endPosition: endPosition) let distance = self.getDistance(endPosition: endPosition) self.valueLabel.text = String.init(format: "%.2fm", arguments: [distance]) } } } } |
実行
MacBook Pro 15インチの横幅は34.93 cmなので、誤差1cm以内で計測できました。
ただ、特徴点でのhitTestの場合、想定と違う位置の座標を取得してしまう事もあるので
今回のラップトップの様に、平面検知が容易な対象の場合は、hitTestのタイプを特徴量でなく、平面アンカーとした方が精度が上がりそうです。