Apple Engine

Apple, iPhone, iOS, その周辺のことについて

ARKit を SpriteKit で試す

注意ベータ版で公開されているドキュメントを元に記事を作成しているため、API や動作が製品版と異なる可能性あり

 

SceneKit や Unity、Unreal Engine でほぼ制作されると思われるが、
一応、SpriteKit の 2DCG を使用した ARKit アプリを作成することができる。

 

WWDC 2017 Going Beyond 2D with SpriteKit セッションでのデモ画像。
絵文字をアンカーとして現実空間に貼り付けている。

f:id:x67x6fx74x6f:20170707184811p:plain

 

平面の認識を必要としないのであれば、
リソースの制作時間が圧倒的に少ない 2DCG で ARKit をやるという割り切り方もありだと思っている。

また、カメラやセンサを使ったロジックなので
HoloLens や Tango のように平面の認識が必ず認識できるというわけではないので。

 

どのようにして SpriteKit で ARKit を実現していのか?

ドキュメントの ARSKViewDelegate あたりを見てみると、
どうやら内部で SceneKit 上の板ポリゴンのマテリアルに SpriteKit を貼り付け、
ビルボードコンストレイントのようなものを使用してカメラを向くように設定している模様。

ちなみに、SceneKit で SpriteKit を使用する機能は初期のころからあり、その逆もできる。

 

Game テンプレートからつくってみる

Xcode 9 のテンプレートについては NDA 上、あんまし触れられないので Xcode 8 の方法で作成してみる。

Xcode 9 を起動し、
いつも通りの iOS の Game テンプレートを選び、Game Technology を SpriteKit にしてプロジェクト作成。

Actions.sks は使わないので削除し、GameScene.sks の helloLabel も削除。

 

その1: カメラを使いので info.plist をいじる

info.plist を開いて「Privacy - Camera Usage Description」を追加。
今回は試すだけなので値は空でも可。
例のごとく、これをしないとアプリが落ちて起動しない。

 

その2: Main.storyboard の View を変更する

Game View Controller > View を選択し、Identity Inspector (Command + Option + 3) のクラスを SKView から ARSKView に変更する。

 

その3: GameScene.swift の変更

以下のコードに変更

import SpriteKit
import ARKit

class GameScene: SKScene {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let sceneView = self.view as? ARSKView else {
            return
        }
        
        if let currentFrame = sceneView.session.currentFrame {
            var translation = matrix_identity_float4x4
            translation.columns.3.z = -0.2
            let transform = simd_mul(currentFrame.camera.transform, translation)
            
            let anchor = ARAnchor(transform: transform)
            sceneView.session.add(anchor: anchor)
        }
    }
}

 

GameViewController.swift

以下のコードに変更

import UIKit
import SpriteKit
import ARKit

class GameViewController: UIViewController, ARSKViewDelegate {
    
    var sceneView:ARSKView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        sceneView = self.view as! ARSKView
        
        sceneView.delegate = self
        sceneView.showsFPS = true
        sceneView.showsNodeCount = true
        
        if let scene = SKScene(fileNamed: "GameScene") {
            sceneView.presentScene(scene)
        }
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        let configuration = ARWorldTrackingSessionConfiguration()
        
        sceneView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        sceneView.session.pause()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    // MARK: - ARSKViewDelegate
    
    func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
        let labelNode = SKLabelNode(text: "👾")
        labelNode.horizontalAlignmentMode = .center
        labelNode.verticalAlignmentMode = .center
        return labelNode;
    }
    
    func session(_ session: ARSession, didFailWithError error: Error) {}
    func sessionWasInterrupted(_ session: ARSession) {}
    func sessionInterruptionEnded(_ session: ARSession) {}
}

 

ビルドして実機で実行

平面認識をさせていないので即動作し、画面タップで絵文字が出る。

 

中身は何をしているのか

基本はドキュメント通りの作り方

Providing 2D Virtual Content with SpriteKit | Apple Developer Documentation

 

GameScene.swift

タップイベント後 ARSKView がない場合は即 return で命令を止める。
その後、ARSession の currentFrame があれば、タッチした位置と Z 軸方向 -0.2 を新しく追加されたメソッド simd_mul で SCNMatrix4 の値を掛け合わし、アンカーをシーン上に置く。

 

GameViewController.swift

viewDidLoad で ARSKViewDelegate を含む初期設定。
viewWillAppear で表示された時 ARKit の設定を行い、run で ARKit の開始。
viewWillDisappear でバックグラウンドに戻った際は必ず pause 止める。

デリゲートである func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? はアンカーが置かれた時に SKNode を返す。

今回はタップ時に呼ばれる。

 

セッションようにランダムで絵文字を置く

アンカーは SKLabelNode なのでただランダムで文字を渡せば良い。

GameViewController.swift のデリゲートを以下のように変更

func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
    let emojiArray:[String] = [
        "🍣","🌍","🐶","🍱","🙅","📱","👾"
    ]

    let labelNode = SKLabelNode(text: emojiArray[Int(arc4random_uniform(UInt32(emojiArray.count)))])
    labelNode.horizontalAlignmentMode = .center
    labelNode.verticalAlignmentMode = .center

    return labelNode;
}

 

画像を置いてみる

せっかくなので SKAction でアニメーションさせてみたいのだが問題がある。

上の方で説明した通り、SceneKit のビルボードコンストレイントを使用しているため、そのままだとアニメーションが反映されない。

なぜなら、ビルボードコンストレイントが SCNMatrix4 で変形を行なっているため、SKAction の変形が反映されないためだ。

ということで、空のノードをつけてから SKNode を返すことで解決できる。

 

以下の画像を LogoARKit.png としてプロジェクトに追加。

f:id:x67x6fx74x6f:20170707185201p:plain

 

GameViewController.swift のデリゲートを再度変更

func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
    let spriteNode = SKSpriteNode(imageNamed: "LogoARKit.png")
    spriteNode.xScale = 0.025
    spriteNode.yScale = 0.025
    
    let rotateAnimation = SKAction.rotate(byAngle: CGFloat(Float.pi * 2.0), duration: 1)
    rotateAnimation.timingMode = .easeIn
    
    spriteNode.run(
        SKAction.repeatForever(
            SKAction.sequence([
                SKAction.wait(forDuration: 3),
                rotateAnimation
            ])
        )
    )
    
    let returnNode = SKNode()
    returnNode.addChild(spriteNode)
    
    return returnNode
}

試しに return spriteNode にすると微妙な動きをすると思われる。

 

SpriteKit シーンファイルに何もないのはなぜ?

予想ではあるが SpriteKit 自体にちゃんとした奥行きの情報がないためだと思われ UI(HUD)などで使用するものだと思われる。

そのため GameScene.sks に SKNode を置くとカメラにノードが張り付いて表示される。

 

まとめ

ひとまず、上記のものの SKNode の中身変更するだけで SpriteKit の配置は通りできると思われる。
今回はやらなかったが ARSKView 自体 SKView と同等なので hitTest で SKNode いじることもできる。

このレベルのものでもそこそこ面白さはあるので、
この夏、お暇な方は ARKit で何かつくってみてわというところ。