Chapter 6: Adding Physics

大綱

想要讓3D物件更加真實化,除了外觀以外,行為也要符合真實世界物理行為。在這章著重講解如何添加行為到骰子上,讓骰子從拋出到落地的行為更加真實。

摘要

Getting started

要讓3D物件擁有真實的物理行為,換成ARKit的說法,就是在SCNode上添加SCNPhysicsBody。

SCNPhysicsBody分成三種 static body, A dynamic body, A kinematic body。

對這三個最清楚的解釋如下: 參考

staticBody是一个不受外力或碰撞影响的物体,不能移动。比如地板、墙壁等。

dynamicBody是一个可以受力和碰撞影响的物体。

kinematicBody是一个不受外力或碰撞影响的物理实体,但它会在移动时造成碰撞影响其他物体。

举一个简单的例子:桌球游戏中,球桌是staticBody,球是dynamicBody,球杆是kinematicBody。

其他物理行為參數

Mass: 质量,以千克为单位。 质量会影响物体对力的反应。dynamicBody的默认质量是1。staticBodykinematicBody的默认质量为0,但它们不受质量影响。 物理模拟的影响取决于不同物体的相对质量,而不是绝对值。所以你不需要对应用程序中的物体进行实际测量。

Friction: 滑动摩擦力。 当两个物体接触并滑动时,摩擦力决定它们对运动的阻力。如果两个物体的摩擦力都是0,它们会自由地滑动。如果都是1,就不会滑动。默认值为0.5。

Restitution: 可以理解成弹性。 决定了物体在碰撞过程中会保留多少动能。比如一个球掉在地面,如果是0,就不会反弹。如果是1,就会弹回原处。如果大于1,它会弹得比原来更高。默认值是0.5。

Rolling friction: 滚动摩擦力。 圆型物体在滚动时的阻力。如果是0,将持续滚动而不减速,除非另有动作。如果是1,将无法滚动。默认值为0。

Damping: 移动阻尼。 类似空气摩擦或水摩擦,就是在移动中的阻力。与其他的力独立并共存。如果是0,表示没有阻力,不会造成速度损失。如果是1,就会阻止物体移动。默认值是0.1。

Angular Damping: 旋转阻尼。 同上,影响物体在旋转中的阻力。默认值是0.1。

Charge: 电荷,以库仑为单位。 当受到电场或磁场影响时,带有正电荷或负电荷的物体的行为会不同。三种类型的物体的默认电荷均为0,即不会受电场和磁场的影响。

Gravity: 否受重力影响

Adding physics

要來替這5顆骰子增加一些合理的物理行為,骰子丟出去至少會受重力影響仼下掉,而不是漂浮在空中。在SceneGraph中選取這5顆骰子,然後在 Physics Inspector中PhysicsBody將type從None改到Dynamic. 在Dynamic可以設定其物理行為參數。接下來 Physics Shape section選擇最近行骰子的shape為boundingBox。

完成到目前的設定,會看到骰子受到重力的影響,很快掉落,可以改變真實世界的速度。這行會讓掉落速度變慢很多。

scene.physicsWorld.speed = 0.05

接下來談談剛剛設定的哪些物理參數,在SceneKit’s是用60fps進行這些物理模擬,當然這個數字越高,物理模擬行為越真實,當然也越操cpu。

scene.physicsWorld.timeStep = 1.0 / 60.0

Plane physics

再來,我們目標就是要骰子可以掉落在一個平面上,然後有一個反彈的效果。

// 建立一個虛擬AR Plane
  func crateARPlanePhysics(geometry: SCNGeometry) -> SCNPhysicsBody {
    // 根據geometry產生合適的shape
    let physicsBody = SCNPhysicsBody(type: .kinematic, shape: SCNPhysicsShape(geometry: geometry, options: nil))
    // 設定這個平面的摩擦力跟彈性
    physicsBody.restitution = 0.5
    physicsBody.friction = 0.5

    return physicsBody
  }

Randomizing rotation

