SceneKitの衝突判定（Swift）

![Xcode-13.0](https://img.shields.io/badge/Xcode-13.0-brightgreen) ![Swift-5.5](https://img.shields.io/badge/Swift-5.5-brightgreen) ![iOS-15.0](https://img.shields.io/badge/iOS-15.0-brightgreen)

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

```swift
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](https://user-images.githubusercontent.com/34936885/145701031-fd235374-dcce-424a-af16-117b959b2fe4.gif)

## SCNPhysicsBody
物体を衝突させるためには `SCNNode` の `SCNPhysicsBody` の `collisionBitMask` などを設定する必要があります。

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

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

* static  
床や壁など固定する物体に指定します
* dynamic  
デフォルトで下方向に重力がかかるので何かにぶつかるまで下に落下します
* kinematic  
これはどういうときに使うかわからない。。。（[ドキュメント](https://developer.apple.com/documentation/scenekit/scnphysicsbody/1514776-kinematic)）

[SCNPhysicsBody：ドキュメント](https://developer.apple.com/documentation/scenekit/scnphysicsbody)

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

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

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

### categoryBitMask と collisionBitMask
物体が衝突するかどうかはこの２つの値が関係しています。[collisionBitMask のドキュメント](https://developer.apple.com/documentation/scenekit/scnphysicsbody/1514772-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. 

`collisionBitMask` と `categoryBitMask` の AND（論理積）が 0 以外の場合に衝突が起こるということだと思います。 

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

* categoryBitMask  
`SCNPhysicsBody` が `static` の場合は `SCNPhysicsCollisionCategory.static`、それ以外の場合は `SCNPhysicsCollisionCategory.default`
* collisionBitMask  
`SCNPhysicsCollisionCategory.all`

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

```swift
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](https://user-images.githubusercontent.com/34936885/145701037-686aa985-4369-4b69-964a-0be02e779050.gif)

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

```swift
// ぶつかる
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
```

* [categoryBitMask：ドキュメント](https://developer.apple.com/documentation/scenekit/scnphysicsbody/1514768-categorybitmask)
* [collisionBitMask：ドキュメント](https://developer.apple.com/documentation/scenekit/scnphysicsbody/1514772-collisionbitmask)

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

`contactTestBitMask` と `categoryBitMask` の AND（論理積）が 0 以外の場合に通知がとぶようです。

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

scnView.scene?.physicsWorld.contactDelegate = self
```

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

```swift
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!!!")
    }
}
```

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

```swift
// 衝突するし通知も呼ばれる
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：ドキュメント](https://developer.apple.com/documentation/scenekit/scnphysicsbody/1514746-contacttestbitmask)

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

```swift
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](https://user-images.githubusercontent.com/34936885/145701071-91b0842e-c37c-4bdc-96a2-96d14e4eb752.gif)

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

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

![tube_2](https://user-images.githubusercontent.com/34936885/145701076-6fbc0b68-c33e-4162-99e1-101e905c9e7e.gif)

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

## 参考
* [SceneKitにおける物体衝突検知を理解する](https://qiita.com/noby111/items/319c727e9527cfbb0692)
* [SpriteKitの CategoryBitMask、CollisionBitMask、ContactTestBitMask まとめ](https://qiita.com/hadashiA/items/4faf77fddc202070e37c)