目次
実装する内容
実装する内容はいたってシンプル。
- 平面検出し、床となる3Dオブジェクトを追加する
- 球体の3Dオブジェクトを床の上に落とす
の2点のみです。
平面検出や、ARSCNViewDelegateのデリゲートメソッドなど基本的なARKitの機能の説明は本記事では省略します。
床となるPlaneNodeクラスを作成
床となるPlaneNode(SCNNode)クラスを作成する際に、physicsBodyプロパティに物理情報を追加する事で、物理シュミレーションが可能となります。
このPlaneNodeは球体を受け止める床となりますので、PhysicsBodyのTypeは固定の.staticとします。
PhysicsBodyのType
- Static : 静止物
- Dynamic : 動く
- Kinematic : 単体だと動かないけど、他の物体の影響で動く
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 | class PlaneNode: SCNNode { fileprivate override init() { super.init() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } init(anchor: ARPlaneAnchor) { super.init() geometry = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z)) let planeMaterial = SCNMaterial() planeMaterial.diffuse.contents = UIColor.white.withAlphaComponent(0.2) geometry?.materials = [planeMaterial] let planeShape = SCNPhysicsShape(geometry: geometry!, options: nil) let planeBody = SCNPhysicsBody(type: .static, shape: planeShape) physicsBody = planeBody let x = CGFloat(anchor.center.x) let y = CGFloat(anchor.center.y) let z = CGFloat(anchor.center.z) position = SCNVector3(x,y,z) eulerAngles.x = -.pi / 2 } func update(anchor: ARPlaneAnchor) { (geometry as! SCNPlane).width = CGFloat(anchor.extent.x) (geometry as! SCNPlane).height = CGFloat(anchor.extent.z) let planeShape = SCNPhysicsShape(geometry: geometry!, options: nil) let planeBody = SCNPhysicsBody(type: .static, shape: planeShape) physicsBody = planeBody let x = CGFloat(anchor.center.x) let y = CGFloat(anchor.center.y) let z = CGFloat(anchor.center.z) position = SCNVector3(x,y,z) } } |
ARSCNViewDelegateのデリゲートメソッドであるrenderer(_:didUpdate:for:)で更新された、より正確なARアンカー(ここでは平面情報)を取得した際に、geometryやpositionを更新できる様にupdate(anchor: ARPlaneAnchor)も用意します。
落下させる球体となるBallNodeクラスを作成
球体となるBallNode(SCNNode)クラスにも、物理情報を追加します。
球体は落下させるので、Typeは.dynamicとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class BallNode: SCNNode { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override init() { super.init() let ballGeometry = SCNSphere(radius: 0.01) geometry = ballGeometry let ballMaterial = SCNMaterial() let ballShape = SCNPhysicsShape(geometry: ballGeometry, options: nil) let ballBody = SCNPhysicsBody(type: .dynamic, shape: ballShape) ballBody.restitution = 1.0 physicsBody = ballBody ballMaterial.diffuse.contents = UIColor.red.withAlphaComponent(1) geometry?.materials = [ballMaterial] } } |
タップしたARアンカー上の3D座標に球体を落とす機能を実装
まずタップした位置を取得する為に、viewDidLoad()にUITapGestureRecognizerを設定します。
1 2 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapView)) sceneView.addGestureRecognizer(tapGesture) |
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 (検出したジオメトリ + 平面サイズとカタチを考慮する)
今回は、平面のサイズを考慮するので、existingPlaneUsingExtentを使用します。
※参考
https://developer.apple.com/documentation/arkit/arhittestresult/resulttype
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @objc func tapView(recognizer: UIGestureRecognizer) { //タップした2D座標を取得 let view = recognizer.view as! ARSCNView let touchLocation = recognizer.location(in: view) //タップ座標にARアンカーがあるか探す let hitTestResult = sceneView.hitTest(touchLocation, types: .existingPlaneUsingExtent) if let result = hitTestResult.first { //タップされた位置のARアンカーの3D座標を取得 let transform = result.worldTransform let thirdColumn = transform.columns.3 //球体ノードを生成 let ballNode = BallNode() //平面のARアンカーの10cm上にポジショニングする ballNode.position = SCNVector3(thirdColumn.x, thirdColumn.y + 0.1, thirdColumn.z) sceneView.scene.rootNode.addChildNode(ballNode) } } |
実行
特徴量が少ないテーブルで平面検出した為か、テーブルの縁の認識が少し甘いですが、ARKitの世界に重力を持ち込む事が出来ました。