WWDC 2017 の SceneKit サンプル Fox 2 を調べる その30
引き続き GameController クラスの残りの関数を見てゆく。
今回はプレイヤーキャラクター Max の色違いのレッサーパンダの仲間の初期設定と助け出した時の処理。
鍵を開けて助ける処理は次回。
助け出した仲間のフレーム毎の処理
friendsAreFree が true になり仲間を助けた際に、SCNSceneRenderer のデリゲートで毎フレーム呼ばれる処理。
全ての仲間に対して、pathCurve と Z 座標を元に X 方向のオフセットを作成。 addFriends 関数で作成される仲間のスピードと経過時間、0.5をかけ Z 座標に設定し全身させる。
ensureNoPenetrationOfIndex 関数で各キャラの重なりを防ぐ。
func updateFriends(deltaTime: CFTimeInterval) { let pathCurve: Float = 0.4 for i in 0..<friendCount { let friend = friends[i] var pos = friend.simdPosition let offsetx = pos.x - sinf(pathCurve * pos.z) pos.z += friendsSpeed[i] * Float(deltaTime) * 0.5 pos.x = sinf(pathCurve * pos.z) + offsetx friend.simdPosition = pos ensureNoPenetrationOfIndex(i) } }
助け出した仲間のアニメーション処理
仲間の Max のアニメーション設定。
シーンファイル max_walk.scn から歩行のアニメーションを取得し、 SCNAnimationPlayer の speed に各 friendsSpeed を入れアニメーションさせる。
func animateFriends() { let walkAnimation = Character.loadAnimation(fromSceneNamed: "Art.scnassets/character/max_walk.scn") SCNTransaction.begin() for i in 0..<friendCount { //unsynchronize let walk = walkAnimation.copy() as! SCNAnimationPlayer walk.speed = CGFloat(friendsSpeed[i]) friends[i].addAnimationPlayer(walk, forKey: "walk") walk.play() } SCNTransaction.commit() }
画面上に仲間を追加する
addFriends はイニシャライズに呼ばれ、3 が設定されている。 その後 unlockDoor 関数で NumberOfFiends が設定される。
func addFriends(_ count: Int) { ... }
中身が多いなため分けてみてゆく。
関数内の変数 count を設定
count に friendCount を足し NumberOfFiends 大きければ処理をする。
初期状態は friendCount は 0 なのでこの命令は通らず、unlockDoor 関数が呼ばれた時初めて動く。
var count = count if count + friendCount > GameController.NumberOfFiends { count = GameController.NumberOfFiends - friendCount }
関数内の変数 count を設定
3種類の異なる色を持つ仲間の Max の設定。
シーンファイル max.scn から Max を取り出し、friend という名前を設定後、コピーした3つジオメトリを配列に設定。 テクスチャを3つ設定し、元の Max のジオメトリからマテリアルをコピーし割り当てる。
自動で動き、何も接触しないので、ジオメトリを入れた配列の全てノードを調べ物理シミュレーション処理を省く。
let friendScene = SCNScene(named: "Art.scnassets/character/max.scn") guard let friendModel = friendScene?.rootNode.childNode(withName: "Max_rootNode", recursively: true) else { return } friendModel.name = "friend" var textures = [String](repeating: "", count: 3) textures[0] = "Art.scnassets/character/max_diffuseB.png" textures[1] = "Art.scnassets/character/max_diffuseC.png" textures[2] = "Art.scnassets/character/max_diffuseD.png" var geometries = [SCNGeometry](repeating: SCNGeometry(), count: 3) guard let geometryNode = friendModel.childNode(withName: "Max", recursively: true) else { return } geometryNode.geometry!.firstMaterial?.diffuse.intensity = 0.5 geometries[0] = geometryNode.geometry!.copy() as! SCNGeometry geometries[1] = geometryNode.geometry!.copy() as! SCNGeometry geometries[2] = geometryNode.geometry!.copy() as! SCNGeometry geometries[0].firstMaterial = geometries[0].firstMaterial?.copy() as? SCNMaterial geometryNode.geometry?.firstMaterial?.diffuse.contents = "Art.scnassets/character/max_diffuseB.png" geometries[1].firstMaterial = geometries[1].firstMaterial?.copy() as? SCNMaterial geometryNode.geometry?.firstMaterial?.diffuse.contents = "Art.scnassets/character/max_diffuseC.png" geometries[2].firstMaterial = geometries[2].firstMaterial?.copy() as? SCNMaterial geometryNode.geometry?.firstMaterial?.diffuse.contents = "Art.scnassets/character/max_diffuseD.png" friendModel.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in node.physicsBody = nil })
初期設定
friendPosition で初期位置、 FRIEND_AREA_LENGTH で各仲間の Z 軸方向の配置を設定。
上の方で friend とつけたジオメトリをシーン内から探し friendsNode へ渡す。 friendsNode がない場合は空の SCNNode を設定する
let friendPosition = simd_make_float3(-5.84, -0.75, 3.354) let FRIEND_AREA_LENGTH: Float = 5.0 var friendsNode: SCNNode? = scene!.rootNode.childNode(withName: "friends", recursively: false) if friendsNode == nil { friendsNode = SCNNode() friendsNode!.name = "friends" scene!.rootNode.addChildNode(friendsNode!) }
仲間の設定
シーンファイル max_idle.scn からアイドル状態(立った状態)のアニメーションを取得。
for を count 分だけ以下の処理をする。
- friend に friendModel をコピー。
- geometryIndex に 0〜2 の幅でランダムに値を入れる
- このループの geometryNode に friend のチルドノード Max を渡し friend の中身を編集する。
- この関数の中でつくられた geometries の配列からランダムの数を入れている geometryIndex を添字にして、先ほどの geometryNode.geometry に入れる
- ランダムで friend ノードの位置を設定。
- idle にアイドル状態のアニメーションを複製し、speed をランダム値を入れ再生。
- friendsSpeed 配列に idle の speed を入れる
- friendsNode 配列に friend を入れる
- friendCount をインクリメントして数を増やす
let idleAnimation = Character.loadAnimation(fromSceneNamed: "Art.scnassets/character/max_idle.scn") for _ in 0..<count { let friend = friendModel.clone() let geometryIndex = Int(arc4random_uniform(UInt32(3))) guard let geometryNode = friend.childNode(withName: "Max", recursively: true) else { return } geometryNode.geometry = geometries[geometryIndex] //place our friend friend.simdPosition = simd_make_float3( friendPosition.x + (1.4 * (Float(arc4random_uniform(UInt32(RAND_MAX))) / Float(RAND_MAX)) - 0.5), friendPosition.y, friendPosition.z - (FRIEND_AREA_LENGTH * (Float(arc4random_uniform(UInt32(RAND_MAX))) / Float(RAND_MAX)))) //unsynchronize let idle = (idleAnimation.copy() as! SCNAnimationPlayer) idle.speed = CGFloat(Float(1.5) + Float(1.5) * Float(arc4random_uniform(UInt32(RAND_MAX))) / Float(RAND_MAX)) friend.addAnimationPlayer(idle, forKey: "idle") idle.play() friendsNode?.addChildNode(friend) self.friendsSpeed[friendCount] = Float(idle.speed) self.friends[friendCount] = friend self.friendCount += 1 }
ぶつからない様に仲間を並べる
friendCount の数まで ensureNoPenetrationOfIndex を呼び、 ぶつからない様に仲間を並べる。
for i in 0..<friendCount { ensureNoPenetrationOfIndex(i) }
addFriends はここでおしまい。
ぶつからない様に位置を変更する関数
引数の index 元に friends の配列からノードの位置を設定する。 for 文から friends の配列を指定し、Max のジオメトリの有効範囲の直径を元に、全ての friends 配列から位置を比較する。 Z 舳 が 3.354 より少ない場合は pos.x 軸を調整する。
func ensureNoPenetrationOfIndex(_ index: Int) { var pos = friends[index].simdPosition let pandaRadius: Float = 0.15 let pandaDiameter = pandaRadius * 2.0 for j in 0..<friendCount { if j == index { continue } let otherPos = float3(friends[j].position) let v = otherPos - pos let dist = simd_length(v) if dist < pandaDiameter { let pen = pandaDiameter - dist pos -= simd_normalize(v) * pen } } if friends[index].position.z <= 3.354 { pos.x = max(pos.x, -6.662) pos.x = min(pos.x, -4.8) } friends[index].simdPosition = pos }
次回は鍵を開けるなどゲームでのアクションの関数を見てゆく。
WWDC 2017 の SceneKit サンプル Fox 2 を調べる その29
今回は GameController クラスの残りの関数を見てゆく。
プレイヤーキャラクター Max の初期位置リセット
Max がマップから落ちた時など、resetPlayerPosition() から Character クラスの queueResetCharacterPosition() を呼ぶ。
func resetPlayerPosition() { character!.queueResetCharacterPosition() }
カメラ処理
宝石のアイテム取得、仲間を助けた時の画面処理と Max の停止を行う関数。
startCinematic() は playingCinematic を true にして isPaused で Max の動作を止める。
startCinematic() その反対をする。
func startCinematic() { playingCinematic = true character!.node!.isPaused = true } func stopCinematic() { playingCinematic = false character!.node!.isPaused = false }
パーティクル処理
particleSystems(with kind: ParticleKind) で ParticleKind no enum から particleSystems 呼び出す処理で、次の addParticles 関数で使用。
addParticle はアイテム取得や仲間を助ける際に表示するパーティクルで、引数の ParticleKind と 座標値を元にシーンに配置している。
func particleSystems(with kind: ParticleKind) -> [SCNParticleSystem] { return particleSystems[kind.rawValue] } func addParticles(with kind: ParticleKind, withTransform transform: SCNMatrix4) { let particles = particleSystems(with: kind) for ps: SCNParticleSystem in particles { scene!.addParticleSystem(ps, transform: transform) } }
トリガー
Max が透明度 0 で見えなくなっている立方体に接触するとトリガーが弾かれカメラ変更が行われ視点が変わるようになっている。
基本的には、実行部分である execTrigger 関数が trigger 関数から呼ばれ動作する。
鍵のアイテム出現後に元の位置へ戻る際、キャラクターが止まっておりトリガーが弾けないので、その時だけ execTrigger 関数を直接叩いている。
execTrigger
カメラ視点を買えるものと仲間を助けるトリガーが設定している。
トリガーのノード、アニメーション時間を引数とし、トリガーノード名が trigCam〜 の場合は "trigCam" を消したものをカメラの名前として setActiveCamera からアニメーション時間とともに呼び出す。
トリガーノード名が trigAction〜 で鍵のアイテムが取得されている場合は
アクションの名前から "trigAction" 取り除く。
取り除いた文字列が "unlockDoor" なら unlockDoor() を呼び出し、仲間を助ける。
func execTrigger(_ triggerNode: SCNNode, animationDuration duration: CFTimeInterval) { if triggerNode.name!.hasPrefix("trigCam_") { let cameraName = (triggerNode.name as NSString?)!.substring(from: 8) setActiveCamera(cameraName, animationDuration: duration) } if triggerNode.name!.hasPrefix("trigAction_") { if collectedKeys > 0 { let actionName = (triggerNode.name as NSString?)!.substring(from: 11) if actionName == "unlockDoor" { unlockDoor() } } } }
trigger(_ triggerNode: SCNNode)
playingCinematic が true の場合はトリガーの処理は行わない。
保存した最後のトリガーノード保持している lastTrigger と 現在のトリガーノードが異なっていたら lastTrigger に現在のトリガーノードを保存。
execTrigger を実行し、トリガーが引かれたこと判別する firstTriggerDone を true する。
func trigger(_ triggerNode: SCNNode) { if playingCinematic { return } if lastTrigger != triggerNode { lastTrigger = triggerNode execTrigger(triggerNode, animationDuration: firstTriggerDone ? GameController.DefaultCameraTransitionDuration: 0) firstTriggerDone = true } }
次回に続く。
WWDC 2017 の SceneKit サンプル Fox 2 を調べる その28
今回は GameController クラスのイニシャライズ関数を見てゆく。
コードのほとんどがこれまで紹介した関数を呼ぶ形となっている。
init(scnView: SCNView)
各プラットフォームで SCNView を受け取り、それに変更を加えてゆく。
init(scnView: SCNView) { super.init() ... }
中身を見てゆく
sceneRenderer
sceneRenderer に scnView を渡し、SCNSceneRenderer のデリゲートを設定し、 FPS など詳細を表示する showsStatistics はコメントアウトされている。
sceneRenderer = scnView sceneRenderer!.delegate = self //scnView.showsStatistics = true
最前面に表示されるオーバーレイに Overlay クラスへ画面サイズと controller 用に自身を渡す。
overlay = Overlay(size: scnView.bounds.size, controller: self) scnView.overlaySKScene = overlay
scene.scn をこれを使用する SCNView のシーンへ渡す。
self.scene = SCNScene(named: "Art.scnassets/scene.scn")
物理シミュレーション、コリジョン、キャラクター設定、敵キャラ、仲間、動く橋、パーティクルの設定を行う。
setupPhysics() setupCollisions() setupCharacter() setupEnemies() addFriends(3) setupPlatforms() setupParticleSystem()
ライトの設定。
ディレクショナルライトのノードを取得し、512x512 のシャドウマップを作成し、shadowCascade の設定をする。
let light = scene!.rootNode.childNode(withName: "DirectLight", recursively: true)!.light light!.shadowCascadeCount = 3 light!.shadowMapSize = CGSize(width: CGFloat(512), height: CGFloat(512)) light!.maximumShadowDistance = 20 light!.shadowCascadeSplittingFactor = 0.5
カメラとゲームコントローラーの設定をする
setupCamera() setupGameController()
このファイルで設定されている configureRenderingQuality 関数を呼ぶ。
tvOS での動作時で表示のクオリティを下げている。
configureRenderingQuality(scnView)
sceneRenderer のシーンにここで設定されているシーンを渡すことで、
プラットフォーム別の SCNView 側の scene と GameController の scene が同じものとなる。
sceneRenderer!.scene = self.scene
オーディオの設定、このシーンのカメラである cameraNode を SCNView の pointOfView に適応。
scene の physicsWorld の contactDelegate を設定している。
setupAudio() sceneRenderer!.pointOfView = self.cameraNode sceneRenderer!.scene!.physicsWorld.contactDelegate = self
次回は GameController クラスのその他の関数を見てゆく。
WWDC 2017 の SceneKit サンプル Fox 2 を調べる その27
GameController クラスのセットアップ関数を見てゆく。
今回は音を設定しているオーディオ設定。
playSound(_ audioName: AudioSourceKind)
AudioSourceKind の enum で指定されているオーディオソースの添字を playSound 関数に渡す。
指定した添字から音を audioSources の配列から探し、 SCNAudioPlayer をシーンに addAudioPlayer で追加するし音を再生する。
func playSound(_ audioName: AudioSourceKind) { scene!.rootNode.addAudioPlayer(SCNAudioPlayer(source: audioSources[audioName.rawValue])) }
setupAudio()
BGM や SE の音を設定する。
- ここで設定している node に rootNode を渡す
- audioSource に ambience.mp3 を渡し、BGM として再生する
- volcanoNode に particles_volcanoSmoke_v2 の名前のノードを渡し、audioSource として volcano.mp3 を設定する。isPositional の設定をしていないため位置によって音が変化する。
- collect.mp3、collectBig.mp3、unlockTheDoor.m4a、hitEnemy.wav を audioSources として audioSources の配列に入れ、各種設定する。
func setupAudio() { let node = scene!.rootNode if let audioSource = SCNAudioSource(named: "audio/ambience.mp3") { audioSource.loops = true audioSource.volume = 0.8 audioSource.isPositional = false audioSource.shouldStream = true node.addAudioPlayer(SCNAudioPlayer(source: audioSource)) } if let volcanoNode = scene!.rootNode.childNode(withName: "particles_volcanoSmoke_v2", recursively: true) { if let audioSource = SCNAudioSource(named: "audio/volcano.mp3") { audioSource.loops = true audioSource.volume = 5.0 volcanoNode.addAudioPlayer(SCNAudioPlayer(source: audioSource)) } } audioSources[AudioSourceKind.collect.rawValue] = SCNAudioSource(named: "audio/collect.mp3")! audioSources[AudioSourceKind.collectBig.rawValue] = SCNAudioSource(named: "audio/collectBig.mp3")! audioSources[AudioSourceKind.unlockDoor.rawValue] = SCNAudioSource(named: "audio/unlockTheDoor.m4a")! audioSources[AudioSourceKind.hitEnemy.rawValue] = SCNAudioSource(named: "audio/hitEnemy.wav")! audioSources[AudioSourceKind.unlockDoor.rawValue].isPositional = false audioSources[AudioSourceKind.collect.rawValue].isPositional = false audioSources[AudioSourceKind.collectBig.rawValue].isPositional = false audioSources[AudioSourceKind.hitEnemy.rawValue].isPositional = false audioSources[AudioSourceKind.unlockDoor.rawValue].volume = 0.5 audioSources[AudioSourceKind.collect.rawValue].volume = 4.0 audioSources[AudioSourceKind.collectBig.rawValue].volume = 4.0 }
次回は GameController クラスのイニシャライズの関数を見てゆく。
WWDC 2017 の SceneKit サンプル Fox 2 を調べる その26
GameController クラスのセットアップ関数を見てゆく。 今回は敵キャラとパーティクルの設定
setupEnemies()
敵の設定。
こちらは以前に紹介しているので割愛。
loadParticleSystems(atPath path: String)
パーティクルセットアップの際に .scn(シーンファイル) や .scnp(パーティクルシステムファイル) から読み込みを容易にするための関数。ファイルパスを指定し呼び出すとパーティクルシステムが入った配列を返す。
引数 path でファイルパスを文字列でとって、ファイル名とディレクトリを使用し、拡張子がパーティクルシステムファイルの場合はそのまま1つのパーティクルが入った配列として返し、シーンファイルの場合はしシーンを全て調べ、見つかったパーティクルを全て配列に入れ返す。
func loadParticleSystems(atPath path: String) -> [SCNParticleSystem] { let url = URL(fileURLWithPath: path) let directory = url.deletingLastPathComponent() let fileName = url.lastPathComponent let ext: String = url.pathExtension if ext == "scnp" { return [SCNParticleSystem(named: fileName, inDirectory: directory.relativePath)!] } else { var particles = [SCNParticleSystem]() let scene = SCNScene(named: fileName, inDirectory: directory.relativePath, options: nil) scene!.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in if node.particleSystems != nil { particles += node.particleSystems! } }) return particles } }
setupParticleSystem()
パーティクルのセットアップ関数。
particleSystems に ParticleKind の enum を添字として、上の loadParticleSystems 関数からパーティクルを格納する。
func setupParticleSystem() { particleSystems[ParticleKind.collect.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/collect.scnp") particleSystems[ParticleKind.collectBig.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/key_apparition.scn") particleSystems[ParticleKind.enemyExplosion.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/enemy_explosion.scn") particleSystems[ParticleKind.keyApparition.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/key_apparition.scn") particleSystems[ParticleKind.unlockDoor.rawValue] = loadParticleSystems(atPath: "Art.scnassets/particles/unlock_door.scn") }
func setupPlatforms()
閉じ込められている仲間の小屋の前の溶岩にある動く橋(mobilePlatform)のアニメーションとパーティクル(particles_platform)の設定。
SCNAction で橋を動かし、パーティクルの orientationDirection を Y 軸 1 の方向に向かせるようにしている。
func setupPlatforms() { let PLATFORM_MOVE_OFFSET = Float(1.5) let PLATFORM_MOVE_SPEED = Float(0.5) var alternate: Float = 1 scene!.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in if node.name == "mobilePlatform" && !node.childNodes.isEmpty { node.simdPosition = simd_float3( node.simdPosition.x - (alternate * PLATFORM_MOVE_OFFSET / 2.0), node.simdPosition.y, node.simdPosition.z) let moveAction = SCNAction.move(by: SCNVector3(alternate * PLATFORM_MOVE_OFFSET, 0, 0), duration: TimeInterval(1 / PLATFORM_MOVE_SPEED)) moveAction.timingMode = .easeInEaseOut node.runAction(SCNAction.repeatForever(SCNAction.sequence([moveAction, moveAction.reversed()]))) alternate = -alternate node.enumerateChildNodes({ (_ child: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) in if child.name == "particles_platform" { child.particleSystems?[0].orientationDirection = SCNVector3(0, 1, 0) } }) } }) }
mobilePlatform
particles_platform
次回は GameController クラスのオーディオ設定を見てゆく。
WWDC 2017 の SceneKit サンプル Fox 2 を調べる その25
GameController クラスのセットアップ関数を見てゆく。
今回はカメラ設定
Fox2 では固定カメラの他に、プレイヤーキャラクターである Max を追尾するカメラがある。
追尾するカメラは、カメラ視点が変えられるものと、変えられないものの2つある。
紹介順の変更
GameController.swift で書かれている内容が前後するが、setupCamera、setupCameraNode、setActiveCamera 関数を先に見てゆく。
setupCamera()
イニシャライズに呼ばれるカメラ設定。
self を weakSelf として weak で設定し、lookAtTarget のコンストレイントで、カスタムのコンストレイント作成。
strongSelf に weakSelf を渡し、self がなければ、そのままの position を返し weakSelf が破棄される。
self があった場合は、character の baseAltitude でプレイヤーキャラクター Max の基準となるの高度 に 0.5 足したものに固定するように返すコンストレイントをつくる。
カメラは Max に対して SCNLookAtConstraint などのコンストレインとが設定されている。
ここで作成したコンストレイントは Max がジャンプした際に他のコンストレイントによってカメラが追従してしまうのを防ぐための処置。
このコンストレイントを使用せずに、そのままの Max の座標を渡すとジャンプ時にカメラも Y 軸方向に動いてしまう。
あとは、lookAtTarget をシーンに追加し、シーン中の全てのカメラに setupCameraNode() を適応、 cameraNode の設定とシーンに追加、最初に表示するカメラを setActiveCamera 関数でアニメーション処理をしている
func setupCamera() { weak var weakSelf = self self.lookAtTarget.constraints = [ SCNTransformConstraint.positionConstraint( inWorldSpace: true, with: { (_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in guard let strongSelf = weakSelf else { return position } guard var worldPosition = strongSelf.character?.node?.simdWorldPosition else { return position } worldPosition.y = strongSelf.character!.baseAltitude + 0.5 return SCNVector3(worldPosition) })] self.scene?.rootNode.addChildNode(lookAtTarget) self.scene?.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in if node.camera != nil { self.setupCameraNode(node) } }) self.cameraNode.camera = SCNCamera() self.cameraNode.name = "mainCamera" self.cameraNode.camera!.zNear = 0.1 self.scene!.rootNode.addChildNode(cameraNode) setActiveCamera("camLookAt_cameraGame", animationDuration: 0.0) }
setupCameraNode(_ node: SCNNode)
この関数はカメラのノードを受け取った際に、そのノードの名前を取得する。
名前が camTrav〜 で始まる場合は setupAxisAlignedCamera 関数を呼び、
camLookAt〜 で始まる場合は setupFollowCamera 関数を呼ぶ。
func setupCameraNode(_ node: SCNNode) { guard let cameraName = node.name else { return } if cameraName.hasPrefix("camTrav") { setupAxisAlignedCamera(node) } else if cameraName.hasPrefix("camLookAt") { setupFollowCamera(node) } }
setActiveCamera
setActiveCamera は現在のカメラから新しいカメラへの変更と移動のアニメーション、HDR がらみのカメラ設定の変更を行なっている。1つの引数と、2つの引数のものが用意されている、
1つの引数のものはアニメーションの初期値を GameController の DefaultCameraTransitionDuration 固定し、2つの引数のものを再度呼び出している。
func setActiveCamera(_ cameraName: String) { setActiveCamera(cameraName, animationDuration: GameController.DefaultCameraTransitionDuration) }
でもって、この関数の実態と処理。
- 引数のカメラの名前から新しいカメラを設定(camera)
- 現在アクティブなカメラと引数から得た新しいカメラが同じなら関数を抜ける
- 最終後に使用したカメラとして現在アクティブなカメラを lastActiveCamera に設定
- アクティブなカメラがあれば lastActiveCameraFrontDirection にアクティブなカメラの前面方向のベクトルを渡す。
- activeCamera に引数から得た新しいカメラ camera を渡して、camera をアクティブにする
- oldTransform に現在のカメラの位置を渡す
- camera に cameraNode を追加
- camera の presentation の位置を parentTransform に渡し、parentInv に逆行列を渡す
- cameraNode を SCNMatrix4Mult を使って過去のカメラの親に移動
- SCNTransaction でアニメーション設定し、cameraNode.transform を 0 に戻す
- 被写界深度やブルームなどの新しいカメラ設定を渡す
- SCNTransaction の設定を終了しアニメーションを開始する
func setActiveCamera(_ cameraName: String, animationDuration duration: CFTimeInterval) { guard let camera = scene?.rootNode.childNode(withName: cameraName, recursively: true) else { return } if self.activeCamera == camera { return } self.lastActiveCamera = activeCamera if activeCamera != nil { self.lastActiveCameraFrontDirection = (activeCamera?.presentation.simdWorldFront)! } self.activeCamera = camera let oldTransform: SCNMatrix4 = cameraNode.presentation.worldTransform camera.addChildNode(cameraNode) let parentTransform = camera.presentation.worldTransform let parentInv = SCNMatrix4Invert(parentTransform) cameraNode.transform = SCNMatrix4Mult(oldTransform, parentInv) SCNTransaction.begin() SCNTransaction.animationDuration = duration SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) cameraNode.transform = SCNMatrix4Identity if let cameraTemplate = camera.camera { cameraNode.camera!.fieldOfView = cameraTemplate.fieldOfView cameraNode.camera!.wantsDepthOfField = cameraTemplate.wantsDepthOfField cameraNode.camera!.sensorHeight = cameraTemplate.sensorHeight cameraNode.camera!.fStop = cameraTemplate.fStop cameraNode.camera!.focusDistance = cameraTemplate.focusDistance cameraNode.camera!.bloomIntensity = cameraTemplate.bloomIntensity cameraNode.camera!.bloomThreshold = cameraTemplate.bloomThreshold cameraNode.camera!.bloomBlurRadius = cameraTemplate.bloomBlurRadius cameraNode.camera!.wantsHDR = cameraTemplate.wantsHDR cameraNode.camera!.wantsExposureAdaptation = cameraTemplate.wantsExposureAdaptation cameraNode.camera!.vignettingPower = cameraTemplate.vignettingPower cameraNode.camera!.vignettingIntensity = cameraTemplate.vignettingIntensity } SCNTransaction.commit() }
func setupFollowCamera(_ cameraNode: SCNNode)
キャラに付いてゆくカメラ。
以下の中身を見ていく。
func setupFollowCamera(_ cameraNode: SCNNode) { ... }
Max へカメラを振り向かせるコンストレイント。
lookAtTarget を元に SCNLookAtConstraint 設定する。isGimbalLockEnabled が true であるためロール方向の回転を制限する。
let lookAtConstraint = SCNLookAtConstraint(target: self.lookAtTarget) lookAtConstraint.influenceFactor = 0.07 lookAtConstraint.isGimbalLockEnabled = true
位置のコンストレイント。
follow に lookAtTarget を元に SCNDistanceConstraint 設定。
distance へ simd_length 使用して cameraNode.simdPosition から距離を設定し
follow の最大値と最小値の距離を設定する。
let follow = SCNDistanceConstraint(target: self.lookAtTarget) let distance = CGFloat(simd_length(cameraNode.simdPosition)) follow.minimumDistance = distance follow.maximumDistance = distance
Max に対して一定の高度を維持するようにコンストレイントを設定する。
cameraNode.simdWorldPosition の Y 軸の絶対値を desiredAltitude に渡し、
カスタムのコンストレイント keepAltitude で desiredAltitude を使用し高さを固定する 。
let desiredAltitude = abs(cameraNode.simdWorldPosition.y) weak var weakSelf = self let keepAltitude = SCNTransformConstraint.positionConstraint(inWorldSpace: true, with: {(_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in guard let strongSelf = weakSelf else { return position } var position = float3(position) position.y = strongSelf.character!.baseAltitude + desiredAltitude return SCNVector3( position ) })
Max 移動した際にカメラの加速度を設定するコンストレイント
let accelerationConstraint = SCNAccelerationConstraint() accelerationConstraint.maximumLinearVelocity = 1500.0 accelerationConstraint.maximumLinearAcceleration = 50.0 accelerationConstraint.damping = 0.05
ユーザー Max を操作するとそれに合わせて徐々に回り込むカスタムのコンストレインとを設定する。
- activeCamera のノードを取り、上で設定した accelerationConstraint の influenceFactor で加速度の影響を上限 1 で 0.01 ずつ増加させる。
- targetPosition で lookAtTarget の presentation の simdWorldPosition、cameraDirection で cameraDirection を取得。
- cameraDirection.allZero() で座標を調べ、全て 0 ならカメラ位置を返し関数を抜ける。
- accelerationConstraint の influenceFactor を 0 にして、accelerationConstraint 止める。
- characterWorldUp に character の presentation の simdWorldUp を渡す。
- transformNode の transform に、このコンストレイントの transform 渡す。
- characterWorldUp を元に CameraOrientationSensitivity に cameraDirection.x にかけたものを simd_quaternion として適応。
- transformNode.simdWorldRight を元に CameraOrientationSensitivity に cameraDirection.x にかけたものを simd_quaternion として適応。
- simd_quaternion 2つで積を出し、transformNode の simdRotate で targetPosition を中心に回転させる
- transformNode.transform をこのコンストレイントの値として返す。
let transformNode = SCNNode() let orientationUpdateConstraint = SCNTransformConstraint(inWorldSpace: true) { (_ node: SCNNode, _ transform: SCNMatrix4) -> SCNMatrix4 in guard let strongSelf = weakSelf else { return transform } if strongSelf.activeCamera != node { return transform } accelerationConstraint.influenceFactor = min(1, accelerationConstraint.influenceFactor + 0.01) let targetPosition = strongSelf.lookAtTarget.presentation.simdWorldPosition let cameraDirection = strongSelf.cameraDirection if cameraDirection.allZero() { return transform } accelerationConstraint.influenceFactor = 0 let characterWorldUp = strongSelf.character?.node?.presentation.simdWorldUp transformNode.transform = transform let q = simd_mul( simd_quaternion(GameController.CameraOrientationSensitivity * cameraDirection.x, characterWorldUp!), simd_quaternion(GameController.CameraOrientationSensitivity * cameraDirection.y, transformNode.simdWorldRight) ) transformNode.simdRotate(by: q, aroundTarget: targetPosition) return transformNode.transform }
カメラのコンストレイントとして、設定した follow, keepAltitude, accelerationConstraint, orientationUpdateConstraint, lookAtConstraint を適応する。
cameraNode.constraints = [follow, keepAltitude, accelerationConstraint, orientationUpdateConstraint, lookAtConstraint]
func setupAxisAlignedCamera(_ cameraNode: SCNNode)
キャラに付いてゆくカメラ。 こちらはキャラの移動でカメラの回転が行われない。
- distance へ simd_length を使い cameraNode.simdPosition でカメラの距離を設定
- originalAxisDirection で前面の方向を cameraNode の simdWorldFront で取得
- lastActiveCameraFrontDirection で最後に取得した前面の方向のベクトルを設定
- symetricAxisDirection で X, Z がマイナスになる originalAxisDirection の座標を取得
- axisAlignConstraint で一定の回転軸に合わせたカスタムのコンストレインとを作成
- SCNAccelerationConstraint、SCNLookAtConstraint の設定を行い 、axisAlignConstraint 共に cameraNode のコンストレイントを設定する
func setupAxisAlignedCamera(_ cameraNode: SCNNode) { let distance: Float = simd_length(cameraNode.simdPosition) let originalAxisDirection = cameraNode.simdWorldFront self.lastActiveCameraFrontDirection = originalAxisDirection let symetricAxisDirection = simd_make_float3(-originalAxisDirection.x, originalAxisDirection.y, -originalAxisDirection.z) weak var weakSelf = self let axisAlignConstraint = SCNTransformConstraint.positionConstraint( inWorldSpace: true, with: {(_ node: SCNNode, _ position: SCNVector3) -> SCNVector3 in guard let strongSelf = weakSelf else { return position } guard let activeCamera = strongSelf.activeCamera else { return position } let axisOrigin = strongSelf.lookAtTarget.presentation.simdWorldPosition let referenceFrontDirection = strongSelf.activeCamera == node ? strongSelf.lastActiveCameraFrontDirection : activeCamera.presentation.simdWorldFront let axis = simd_dot(originalAxisDirection, referenceFrontDirection) > 0 ? originalAxisDirection: symetricAxisDirection let constrainedPosition = axisOrigin - distance * axis return SCNVector3(constrainedPosition) }) let accelerationConstraint = SCNAccelerationConstraint() accelerationConstraint.maximumLinearAcceleration = 20 accelerationConstraint.decelerationDistance = 0.5 accelerationConstraint.damping = 0.05 // look at constraint let lookAtConstraint = SCNLookAtConstraint(target: self.lookAtTarget) lookAtConstraint.isGimbalLockEnabled = true // keep horizon horizontal cameraNode.constraints = [axisAlignConstraint, lookAtConstraint, accelerationConstraint] }
次回は引き続き GameController クラスのセットアップの関数を見てゆく。
WWDC 2017 の SceneKit サンプル Fox 2 を調べる その24
GameController クラスのセットアップ関数を見てゆく。
量が多いため何回かに分ける予定。
セットアップ
GameController クラスのイニシャライズ時にいくつかの関数が呼ばれる。
setupGameController()
ゲームコントローラー設定。
Bluetooth で接続や切断されるため、NotificationCenter の addObserver で接続状態と切断状態を監視する。
接続されれば、GameController で設定している handleControllerDidConnect 関数が呼ばれ、切断されれば handleControllerDidDisconnect が呼ばれゲームコントローラーでの操作の処理が行われる。
イニシャライズ時に setupGameController() が呼ばれ、ゲームコントローラーがすでに接続されている場合は、このファイルの最後の方にある registerGameController 関数が呼ばれゲームコントローラーが使用できるよう処理をする。
func setupGameController() { NotificationCenter.default.addObserver( self, selector: #selector(self.handleControllerDidConnect), name: NSNotification.Name.GCControllerDidConnect, object: nil) NotificationCenter.default.addObserver( self, selector: #selector(self.handleControllerDidDisconnect), name: NSNotification.Name.GCControllerDidDisconnect, object: nil) guard let controller = GCController.controllers().first else { return } registerGameController(controller) }
setupCharacter()
プレイヤーキャラクター Max の設定。
クラス内の変数 character に Character クラスを scene と共に渡す。
キャラクターは物理判定を行うため、キャラクターの physicsWorld にシーンの physicsWorld の設定を渡す。
character の中身の Character クラスにある node の返り値から、Character クラスで設定した Max のノードを返す。 そのノードを addChildNode して GameController のシーンの追加する。
func setupCharacter() { character = Character(scene: scene!) character!.physicsWorld = scene!.physicsWorld scene!.rootNode.addChildNode(character!.node!) }
setupPhysics()
物理シミュレーション設定。
enumerateHierarchy で階層の全てのノードを調べ Physics Body の collisionBitMask へ character のビットマスクを渡す。
func setupPhysics() { self.scene?.rootNode.enumerateHierarchy({(_ node: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) -> Void in node.physicsBody?.collisionBitMask = Int(Bitmask.character.rawValue) }) }
setupCollisions()
障害物の判定のノードを設定する。
collision.scn を読み込み、階層の全てのノードを調べる。
見つかった全ての透明度を 0 にして、そのノードを addChildNode して GameController のシーンの追加する。
func setupCollisions() { let collisionsScene = SCNScene( named: "Art.scnassets/collision.scn" ) collisionsScene!.rootNode.enumerateChildNodes { (_ child: SCNNode, _ _: UnsafeMutablePointer<ObjCBool>) in child.opacity = 0.0 self.scene?.rootNode.addChildNode(child) } }
次回は GameController クラスのカメラ設定を見てゆく。