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 ファイルと異なっている。
ファイルの作成
Xcode のメニューバー、File > New > File… か、Command + N から「SceneKit Particle System File」を選ぶ。
テンプレートの選択が出るので必要なものを選択すると、scnp ファイルと使用する画像ファイルができあがる。
テンプレートの種類
以下のテンプレートが用意されている。
- Bokeh
- Confetti
- Rain
- Reactor
- Smoke
- Stars
Bokeh
円状のパーティクルの輪郭がボケながら消えてゆくテンプレート。
Attributes Inspector (Command + Option + 4) の Image の Color を変更すると色が変わる。
bokeh.png は 4x4 のアニメーションを並べたテクスチャで、例えば Initial frame の 0 にするとアニメーションが行われないため、ボケは行われず透過で消える。
Confetti
紙吹雪の動きを模したテンプレート。
テンプレートのままだと Scene Editor で表示されないため、 コード上で設定を行う。
Fire
炎を模したテンプレート
Rain
雨粒が下に落ちる雨を模したテンプレート。
重力の物理アニメーションで落としているの PhysicsWorld の重力値を変更する場合は注意。
Reactor
バーナーのような炎を模したテンプレート
Smoke
煙を模したアニメーション。
Stars
小さな星が奥から手前に移動し、宇宙空間を慰労しているかのように見せるテンプレート。
薄い黄色から薄い青にアニメーションさせているため、 Scene Editor で若干ズームアウトすると見た目が変わる。
任意の 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)
ビルドすると以下のような感じになる
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 で用意されている物理フィールド
名称 | 機能 |
---|---|
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 での設定
適応範囲を決めることでこのような効果を得られる。
scope の outside
デフォルト値は内側の設定になっているので問題はないが、 outside にした場合、設定した適応範囲の外側すべてになるので注意。
フィールド効果の減衰
フィールド効果の減衰は pow(distance - minRadius, -falloff) で計算されている。
falloffExponent の数が 0 より大きい場合、フィールドの効果は近くの PhysicsBody が強くなる。 デフォルトの falloffExponent の値は、フィールドタイプによって異なる。
Scene Editor にだけあるパラメーター
Display - Field Scale でデフォルト値 1。
ガイドで表示されているポリライン(例えば Vartex の場合は渦状)の長さや大きさを変化させる。
Strength の値を増やしても見た目は変わるが、シーンが大きくて見えなかったりする場合などで確認するためのもの。
フイールド別の個別で設定できるパラメーター
Drag Field
Strength
適応範囲での抵抗の力。
個別のパラメーター
なし
Vartex Field
Strength
中心から渦状に外へ弾き出す力。
個別のパラメーター
パラメーター | 型 | 説明 |
---|---|---|
Direction | SCNVector3 | 回転軸の方向。デフォルト値は (x:0, y:-1, z:0) で Y 軸方向に反時計回りで回る |
Offset | SCNVector3 | 回転軸の中央位置を変更する |
Radial Gravity Field
Strength
メートル/秒でフィールドの中心へ加速させる力。
マイナスの場合は押し出す。
重力のフィールドあるため質量が考慮される。
個別のパラメーター
パラメーター | 型 | 説明 |
---|---|---|
Offset | SCNVector3 | 効果が発生する中心の位置を変更する |
Linner Gravity Field
Strength
指定した方向にメートル/秒で加速させる。
重力のフィールドあるため質量が考慮される。
個別のパラメーター
パラメーター | 型 | 説明 |
---|---|---|
Direction | SCNVector3 | 重力が発生する方向。デフォルト値は (x:0, y:-1, z:0) で Y 軸方向に下向きに |
Noise Field
Strength
Simplex noise のアルゴリズムを使用したノイズの大きさ。
個別のパラメーター
以下、コードでイニシャライズ時に使用できる。
パラメーター | 型 | 説明 |
---|---|---|
smoothness | CGFloat | ノイズの滑らかさ。0.0 が最大で、1.0 でなくなる |
speed | CGFloat | 時間でのノイズの変化。0.0 で変化しなくなる |
Turbulance Field
Strength
Noise と同じような乱気流効果の大きさ。
Noise より速く激しく揺れるように振る舞う。
個別のパラメーター
以下、コードでイニシャライズ時に使用できる。
パラメーター | 型 | 説明 |
---|---|---|
smoothness | CGFloat | ノイズの滑らかさ。0.0 が最大で、1.0 でなくなる |
speed | CGFloat | 時間でのノイズの変化。0.0 で変化しなくなる |
Spring Field
Strength
バネの堅さ。
値を高くすると速く中央に戻る。
個別のパラメーター
パラメーター | 型 | 説明 |
---|---|---|
Offset | SCNVector3 | 効果が発生する中心の位置を変更する |
Electoric Field
Strength
電荷によって引き付ける(遠ざける)力。
こちらは中央に引き付ける。
個別のパラメーター
パラメーター | 型 | 説明 |
---|---|---|
Offset | SCNVector3 | 磁界の中央位置を変更する |
Magnetic Field
Strength
Electoric Field 同様。
こちらは直線電流の磁場のようなフィールドであるため、プラス値で半時計回り、マイナス値で時計周りになる。
(PhysicsBody が 負の電荷を持つ場合は逆)
個別のパラメーター
パラメーター | 型 | 説明 |
---|---|---|
Direction | SCNVector3 | 力の方向。デフォルト値は (x:0, y:-1, z:0) で Y 軸方向に下方向 |
Offset | SCNVector3 | 中央位置を変更する |
パーティクルを物理フィールドでも動くようにする
デフォルトではオフになっている。
コードから isAffectedByPhysicsFields を true にするか、
SceneEditor の Attributes Inspector から Affected by physics field のチェックを入れる。
コードから設定する
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 ・ ・ ・
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)
設定手順
- ViewController などのクラスで SCNPhysicsContactDelegate を呼ぶ。
- シーンのメソッドで physicsWorld.contactDelegate を設定する
- 調べたい PhysicsBody の contactTestBitMask を 0 以外にする
- 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 }
その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))") }
設定完了。
ビルドしてみる
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) でも、
同様のパラメーターがある
Category mask
PhysicsBody の Category mask となり、AND演算の結果が 0 以外であれば同じカテゴリーに所属することとなる。
Collision mask
衝突判定となる Collision mask。
CategoryBitmask で同じカテゴリーに所属していて、互いの CollisionBitmask が AND演算の結果が 0 以外であれば、衝突判定が行われる。
そのため、CategoryBitmask と CollisionBitmask の要件が合わないと、Dynamic の PhysicsBody は障害物から突き抜けてしまう。
Category mask と Collision mask の使用例
球
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 の影響があるため、実際に動かしてみないと動作の把握が難しい感じはある。
今回はここまで。