5
1

More than 1 year has passed since last update.

SceneKitの衝突判定(Swift)

Last updated at Posted at 2021-12-13

Xcode-13.0 Swift-5.5 iOS-15.0

はじめに

SceneKit で物体を衝突させようと思うといろいろ設定しないといけません:dizzy_face:

let floorNode: SCNNode = {
    let floor = SCNFloor()
    floor.firstMaterial?.diffuse.contents = UIColor.blue
    let floorNode = SCNNode(geometry: floor)
    return floorNode
}()
scnView.scene?.rootNode.addChildNode(floorNode)

let boxNode: SCNNode = {
    let box = SCNBox(width: 1.0, height: 1.5, length: 1.0, chamferRadius: 0)
    box.firstMaterial?.diffuse.contents = UIColor.red

    let boxNode = SCNNode(geometry: box)
    boxNode.position = SCNVector3(x: 0, y: 2, z: 0)
    boxNode.physicsBody = .init(type: .dynamic, shape: .init(geometry: box))
    return boxNode
}()
scnView.scene?.rootNode.addChildNode(boxNode)

上記のコードだと床とキューブは衝突せずにすり抜けてしまいます。床にキューブを衝突させる方法について記載します。

not_contact

SCNPhysicsBody

物体を衝突させるためには SCNNodeSCNPhysicsBodycollisionBitMask などを設定する必要があります。

SCNNodeSCNPhysicsBody はデフォルトが nil なのでまず SCNPhysicsBody を設定してやる必要があります。

init(type:shape:) で初期化し SCNPhysicsBodyType には下記の3つが指定できます。

  • static
    床や壁など固定する物体に指定します
  • dynamic
    デフォルトで下方向に重力がかかるので何かにぶつかるまで下に落下します
  • kinematic
    これはどういうときに使うかわからない。。。(ドキュメント

SCNPhysicsBody:ドキュメント

衝突

物体の衝突には SCNPhysicsBody の以下の3つのプロパティが関係しています。

  • categoryBitMask
    この物体のカテゴリ
  • collisionBitMask
    この物体と衝突できる物体のカテゴリ
  • contactTestBitMask
    この物体と衝突したときに通知する物体のカテゴリ

これが非常にややこしいです(ちょっと理解できてるかわからないです。。。)。

categoryBitMask と collisionBitMask

物体が衝突するかどうかはこの2つの値が関係しています。collisionBitMask のドキュメントには下記のように記載があります。

SceneKit compares the body’s collision mask to the other body’s category mask by performing a bitwise AND operation. If the result is a nonzero value, then the body is affected by the collision.

collisionBitMaskcategoryBitMask の AND(論理積)が 0 以外の場合に衝突が起こるということだと思います。

それぞれのデフォルト値は下記のようになっています。

  • categoryBitMask
    SCNPhysicsBodystatic の場合は SCNPhysicsCollisionCategory.static、それ以外の場合は SCNPhysicsCollisionCategory.default
  • collisionBitMask
    SCNPhysicsCollisionCategory.all

とりあえず下記のように床にも physicsBody を設定してやると床とキューブは衝突するようになります。

let floorNode: SCNNode = {
    let floor = SCNFloor()
    floor.firstMaterial?.diffuse.contents = UIColor.blue
    let floorNode = SCNNode(geometry: floor)
    floorNode.physicsBody = .init(type: .static, shape: .init(geometry: floor)) // ここ
    return floorNode
}()
scnView.scene?.rootNode.addChildNode(floorNode)

let boxNode: SCNNode = {
    let box = SCNBox(width: 1.0, height: 1.5, length: 1.0, chamferRadius: 0)
    box.firstMaterial?.diffuse.contents = UIColor.red

    let boxNode = SCNNode(geometry: box)
    boxNode.position = SCNVector3(x: 0, y: 2, z: 0)
    boxNode.physicsBody = .init(type: .dynamic, shape: .init(geometry: box))
    return boxNode
}()
scnView.scene?.rootNode.addChildNode(boxNode)

contact

下記のように色々試してやるとぶつかったりぶつからなかったりすることがわかります。

// ぶつかる
floorNode.physicsBody?.collisionBitMask = 1
boxNode.physicsBody?.categoryBitMask = 1

// ぶつからない
floorNode.physicsBody?.collisionBitMask = 1
boxNode.physicsBody?.categoryBitMask = 2

// ぶつかる
floorNode.physicsBody?.collisionBitMask = 1
boxNode.physicsBody?.categoryBitMask = 3

// ぶつからない
floorNode.physicsBody?.collisionBitMask = 5
boxNode.physicsBody?.categoryBitMask = 10

contactTestBitMask

こちらは SCNPhysicsContactDelegate で衝突の通知を受け取りたいときに使用するやつです。

contactTestBitMaskcategoryBitMask の AND(論理積)が 0 以外の場合に通知がとぶようです。

floorNode.physicsBody?.contactTestBitMask = 1
boxNode.physicsBody?.categoryBitMask = 1

scnView.scene?.physicsWorld.contactDelegate = self

上記のように設定してやると床とキューブが衝突する際に下記のデリゲートが呼ばれます。

extension ViewController: SCNPhysicsContactDelegate {

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

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

    func physicsWorld(_ world: SCNPhysicsWorld, didEnd: SCNPhysicsContact) {
        print("End!!!")
    }
}

下記のように色々設定して試してみました。

// 衝突するし通知も呼ばれる
floorNode.physicsBody?.contactTestBitMask = 1
boxNode.physicsBody?.categoryBitMask = 1

// 衝突するが通知は呼ばれない
floorNode.physicsBody?.collisionBitMask = 2
floorNode.physicsBody?.contactTestBitMask = 1
boxNode.physicsBody?.categoryBitMask = 2

// 衝突しないが通知は呼ばれる
floorNode.physicsBody?.collisionBitMask = 5
floorNode.physicsBody?.contactTestBitMask = 3
boxNode.physicsBody?.categoryBitMask = 10

// 衝突しないし通知も呼ばれない
floorNode.physicsBody?.collisionBitMask = 5
floorNode.physicsBody?.contactTestBitMask = 21
boxNode.physicsBody?.categoryBitMask = 10

contactTestBitMask:ドキュメント

おまけ

チューブの中にキューブを落とそうと思い下記のようにチューブを追加するとキューブが空中に浮いてしまいます。

let tubeNode: SCNNode = {
    let tube = SCNTube(innerRadius: 3, outerRadius: 5, height: 3)
    tube.firstMaterial?.diffuse.contents = UIColor.green
    let tubeNode = SCNNode.init(geometry: tube)
    tubeNode.physicsBody = .init(type: .static, shape: .init(geometry: tube))
    return tubeNode
}()
floorNode.addChildNode(tubeNode)

tube_1

よくわかりませんが下記のように修正してやることで無事チューブの中に落とすことができました:tada:

tubeNode.physicsBody = .init(type: .static, shape: .init(geometry: tube, options: [.type: SCNPhysicsShape.ShapeType.concavePolyhedron]))

tube_2

おわりに

ビットマスクというのがよく理解できませんでしたがとりあえず物体同士を衝突させて通知を受信できるようになりました:raised_hands:

参考

5
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1