Apple Engine

Apple, iPhone, iOS, その周辺のことについて

iOS で SceneKit を試す(Swift 3) その90 - ここで一区切り

Swift 3 (Xcode 8) の SceneKit の解説に関してはここで一区切り。

カスタムの Action、ParticleSystem、PhysicsField、 シーンを管理する SCNSceneRenderer、 scn ファイルを読み書きする SCNSceneSource、JS で SceneKit を設定する SCNExportJavaScriptModule

などなど、紹介していない機能もあるが、ある程度は紹介できたと思う。

 

とりあえず、技術書典3 で Swift Playground 2.0 for iPad + SceneKit で薄い厚い本を出す予定なので Xcode 9 で追加したものは、そちらでご紹介という形になるかもというところ。

 

techbookfest.org

iOS で SceneKit を試す(Swift 3) その89 - SCNNode をフラット化する。

もう1つ忘れていたものノードのフラット化について。

Scene Graph でチルドノードが増えすぎたりするとドローコールが増えるため、パフォーマンスが落ちる可能性があり、 SceneKit の機能でノードのジオメトリをひとまとめにする Flatten Node の処理を行うことで緩和できる可能性がある。

 

フラット化できるもの

基本、ジオメトリとマテリアルのみで他は無視されたはず。

 

Scene Editor での使用

今回、ピンク、青と青のクローン、緑と緑のクローンの球を設定。

f:id:x67x6fx74x6f:20170904193605p:plain

 

Scene Graph View でチルドノードのあるノードを選択。
右クリックから Flatten Node を押すとひとまとめになる。

f:id:x67x6fx74x6f:20170904193631p:plain

 

Attributes Inspector の Geometory Elements のエレメントが3つになっており、 最適化がされている。

f:id:x67x6fx74x6f:20170904193645p:plain

 

コードでの使用

SCNNode に flattenedClone() メソッドがあり、実行するとフラット化された SCNNode が返る。

 

注意点

たまに中身がおかしくなる場合があるので、必ず確認しよう。

特にコードで行う場合。

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その88 - SceneKit で SpriteKit の SKVideoNode を使用して動画のテクスチャを適応してみる

書き忘れていたが、SceneKit では SpriteKit の SKScene シーンを SCNMaterialProperty の各 contents に渡すことができる。

 

やり方としては作成した SKScene をジオメトリの firstMaterial.diffuse.contents を渡すだけ。 2D描画処理のコストはかかるが、複雑なテクスチャを作成できる。

ちなみに、SpriteKit 側も SK3DNode でイニシャライズして scnScene に設定した SCNScene を放り込むと、SceneKit の表示を 2D のテクスチャにしてくれる。

 

今回やること

AVFoundation の AVPlayer で動画を読み込み、それを SpriteKit でシーンを作り SKVideoNode を作成。 SpriteKit でシーンを SceneKit のテクスチャに割り当てる。

 

コードを書いてみる

いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成する。

f:id:x67x6fx74x6f:20170904183836g:plain

 

動画をアップロードできないため、iOS で読み込める mov や m4v 動画ファイルをプロジェクトに自前で追加してほしい。

 

必要のないコードを変更する

GameViewController.swift を開き、宇宙船は今回使用しないので 19 行目を以下に変更。

let scene = SCNScene(named: "art.scnassets/ship.scn")!
let scene = SCNScene()

 

アニメーションも必要ないので 44 行目と 47 行目の以下のコードを削除

let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))

 

インポート部分の変更

今回、SpriteKit と動画再生用の AVFoundation を使用するため、 GameViewController.swift の「import SceneKit」の下に以下のコードを追加

import SpriteKit
import AVFoundation

viewDidLoad を編集

以下のコードを追加。

流れとしては AVPlayerItem で動画ファイルを指定して、AVPlayer で先ほどの AVPlayerItem を設定し再生。 SKScene 作成後、SKVideoNode で作成した AVPlayer を設定し SKScene へ設定。 SCNBox の Diffuse の contents に設定した SKScene を適応するだけ。

