Apple Engine

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

iOS で SceneKit を試す(Swift 3) その75 - SceneKit Particle System File と パーティクルシステム の emitterShape

SceneKit Particle System File (scnp) は Particle System 専用のファイル。
Scene Editor 上で、Particle System の確認ができる。

scnp ファイルは Particle System のファイルではあるが、scn では参照ができないため、コード場で行う。

 

scnp ファイルを Scene Editor で見た場合

パーティクルシステムの情報しかないので、Scene Graph View は存在せず、Inspector のも少なくなっている。

Scene Editor の下の部分は scn ファイルと異なっている。

f:id:x67x6fx74x6f:20170822155920p:plain

 

ファイルの作成

Xcode のメニューバー、File > New > File… か、Command + N から「SceneKit Particle System File」を選ぶ。

f:id:x67x6fx74x6f:20170822154139p:plain

 

テンプレートの選択が出るので必要なものを選択すると、scnp ファイルと使用する画像ファイルができあがる。

f:id:x67x6fx74x6f:20170822154154p:plain

 

テンプレートの種類

以下のテンプレートが用意されている。

  • Bokeh
  • Confetti
  • Rain
  • Reactor
  • Smoke
  • Stars

 

Bokeh

円状のパーティクルの輪郭がボケながら消えてゆくテンプレート。

Attributes Inspector (Command + Option + 4) の Image の Color を変更すると色が変わる。

bokeh.png は 4x4 のアニメーションを並べたテクスチャで、例えば Initial frame の 0 にするとアニメーションが行われないため、ボケは行われず透過で消える。

f:id:x67x6fx74x6f:20170822155207p:plain

 

Confetti

紙吹雪の動きを模したテンプレート。

テンプレートのままだと Scene Editor で表示されないため、 コード上で設定を行う。

 

Fire

炎を模したテンプレート

f:id:x67x6fx74x6f:20170822155229p:plain

 

Rain

雨粒が下に落ちる雨を模したテンプレート。

重力の物理アニメーションで落としているの PhysicsWorld の重力値を変更する場合は注意。

f:id:x67x6fx74x6f:20170822155246p:plain

 

Reactor

バーナーのような炎を模したテンプレート

f:id:x67x6fx74x6f:20170822155308p:plain

 

Smoke

煙を模したアニメーション。

f:id:x67x6fx74x6f:20170822155319p:plain

 

Stars

小さな星が奥から手前に移動し、宇宙空間を慰労しているかのように見せるテンプレート。

薄い黄色から薄い青にアニメーションさせているため、 Scene Editor で若干ズームアウトすると見た目が変わる。

f:id:x67x6fx74x6f:20170822155341p:plain

 

 

任意の Shape にパーティクルを適応してみる。

普通にシーンにパーティクルを適応しても scn のパーティクルシステムとかわらないので emitterShape を使用してみる。

ひとまず、Xcode で SceneKit の Game テンプレート作成。

 

SCNP の作成

Command + N から「SceneKit Particle System File」を選美、Fire テンプレートを選択。
ここでは、ファイル名を Fire.scnp にしてプロジェクトに追加する。

 

GameViewController.swift の設定

GameViewController.swift を開く。

ship.scn を使用しないので、19行目を以下に変更。

let scene = SCNScene()

 

ship.scn を使用しなくなり、宇宙船のアニメーションはいらないので、 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)))

   

パーティクルの設定

GameViewController.swift の viewDidLoad() でビルトインジオメトリ SCNText でシェイプを作成。
そのシェイプからエミッターとしてパーティクルを吐き出す。

// シェイプ
let textShape = SCNText(string: "つぶ", extrusionDepth: 1)
textShape.flatness = 0.0

// パーティクルシステム
let particle = SCNParticleSystem(named: "Fire.scnp", inDirectory: "")
particle?.emitterShape = textShape
let particleShapePosition = particle?.emitterShape?.boundingSphere.center

// ノードにパーティクルシステムをピボット変更後して紐付ける
let particleNode = SCNNode()
particleNode.pivot = SCNMatrix4MakeTranslation(particleShapePosition!.x, particleShapePosition!.y, 0)
particleNode.addParticleSystem(particle!)

scene.rootNode.addChildNode(particleNode)

 

そのままでは大きいので、27行目の カメラの Z軸を 50 に変更

cameraNode.position = SCNVector3(x: 0, y: 0, z: 50)

 

ビルドすると以下のような感じになる

f:id:x67x6fx74x6f:20170822154217g:plain

 

GameViewController.swift のコード

import UIKit
import QuartzCore
import SceneKit

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: 50)
        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)
        
        let scnView = self.view as! SCNView
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.showsStatistics = true
        scnView.backgroundColor = UIColor.black
        
        // シェイプ
        let textShape = SCNText(string: "つぶ", extrusionDepth: 1)
        textShape.flatness = 0.0
        
        // パーティクルシステム
        let particle = SCNParticleSystem(named: "Fire.scnp", inDirectory: "")
        particle?.emitterShape = textShape
        let particleShapePosition = particle?.emitterShape?.boundingSphere.center
        
        // ノードにパーティクルシステムをピボット変更後して紐付ける
        let particleNode = SCNNode()
        particleNode.pivot = SCNMatrix4MakeTranslation(particleShapePosition!.x, particleShapePosition!.y, 0)
        particleNode.addParticleSystem(particle!)
        
        scene.rootNode.addChildNode(particleNode)
    }
    
    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()
    }
}

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その74 - SceneKit のパーティクル、SCNParticleSystem について

