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. } }
今回はここまで。