let item = AVPlayerItem(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ファイル名", ofType: "m4v")!))
let videoPlayer = AVPlayer(playerItem: item)
videoPlayer.play()

let skScene = SKScene()
skScene.backgroundColor = UIColor.black
skScene.size = CGSize(width: 1024, height: 1024)

let skVideoNode = SKVideoNode(avPlayer: videoPlayer)
skVideoNode.size = CGSize(width: 1024, height: 1024)
skVideoNode.position = CGPoint(x: 512, y: 512)

skScene.addChild(skVideoNode)

let boxNode = SCNNode(geometry: SCNBox(width: 4, height: 4, length: 4, chamferRadius: 0))
boxNode.geometry?.firstMaterial?.diffuse.contents = skScene
scene.rootNode.addChildNode(boxNode)

 

1回しか再生されないのでループしたい

このままの実装だと1回しか再生されないので、AVPlayer ループの処理をする。

終了時に何もしないようにして、AVPlayerItemDidPlayToEndTimeNotification で通知を飛ばす。 通知を受けたら再生位置を最初に戻し再生する。

 

AVPlayer の初期化の後に以下を追加。

videoPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.none;
NotificationCenter.default.addObserver(self,
                                        selector: #selector(self.stateEnd),
                                        name: NSNotification.Name("AVPlayerItemDidPlayToEndTimeNotification"),
                                        object: videoPlayer.currentItem)

 

GameViewController 内のどこかに以下の関数を追加

func stateEnd(notification: NSNotification) {
    let avPlayerItem = notification.object as? AVPlayerItem
    avPlayerItem?.seek(to: kCMTimeZero)
}

 

もっと簡単に書く

SKVideoNode は再生と一時停止ぐらいしか機能がないため、今回は AVPlayer を使用したが、 1回のみの再生で使用するのなら、AVPlayer の記述を消して init(fileNamed: String) や init(url: URL) で直接ファイルを参照できる。

 

パフォーマンスについて

それなりに 2D の描画処理が必要になる模様。

f:id:x67x6fx74x6f:20170904183815p:plain

 

GameViewController.swift の全コード

import UIKit
import QuartzCore
import SceneKit
import SpriteKit
import AVFoundation

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let scene = SCNScene()
        
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        scene.rootNode.addChildNode(cameraNode)

        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
        scene.rootNode.addChildNode(lightNode)
        
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = UIColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)

        // AVPlayer の設定
        let item = AVPlayerItem(url: URL(fileURLWithPath: Bundle.main.path(forResource: "ファイル名", ofType: "m4v")!))
        let videoPlayer = AVPlayer(playerItem: item)
        
        videoPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.none;
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(self.stateEnd),
                                               name: NSNotification.Name("AVPlayerItemDidPlayToEndTimeNotification"),
                                               object: videoPlayer.currentItem)
        
        videoPlayer.play()
        
        // SKScene の設定
        let skScene = SKScene()
        skScene.backgroundColor = UIColor.black
        skScene.size = CGSize(width: 1024, height: 1024)
        
        let skVideoNode = SKVideoNode(avPlayer: videoPlayer)
        skVideoNode.size = CGSize(width: 1024, height: 1024)
        skVideoNode.position = CGPoint(x: 512, y: 512)
        
        skScene.addChild(skVideoNode)
        
        // SCNBox の設定
        let boxNode = SCNNode(geometry: SCNBox(width: 4, height: 4, length: 4, chamferRadius: 0))
        boxNode.geometry?.firstMaterial?.diffuse.contents = skScene
        scene.rootNode.addChildNode(boxNode)
        
        let scnView = self.view as! SCNView
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.showsStatistics = true
        scnView.backgroundColor = UIColor.black
    }
    
    func stateEnd(notification: NSNotification) {
        let avPlayerItem = notification.object as? AVPlayerItem
        avPlayerItem?.seek(to: kCMTimeZero)
    }
    
    override var shouldAutorotate: Bool {
        return true
    }
    
    override var prefersStatusBarHidden: Bool {
        return true
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return .allButUpsideDown
        } else {
            return .all
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Release any cached data, images, etc that aren't in use.
    }
}

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その87 - SceneKit でマルチパスレンダリングを行う SCNTechnique を試す

