非ゲーム系アプリの開発が中心だと、3Dオブジェクトを取り扱う事がほとんどないので、3Dゲーム開発用のフレームワークであるSceneKitを利用する機会がありません。
しかし現在リサーチしている、ARKitを使いこなすには、SceneKitの理解は必須です。なぜならARKitが現実世界の検出したあとの3DオブジェクトのハンドリングはSceneKitが担当するからです。
2次元世界であるUIKitの文化圏と大分異なるSceanKitですが、しっかり理解していきたいと思います。
目次
2次元 → 3次元への入り口
- シーン(空間)
- オブジェクト(物体)
- ライト(照明)
- カメラ
x軸、y軸だけでなく、奥行きとしてz軸が存在します。
SceanKitの基本的な構成と流れ
- 大元となるSCNViewのsceanプロパティにSCNSceneのオブジェクト(scene)を設定
- SCNNodeに、3Dモデルや、カメラ、ライトなどを設定
- sceneのrootNodeに、childNodeとして各ノードを追加
シーンを設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import UIKit import SceneKit class ViewController: UIViewController { @IBOutlet weak var scnView: SCNView! override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene() scnView.backgroundColor = UIColor.gray //カメラ位置をタップでコントロール可能にする scnView.allowsCameraControl = true scnView.scene = scene } } |
3Dモデルノードを追加
まずSCNGeometryでジオメトリを生成。SCNGeometryは箱型、球体、円柱など様々な形があります。
ジオメトリには、色やテクスチャもマテリアルとして設定可能です。
SCNNodeのgeometryプロパティにジオメトリを代入して、ノードを生成。
ノードにpostion、nameを設定する。
このpositionの設定が、3次元慣れしていないとややこしい…。
箱型ノードの設定
1 2 3 4 | let box: SCNGeometry = SCNBox(width: 10, height: 5, length: 2, chamferRadius: 1) box.firstMaterial?.diffuse.contents = UIColor.red let boxNode = SCNNode(geometry: box) boxNode.name = "box" |
箱型ジオメトリを代入したノードに、文字列ジオメトリを代入したノードを追加します。
文字列を画面中央に配置するために、positonも調整します。最後に文字列ノードがぶら下がった、箱型ノードをroodNodeに追加します。
文字列ノードの設定 + rootNodeに箱型ノードを追加
1 2 3 4 5 6 7 8 9 10 | let text = SCNText(string: "techpartner", extrusionDepth: 1) let textNode = SCNNode(geometry: text) text.font = UIFont(name: "GurmukhiMN-Bold", size: 1); let (min, max) = (textNode.boundingBox) textNode.name = "text" let x = CGFloat(max.x - min.x) textNode.position = SCNVector3(-(x/2), -1, 1) boxNode.addChildNode(textNode) scene.rootNode.addChildNode(boxNode) |
※参考にさせていただいたサイト
https://dev.classmethod.jp/smartphone/ios-11-arkit-scntext-2/
カメラノードを追加
SCNCameraをSCNNodeのcameraプロパティに設定。カメラが全体を写せるように、positonのz軸を調整します。
1 2 3 4 | let cameraNode = SCNNode() cameraNode.camera = SCNCamera() cameraNode.position = SCNVector3(x: 0, y: 0, z: 20) scene.rootNode.addChildNode(cameraNode) |
ライトノードを追加
SCNLightをSCNNodeのlightプロパティに設定。こちらも、positonを調整して光の当て方を調整します。
各種光源タイプの詳細は、Apple公式を参照
https://developer.apple.com/documentation/scenekit/scnlight/lighttype
1 2 3 4 5 | let lightNode = SCNNode() lightNode.light = SCNLight() lightNode.light?.type = .spot lightNode.position = SCNVector3(x: 0, y: 5, z: 20) scene.rootNode.addChildNode(lightNode) |
途中経過
インタラクションさせてみる
UITapGestureRecognizerでタップされた位置情報を取得し、SCNViewのhitTestメソッドでタップした位置にあるノードの情報を取得します。
もしヒットしたノードがboxノードであれば、タップする度にz軸を-10して奥に押し込まれる様なインタラクションを実現します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | func addTapGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapped)) scnView.addGestureRecognizer(tapGesture) } @objc func tapped(recognizer: UIGestureRecognizer) { let view = recognizer.view as? SCNView let touchLocation = recognizer.location(in: view) let hitTestResult = scnView.hitTest(touchLocation, options: nil) if hitTestResult.count > 0 { if hitTestResult.first!.node.name == "box" { let boxNode = hitTestResult.first!.node boxNode.position.z -= 10 } } } |
hitTest
https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522929-hittest
アニメーションさせてみる
CoreAnimationを利用して3Dオブジェクトにアニメーションをつける事も可能です。
CABasicAnimationのkeyPathに変化させたいノードのプロパティ(ここではposition)を指定して、fromValueとtoValueで変化をつけます。
アニメーションをつけたいノードにaddAnimationするだけでOK。
1 2 3 4 5 6 7 | let animation = CABasicAnimation(keyPath: "position") animation.fromValue = SCNVector3(x: 0, y: 0, z: 0) animation.toValue = SCNVector3(x: 0, y: 0, z: 100) animation.duration = 30 animation.repeatCount = .infinity boxNode.addAnimation(animation, forKey: nil) |
CABasicAnimation
https://developer.apple.com/documentation/quartzcore/cabasicanimation
これで箱型ノードのオブジェクトが迫ってくる様なアニメーションをつけれました。
ライトノードの位置は固定のままなので、ライトノードのz軸より手前に移動すると箱型ノードが照らされなくなっているのがわかります。