LoginSignup
22
15

More than 5 years have passed since last update.

ARKitを使って結婚式の賑やかしアプリを作った

Posted at

はじめに

自分の結婚式に来てくれる人に、少しでも楽しんでもらおうとARKitを使ってiPhoneアプリを作りました。これまでの思い出の写真やムービーが見れるようにしておき、結婚式後の小規模なディナーパーティーで使ってもらいました。

機能

主な機能は2つです。

  • テーブルや地面から写真を表示する
    平面を認識して、写真入りのシャボン玉が出ます。シャボン玉はランダムな場所に動いていき、しばらくすると消えます。

  • スケッチブックにムービーを表示する
    スケッチブックの中のイラストを認識して、ムービーが表示されます。

表示コンテンツ

表示される写真やムービーは、画面右上(右下)の名前が書かれたボタンをタップすることで切り替わるようにしました。実際と上記のデモムービーでは次のような形で分けました。

Everyone (AR_0) それぞれの名前 (AR_1,2,3…)
実際 結婚式の様子 その人との写真・ムービー
デモ 色々な芸能人 阿部寛の写真・ムービー(Abe)

また、直前までコンテンツの入れ替え(結婚式の写真を入れるなど)に対応したかったため、コンテンツはマイアルバムで管理しました。マイアルバムの名前をAR_※にし、※の部分をソースコード内のIDと対応付けています。

ソースコード

Github - ykshr/AR-PhotoViewerにあげています。
※ 無理やりなんとか動くレベルのソースコードなので、あまり参考にはならないと思います。

シャボン玉

写真入りシャボン玉はSCNSphereの中に、UIImageを貼り付けたSCNPlaneを入れることで作っています。SCNAction.moveでランダムな位置に移動させた後、removeFromParentNodeで削除しています。

func createBubble(image: UIImage) -> SCNNode {
    let plane = SCNPlane(width: 0.1, height: 0.1)
    plane.firstMaterial?.isDoubleSided = true
    plane.firstMaterial?.diffuse.contents = image
    let planeNode = SCNNode(geometry: plane)
    let prx = Float(arc4random_uniform(360)) * (Float.pi / 180)
    let pry = Float(arc4random_uniform(360)) * (Float.pi / 180)
    planeNode.eulerAngles = SCNVector3(prx, pry, 0)

    let sphere = SCNSphere(radius: 0.1)
    sphere.firstMaterial?.diffuse.contents = #imageLiteral(resourceName: "ar-bubbleText")
    sphere.firstMaterial?.isDoubleSided = true
    sphere.firstMaterial?.writesToDepthBuffer = false
    sphere.firstMaterial?.blendMode = .screen
    //sphere.firstMaterial?.lightingModel = .physicallyBased
    //sphere.firstMaterial?.metalness.contents = 1.0
    //sphere.firstMaterial?.roughness.contents = 0
    let sphereNode = SCNNode(geometry: sphere)
    sphereNode.opacity = 0.1
    let srx = Float(arc4random_uniform(360)) * (Float.pi / 180)
    let sry = Float(arc4random_uniform(360)) * (Float.pi / 180)
    sphereNode.eulerAngles = SCNVector3(srx, sry, 0)

    let bubbleNode = SCNNode()
    bubbleNode.addChildNode(planeNode)
    bubbleNode.addChildNode(sphereNode)

    let box = SCNBox(width: 0.2, height: 0.2, length: 0.2, chamferRadius: 0)
    box.firstMaterial?.diffuse.contents = UIColor.white.withAlphaComponent(0.0)
    let bubbleBoxNode = SCNNode(geometry: box)
    bubbleBoxNode.addChildNode(bubbleNode)

    // ボックスの外に出す
    let firstAction = SCNAction.move(by: SCNVector3(0, 0.4, 0), duration: 2)
    firstAction.timingMode = .easeOut
    bubbleNode.runAction(firstAction)
    // フワフワさせる
    let secondAction = SCNAction.move(by: SCNVector3(randomNumbers(firstNum: -1.5, secondNum: 1.5 ),randomNumbers(firstNum: 0, secondNum: 3), randomNumbers(firstNum: -1.5, secondNum: 1.5 )), duration: TimeInterval(randomNumbers(firstNum: 10, secondNum: 30)))
    secondAction.timingMode = .easeOut
    bubbleNode.runAction(secondAction, completionHandler: {
        bubbleNode.runAction(SCNAction.fadeOut(duration: 0), completionHandler: {
            DispatchQueue.main.async {
                self.playSoftImpact()
            }
            bubbleNode.removeFromParentNode()
            bubbleBoxNode.removeFromParentNode()
        })
    })

    return bubbleBoxNode
}