パーティクルシステムとは粒状のものに画像など使用し、振る舞いの設定を行い、
煙、雨、紙吹雪、花火の効果を再現する。

そして、その粒子の一つ一つをパーティクルと呼ぶ。

パーティクルは SceneKit で存在するオブジェクトで唯一 Scene Graph と切り離され存在となっており、 数千、数万と描画される可能性があるため、内部的に処理している。

ちなみに他のゲームエンジンと異なりパーティクルをジオメトリに設定することはできないので自力で頑張る。

 

パーティクルシステムの流れ

パーティクルの画像設定

各パーティクルにテクスチャ画像を適応し、色合い、ブレンディングモードなどのパラメータを調整し外観を設定する。
また、アニメーション用にシーケンス画像を設定して、鳥の群れや多段階の爆発など表現することができる。

 

パーティクルの寿命

パーティクルは発射口となるエミッタの始点から、消えるまで位置や色を変更することでアニメーションを表現する。

各パーティクルのが消えるまでを birthRate と particleLifeSpan とし、掛け合わしたものを寿命として設定するが、寿命を長くしすぎると画面上のパーティクルが多くなりすぎ消費電力と演算に時間がかかるので注意。

 

エミッタ

発射口となる部分で、単一点もしくはジオメトリをエミッタに設定することができる。 emissionDuration の設定次第で一定期間アイドルさせたりすることができる。

 

バリエージョン

パーティクルシステムプロパティには、ランダムに変化させるプロパティがある。
例えば、大きさをランダム化し大きさに幅を持たせるとか。

 

動き

パーティクルは方向、速度、角速度、加速度などのシンプルな物理シミュレーションによって移動が行われ、設定している寿命がきて消滅するまでアニメーションを行う。
パーティクルは物理シミュレーションであるため PhysicsWorld のジオメトリによる障害物や PhysicsField を使うことで複雑な効果をアニメーションで行うことができる。

 

他の機能

  • SCNParticlePropertyController を使用すると CoreAnimation の機能から個々のパーティクルの振る舞いを調整しエフェクトを作成すことができる
  • systemSpawnedOnCollision を使用することでパーティクルが障害物に衝突した際に処理ができる。新たなパーティクルが生成される設定が可能
  • パーティクルシステム用に各イベントハンドラにパーティクルモディファイアブロックを付加しアニメーションさせることができる。ブロックはパーティクルのプロパティを一括で変更することができるがレンダリングコストは高くなる。

 

SCNParticleSystem の設定方法の種類

  • 全てコードで作成
  • SceneKit Particle System File (scnp) を作成し、コードから利用
  • Scene ファイル (scn) の Scene Graph で Particle System を作成する。

設定項目が多く動作の状況が掴みづらいので、大体は scn か scnp から利用することなる。

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その73 - 物理シミュレーションとパーティクルの空間に影響を与える PhysicsField について

重力、電磁気、乱気流などの外部から与えられる力を領域内に反映させるオブジェクト、PhysicsField (物理フィールド) について見ていこうと思う。

PhysicsField は物理アニメーションとパーティクルに適応でき、 コード上からカスタムの PhysicsField を作成することができる。

 

SceneKit で用意されている物理フィールド

f:id:x67x6fx74x6f:20170818163631p:plain

名称 機能
Drag 動きを減速させる流体摩擦や空気抵抗のフィールド
Vartex 指定した軸を中心に力が循環する渦のフィールド
Radial Gravity 中心に向かって加速する重力フィールド
Linner Gravity 特定の方向に加速する重力フィールド
Noise ランダムな力を適用するフィールド
Turbulance オブジェクトの速度に比例した大きさでランダムな力を適用するフィールド
Spring 中心に向かってバネのような力で引っ張るフィールド
Electoric 電荷によって中心から距離に基づいてオブジェクトを引き付けるか、または反発させるフィールド
Magnetic 電荷、速度、フィールド軸の距離に基づいて、オブジェクトを引き付けるか、または反発させるフィールド

 

共通のパラメーター

パラメーター Scene Editor 機能
strength PhysicsField が適応範囲に与える力の数値
falloffExponent 距離とともに strength の値がどのように減少するかを設定する数
minimumDistance 距離に基づく効果の最小値
isActive PhysicsField 効果の有効/無効を設定する
isExclusive PhysicsField が重なっている他のフィールドを無視するかどうかを指定する。デフォルト値は false。true にするとこれに重なる Field は無視される
halfExtent PhysicsField の適応範囲を設定する。デフォルト値は無限遠になっているため、Scene Editor で設定を変更する場合は infinite のチェックを外す
scope 適応範囲を指定している内側か外側かを決める。halfExtent がデフォルト値の場合は無限遠であるため変更しても変わらない
usesEllipsoidalExtent デフォルトでは適応範囲は立方体になっているが、こちらを true にすると球状に変更することができる
categoryBitMask × PhysicsField 用のカテゴリービットマスク。PhysicsBody と PhysicsField のビットマスクの AND 演算が 0 でなければ、PhysicsField を適応する

 

