ARKitを使って空間に線を描く方法(初級編)


ARKitを使って線を書けるアプリで有名なのがJust a lineWorld Brushなど。その他にもFacebookのカメラでも空間に落書きできたり、落書きアプリというのはいくつかあります。

また僕の所属するGraffityでも空間に落書きするアプリを作ってきました。

今記事では、どういった原理で空間に落書きしているのか解説します。

一番カンタンな線を書く方法

「カメラから一定の距離に球を置き、その連続で線を作る」という方法がもっとも簡単です。

連続で点を打つ 点を打つスパンを短く
// タップした状態で指を動かすと呼ばれるメソッド
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let point = touches.first?.location(in: sceneView) else {
        return
    }
    let point3D = sceneView.unprojectPoint(SCNVector3(point.x, point.y, 0.997))

    // 球のNodeを作る
    let ball = SCNSphere(radius: 0.005)
    ball.firstMaterial?.diffuse.contents = UIColor.red
    let node = SCNNode(geometry: ball)
    node.position = point3D
    sceneView.scene.rootNode.addChildNode(node)
}

フレームの更新を利用する方法

先程の方法だと、指の動きをトラックしているだけなので、デバイスを動かしたときに動いたとみなされず、お絵かきの幅が広がりません。

以下のように、踊りながらお絵描きができないわけです。

そこで、フレームの更新を利用する方法があります。タッチ中はフラグを立てておいて(必要に応じてタッチのポジションを先のtouchesMovedメソッドで取得し)、フレームが更新するたびに呼ばれるARSCNViewDelegateのrenderer(:updateTime)の中で球を置いていくパターンです。

ちなみにARKitでは、デフォルトでは60fpsなので、1秒間に60回フレームが更新されてカメラが動画として見えます。つまり、この方法では1秒間に60回球を置いておくことになります。

private var pointTouching: CGPoint = .zero

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    isTouching = true

    guard let location = touches.first?.location(in: nil) else {
        return
    }
    pointTouching = location
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let location = touches.first?.location(in: nil) else {
        return
    }
    pointTouching = location
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    isTouching = false
}

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    guard isTouching else {return}

    // 球のNodeを作って置く
    let point3D = sceneView.unprojectPoint(SCNVector3(pointTouching.x, pointTouching.y, 0.997))

    // 球のNodeを作る
    let ball = SCNSphere(radius: 0.005)
    ball.firstMaterial?.diffuse.contents = UIColor.red
    let node = SCNNode(geometry: ball)
    node.position = point3D
    sceneView.scene.rootNode.addChildNode(node)
}

球を連続で置くことに対する課題

ここまで述べたように、「球を連続に置く」ことで線が出来上がります。しかしSCNNodeを短い時間に連続で置きまくるととても重くなります。そして、そんなにきれいな線じゃないですよね。

ここで3DBrushというアプリを見てみましょう。

3DBrushは球の連続で線を書くのではなく、1本の線単位の曲線のジオメトリを持つSCNNodeを作って置いているように見えます。よりメモリ効率の良い、より綺麗な線を描画するためにはこのアプローチが良さそうです。

では、どのようにこのアプローチをしていくのでしょうか?

こちらは、また明日の記事で上級編として書きたいと思います。

では!