Apple Engine

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

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.
    }
}

 

今回はここまで。