上一節(6-4a)提到SceneKit物理模擬主要模擬力學以及碰撞,力學有各種物理定律可遵循,而碰撞則靠「碰撞偵測」演算法,並沒有想像中簡單。想想看,兩個3D物件如何判斷發生碰撞呢?若是球體最簡單,只要判斷球心距離是否小於或等於半徑和即可;但若不是球體,如長方體、角錐體甚至不規則形狀呢?
基於目前的演算法,SceneKit碰撞偵測較擅長平面或凸形輪廓的剛性物體,也就是說,對於有凹洞(像被咬一口的蘋果)或變形的物體(如麻花),比較難準確偵測到碰撞。所謂剛性物體是指碰撞之後不會發生形變,維持固定的形狀輪廓。
單純用 SceneKit程式也較難做出內凹模型,例如缺一斜角的長方體、梯形體、彎月體等,這是因為SceneKit並不支援幾何結構的「減法」。這類物體必須借助第三方3D繪圖軟體(如Tinkercad, Blender)來製作。
至於在力學方面的模擬則還算完整,包括牛頓力學、電磁學、流體力學、重力、扭力等,都可透過各種物理屬性加以調整,目前 physicsBody 的物理屬性及預設值如下:
| # | physicsBody.屬性名稱 | 預設值 | 說明 |
|---|---|---|---|
| 1 | isAffectedByGravity | true | 是否受重力影響(僅適用於動態本體) |
| 2 | mass | 1.0 | 質量,預設為 1Kg |
| 3 | centerOfMassOffset | (0, 0, 0) | 重心偏移(相對於內部座標原點) |
| 4 | charge | 0.0 | 帶電量(單位庫倫C),若帶電則受電磁力影響 |
| 5 | friction | 0.5 | (滑動)摩擦力(0~1.0) |
| 6 | rollingFriction | 0.0 | 滾動摩擦力(0~1.0) |
| 7 | restitution | 0.5 | 回彈係數,0 (完全不反彈) ~ 1.0 (完全不損失動能) |
| 8 | damping | 0.1 | (流體或氣體的)阻尼系数,0(無阻尼,保持原速) ~ 1.0 (阻尼大到無法動彈) |
| 9 | angularDamping | 0.1 | (流體或氣體的)旋轉阻尼0~1.0 |
| 10 | momentOfInertia | (0, 0, 0) | 轉動慣量,數值越大越不容易轉動(受扭力影響) |
| 11 | velocity | (0, 0, 0) | 目前在空間中的前進速度(SCNVector3),單位「米/秒」(m/sec) |
| 12 | angularVelocity | (0, 0, 0, 0) | 目前的角速度(SCNVector4),單位「弧度/秒」(rad/sec) |
| 13 | velocityFactor | (1, 1, 1) | 速度倍數 |
| 14 | angularVelocityFactor | (1, 1, 1) | 角速度倍數 |
| 15 | isResting | false | 目前是否靜止 |
| 16 | allowsResting | true | 是否允許靜止狀態 |
| 17 | linearRestingThreshold | 0.1 | 視為靜止的(線性)速率門檻 |
| 18 | angularRestingThreshold | 0.1 | 視為靜止的角速率門檻 |
| 19 | applyForce() | 帶入參數 | 對某個方向施力(單位:牛頓) |
| 20 | applyTorque() | 帶入參數 | 依某個軸心施以扭力(讓物體旋轉) |
| 21 | clearAllForces() | 無參數 | 清除所有外力 |
例如,對於上一節的範例程式,我們可以調整小球的物理特性,增加反彈(restitution)係數,並施以外力(applyForce):
// 小球節點與physicsBody都是Optional類型,所以變數名稱後面會加上問號?
.onTapGesture {
let 小球節點 = 物理教室.rootNode.childNode(withName: "小球", recursively: true)
小球節點?.physicsBody = .dynamic()
小球節點?.physicsBody?.restitution = 0.9
小球節點?.physicsBody?.applyForce(SCNVector3(0.3, 4, -0.3), asImpulse: true)
}
反彈係數從預設的0.5增加到0.9,這樣在落下時,反彈效果較明顯;另外加上(0.3, 4, -0.3)的外力,此值表示力的向量,往東北方(x=0.3, z=-0.3)向上(y=4)施力,單位是牛頓。根據公式 f = ma:
👉 1 newton = 1Kg x 1m/sec²
往垂直上方施以4牛頓的力,可以產生 4m/sec² 往上的加速度(小球預設質量恰為1Kg)。第二個參數 "asImpulse: true” 表示以衝量(Impulse)施力1秒鐘,得到垂直向上4m/sec的初始速度。加了這兩行程式,就可看到小球沿拋物線運動的結果:

修改後的完整程式如下:
// 補充(5) 物理屬性 physicsBody
// Revised by Heman, 2024/04/12
import SceneKit
import SwiftUI
struct 物理模擬: View {
let 物理教室 = SCNScene()
func 空間座標系(尺寸半徑: CGFloat) -> SCNNode {
let x軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0)
let x軸節點 = SCNNode(geometry: x軸)
x軸節點.rotation = SCNVector4(x: 0, y: 0, z: 1, w: .pi / -2.0)
let y軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0)
let y軸節點 = SCNNode(geometry: y軸)
let z軸 = SCNTube(innerRadius: 0, outerRadius: 0.01, height: 尺寸半徑 * 2.0)
let z軸節點 = SCNNode(geometry: z軸)
z軸節點.rotation = SCNVector4(x: 1, y: 0, z: 0, w: .pi / 2.0)
let 座標原點 = SCNNode(geometry: SCNSphere(radius: 0.02))
座標原點.addChildNode(x軸節點)
座標原點.addChildNode(y軸節點)
座標原點.addChildNode(z軸節點)
return 座標原點
}
var body: some View {
SceneView(scene: 物理教室, options: [.allowsCameraControl, .autoenablesDefaultLighting])
.onTapGesture {
let 小球節點 = 物理教室.rootNode.childNode(withName: "小球", recursively: true)
小球節點?.physicsBody = .dynamic()
小球節點?.physicsBody?.restitution = 0.9
小球節點?.physicsBody?.applyForce(SCNVector3(0.3, 4, -0.3), asImpulse: true)
}
.onAppear {
let 小球 = SCNSphere(radius: 0.05)
let 小球節點 = SCNNode(geometry: 小球)
小球節點.name = "小球"
小球節點.position = SCNVector3(0, 0.5, 0)
// 小球節點.physicsBody = .dynamic()
let 金字塔 = SCNPyramid(width: 1.6, height: 0.2, length: 1.0)
let 金字塔節點 = SCNNode(geometry: 金字塔)
金字塔節點.position.y = -0.8
金字塔節點.physicsBody = .kinematic()
let 轉動 = SCNAction.rotateBy(x: 0, y: .pi * 2.0, z: 0, duration: 10)
金字塔節點.runAction(.repeatForever(轉動))
let 地面節點 = SCNNode(geometry: SCNFloor())
地面節點.position.y = -1.0
地面節點.physicsBody = .static()
物理教室.rootNode.addChildNode(空間座標系(尺寸半徑: 1.0))
物理教室.rootNode.addChildNode(小球節點)
物理教室.rootNode.addChildNode(金字塔節點)
物理教室.rootNode.addChildNode(地面節點)
物理教室.background.contents = UIColor.green
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(物理模擬())
💡 註解
- 用 applyForce() 或 applyTorque() 對物理本體施力,僅對動態(dynamic)本體有效。
- 【作業】仔細觀察小球與金字塔碰撞反彈的位置是否精確。
- 【作業】更改applyForce第二個參數為”asImpulse: false”,看結果如何?(提示:僅作用於單幀,也就是1/60秒,故作用力需放大60倍才能得到同樣效果,或用於 SCNSceneRendererDelegate)





















































































