SceneKit では他のゲームエンジンと同様にシーンを複数回レンダリングし画像を合成するマルチパスレンダリングを行うことができる。

内容的には結構複雑なので、こんなことができるよというのがわかってもらえれば良いかと。

 

マルチパスレンダリングの使用用途は?

主に2タイプある。

 

レンダリングされたカラーバッファを入力として使用し、それをフラグメントシェーダーで処理するポストプロセッシング

カラーグレーディングやディスプレースメントマッピングなどに使用。

 

もう一つは、最初にレンダリングしパスとして仲介するバッファにキャプチャ、そのバッファを使用して追加の描画パスを実行して最終出力イメージを作成する遅延シェーディング

モーションブラーやスクリーンスペースアンビエントオクルージョン(SSAO)などに使用。

 

公式ドキュメントでの遅延シェーディングを使用した SSAO の例。

  • 最初のレンダリング 0 パス目で色情報と深度情報を作成
  • 1 パス で 0 パス で作成した深度情報とシーンの法線情報を設定
  • 2 パスで 1 パスから作成した情報から SSAO の影の処理とノイズのテクスチャを作成
  • 3 パス目で 2 パスと 0 パスの色情報とを合成し最終画像を作る

f:id:x67x6fx74x6f:20170904154407p:plain

SCNTechnique - SceneKit | Apple Developer Documentation

 

実装するには?

Metal か GLSL のシェーダーを作成、plist などでそれらのシェーダーとともに初期設定をして、その plist を呼び出す。
そして、その plist を SCNTechnique に設定するとマルチパスレンダリング実行される。

今回 plist を使用しているが、SCNTechnique 初期化の際 NSDictionary 渡すだけなので、JSON から変換してもよいし、直接 NSDictionary に設定値を書き込んでもよい。

 

コードを書いてみる

かなり複雑なことが可能なのだが、今回は簡単な画面全体をドット絵っぽくするサンプルをつくる。
内容としては 0 パス目で画像を取得し、1 パス目で縮小された画像を等倍で表示するだけのもの。

f:id:x67x6fx74x6f:20170904154617p:plain

 

てことで、いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成する。

 

Metal シェーダーを設定する

メニューバーの File > New > File… か Project Navigator (Command + 1) 右クリックからの New File… もしくは Command + N で新規ファイルを作成を行い、
Metal File を選ぶか、テキストファイルとして作成して .metal ファイルを変更する。

今回は Pixelate.metal というファイル名にした。

作成したら以下のコードを書く。

#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>

struct custom_vertex_t
{
    float4 position [[attribute(SCNVertexSemanticPosition)]];
};

struct out_vertex_t
{
    float4 position [[position]];
    float2 uv;
};

constexpr sampler s = sampler(coord::normalized,
                              address::repeat,
                              filter::nearest);

vertex out_vertex_t pixelate_pass_through_vertex(custom_vertex_t in [[stage_in]],
                                                 constant SCNSceneBuffer& scn_frame [[buffer(0)]])
{
    out_vertex_t out;
    out.position = in.position;
    out.uv = float2((in.position.x + 1.0) * 0.5 , (in.position.y + 1.0) * -0.5);
    return out;
};

fragment half4 pixelate_pass_through_fragment(out_vertex_t vert [[stage_in]],
                                              texture2d<float, access::sample> colorSampler [[texture(0)]])
{
    float4 fragment_color = colorSampler.sample( s, vert.uv);
    return half4(fragment_color);
};

