工場裏のアーカイブス

素人によるiPhoneアプリ開発の学習記

SceneKitに触れてみる(1)

iOS 8に対応したXcode 6では、プロジェクトテンプレートの種類が変わり、OpenGL ES2 や SpriteKit を用いたアプリ開発は「Game」テンプレートに統合され、下図のようにプロジェクト作成時に「Game Technology」欄でフレームワークを選択する方式となりました。
f:id:fleron:20150120230541p:plain

そして、ここには「SceneKit」「Metal」という、これまでに無かったフレームワークも新たに登場しています。このうちSceneKitは、言わば3Dグラフィックス版のSpriteKitであり、前々から「SpriteKitと同じような手軽さで、3Dゲームなどのアプリ作れるツールは無いかな…」と願っていた身としては、まさに待望のツールです。

早速色々と触れてみたのですが、SceneKit では 3D空間のシーンにカメラ、光源、立体図形などをノードとして配置することによって、比較的手軽に3Dグラフィックスを用いたアプリを作成することが可能です(少なくともOpenGL ES2 + GLKitなどよりもよっぽど)。

またSpriteKitと同様に、ノードへの様々なアクションの設定や、物理エンジンやパーティクルシステムの利用も可能であり、上手く使いこなせばかなり凝った3Dゲームを作成することも夢ではなさそうです。

SceneKitの自作サンプル公開

まだまだSceneKitはリリースされてからの日が浅く、ネットの参考情報も限られている状況(日本語は特に)のようです。そこで本記事では、どこかで誰かの役に立つこともあるかもと願い、拙いながらも自作のサンプルアプリを公開してみたいと思います(下図、言語はSwift)。
f:id:fleron:20150121232136p:plain

このアプリでは画面内に半透明の箱が浮かんでおり、これをタップすると様々な形状、色の立体モデルがランダムに生成されます。立体モデルは物理エンジンによって落下し、互いに接触して積み重なったり散らかったりします。そして立体モデルをタップするとフェードアウトして消滅します。また、斜め上方から地面中央を照らすスポットライトによって、各立体モデルの影が投影されます。ただこれだけのアプリですが、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()
                    ]))
                }
            }
        }
    }
}