補足

halfExtent の設定

PhysicsField の中心である pivot から x, y, z 座標を指定するとそれに合わせた立方体が適応範囲となる。

PhysicsField を (x: 2.0, y:2.0, z:2.0) に配置した際、halfExtent を (x: 0.5, y:0.5, z:0.5) とした設定は、 x, y, z で 1.5 〜 2.5 が有効範囲となる。

 

Scene Editor での設定

f:id:x67x6fx74x6f:20170818163815p:plain

 

適応範囲を決めることでこのような効果を得られる。

f:id:x67x6fx74x6f:20170818163839g:plain

 

scope の outside

デフォルト値は内側の設定になっているので問題はないが、 outside にした場合、設定した適応範囲の外側すべてになるので注意。

 

フィールド効果の減衰

フィールド効果の減衰は pow(distance - minRadius, -falloff) で計算されている。

falloffExponent の数が 0 より大きい場合、フィールドの効果は近くの PhysicsBody が強くなる。 デフォルトの falloffExponent の値は、フィールドタイプによって異なる。

 

Scene Editor にだけあるパラメーター

Display - Field Scale でデフォルト値 1。

ガイドで表示されているポリライン(例えば Vartex の場合は渦状)の長さや大きさを変化させる。
Strength の値を増やしても見た目は変わるが、シーンが大きくて見えなかったりする場合などで確認するためのもの。

 

 

フイールド別の個別で設定できるパラメーター

 

Drag Field

f:id:x67x6fx74x6f:20170818171635p:plain

f:id:x67x6fx74x6f:20170818163948p:plain

 

Strength

適応範囲での抵抗の力。

 

個別のパラメーター

なし

 

Vartex Field

f:id:x67x6fx74x6f:20170818171551p:plain

f:id:x67x6fx74x6f:20170818164045p:plain

 

Strength

中心から渦状に外へ弾き出す力。

 

個別のパラメーター
パラメーター 説明
Direction SCNVector3 回転軸の方向。デフォルト値は (x:0, y:-1, z:0) で Y 軸方向に反時計回りで回る
Offset SCNVector3 回転軸の中央位置を変更する

 

Radial Gravity Field

f:id:x67x6fx74x6f:20170818171745p:plain

f:id:x67x6fx74x6f:20170818165427p:plain

 

Strength

メートル/秒でフィールドの中心へ加速させる力。
マイナスの場合は押し出す。

重力のフィールドあるため質量が考慮される。

 

個別のパラメーター
パラメーター 説明
Offset SCNVector3 効果が発生する中心の位置を変更する

 

Linner Gravity Field

f:id:x67x6fx74x6f:20170818171801p:plain

f:id:x67x6fx74x6f:20170818165449p:plain

 

Strength

指定した方向にメートル/秒で加速させる。

重力のフィールドあるため質量が考慮される。

 

個別のパラメーター
パラメーター 説明
Direction SCNVector3 重力が発生する方向。デフォルト値は (x:0, y:-1, z:0) で Y 軸方向に下向きに

 

Noise Field

f:id:x67x6fx74x6f:20170818171825p:plain

f:id:x67x6fx74x6f:20170818165526p:plain

 

Strength

Simplex noise のアルゴリズムを使用したノイズの大きさ。

 

個別のパラメーター

以下、コードでイニシャライズ時に使用できる。

パラメーター 説明
smoothness CGFloat ノイズの滑らかさ。0.0 が最大で、1.0 でなくなる
speed CGFloat 時間でのノイズの変化。0.0 で変化しなくなる

 

Turbulance Field

f:id:x67x6fx74x6f:20170818171635p:plain

f:id:x67x6fx74x6f:20170818165541p:plain

 

Strength

Noise と同じような乱気流効果の大きさ。
Noise より速く激しく揺れるように振る舞う。

 

個別のパラメーター

以下、コードでイニシャライズ時に使用できる。

パラメーター 説明
smoothness CGFloat ノイズの滑らかさ。0.0 が最大で、1.0 でなくなる
speed CGFloat 時間でのノイズの変化。0.0 で変化しなくなる

 

Spring Field

f:id:x67x6fx74x6f:20170818171905p:plain

f:id:x67x6fx74x6f:20170818165600p:plain

 

Strength

バネの堅さ。
値を高くすると速く中央に戻る。

 

個別のパラメーター
パラメーター 説明
Offset SCNVector3 効果が発生する中心の位置を変更する

 

Electoric Field

f:id:x67x6fx74x6f:20170818171925p:plain

f:id:x67x6fx74x6f:20170818165620p:plain

 

Strength

電荷によって引き付ける(遠ざける)力。
こちらは中央に引き付ける。

 

個別のパラメーター
パラメーター 説明
Offset SCNVector3 磁界の中央位置を変更する

 

Magnetic Field

f:id:x67x6fx74x6f:20170818171937p:plain

f:id:x67x6fx74x6f:20170818165636p:plain

 

Strength