ヴァーテックスの構造体とフラグメントの構造体を作成し、レンダー画像の再サンプルの再ニアレストネイバーを使用するように指定。
pixelate_pass_through_vertex を情報を取得し、pixelate_pass_through_fragment で色の情報を返している。

 

plist を設定する

新規ファイルで plist を選択するか、テキストファイルを .plist にして作成。

今回は Pixelate.plist として保存した。

plist をそのまま設定するのが面倒なので、
Project Navigator から Pixelate.plist 右クリックして Open As > Source Code で表示を XML にして、
以下のコードを書く

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>passes</key>
    <dict>
        <key>pixelate_scene</key>
        <dict>
            <key>draw</key>
            <string>DRAW_SCENE</string>
            <key>inputs</key>
            <dict/>
            <key>outputs</key>
            <dict>
                <key>color</key>
                <string>color_scene</string>
            </dict>
            <key>colorStates</key>
            <dict>
                <key>clear</key>
                <true/>
                <key>clearColor</key>
                <string>sceneBackground</string>
            </dict>
        </dict>
        <key>resample_pixelation</key>
        <dict>
            <key>draw</key>
            <string>DRAW_QUAD</string>
            <key>program</key>
            <string>doesntexist</string>
            <key>metalVertexShader</key>
            <string>pixelate_pass_through_vertex</string>
            <key>metalFragmentShader</key>
            <string>pixelate_pass_through_fragment</string>
            <key>inputs</key>
            <dict>
                <key>colorSampler</key>
                <string>color_scene</string>
            </dict>
            <key>outputs</key>
            <dict>
                <key>color</key>
                <string>COLOR</string>
            </dict>
        </dict>
    </dict>
    <key>sequence</key>
    <array>
        <string>pixelate_scene</string>
        <string>resample_pixelation</string>
    </array>
    <key>targets</key>
    <dict>
        <key>color_scene</key>
        <dict>
            <key>type</key>
            <string>color</string>
            <key>size</key>
            <string>64x114</string>
        </dict>
    </dict>
    <key>symbols</key>
    <dict/>
</dict>
</plist>

target の color_space で画像を変更するサイズを指定。
passes で pixelate_scene でレンダリング情報を取得し、resample_pixelation で Metal シェーダーの処理を設定。
sequence で pixelate_scene を 0 パス目で処理、その後の 1パス目で resample_pixelation の処理を行うよう指定している。

 

SCNView に SCNTechnique を適応する

GameViewController.swift を開き、viewDidLoad へ以下のコードを書く。

plist から NSDictionary に変換、SCNTechnique へ初期化、SCNView に SCNTechnique を渡す。

if let path = Bundle.main.path(forResource: "Pixelate", ofType: "plist") {
    if let dict = NSDictionary(contentsOfFile: path)  {
        let dict2 = dict as! [String : AnyObject]
        let technique = SCNTechnique(dictionary:dict2)
        scnView.technique = technique
    }
}

 

ガウシアンブラーを試してみる

4パスで処理する必要があるのため Metal の処理の部分で若干厳しさはある。

f:id:x67x6fx74x6f:20170904154654p:plain

 

まとめ

公式ドキュメントの PDF (英語) で Metal Shader を覚えて、SCNTechnique の使用方法を覚える必要があるため大変だが、ここら辺がひと通りできるようになると、SceneKit や Metal での表現の幅が広がるので試してみると良いかもしれない。

https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf

SCNTechnique - SceneKit | Apple Developer Documentation

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その86 - SceneKit のカスタムシェーダーについて

SceneKit でのカスタムシェーダーは主に3つ

  • SCNProgram でプリコンパイルしたものを使う
  • SCNShadable の shaderModifiers に Metal / GLSL のスニペットであるテキストデータを使う
  • SCNTechnique で設定し主にポストプロセスのように画面全体の変更する際に使う

