前回は平面検出後に、平面上の位置を指定せずに3Dモデルを置きました。
今回はARSCNViewのhitTestメソッドを利用して検出した平面上の任意箇所をタップで指定して、3Dモデルを置いてみます。
※平面検出は前回の記事参照
https://techpartner.jp/blog/plain-detection-and-3d-object
ARSCNViewのhitTestについて
ARSCNViewに用意されているhitTestメソッドを利用する事で、現実世界に配置されたARアンカーの位置を取得する事が可能です。
第1引数で検知する2D座標軸、第2引数で検出タイプを指定します。
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(検出タイプ)
第2引数に指定するARHitTestResult.ResultTypeの種類は下記の通りです。
- featurePoint (特徴点)
- 平面に限らず最も近い検出した特徴点の位置を返す
- estimatedHorizontalPlane (推定の水平面)
- 推定した水平面の位置を返す。検出時間は短いメリットはあるが、あくまで推定なので精度が高くない。
- estimatedVerticalPlane (推定の垂直の平面)
- 推定した垂直面の位置を返す。検出時間は短いメリットはあるが、あくまで推定なので精度が高くない。
- existingPlane (検出した平面 + 平面サイズを考慮しない)
- 検出した平面の位置を返す。平面サイズを考慮しない為、検出した平面の延長線上であれば、現実世界で平面でなくとも対象となる。
- existingPlaneUsingExtent (検出した平面 + 平面サイズを考慮する)
- 検出した平面の位置を返す。平面サイズを考慮するので、現実世界の平面のみ対象にできる。
- existingPlaneUsingGeometry (検出したジオメトリ + 平面サイズとカタチを考慮する)
- 検出したジオメトリを対象とする
今回は、existingPlaneUsingExtentを使用する。
※参考
https://developer.apple.com/documentation/arkit/arhittestresult/resulttype
hit Test(_: types:)を平面検出コードに組み込む
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | class ViewController: UIViewController { @IBOutlet var sceneView: ARSCNView! override func viewDidLoad() { super.viewDidLoad() // fps情報などを表示 sceneView.showsStatistics = true // デバッグ用に特徴点を表示 sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints //Viewに初期化したsceneをセット sceneView.scene = SCNScene() //タップを検知する設定 addTapGesture() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // sessionの設定 let configuration = ARWorldTrackingConfiguration() // .horizontalで水平面 (.verticalだと垂直面を検知) configuration.planeDetection = .horizontal //空間から光の情報を取得し画面上のライトの情報に適応 configuration.isLightEstimationEnabled = true // sessionをスタート sceneView.session.run(configuration) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // sessionをストップ sceneView.session.pause() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } @objc func tapped(recognizer: UIGestureRecognizer) { //タップ座標を取得 let view = recognizer.view as! ARSCNView let touchLocation = recognizer.location(in: view) //タップ座標にARアンカーがあるか探す let hitTestResults = sceneView.hitTest(touchLocation, types: .existingPlaneUsingExtent) if hitTestResults.isEmpty == false { //タップ座標にARアンカーが存在すれば、itemを置く if let hitResult = hitTestResults.first { putItemObject(hitTestResult: hitResult) } } } func addTapGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapped)) self.sceneView.addGestureRecognizer(tapGesture) } func putItemObject(hitTestResult: ARHitTestResult) { //現実世界の座標を取得 let transform = hitTestResult.worldTransform let thirdColumn = transform.columns.3 //アイテムを置く let item = load3dObjectNode() item.position = SCNVector3(thirdColumn.x, thirdColumn.y, thirdColumn.z) sceneView.scene.rootNode.addChildNode(item) } func load3dObjectNode() -> SCNNode { let url = Bundle.main.url(forResource: "3d.scnassets/bottle", withExtension: "dae")! let sceneSource = SCNSceneSource(url: url, options: nil)! let virtualObjectNode = sceneSource.entryWithIdentifier("bottle", withClass: SCNNode.self)! return virtualObjectNode } } |
実行!
黒ビールが飲みたくなってきました…。
※本記事はプロダクト実装を前提とした内容でないので、コードの利用は自己責任でお願いします!