Electoric Field 同様。 こちらは直線電流の磁場のようなフィールドであるため、プラス値で半時計回り、マイナス値で時計周りになる。
(PhysicsBody が 負の電荷を持つ場合は逆)

 

個別のパラメーター
パラメーター 説明
Direction SCNVector3 力の方向。デフォルト値は (x:0, y:-1, z:0) で Y 軸方向に下方向
Offset SCNVector3 中央位置を変更する

 

パーティクルを物理フィールドでも動くようにする

デフォルトではオフになっている。

コードから isAffectedByPhysicsFields を true にするか、
SceneEditor の Attributes Inspector から Affected by physics field のチェックを入れる。

f:id:x67x6fx74x6f:20170818172729p:plain

 

コードから設定する

SCNPhysicsField をイニシャライズして SCNNode の physicsField に渡す。

let field = SCNPhysicsField.vortex()

field.isActive = true
field.isExclusive = false

field.strength = 1
field.falloffExponent = 0
field.minimumDistance = 0

field.halfExtent = SCNVector3(10,10,10)
field.usesEllipsoidalExtent = false
field.scope = .insideExtent

field.direction = SCNVector3(0,0,0)
field.offset = SCNVector3(0,0,0)

field.categoryBitMask = -1

let fieldNode = SCNNode()
fieldNode.physicsField = field

scene.rootNode.addChildNode(fieldNode)

 

イニシャライズの命令はこちらを参照。

SCNPhysicsField - SceneKit | Apple Developer Documentation

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その72 - 物理シミュレーションでのジョイントアニメーションと SCNPhysicsBehavior

SceneKit の物理シミュレーションでは、2つのノードを接合しその個所を考慮した物理アニメーションが用意されており、
設定されたジョイントは PhysicsWorld が持つ SCNPhysicsBehavior で設定することで物理シミュレーションが適応される。

また、ジョイントの稼働位置ごとに設定するため、ノードでの位置設定は意味がなくなる可能性あり。

SCNPhysicsBehavior はコードでしか設定できないため、 ジョイント設定は Scene Editor からは行えない。

 

ジョイントの種類

クラス名 説明
SCNPhysicsHingeJoint 1つの軸を固定したヒンジのような物理アニメーション
SCNPhysicsSliderJoint 移動方向のみに固定した物理アニメーション
SCNPhysicsBallSocketJoint 球状の接合部を模した物理アニメーション

iOS 11 では SCNPhysicsConeTwistJoint が追加されている。

ちなみに、Unity のようなスプリングジョイントはないので自前で頑張る。

 

SCNPhysicsBehavior で設定できるもの

上記のものの他に、SCNPhysicsVehicle という車の構造を模した物理アニメーション適応できる。

SCNPhysicsVehicle とその車輪部分である SCNPhysicsVehicleWheel は使い所が少ないのでこの Blog では紹介を割愛する。

 

試してみる

SCNPhysicsHingeJoint を試してみる。
SCNPhysicsSliderJoint、SCNPhysicsBallSocketJoin もほぼ同様の方法なので簡単に試せると思う。

 

準備

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

GameViewController.swift を開き、viewDidLoad() の中身を変更する。

ごっそり変えるので以下のものに変更。
シーン設定して、カメラ、ライト、床のジオメトリを置く。

override func viewDidLoad() {
    super.viewDidLoad()
    
    let scene = SCNScene()
    
    let cameraNode = SCNNode()
    cameraNode.camera = SCNCamera()
    cameraNode.position = SCNVector3(x: 0, y: 3, 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)
    
    let floorNode = SCNNode(geometry: SCNFloor())
    floorNode.geometry?.firstMaterial?.diffuse.contents = UIColor.black
    floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
    scene.rootNode.addChildNode(floorNode)

    let scnView = self.view as! SCNView
    scnView.scene = scene
    scnView.allowsCameraControl = true
    scnView.showsStatistics = true
    scnView.backgroundColor = UIColor.black
    
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
    scnView.addGestureRecognizer(tapGesture)
}

 

SCNPhysicsHingeJoint 設定する

SCNPhysicsHingeJoint の初期化は以下のパラメーター

init(bodyA: SCNPhysicsBody, axisA: SCNVector3, anchorA: SCNVector3, bodyB: SCNPhysicsBody, axisB: SCNVector3, anchorB: SCNVector3)
パラメーター 説明
bodyA 1つ目の PhysicsBody
axisA 1つ目の PhysicsBody の回転軸方向
anchorA 1つ目の PhysicsBody の回転軸の位置
bodyB 2つ目の PhysicsBody
axisB 2つ目の PhysicsBody の回転軸方向
anchorB 2つ目の PhysicsBody の回転軸の位置

 

では、viewDidLoad() の floorNode の下に以下のコードを追加してビルド。

板の2枚を作成し、互いの間に SCNPhysicsHingeJoint で X 軸方向にヒンジの接合部分を作成。 1枚目は空間に固定するため type が static 担っており X軸 プラスマイナス 90 度回転でアニメーションしている。

     ・
     ・
     ・
scene.rootNode.addChildNode(floorNode)