基本、シェーダーは導入の説明を書くだけで、 何個か記事を書くことになりそうだから shaderModifiers を今回、SCNTechnique を次回軽く紹介していく。

ちなみに SCNProgram は面倒なので割愛。

 

SCNShadable の shaderModifiers とは?

ノードのマテリアルに対して個別でカスタムシェーダーを適応することができ、
カスタムシェーダーを適応する際は、定義されたエントリポイントを使用することとなる。

エントリポイントは以下の4つとなり、上から実行される。

エントリーポイント名 説明
geometry ジオメトリ形状を変更する
surface サーフェスプロパティ(Diffuse とか Material で設定しているもの)を変更する
lightingModel ライト情報の適応する情報を変更する
fragment 全ての情報を計算後に色を変更する

使い方としては SCNMaterial を初期化して、初期化したものの shaderModifiers に SCNShaderModifierEntryPoint のエントリポイントに設定する。

シェーダーの記述は String 型なので、テキストファイルを読み込む形にしても使用できる。

ちなみに、マテリアルの情報が書き換えられるため、ジオメトリが持つ firstMaterial は無視される。

また、WWDC 2017 の SceneKit のセッションで言っていたように Xcode 9 からは Scene Editor でのシェーダーの実行テストができるようになった。

 

コードを書いてみる

いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成。
GameViewController.swift を開く。

以下のコードを viewDidLoad() 内に書いてビルドすると宇宙船がトゥーンシェーディングされる

let shipMesh = scene.rootNode.childNode(withName: "shipMesh", recursively: true)!

let toonMaterial = SCNMaterial()
toonMaterial.shaderModifiers = [
    SCNShaderModifierEntryPoint.lightingModel:
    "vec3 lDir = normalize(vec3(0.1, 1.0, 1.0));" +
    "float dotProduct = dot(_surface.normal, lDir);" +
    "_lightingContribution.diffuse += (dotProduct * dotProduct * _light.intensity.rgb);" +
    "_lightingContribution.diffuse = floor(_lightingContribution.diffuse * 3.0) / 3.0;"
]

shipMesh.geometry?.materials = [toonMaterial]
shipMesh.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "art.scnassets/texture.png")

f:id:x67x6fx74x6f:20170903033110p:plain

 

コード的には、宇宙船のジオメトリを指定し、SCNMaterial を初期化。
初期化したものの shaderModifiers に SCNShaderModifierEntryPoint の lightingModel をエントリーポイントとし、法線情報から外積を使用しライトの光の色ともに色を平滑化させている。

 

例えば、shaderModifiers を以下のものに変更すると色が反転する。

SCNShaderModifierEntryPoint.fragment:
            "_output.color.rgb = vec3(1.0) - _output.color.rgb;"

 

f:id:x67x6fx74x6f:20170903033353p:plain

色を反転させているが、宇宙船の部分だけが色の反転が行われており、背景は反転していない。

 

と、このような感じでカスタムシェーダーを作成できる。

基本、一連の SceneKit の記事では数学の話を省いているが、 シェーダーに関してはそれなりに数学の知識が必要なのでいつかじっくり説明したい気はする。

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その85 - SceneKit で 2D ライブラリ SpriteKit の SKTexutre をテクスチャとして使用する

今まで SCNMaterial が持つ SCNMaterialProperty の content で UIImage を設定し、テクスチャを決めていたが SpriteKit の SKTexutre を設定することができる。

 

SKTexutre を適応してみる

いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成。 GameViewController.swift を開く。

以下のコードを追加して、宇宙船のマテリアルの Normal に SKTexutre としてしたの画像を適応してみた

f:id:x67x6fx74x6f:20170901190955p:plain

let shipMesh = scene.rootNode.childNode(withName: "shipMesh", recursively: true)!
let normalTexture = SKTexture(imageNamed: "cloud.png")
shipMesh.geometry?.firstMaterial?.normal.contents = normalTexture
shipMesh.geometry?.firstMaterial?.normal.intensity = 1.0

 