OK, 到目前為止,我們可以透過swipe手勢,骰子就會受重力影響掉落到一個虛擬平面上,但會發現5顆骰子掉到平面後,滾動的方向都一致。這不合乎物理現象,所以現在要添加隨機的滾動方向。

    let rotation = SCNVector3(Double.random(min: 0, max: Double.pi),
        Double.random(min: 0, max: Double.pi),
        Double.random(min: 0, max: Double.pi))
    // 利用eulerAngles來決定滾動行為
    diceNode.eulerAngles = rotation

Force

接下來,就是要添加一丟骰子的感覺,就是骰子會朝向某個方向拋出去的感覺。

// 先決定骰子跟focuse node的距離
    let distance = simd_distance(focusNode.simdPosition,
                                 simd_make_float3(transform.m41,
                                                  transform.m42,
                                                  transform.m43))

// 利用距離來計算丟骰子的方向
    let direction = SCNVector3(-(distance * 2.5) * transform.m31,
        -(distance * 2.5) * (transform.m32 - Float.pi / 4),
        -(distance * 2.5) * transform.m33)

// 更新骰子的physicsBody位置來符合實際的位置
    diceNode.physicsBody?.resetTransform()
// 利用這個direction,讓ARKit來模擬出丟出去力量
    diceNode.physicsBody?.applyForce(direction, asImpulse: true)

Lights and shadows

添加燈光跟陰影,讓整個場景更加的真實。

  • 在 Object Library選擇Floor, 添加到DiceScene中。

  • 在 Object Library選擇Directional Light, 添加到DiceScene中。

  • 在Node Inspector分別調整Floor node和Directional Light node的position。

  • 到Attributes Inspector的Shadow section將Casts shadows勾選起來,並將mode改成Deferred。

    • 這步很重要,這可以讓ARPlane來自動調整這個shadow。

  var lightNode: SCNNode!
  lightNode = diceScene.rootNode.childNode(withName: "directional", recursively: false)!
  sceneView.scene.rootNode.addChildNode(lightNode)

  config.isLightEstimationEnabled = true

Wrapping things up

  • 暫停ARPlane偵測,一但開始玩骰子遊戲後,就不需要一直讓ARKit不斷偵測平面。只需要順利偵測到一個需要的平面就可以了。

  func suspendARPlaneDetection() {
    let config = sceneView.session.configuration as! ARWorldTrackingConfiguration
    config.planeDetection = []
    sceneView.session.run(config)
  }
}
  • 隱藏已顯示的平面。

  func hideARPlaneNode() {
    // 找到目前所有已經定位到的anchor
    for anchor in (self.sceneView.session.currentFrame?.anchors)! {
        // 找到跟這個anchor有關的node
        if let node = self.sceneView.node(for: anchor) {
            for child in node.childNodes {
                // 只要將找到第一個可用的matrial的color buffer清空,即可hide
                let material = child.geometry?.materials.first!
                material?.colorBufferWriteMask = []
            }
        }
    }
  }
  • ARSession重置。

  func resetARSession() {
    let config = sceneView.session.configuration as! ARWorldTrackingConfiguration
    config.planeDetection = .horizontal
    // resetTracking,進行ARKit的重置
    // removeExistingAnchors, 移除之前所有偵測到的anchors
    sceneView.session.run(config, options: [.resetTracking, .removeExistingAnchors])
  }

Hit testing

最後要模擬的就是把丟出去的骰子在撿回來。透過hit test, 讓接觸手機螢幕的位置的射線是否跟骰子有產生交點。

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        DispatchQueue.main.async {
            // 取得user在螢幕的tocuh位置
            if let touchLocation = touches.first?.location(in: self.sceneView) {
                // 根據touch位置進行ray cast, 確認是否有touch到任何3D物件
                if let hit = self.sceneView .hitTest(touchLocation, options: nil).first {
                    // 若有,且touch物件是骰子
                    if hit.node.name == "dice" {
                        hit.node.removeFromParentNode()
                        self.diceCount += 1
                    }
                }
            }
        }
    }

Last updated

Was this helpful?