let box = SCNBox(width: 4.0, height: 0.5, length: 0.1, chamferRadius: 0.0)
let boxNode1 = SCNNode(geometry: box)
boxNode1.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
boxNode1.position = SCNVector3(0, 10, 0)
scene.rootNode.addChildNode(boxNode1)

let boxNode2 = SCNNode(geometry: box)
boxNode2.physicsBody = SCNPhysicsBody.dynamic()
boxNode2.position = SCNVector3(0, 9, 0)
scene.rootNode.addChildNode(boxNode2)

let joint = SCNPhysicsHingeJoint(
    bodyA: boxNode1.physicsBody!,
    axisA: SCNVector3(x: 1.0, y: 0.0, z: 0.0),
    anchorA: SCNVector3(x: 0.0, y: -0.5, z: 0.0),
    bodyB: boxNode2.physicsBody!,
    axisB: SCNVector3(x: 1.0, y: 0.0, z: 0.0),
    anchorB: SCNVector3(x: 0.0, y: 0.5, z: 0.0)
)

scene.physicsWorld.addBehavior(joint)

boxNode1.runAction(SCNAction.repeatForever(SCNAction.sequence([
    SCNAction.rotateTo(x: CGFloat(Float.pi * 0.5), y: 0, z: 0, duration: 1.0),
    SCNAction.rotateTo(x: CGFloat(Float.pi * -0.5), y: 0, z: 0, duration: 1.0)
    ])))

 

ジョイントを解除してみる

viewDidLoad() の下にある func handleTap(_ gestureRecognize: UIGestureRecognizer) の中身を以下のものに変えてみる。

func handleTap(_ gestureRecognize: UIGestureRecognizer) {
    let scnView = self.view as! SCNView
    scnView.scene?.physicsWorld.removeAllBehaviors()
}

 

画面をタップするとジョイントの接合が途切れる。

 

せっかくなのでジョイントを増やす。

viewDidLoad() の floorNode の下に、さきほど追加した板のノードとジョイントアニメーションを消して、以下を追加。

     ・
     ・
     ・
scene.rootNode.addChildNode(floorNode)

// --- 追加 ---
let box = SCNBox(width: 4.0, height: 0.5, length: 0.1, chamferRadius: 0.0)
let boxNode1 = SCNNode(geometry: box)
boxNode1.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
boxNode1.position = SCNVector3(0, 10, 0)
scene.rootNode.addChildNode(boxNode1)

boxNode1.runAction(SCNAction.repeatForever(SCNAction.sequence([
    SCNAction.rotateTo(x: CGFloat(Float.pi * 0.5), y: 0, z: 0, duration: 1.0),
    SCNAction.rotateTo(x: CGFloat(Float.pi * -0.5), y: 0, z: 0, duration: 1.0)
    ])))

var tmp : SCNNode?;
for i in 1...9 {
    let copyNode = SCNNode(geometry: box)
    copyNode.position = SCNVector3(0, 10.0 - Double(i), 0)
    copyNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
    scene.rootNode.addChildNode(copyNode)
    
    if tmp == nil {
        tmp = boxNode1
    }
    
    let joint = SCNPhysicsHingeJoint(
        bodyA: (tmp?.physicsBody!)!,
        axisA: SCNVector3(x: 1.0, y: 0.0, z: 0.0),
        anchorA: SCNVector3(x: 0.0, y: -0.5, z: 0.0),
        bodyB: copyNode.physicsBody!,
        axisB: SCNVector3(x: 1.0, y: 0.0, z: 0.0),
        anchorB: SCNVector3(x: 0.0, y: 0.5, z: 0.0)
    )
    
    scene.physicsWorld.addBehavior(joint)
    
    tmp = copyNode
}
// --- 追加 ---

let scnView = self.view as! SCNView
     ・
     ・
     ・

f:id:x67x6fx74x6f:20170817154811g:plain

 

GameViewController.swift の全コード

import UIKit
import QuartzCore
import SceneKit

class GameViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let scene = SCNScene()
        
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 3, 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)
        
        let floorNode = SCNNode(geometry: SCNFloor())
        floorNode.geometry?.firstMaterial?.diffuse.contents = UIColor.black
        floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
        scene.rootNode.addChildNode(floorNode)
        
        let box = SCNBox(width: 4.0, height: 0.5, length: 0.1, chamferRadius: 0.0)
        let boxNode1 = SCNNode(geometry: box)
        boxNode1.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
        boxNode1.position = SCNVector3(0, 10, 0)
        scene.rootNode.addChildNode(boxNode1)
        
