はじめに
自分の結婚式に来てくれる人に、少しでも楽しんでもらおうとARKitを使ってiPhoneアプリを作りました。これまでの思い出の写真やムービーが見れるようにしておき、結婚式後の小規模なディナーパーティーで使ってもらいました。
ar application for my wedding reception. pic.twitter.com/GIs8HELnA9
— ykshr (@ykshr2) September 25, 2018
機能
主な機能は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
使ってAVPlayer
をSCNPlane
に貼り付けています。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が使えるのでもっときれいなシャボン玉ができるかもしれないです。
- 横画面への対応忘れていました。縦画面ロックの状態で使ってもらいました。