シャボン玉の生成は、一定時間間隔でランダムなディスプレイ上の点を取得し、その点に平面アンカーが存在していた場合、シャボン玉を生成するようにしています。ここはTimerではなく、もっといい方法があるかもしれません。

Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) {Void in
    let p = CGPoint(x: Int(arc4random() % 1000), y: Int(arc4random() % 1000))
    let hitTestResult = self.sceneView.hitTest(p, types: .existingPlaneUsingExtent)
    if !hitTestResult.isEmpty {
        if let hitResult = hitTestResult.first {
            let bubbleBoxNode = self.createBubble(image: self.imgAssets[Int(arc4random_uniform(UInt32(self.imgAssets.count)))])

            bubbleBoxNode.position = SCNVector3(hitResult.worldTransform.columns.3.x, hitResult.worldTransform.columns.3.y - 0.2, hitResult.worldTransform.columns.3.z)

            scene.rootNode.addChildNode(bubbleBoxNode)
        }
    }
}

スケッチブック

スケッチブックへの動画の表示は、SKScene使ってAVPlayerSCNPlaneに貼り付けています。ARImageAnchorが追加されたタイミング(didAdd)ではなく、更新されたタイミング(didUpdate)で生成しているのですが、これは、画面上からスケッチブックがなくなってもなかなか削除イベント(didRemove)が呼ばれなかったためです。

if let imageAnchor = anchor as? ARImageAnchor {
    // 平面ジオメトリのサイズを更新
    if (node.childNodes.count == 0) {
        // AVPlayerを生成する
        let avPlayer = AVPlayer(url: self.avUrls[Int(arc4random_uniform(UInt32(self.avUrls.count)))])
        avPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.none;
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(ViewController.didPlayToEnd),
                                               name: NSNotification.Name("AVPlayerItemDidPlayToEndTimeNotification"),
                                               object: avPlayer.currentItem)

        let skScene = SKScene(size: CGSize(width: 1000, height: 1000))
        let skNode = SKVideoNode(avPlayer: avPlayer)
        skNode.position = CGPoint(x: skScene.size.width / 2.0, y: skScene.size.height / 2.0)
        skNode.size = skScene.size
        skNode.yScale = -1.0
        skScene.addChild(skNode)

        // 平面ジオメトリを作成
        let geometry = SCNPlane(width: CGFloat(imageAnchor.referenceImage.physicalSize.width), height: CGFloat(imageAnchor.referenceImage.physicalSize.height))
        // geometry.materials.first?.diffuse.contents = UIColor.darkGray.withAlphaComponent(0.5)
        geometry.materials.first?.diffuse.contents = skScene

        let planeNode = SCNNode(geometry: geometry)
        planeNode.eulerAngles.x = -.pi / 2
        planeNode.runAction(
            SCNAction.sequence([
                SCNAction.fadeIn(duration: 3),
                SCNAction.wait(duration: 15),
                SCNAction.fadeOut(duration: 3)]),
            completionHandler: {
                planeNode.removeFromParentNode()
        })

        node.addChildNode(planeNode)

        skNode.play()

    } else {
        for childNode in node.childNodes {
            guard let plane = childNode.geometry as? SCNPlane else { continue }
            plane.width = CGFloat(imageAnchor.referenceImage.physicalSize.width)
            plane.height = CGFloat(imageAnchor.referenceImage.physicalSize.height)
            break
        }
    }
}

コンテンツの読み込み

PHAssetCollectionを使ってマイアルバムより写真や動画を読み込んでいます。写真はシャボン玉、動画はスケッチブックでの表示に使っています。

func updateImgAssets(personNo: Int) -> Void {
    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = NSPredicate(format: "title = %@", "AR_" + String(personNo))
    let collections: PHFetchResult = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
    for i in 0 ..< collections.count {
        let collection = collections.object(at: i)
        let assetCollection: PHAssetCollection = collection as! PHAssetCollection
        let arAssets = PHAsset.fetchAssets(in: assetCollection, options: nil)

        self.imgAssets = [UIImage]()
        self.avUrls = [URL]()
        arAssets.enumerateObjects { (asset, index, stop) -> Void in
            let filename: String = asset.value(forKey: "filename") as! String
            let isPhoto: Bool = asset.value(forKey: "isPhoto") as! Bool
            let isVideo: Bool = asset.value(forKey: "isVideo") as! Bool

            // Photo
            if (isPhoto) {
                PHImageManager.default().requestImage(for: asset,
                                                      targetSize: CGSize(width: 400, height: 400),
                                                      contentMode: .aspectFill,
                                                      options: nil) {
                                                        (image, info) -> Void in self.imgAssets.append(image!)
                }

            // Video
            } else if (isVideo) {
                PHImageManager.default().requestAVAsset(forVideo: asset,
                                                        options: nil) {
                                                            (avurlAsset, audioMix, info) -> Void in
                                                            let avurlasset = avurlAsset as! AVURLAsset
                                                            self.avUrls.append(avurlasset.url)
                }
            }
        }
    }
}

その他

  • ARKitは公式ドキュメントが分かりやすかったです。
  • iOS 12だと、Environment Texturingが使えるのでもっときれいなシャボン玉ができるかもしれないです。
  • 横画面への対応忘れていました。縦画面ロックの状態で使ってもらいました。
22
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
15