//        let boxNode2 = SCNNode(geometry: box)
//        boxNode2.physicsBody = SCNPhysicsBody.dynamic()
//        boxNode2.position = SCNVector3(0, 9, 0)
//        scene.rootNode.addChildNode(boxNode2)
//        
//        let joint = SCNPhysicsHingeJoint(
//            bodyA: boxNode1.physicsBody!,
//            axisA: SCNVector3(x: 1.0, y: 0.0, z: 0.0),
//            anchorA: SCNVector3(x: 0.0, y: -0.5, z: 0.0),
//            bodyB: boxNode2.physicsBody!,
//            axisB: SCNVector3(x: 1.0, y: 0.0, z: 0.0),
//            anchorB: SCNVector3(x: 0.0, y: 0.5, z: 0.0)
//        )
//        
//        scene.physicsWorld.addBehavior(joint)

        boxNode1.runAction(SCNAction.repeatForever(SCNAction.sequence([
            SCNAction.rotateTo(x: CGFloat(Float.pi * 0.5), y: 0, z: 0, duration: 1.0),
            SCNAction.rotateTo(x: CGFloat(Float.pi * -0.5), y: 0, z: 0, duration: 1.0)
            ])))
        
        var tmp : SCNNode?;
        for i in 1...9 {
            let copyNode = SCNNode(geometry: box)
            copyNode.position = SCNVector3(0, 10.0 - Double(i), 0)
            copyNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
            scene.rootNode.addChildNode(copyNode)
            
            if tmp == nil {
                tmp = boxNode1
            }
            
            let joint = SCNPhysicsHingeJoint(
                bodyA: (tmp?.physicsBody!)!,
                axisA: SCNVector3(x: 1.0, y: 0.0, z: 0.0),
                anchorA: SCNVector3(x: 0.0, y: -0.5, z: 0.0),
                bodyB: copyNode.physicsBody!,
                axisB: SCNVector3(x: 1.0, y: 0.0, z: 0.0),
                anchorB: SCNVector3(x: 0.0, y: 0.5, z: 0.0)
            )
            
            scene.physicsWorld.addBehavior(joint)
            
            tmp = copyNode
        }
        
        let scnView = self.view as! SCNView
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.showsStatistics = true
        scnView.backgroundColor = UIColor.black
        
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        scnView.addGestureRecognizer(tapGesture)
    }
    
    func handleTap(_ gestureRecognize: UIGestureRecognizer) {
        
        let scnView = self.view as! SCNView
        
        scnView.scene?.physicsWorld.removeAllBehaviors()
        
    }
    
    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) その71 - SCNPhysicsContact と SCNPhysicsContactDelegate

PhysicsWorld 上で、2つ以上の PhysicsBody の接触が起こった場合に SCNPhysicsContactDelegate を呼ぶことができる。

 

注意点

PhysicsBody は contactTestBitMask が 0 以外でないと SCNPhysicsContactDelegate へ情報が送られないので注意。

当然だが、SCNPhysicsContact は SCNPhysicsContactDelegate の内部で情報が更新される。
そのため、それ以外の場所で SCNPhysicsContact を呼んでも接触の情報は取得できない。

追記: SCNView へ SCNScene を設定後、physicsWorld.contactDelegate = self を行ってもデリゲートが動作しないことが判明。コードは修正済み  

SCNPhysicsContact で取得できるもの

プロパティ名 説明
nodeA 1個目のノードを返す
nodeB 2個目のノードを返す
contactPoint 接地点の座標
contactNormal 接点の法線ベクトル。どの方向から衝突されているかを示す
collisionImpulse ニュートン秒で表された衝突時の力。弱く衝突しているか、激しく衝突しているかなど調べることができる
penetrationDistance 互いのノードが重なっている距離

 

SCNPhysicsContactDelegate のメソッド

用意されているメソッドは、接触した時、接触状態が更新された時、接触が終わった時の3つ

  • physicsWorld(SCNPhysicsWorld, didBegin: SCNPhysicsContact)
  • physicsWorld(SCNPhysicsWorld, didUpdate: SCNPhysicsContact)
  • physicsWorld(SCNPhysicsWorld, didEnd: SCNPhysicsContact)

 

設定手順

  1. ViewController などのクラスで SCNPhysicsContactDelegate を呼ぶ。
  2. シーンのメソッドで physicsWorld.contactDelegate を設定する
  3. 調べたい PhysicsBody の contactTestBitMask を 0 以外にする
  4. SCNPhysicsContactDelegate メソッドを追加する

 

つくってみる

いつも通り、XCode で SceneKit の Game テンプレートプロジェクトを作成。

今回は GameViewController.swift のみ修整。

  

その1

GameViewController.swift を開き、13行目に UIViewController の隣に SCNPhysicsContactDelegate を追加。