UIImage と同様に Normal Map の画像に最適化されていないため、おかしな表示になっている。

f:id:x67x6fx74x6f:20170901191048p:plain

 

SKTexutre から Normal Map を作成する

SKTexutre には generatingNormalMap() という通常の画像を Normal Map に変換するメソットがある。

 

先ほどのコードの変更して試してみる。

変更前

shipMesh.geometry?.firstMaterial?.normal.contents = normalTexture

変更後

shipMesh.geometry?.firstMaterial?.normal.contents = normalTexture.generatingNormalMap()

 

機体に雲模様のノーマルマップが適応されているのがわかる。

f:id:x67x6fx74x6f:20170901191239p:plain

 

一応、端末で変換するため、Normal Map を他のアプリケーションで作成できるのであれば、事前に行って画像を作成しておいた方が全体の処理が軽くなる。

 

generatingNormalMap について

ちなみに generatingNormalMap には generatingNormalMap(withSmoothness:contrast:) というメソッドもあり、滑らかさとコントラスト設定できる。

また、SKTexutre の generatingNormalMap は、Model I/O の MDLTexture の機能を使用している。

SCNMaterialProperty の content は MDLTexture や Metal の MTLTexture を読み込むことができるので、より複雑なことが可能ではある。

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その84 - SceneKit の画面上の UI (HUD) を 2D ライブラリ SpriteKit で実装してみる

SceneKit には、SCNView には overlaySKScene というプロパティがあり、SCNView の最前面に 2D ライブラリ SpriteKit のシーンである SKScene を貼り付けることができる。 そのため、UIKit にはない パーティクルや派手な演出のある UI を作成できる。

SpriteKit の詳細についてはネットや書籍で多く語られているので割愛。

 

Swift の バージョンが古いので手直しが必要だが、SpriteKit の書籍ならこちらがおすすめ。

 

今回やること

SpriteKit の SKScene で SKSpriteNode のボタンをを配置し、SceneKit のオーバーレイに設置。

SpriteKit 同様、タッチイペントから SKSpriteNode を調べ、SceneKit の Game テンプレートの宇宙船を X 軸方向で回転させる。

 

まず SKScene をつくる

いつも通り、Xcode の Game テンプレートで SceneKit を選択し作成。
GameViewController.swift を開く。

SpriteKit を使用するので imoprt SceneKit の下にインポート文を書く。

import SceneKit
import SpriteKit

 

SKScene のサブクラスを作成する。
その、前に今回使う画像。

f:id:x67x6fx74x6f:20170901183126p:plain

 

今回は、GameViewController.swift の一番下に書いて試しているが、新規の .swift ファイルとして作成しても構わない。 以下のコードを書く。

