ARKitの紹介でよく見る、空間に絵を書くデモ。どの様な仕組みで書いているのか調べてみました。
目次
実装する内容
タッチ中の座標に、renderer(_:updateAtTime:)の更新を利用して
球体ノードを連続して追加するとお絵かきっぽくなります。
1 2 | optional func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) |
※公式リファレンス
https://developer.apple.com/documentation/scenekit/scnscenerendererdelegate/1522937-renderer
今回は、カスタムジオメトリは作成せずに、単に球体のノードを追加していく事で
お絵かきを実現していきます。
以下に実装の流れを記載します。
※ARKitの基本的な実装は、今回の説明からは省略しています。
タッチの座標と、状態を保持する変数を用意
タッチの座標とタッチの状態を保持するグローバル変数を用意します。
1 2 | var touchedLocation: CGPoint = .zero var isTouching = false |
UIResponderを利用し、タッチ情報を取得
touchesBegan
タッチ状態をtrueに変更、タッチ座標を取得しtouchedLocation変数に格納
1 2 3 4 5 6 7 8 9 | override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { isTouching = true guard let location = touches.first?.location(in: nil) else { return } touchedLocation = location } |
touchesMoved
タッチの動きに合わせて、座標を取得を続ける
1 2 3 4 5 6 | override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard let location = touches.first?.location(in: nil) else { return } touchedLocation = location } |
touchesEnded
タッチ終了(指が離れる)の検知で、タッチ状態をfalseに変更
1 2 3 | override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { isTouching = false } |
renderer(_: update At Time:)で球体ノードを追加
renderer(_:updateAtTime:)は、フレーム更新毎する度に呼ばれます。
isTouchingがtrue(タッチされている状態)の間は、ここで球体のノードを追加します。
端末負荷を下げる為に、一度作成したノードはコピーして再利用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | var existentNode: SCNNode? func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { guard isTouching else { return } let ballNode: SCNNode //既にノードが作成したら、clone()でコピーして再利用する if let node = existentNode { ballNode = node.clone() } else { ballNode = createBallNode() existentNode = ballNode } //スクリーン座標から、ワールド座標に変換する let wordPostion = sceneView.unprojectPoint(SCNVector3(touchedLocation.x, touchedLocation.y, 0.995)) ballNode.position = wordPostion sceneView.scene.rootNode.addChildNode(ballNode) } |
実行
お絵かきっぽくはなりますが、球体ノードの連続だとあまりきれいに描けないですね。
次回はカスタムジオメトリを作成して実装してみようと思います。
コード全文
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 | import UIKit import ARKit class ViewController: UIViewController { @IBOutlet var sceneView: ARSCNView! var touchedLocation: CGPoint = .zero var isTouching = false var existentNode: SCNNode? override func viewDidLoad() { super.viewDidLoad() sceneView.delegate = self sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints] sceneView.scene = SCNScene() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let configuration = ARWorldTrackingConfiguration() configuration.isLightEstimationEnabled = true sceneView.session.run(configuration) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) sceneView.session.pause() } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { isTouching = true guard let location = touches.first?.location(in: nil) else { return } touchedLocation = location } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard let location = touches.first?.location(in: nil) else { return } touchedLocation = location } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { isTouching = false } func createBallNode()-> SCNNode { let ball = SCNSphere(radius: 0.003) ball.firstMaterial?.diffuse.contents = UIColor.blue return SCNNode(geometry: ball) } } extension ViewController: ARSCNViewDelegate { func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { guard isTouching else { return } let ballNode: SCNNode //既にノードが作成したら、clone()でコピーして再利用する if let node = existentNode { ballNode = node.clone() } else { ballNode = createBallNode() existentNode = ballNode } //スクリーン座標から、ワールド座標に変換する let wordPostion = sceneView.unprojectPoint(SCNVector3(touchedLocation.x, touchedLocation.y, 0.995)) ballNode.position = wordPostion sceneView.scene.rootNode.addChildNode(ballNode) } } |