前回につづいてARKit関連の記事になります。

ARのアプリというと3Dモデルを使ってなんやかんやするというイメージが強く、ハードルが高いものだと思われがちです。

ただ、球体や立方体などのオブジェクトであれば簡単に表示することができます。

そこで、ちょっとしたサイコロアプリを作ってみることにしました。

Xcodeで新規プロジェクト作成時に「Augmented Reality App」を選択すると、ARKitを使用するアプリのテンプレートが作成されます。
今回はこれをベースに作りました。

最終的なプロジェクトファイルはGithubに上げましたが、いくつかピックアップして解説していきたいと思います。

https://github.com/micchymouse/ARKitDise

まずは ARSCNViewDelegate のメソッドである func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) にて、平面を認識した際の処理を記述します。
端末が平面を認識するとこのメソッドが呼び出され、ここで認識した平面を元に仮想空間上に床を生成します。
今回は簡単のために、平面認識を一度のみとしています。

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else {
        return
    }

    // 平面は最初に認識された1つのみ
    if !isFloorRecognized {
        node.addChildNode(makeFloor(initAnchor: planeAnchor))
        sceneView.debugOptions = []
        isFloorRecognized = true
    }
}

makeFloor では床を作成しています。
平面認識時に作成された平面データの位置に直方体のオブジェクトを作成し、それを床として扱います。
床が作成されたことをわかりやすくするために、黒い半透明の色で塗りつぶしています。

/// 床を作成
private func makeFloor(initAnchor: ARPlaneAnchor) -> SCNNode {

    let floorGeometry = SCNBox(width: CGFloat(initAnchor.extent.x),
                           height: thickness,
                           length: CGFloat(initAnchor.extent.z),
                           chamferRadius: 0)
    let floorNode = SCNNode(geometry: floorGeometry)

    // 床の位置を指定
    floorNode.position = SCNVector3Make(initAnchor.center.x, 0, initAnchor.center.z)
    // 床の判定を追加
    floorNode.physicsBody = SCNPhysicsBody(type: .kinematic,
                                           shape: SCNPhysicsShape(geometry: floorGeometry,
                                                                  options: nil))
    // 床を黒くする
    let material = SCNMaterial()
    material.diffuse.contents = UIColor(white: 0.0, alpha: 0.9)
    floorNode.geometry?.firstMaterial = material

    return floorNode
}

initDice ではサイコロを作成しています。
立方体を作成し、その各面に対してあらかじめ用意した画像を貼り付けています。
また、先ほど作成した床とぶつかるように、物理演算設定も行っています。

/// サイコロを初期化
private func initDice() {

    let dice = SCNBox(width: diceLength, height: diceLength, length: diceLength, chamferRadius: 0)

    // テクスチャを設定
    let m1 = SCNMaterial()
    let m2 = SCNMaterial()
    let m3 = SCNMaterial()
    let m4 = SCNMaterial()
    let m5 = SCNMaterial()
    let m6 = SCNMaterial()
    m1.diffuse.contents = #imageLiteral(resourceName: "food1")
    m2.diffuse.contents = #imageLiteral(resourceName: "food2")
    m3.diffuse.contents = #imageLiteral(resourceName: "food3")
    m4.diffuse.contents = #imageLiteral(resourceName: "food4")
    m5.diffuse.contents = #imageLiteral(resourceName: "food5")
    m6.diffuse.contents = #imageLiteral(resourceName: "food6")
    dice.materials = [m1, m2, m3, m4, m5, m6]

    diceNode = SCNNode(geometry: dice)

    // サイコロの判定を追加
    let diceShape = SCNPhysicsShape(geometry: dice, options: nil)
    diceNode!.physicsBody = SCNPhysicsBody(type: .dynamic, shape: diceShape)
}

画面タップ時に makeDice でサイコロを出現させています。
ランダムな面が表示されるように、出現時に乱数値を使って回転を与えています。

/// サイコロを出現させる
///
/// - Parameter point: 作成の基準となる画面上の位置
private func makeDice(point: CGPoint) {

    // 既存のサイコロに加わる力をリセット
    diceNode.physicsBody?.clearAllForces()
    // 既存のサイコロを削除
    diceNode.removeFromParentNode()

    // scneView上の位置を取得
    let results = sceneView.hitTest(point, types: .existingPlaneUsingExtent)

    guard let hitResult = results.first else {
        // タップした場所が認識された平面上でなければ何もしない
        return
    }

    // sceneView上のタップ座標のどこに箱を出現させるかを指定
    diceNode.position = SCNVector3Make(hitResult.worldTransform.columns.3.x,
                                       hitResult.worldTransform.columns.3.y + 0.2,
                                       hitResult.worldTransform.columns.3.z)

    // サイコロの回転状態を乱数で設定
    let randX = makeRandomSpinValue()
    let randY = makeRandomSpinValue()
    let randZ = makeRandomSpinValue()
    let randW = makeRandomSpinValue()
    diceNode.physicsBody?.applyTorque(SCNVector4(randX, randY, randZ, randW), asImpulse: false)
    // サイコロを少し上方向に投げる
    diceNode.physicsBody?.applyForce(SCNVector3(0, 1, 0), asImpulse: true)

    // ノードを追加
    sceneView.scene.rootNode.addChildNode(diceNode)
}

こんな感じで下記のようなアプリを作成しました。
各面には色々なご飯の絵が設定されており、今日何食べるか迷ったらこのアプリが決めてくれます(笑)

ARKitを使うと、3Dモデルや物理演算などの知識がなくとも、ちょっとしたアプリ程度であれば簡単に作成することができるので、是非とも皆さん挑戦してみてください!

最後に、ブレイブソフト初のARKitを使ったアプリ「ARケチャマヨバトル」も是非ともダウンロードして遊んでみてください!