class OverlayScene: SKScene {
    override public init(size: CGSize){
        super.init(size: size)
        
        self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        self.scaleMode = SKSceneScaleMode.resizeFill
        
        let button:SKSpriteNode = SKSpriteNode(imageNamed: "button.png")
        button.position = CGPoint(x:0, y:-size.height*0.4);
        button.name = "button"
        button.xScale = 0.5;
        button.yScale = 0.5;
        self.addChild(button)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

 

SKScene のイニシャライザをオーバーライドしてボタンを配置している。

f:id:x67x6fx74x6f:20170901183212p:plain

 

SceneKit のオーバーレイに設定した SKScene を適応してみる

class GameViewController の viewDidLoad() に以下のものを入れるだけ。

scnView.overlaySKScene = OverlayScene(size:scnView.bounds.size)

作成した SKScene の OverlayScene に SCNScene のサイズをイニシャライズ時に入れ、overlaySKScene に設定するだけ。
結構、簡単。

 

タッチイベントを入れてみる

いつも通り、GameViewController の func handleTap(_ gestureRecognize: UIGestureRecognizer) を変更する。
中身は必要のないのでからにして以下のように変更する。

func handleTap(_ gestureRecognize: UIGestureRecognizer) {
    let scnView = self.view as! SCNView
    let shipMesh = scnView.scene?.rootNode.childNode(withName: "shipMesh", recursively: true)!
    
    let scene:SKScene = scnView.overlaySKScene!
    var p:CGPoint = gestureRecognize.location(in: scnView)
    p = scene.convertPoint(fromView: p)
    let skNode = scene.nodes(at: p)
    
    if (skNode.first?.name! == "button") {
        shipMesh?.runAction(SCNAction.rotateBy(x: CGFloat(Float.pi * 0.125), y: 0, z: 0, duration: 1))
    }
}

中身としては、宇宙船のジオメトリを検索し、overlaySKScene から SKScene を呼び出す。

その後、タッチされたポイントから SKScene 上のノードを探して、初めに見つかったものが、ボタンとして作成した SKSpriteNode であれば宇宙船を X 軸方向に回転させる。

 

今回は SpriteKit での動きが全くないのでかなり簡素だが、SpriteKit の機能を使用すると、もっと派手な演出ができる。
例えば、overlaySKScene から SKScene 呼び出すことができるので、SCNSceneRenderer でフレーム毎に overlay された SKScene 内のノードを SceneKit からアニメーションをさせるなど。

また、画面上のコントローラーとして Apple のサンプルの Fox や Fox 2 にあるので、そちらを参考にしていただきたい。

 

developer.apple.com

developer.apple.com

 

GameViewController.swift の全コード

import UIKit
import QuartzCore
import SceneKit
import SpriteKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
        
        // create and add a camera to the scene
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)
        
        // place the camera
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        
        // create and add a light to the scene
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
        scene.rootNode.addChildNode(lightNode)
        
        // create and add an ambient light to the scene
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = UIColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)
        
        // retrieve the ship node
        let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
        
        // animate the 3d object
        ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
        
        // retrieve the SCNView
        let scnView = self.view as! SCNView
        
        // set the scene to the view
        scnView.scene = scene
        
        // allows the user to manipulate the camera
        scnView.allowsCameraControl = true
        
        // show statistics such as fps and timing information
        scnView.showsStatistics = true
        
        // configure the view
        scnView.backgroundColor = UIColor.black
        
        // add a tap gesture recognizer
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        scnView.addGestureRecognizer(tapGesture)
     
        scnView.overlaySKScene = OverlayScene(size:scnView.bounds.size)
    }
    
    func handleTap(_ gestureRecognize: UIGestureRecognizer) {
        let scnView = self.view as! SCNView
        let shipMesh = scnView.scene?.rootNode.childNode(withName: "shipMesh", recursively: true)!
        
        let scene:SKScene = scnView.overlaySKScene!
        var p:CGPoint = gestureRecognize.location(in: scnView)
        p = scene.convertPoint(fromView: p)
        let skNode = scene.nodes(at: p)
        
        if (skNode.first?.name! == "button") {
            shipMesh?.runAction(SCNAction.rotateBy(x: CGFloat(Float.pi * 0.125), y: 0, z: 0, duration: 1))
        }
    }
    
    override var shouldAutorotate: Bool {
        return true
    }
    
    override var prefersStatusBarHidden: Bool {
        return true
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return .allButUpsideDown
        } else {
            return .all
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Release any cached data, images, etc that aren't in use.
    }

}

class OverlayScene: SKScene {
    override public init(size: CGSize){
        super.init(size: size)
        
        self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        self.scaleMode = SKSceneScaleMode.resizeFill
        
        let button:SKSpriteNode = SKSpriteNode(imageNamed: "button.png")
        button.position = CGPoint(x:0, y:-size.height*0.4);
        button.name = "button"
        button.xScale = 0.5;
        button.yScale = 0.5;
        self.addChild(button)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

 

今回はここまで。