24/7 twenty-four seven

iOS/OS X application programing topics.

SceneKitとCALayerで作る3Dのスライドショー

大型ディスプレイに投影するデジタルサイネージを作る仕事をしました。

できあがったのがこれです。

github.com

f:id:KishikawaKatsumi:20190415032635g:plain


まず、アートディレクターと相談して、下記の映像を参考にして3D空間を飛び回るようなスライドショーでいこうと決めました。

www.youtube.com

www.youtube.com


最初のプロトタイプはCALayerだけで作りました。

f:id:KishikawaKatsumi:20190415031617p:plain:w500

CALayerは3Dの変形をサポートしていて、かつmacOSのCALayerではCore Imageのフィルタがエフェクトに使えるので、各層のレイヤーに次のように書くだけで遠くなるにつれてブラーをかけてぼやかせる、ということが簡単に実現できます。

let frontLayer = CALayer()
frontLayer.frame = layerFrame
if let filter = CIFilter(name: "CIGaussianBlur", parameters: [kCIInputRadiusKey: 4]) {
    frontLayer.backgroundFilters = [filter]
}
contentLayer.addSublayer(frontLayer)


おそらく、表現だけならCALayerで作るのがもっとも簡単に美しい結果が得られます。

これを単純にX, Y軸平面をアニメーションで動かすだけでも(CAScrollLayerに載せるだけで簡単にできる)そこそこいい感じだったのですが、やはり前後(Z軸)の動きがある方がよりダイナミックで楽しい動きになるだろうということで、SceneKitと組み合わせることにしました。

実はSceneKitのNodeにはテクスチャとしてCALayerを設定することができるので、事実上、任意のAppKitもしくはUIKitのUIを3D空間に表示できます。

そこまで分かっていれば、あとは先ほどのCALayerで作ったパーツをSceneKitに標準で用意されている平面のオブジェクトであるSCNPlaneに設定して、Z軸をいい感じにして配置します。

for board in boards {
    for image in images.shuffled().prefix(50) {
        let layer = PhotoLayer()

        if let url = image.urls["regular"] {
            ImagePipeline.shared.load(url, into: layer.imageLayer)
        }
        layer.setTitle(image.description ?? image.alt_description ?? "")

        let size = CGSize(width: planeSize.width * 1.5, height: planeSize.height * 1.5)
        if let point = randomPoint(size: size, in: board.nodes) {
            let node = planeNode(layer: layer, position: SCNVector3(x: point.x, y: point.y, z: board.zPosition))
            node.name = image.id
            scene?.rootNode.addChildNode(node)

            board.nodes.append(node)
        }
    }
}

f:id:KishikawaKatsumi:20190415033716p:plain:w500

あとは3D空間を飛び回ってそれぞれの写真にズームしていくようなアニメーションを付けるだけです。 SceneKitのオブジェクトのほとんどのプロパティはCALayerの各プロパティと同様にアニメーション可能に設定されています。

3D空間を移動してそれぞれの写真にフォーカスする動きはカメラ(SceneKitではカメラや光源もNodeの1つとして扱われます)を動かすことで実現しています。

private func randomCameraAction(node: SCNNode, completion: @escaping () -> Void) {
    switch Int.random(in: 0...2) {
    case 0:
        self.cameraNode.yaw(to: node.position, angle: 0, completion: completion)
    case 1:
        var position = node.position
        position.x += 10
        self.cameraNode.yaw(to: position, angle: 15, completion: completion)
    case 2:
        var position = node.position
        position.x -= 10
        self.cameraNode.yaw(to: position, angle: -15, completion: completion)
    default:
        break
    }
}


最後に、フォーカスした写真をちょっと他より大きくするという「味付け」のアニメーションを追加して完成です。

private func loop(start node: SCNNode, level: Int, wait: TimeInterval = 2) {
    ...
    cameraNode.wait(wait) { [weak self] in
        ...
        node.resetScale()
        nextNode.scaleUp()

        self.randomCameraAction(node: nextNode) { [weak self] in
            guard let self = self else { return }
            self.loop(start: nextNode, level: nextLevel)
        }
    }
}

private func randomCameraAction(node: SCNNode, completion: @escaping () -> Void) {
    ...
}

func scaleUp(duration: TimeInterval = 2) {
    runAction(.scale(to: 1.8, duration: duration))
}

func resetScale(duration: TimeInterval = 2) {
    runAction(.scale(to: 1, duration: duration))
}


できあがったものは下記のリポジトリで公開しています。

github.com

f:id:KishikawaKatsumi:20190415032635g:plain