class GameViewController: UIViewController, SCNPhysicsContactDelegate {

 

その2

viewDidLoad() の中身を大幅に変えるので以下に変更。 SCNFloor と 最後に physicsWorld.contactDelegate を設定している

override func viewDidLoad() {
    super.viewDidLoad()
    
    let scene = SCNScene()

    scene.physicsWorld.contactDelegate = self
    
    let cameraNode = SCNNode()
    cameraNode.camera = SCNCamera()
    cameraNode.position = SCNVector3(x: 12.68, y: 7.445, z: 12.86)
    cameraNode.eulerAngles = SCNVector3(x: ((Float.pi * -22.129) / 180), y: ((Float.pi * 44.576) / 180), z: 0.0)
    scene.rootNode.addChildNode(cameraNode)

    let lightNode = SCNNode()
    lightNode.light = SCNLight()
    lightNode.light!.type = .omni
    lightNode.position = SCNVector3(x: 0, y: 1, z: 0)
    scene.rootNode.addChildNode(lightNode)

    let floorNode = SCNNode(geometry: SCNFloor())
    floorNode.name = "floor"
    floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
    floorNode.physicsBody?.contactTestBitMask = 1
    scene.rootNode.addChildNode(floorNode)
    
    let scnView = self.view as! SCNView
    scnView.scene = scene
    scnView.allowsCameraControl = true
    scnView.showsStatistics = true
    scnView.backgroundColor = UIColor.black
}   

 

f:id:x67x6fx74x6f:20170816190745p:plain

 

その3

以下のコードは contactTestBitMask を 1 にした SCNSphere。

floorNode の上に書いておく。

ちなみに floorNode の contactTestBitMask はすでに設定済み。

let ballNode = SCNNode(geometry: SCNSphere())
ballNode.name = "ball"
ballNode.position = SCNVector3(x: 0, y: 5, z: 0)
ballNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
ballNode.physicsBody?.contactTestBitMask = 1
scene.rootNode.addChildNode(ballNode)

 

その4

contactDelegate のメソッドを書いてみる。
viewDidLoad(){ ... } の下あたりに書くと良いかと。

didEnd で SCNPhysicsContact で調べられるすべての値を出力している

func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
    print("contactDelegate: Begin")
}

func physicsWorld(_ world: SCNPhysicsWorld, didUpdate contact: SCNPhysicsContact) {
    print("contactDelegate: Update")
}

func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
    print("contactDelegate: End")
    
    let firstNode = contact.nodeA
    let secondNode = contact.nodeB
    
    print("NodeA: \(String(describing: firstNode.name!))")
    print("NodeB: \(String(describing: secondNode.name!))")
    
    print("contactPoint: \(String(describing: contact.contactPoint))")
    print("contactNormal: \(String(describing: contact.contactNormal))")
    print("collisionImpulse: \(String(describing: contact.collisionImpulse))")
    print("penetrationDistance: \(String(describing: contact.penetrationDistance))")
}

 

設定完了。

 

ビルドしてみる

f:id:x67x6fx74x6f:20170816190818g:plain

 

print から出力される情報は以下のような感じ。

contactDelegate: Begin
contactDelegate: End
NodeA: ball
NodeB: floor
contactPoint: SCNVector3(x: 0.0, y: -9.31322575e-10, z: 0.0)
contactNormal: SCNVector3(x: 0.0, y: 0.99999994, z: 0.0)
collisionImpulse: 11.4864807128906
penetrationDistance: -0.0325339697301388
contactDelegate: Begin
contactDelegate: Update
     ・
     ・
     ・
contactDelegate: Update

 

didEnd で情報を調べているが弾んだ後は didEnd が呼ばれないが、 着地の際は Begin が呼ばれている。
また、PhysicsBody が静止するまで Update が呼び続けられる。

 

まとめ

今回は SCNPhysicsContact の情報の出力しかしていないが、 SCNPhysicsContact から nodeA と nodeB で接触している SCNNode を比較できる。

nodeA、nodeB の name や contactTestBitMask などを調べたりして applyForce を加えたり、接触したノードを消して爆発のパーティクルを表示するような表現ができることがわかると思う。

あと、今回は2つの PhysicsBody で試したが2つ以上でも同様。
例えば、X軸 1.5 にもう1つ SCNSphere を置くと contactDelegate は1個目の球と床、2個目の球と床の接地した情報を取得す続ける。

 

GameViewController.swift の全コード

import UIKit
import QuartzCore
import SceneKit

class GameViewController: UIViewController, SCNPhysicsContactDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let scene = SCNScene()

        scene.physicsWorld.contactDelegate = self
        
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 12.68, y: 7.445, z: 12.86)
        cameraNode.eulerAngles = SCNVector3(x: ((Float.pi * -22.129) / 180), y: ((Float.pi * 44.576) / 180), z: 0.0)
        scene.rootNode.addChildNode(cameraNode)

        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 1, z: 0)
        scene.rootNode.addChildNode(lightNode)

        let ballNode = SCNNode(geometry: SCNSphere())
        ballNode.name = "ball"
        ballNode.position = SCNVector3(x: 0, y: 5, z: 0)
        ballNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
        ballNode.physicsBody?.contactTestBitMask = 1
        scene.rootNode.addChildNode(ballNode)
        
        let floorNode = SCNNode(geometry: SCNFloor())
        floorNode.name = "floor"
        floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
        floorNode.physicsBody?.contactTestBitMask = 1
        scene.rootNode.addChildNode(floorNode)
        
        let scnView = self.view as! SCNView
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.showsStatistics = true
        scnView.backgroundColor = UIColor.black
    }

    func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
        print("contactDelegate: Begin")
    }
    
    func physicsWorld(_ world: SCNPhysicsWorld, didUpdate contact: SCNPhysicsContact) {
        print("contactDelegate: Update")
    }
    
    func physicsWorld(_ world: SCNPhysicsWorld, didEnd contact: SCNPhysicsContact) {
        print("contactDelegate: End")
        
        let firstNode = contact.nodeA
        let secondNode = contact.nodeB
        
        print("NodeA: \(String(describing: firstNode.name!))")
        print("NodeB: \(String(describing: secondNode.name!))")
        
        print("contactPoint: \(String(describing: contact.contactPoint))")
        print("contactNormal: \(String(describing: contact.contactNormal))")
        print("collisionImpulse: \(String(describing: contact.collisionImpulse))")
        print("penetrationDistance: \(String(describing: contact.penetrationDistance))")
    }
    
    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()
    }
}

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その70 - PhysicsBody にある3つのビットマスク

