先日の記事で紹介した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のタイプを特徴量でなく、平面アンカーとした方が精度が上がりそうです。
コメント