SceneKitに触れてみる(1)
iOS 8に対応したXcode 6では、プロジェクトテンプレートの種類が変わり、OpenGL ES2 や SpriteKit を用いたアプリ開発は「Game」テンプレートに統合され、下図のようにプロジェクト作成時に「Game Technology」欄でフレームワークを選択する方式となりました。
そして、ここには「SceneKit」「Metal」という、これまでに無かったフレームワークも新たに登場しています。このうちSceneKitは、言わば3Dグラフィックス版のSpriteKitであり、前々から「SpriteKitと同じような手軽さで、3Dゲームなどのアプリ作れるツールは無いかな…」と願っていた身としては、まさに待望のツールです。
早速色々と触れてみたのですが、SceneKit では 3D空間のシーンにカメラ、光源、立体図形などをノードとして配置することによって、比較的手軽に3Dグラフィックスを用いたアプリを作成することが可能です(少なくともOpenGL ES2 + GLKitなどよりもよっぽど)。
またSpriteKitと同様に、ノードへの様々なアクションの設定や、物理エンジンやパーティクルシステムの利用も可能であり、上手く使いこなせばかなり凝った3Dゲームを作成することも夢ではなさそうです。
SceneKitの自作サンプル公開
まだまだSceneKitはリリースされてからの日が浅く、ネットの参考情報も限られている状況(日本語は特に)のようです。そこで本記事では、どこかで誰かの役に立つこともあるかもと願い、拙いながらも自作のサンプルアプリを公開してみたいと思います(下図、言語はSwift)。
このアプリでは画面内に半透明の箱が浮かんでおり、これをタップすると様々な形状、色の立体モデルがランダムに生成されます。立体モデルは物理エンジンによって落下し、互いに接触して積み重なったり散らかったりします。そして立体モデルをタップするとフェードアウトして消滅します。また、斜め上方から地面中央を照らすスポットライトによって、各立体モデルの影が投影されます。ただこれだけのアプリですが、SceneKitの機能を色々と盛り込んでいます。
ソースコードを以下に示します。実際に動かすためには、まずXcode(本記事執筆時点:Version 6.1.1)で「Game」テンプレートを選択し、「Language」欄は「Swift」、「Game Technology」欄は「SceneKit」を選択してプロジェクトを作成します。あとは「GameViewController.swift」の中身をこのソースコードに書き換えればOKです。
import UIKit import QuartzCore import SceneKit class GameViewController: UIViewController { weak var scnView : SCNView? override func viewDidLoad() { super.viewDidLoad() let scene = SCNScene() scnView = self.view as? SCNView scnView?.scene = scene scnView?.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.6, alpha: 1.0) scnView?.allowsCameraControl = true; setObject() setCamera() setLight() } func setObject(){ let generator = SCNBox(width: 6.0, height: 6.0, length: 6.0, chamferRadius: 0.0) generator.firstMaterial?.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.7) let generatorNode = SCNNode() generatorNode.geometry = generator generatorNode.position = SCNVector3(x: 0, y: 30, z: 0) generatorNode.name = "generator" self.scnView?.scene?.rootNode.addChildNode(generatorNode) let ground = SCNBox(width: 200.0, height: 2.0, length: 200.0, chamferRadius: 0.0) ground.firstMaterial?.diffuse.contents = UIColor(red: 0.8, green: 0.8, blue: 0.3, alpha: 1.0) let groundNode = SCNNode() groundNode.geometry = ground groundNode.position = SCNVector3(x: 0, y: 0, z: 0) groundNode.name = "ground" groundNode.physicsBody = SCNPhysicsBody.staticBody() groundNode.physicsBody?.physicsShape = SCNPhysicsShape(node: groundNode, options: nil) self.scnView?.scene?.rootNode.addChildNode(groundNode) } func setCamera(){ let cameraNode = SCNNode() cameraNode.camera = SCNCamera() self.scnView?.scene?.rootNode.addChildNode(cameraNode) cameraNode.position = SCNVector3(x: 0, y: 50, z: 50) cameraNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: -0.5) } func setLight(){ let ambientLightNode = SCNNode() ambientLightNode.light = SCNLight() ambientLightNode.light?.type = SCNLightTypeAmbient ambientLightNode.light?.color = UIColor.darkGrayColor() self.scnView?.scene?.rootNode.addChildNode(ambientLightNode) let lightNode = SCNNode() lightNode.light = SCNLight() lightNode.light?.type = SCNLightTypeSpot lightNode.light?.spotOuterAngle = 90 lightNode.light?.color = UIColor.whiteColor() lightNode.light?.castsShadow = true lightNode.position = SCNVector3(x: 0, y: 50, z: 10) if let ground = self.scnView?.scene?.rootNode.childNodeWithName("ground", recursively: false){ let cons = SCNLookAtConstraint(target: ground) cons.influenceFactor = 1.0 lightNode.constraints = [cons] } self.scnView?.scene?.rootNode.addChildNode(lightNode) } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { let p = touches.anyObject()!.locationInView(self.scnView) if let hitResults = self.scnView?.hitTest(p, options: nil) { if hitResults.count > 0 { let result = hitResults.first?.node if result?.name? == "generator"{ let geom = generateRandomGeometry() geom.firstMaterial?.diffuse.contents = UIColor(red: colorValue(), green: colorValue(), blue: colorValue(), alpha: 1.0) let geomNode = SCNNode(geometry: geom) geomNode.position = SCNVector3(x: 0, y: 30, z: 0) geomNode.name = "geometry" geomNode.physicsBody = SCNPhysicsBody.dynamicBody() geomNode.physicsBody?.physicsShape = SCNPhysicsShape(node: geomNode, options: nil) self.scnView?.scene?.rootNode.addChildNode(geomNode) }else if result?.name? == "geometry"{ result!.runAction(SCNAction.sequence([ SCNAction.fadeOutWithDuration(0.5), SCNAction.removeFromParentNode() ])) } } } } func generateRandomGeometry() -> SCNGeometry{ var kind = arc4random() % 6 switch kind{ case 0: let pyramid = SCNPyramid(width: 3.0, height: 3.0, length: 3.0) return pyramid case 1: let cylinder = SCNCylinder(radius: 1.5, height: 3.0) return cylinder case 2: let sphere = SCNSphere(radius: 1.5) return sphere case 3: let torous = SCNTorus(ringRadius: 1.5, pipeRadius: 0.5) return torous case 4: let chamferBox = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0.8) return chamferBox default: let box = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0.0) return box } } func colorValue() -> CGFloat{ let rand = CGFloat(arc4random() % 10000) return rand / 20000.0 + 0.5 } override func shouldAutorotate() -> Bool { return true } override func prefersStatusBarHidden() -> Bool { return true } override func supportedInterfaceOrientations() -> Int { if UIDevice.currentDevice().userInterfaceIdiom == .Phone { return Int(UIInterfaceOrientationMask.AllButUpsideDown.rawValue) } else { return Int(UIInterfaceOrientationMask.All.rawValue) } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Release any cached data, images, etc that aren't in use. } }
ソースコードの要点などについては、次回の記事にまとめたいと思います。
(2015/05/27 追記)
Xcode 6.3からSwiftのバージョンが1.2となり、それに伴ってtouchesBegansなどのタッチイベント系メソッドに変更が生じました。Swift 1.2以降を使用する場合は、上記ソースのtouchesBegansメソッド部分を以下のように書き換えてください。
//Swift 1.2に対応したtouchBegansメソッドの修正 override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) { if let touch = touches.first as? UITouch{ let p = touch.locationInView(self.scnView) if let hitResults = self.scnView?.hitTest(p, options: nil) { if hitResults.count > 0 { let result = hitResults.first?.node if result?.name == "generator"{ let geom = generateRandomGeometry() geom.firstMaterial?.diffuse.contents = UIColor(red: colorValue(), green: colorValue(), blue: colorValue(), alpha: 1.0) let geomNode = SCNNode(geometry: geom) geomNode.position = SCNVector3(x: 0, y: 30, z: 0) geomNode.name = "geometry" geomNode.physicsBody = SCNPhysicsBody.dynamicBody() geomNode.physicsBody?.physicsShape = SCNPhysicsShape(node: geomNode, options: nil) self.scnView?.scene?.rootNode.addChildNode(geomNode) }else if result?.name == "geometry"{ result!.runAction(SCNAction.sequence([ SCNAction.fadeOutWithDuration(0.5), SCNAction.removeFromParentNode() ])) } } } } }