PhysicsBody には SCNNode のビットマスクとは別に、
以下のビットマスクが存在している。

  • Category mask
  • Collision mask
  • Contact mask

 

Physics Inspector (Command + Option + 6) でも、
同様のパラメーターがある

f:id:x67x6fx74x6f:20170816151853p:plain

 

Category mask

PhysicsBody の Category mask となり、AND演算の結果が 0 以外であれば同じカテゴリーに所属することとなる。

 

Collision mask

衝突判定となる Collision mask。

CategoryBitmask で同じカテゴリーに所属していて、互いの CollisionBitmask が AND演算の結果が 0 以外であれば、衝突判定が行われる。

そのため、CategoryBitmask と CollisionBitmask の要件が合わないと、Dynamic の PhysicsBody は障害物から突き抜けてしまう。

 

Category mask と Collision mask の使用例

f:id:x67x6fx74x6f:20170816151736g:plain

 

X 値 -1.5 (奥) 0 (中央) 1.5 (手前)
Category mask 2 1 1
Collision mask -1 1 -1

 

ピンクの板

3つとも Static のデフォルト値

  • Category mask 2
  • Collision mask -3

 

床(SCNFloor)
  • Category mask 1
  • Collision mask -3

 

今までの Bitmask と同様マイナスはすべて許容する。

奥の球は Category mask、中央の球は Collision mask の演算で弾かれるため板を通過する。

さらに、奥の球は Category mask が床との演算でも弾かれるため、床からも通過してしまう。

 

Contact mask

他の PhysicsBody が接触した際に使用する Contact mask でデフォルト値は 0。

値が 0 以外のノード同士がぶつかり合うと、 SCNPhysicsContactDelegate に SCNPhysicsContact オブジェクトがメッセージとして送られる。
(仕様上、片方が 0 の場合スルーされるはずだが、SCNPhysicsContact が送られる場合があり謎)

Category mask や Collision mask のように衝突処理自体に影響を与えない。

 

コード

コードでの設定方法。Dynamic での初期値。

let ballNode = SCNNode(geometry: SCNSphere())
ballNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
ballNode.physicsBody?.categoryBitMask = 1
ballNode.physicsBody?.collisionBitMask = -1
ballNode.physicsBody?.contactTestBitMask = 0

scene.rootNode.addChildNode(ballNode)

 

今回はここまで。

iOS で SceneKit を試す(Swift 3) その69 - PhysicsBody の振る舞い 3

コードでしか変更できない物理シミュレーション設定についてのご紹介。

 

momentOfInertia、usesDefaultMomentOfInertia

PhysicsBody の慣性モーメントを変更する。

SceneKit では形状と質量に合わせて自動設定されるが usesDefaultMomentOfInertia を false にして、 momentOfInertia を設定すると独自の慣性モーメントを付加できる。

例えば、X 軸回転方向に制限をかける

let ball = SCNSphere(radius: 1)
let ballNode = SCNNode(geometry: ball)
ballNode.position.y = 10
ballNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)

ballNode.physicsBody?.usesDefaultMomentOfInertia = false
ballNode.physicsBody?.momentOfInertia = SCNVector3(0, 1, 1)

scene.rootNode.addChildNode(ballNode)

 

力、トルク、衝動(力積、インパルス)

3つのメソッドとそれらをキャンセルするメソッドがある。

ユーザー操作で PhysicsBody を動かす場合はこちらを使うことになる。

  • applyForce(SCNVector3, asImpulse: Bool)
  • applyForce(SCNVector3, at: SCNVector3, asImpulse: Bool)
  • applyTorque(SCNVector4, asImpulse: Bool)

 

説明

applyForce の2つは移動の力となり、applyTorque は回転であるトルクの力が現在与えられている力に加わる。

1つ目の applyForce はジオメトリの重心から力を与え、
2つ目の applyForce の at はノードのローカル座標の場所から力を与える。

asImpulse を true にすると運動量の瞬間的な変化が適用され、false にすると最初のアニメーションループ(動作1フレーム目)にのみ適応されている模様。

 

単位

  • Force の単位はニュートン
  • Torque の単位はニュートンメートル

 

asImpulse が true の場合は

  • Force の単位はニュートン秒
  • Torque の単位はニュートンメートル秒

 

コード

node.physicsBody?.applyForce(SCNVector3(1,0,0), asImpulse: true)
node.physicsBody?.applyForce(SCNVector3(1,0,0), at: SCNVector3(0,0,0), asImpulse: true)
node.physicsBody?.applyTorque(SCNVector4(0,0,-1,1), asImpulse: true)

// Force や Torque のキャンセル
node.physicsBody?.clearAllForces()

 

重力や空気抵抗、他の PhysicsBody の摩擦や衝突などによる力の減衰、PhysicsField の影響があるため、実際に動かしてみないと動作の把握が難しい感じはある。

 

今回はここまで。