在前面準備 RealityKit 教材的過程中,筆者藉助於 AI(如ChatGPT或Gemini)的機會並不多,一來因為所用元件多數是去(2024)年才發佈,目前的 AI 版本還來不及學;二來自己得先掌握基礎知識,熟悉官方文件之後,才好對 AI 提問,才能判斷 AI 回答的內容哪些正確、哪些錯誤。
上一節遇到的問題,是個求助 AI 的好機會,一方面我們對 RealityKit 已有基礎,對物理關節也初步熟悉,即使AI回答錯誤,我們也有能力糾正過來。
我跟 ChatGPT 描述問題,並提供程式碼之後,ChatGPT 回答:
我幫你看過你的程式碼,車子「不受控制地翻滾」主要是由幾個物理上的問題造成的:
1. 重心太高
你的車體是 `0.5 × 0.3 × 1.2` 的長方體,厚度(高度)0.3 其實不算小,加上輪胎只有 0.2 半徑,車體重心會懸在比較高的位置 → 一加速就容易翻滾。
2. 輪胎方向與關節軸心不一致
你建立輪胎的時候用了 cylinder,然後用 `.orientation = .init(angle: .pi*0.5, axis: ...)` 去轉,接點(pin)又再加了一個 orientation。這樣容易導致「旋轉軸心」不是完全水平 → 輪子在滾動時會往上或往旁邊推,造成車身翻滾。
3. 施加的扭矩不對稱
你在 `SceneEvents.Update` 裡加了:左前輪.addTorque([0, 80, 0], relativeTo: 左前輪)
右前輪.addTorque([0, -20, 0], relativeTo: 右前輪)
左後輪.addTorque([0, 80, 0], relativeTo: 左後輪)
右後輪.addTorque([0, -20, 0], relativeTo: 右後輪)
→ 左輪 +80,右輪 -20,不平衡,車子自然會失控翻掉。
4. 缺乏摩擦與阻尼設定
輪胎和地板都用了 `generateCollisionShapes`,但 RealityKit 預設的碰撞摩擦係數很小,等於在冰面上開車,很容易打滑 → 一打滑車身就會翻。
不得不說,AI 對程式的理解相當充分,分析問題的邏輯也是有理有據。
根據這幾則判斷,ChatGPT 提供了改善建議,並且附帶程式碼:
✅ 改進建議:
(A) 調整車體與輪胎比例
- 車體高度降低,例如 `height: 0.15`,讓重心更低。
- 輪胎半徑加大到 `0.3 ~ 0.4`,避免「車子比輪胎高」。
(B) 確保 RevoluteJoint 軸心正確
在設定 pin 的 orientation 時,確定「旋轉軸」就是車子左右方向 (X 軸),例如:let 左前輪接點 = 左前輪.pins.set(
named: "左前輪接點",
position: [0, 0, 0],
orientation: simd_quatf(angle: .pi/2, axis: [0, 0, 1])) //保證輪子繞X軸旋轉
你可能不需要同時在 輪胎 entity 和 pin 都轉向,只要固定一個地方就好。
(C) 扭矩施加要對稱
如果只是要讓車子往前跑,四輪都加相同方向的扭矩:左前輪.addTorque([0, 50, 0], relativeTo: 左前輪)
右前輪.addTorque([0, 50, 0], relativeTo: 右前輪)
左後輪.addTorque([0, 50, 0], relativeTo: 左後輪)
右後輪.addTorque([0, 50, 0], relativeTo: 右後輪)
如果想模擬轉向,可以之後再調整前輪的方向,不要一開始就不平衡。
(D) 增加摩擦與阻尼
你可以透過 `PhysicsMaterialResource` 設定摩擦:let 輪胎材質物理 = try! PhysicsMaterialResource.generate(
friction: 1.0,
restitution: 0.0
)
輪胎模型.physicsBody = .init(
PhysicsBodyComponent(
shapes: 輪胎模型.collision!.shapes,
mass: 1.0,
material: 輪胎材質物理,
mode: .dynamic
)
)
地板也同樣設一個高摩擦的材質。
這其中,(B)與(D)是不錯的建議,對問題有改善,可惜還是沒有完全解決問題。因此,我繼續鑽入原廠文件(參考6-11b 物理模擬預設屬性),最後找到一個關鍵屬性:旋轉慣量(inertia),用法如下:
車體模型.physicsBody?.massProperties = .init(mass: 300, inertia: [300, 300, 300])這行設定車身質量300Kg,轉動慣量在X/Y/Z軸均為300(單位為 Kg⋅m²)。
轉動慣量(inertia)的物理定義為:
對於一個繞著定軸轉動的質點,其轉動慣量I 的公式為 "I = mr²",其中 "m" 是質點的質量,而 "r" 是質點到旋轉軸的距離。
所以轉動慣量300,相當於300公斤的物體距離轉動軸心1公尺。
這個物理量代表物體旋轉的難易程度,轉動慣量愈大,物體越不容易旋轉。上節程式的問題是車身被四個輪子帶動旋轉,如果我們希望車身保持穩定,那就把轉動慣量設大一點,車身就不會亂轉了。
那轉動慣量與質量的用途有何差別呢?質量與牛頓第二運動定律(f = ma)有關,應用於直線運動,會影響「個體.addForce()」施力所產生的加速度;而轉動慣量適用於物體旋轉,對應「個體.addTorque()」,影響力矩所產生的角速度變化。
之後再經過不斷調整,最後終於將四輪車穩定下來,可以開出去了,前後花兩星期,真是不容易!
執行結果如下:
一開始先暫停地心引力的影響,觀察四輪轉動的情況。之後再恢復引力,讓車子落地,經由四輪帶動往前走。
附上完整的原始程式:
// 補充(29) 四輪車
// Created by Heman Lu on 2025/08/25
// Minimum Requirement: macOS 15 or iPadOS 18 + Swift Playground 4.6
import SwiftUI
import RealityKit
struct 物理關節: View {
var body: some View {
RealityView { 內容 in
內容.add(座標軸())
var 金屬材質 = PhysicallyBasedMaterial()
金屬材質.roughness = 0.1
金屬材質.metallic = 0.9
// 小球:用於覆蓋關節部位
let 球體 = MeshResource.generateSphere(radius: 0.05)
let 小球 = ModelEntity(mesh: 球體, materials: [金屬材質])
var 車體材質 = PhysicallyBasedMaterial()
車體材質.metallic = 0.2
車體材質.roughness = 0.8
車體材質.baseColor.tint = .orange
// 車體
let 車體 = MeshResource.generateBox(
width: 0.5,
height: 0.3,
depth: 1.2,
cornerRadius: 0.1)
let 車體模型 = ModelEntity(mesh: 車體, materials: [車體材質])
車體模型.physicsBody = .init(mode: .dynamic)
車體模型.generateCollisionShapes(recursive: false)
車體模型.physicsBody?.massProperties = .init(mass: 300, inertia: [300, 300, 300])
車體模型.physicsBody?.angularDamping = 300.0
車體模型.physicsBody?.material = .generate(restitution: 0.01)
// 車體模型.physicsBody?.isAffectedByGravity = false
車體模型.components.set(GroundingShadowComponent(castsShadow: true))
內容.add(車體模型)
// 裝飾小球(覆蓋連接點)
let 左前輪軸 = 小球.clone(recursive: false)
左前輪軸.position = [0.25, 0, 0.4]
車體模型.addChild(左前輪軸)
let 右前輪軸 = 小球.clone(recursive: false)
右前輪軸.position = [-0.25, 0, 0.4]
車體模型.addChild(右前輪軸)
let 左後輪軸 = 小球.clone(recursive: false)
左後輪軸.position = [0.25, 0, -0.4]
車體模型.addChild(左後輪軸)
let 右後輪軸 = 小球.clone(recursive: false)
右後輪軸.position = [-0.25, 0, -0.4]
車體模型.addChild(右後輪軸)
let 車頭標誌 = 小球.clone(recursive: false)
車頭標誌.position = [0, 0, 0.6]
車體模型.addChild(車頭標誌)
// 車體連接點x4
let 車體左前接點 = 車體模型.pins.set(
named: "車體左前",
position: [0.35, 0, 0.4])
let 車體右前接點 = 車體模型.pins.set(
named: "車體右前",
position: [-0.35, 0, 0.4])
let 車體左後接點 = 車體模型.pins.set(
named: "車體左後",
position: [0.35, 0, -0.4])
let 車體右後接點 = 車體模型.pins.set(
named: "車體右後",
position: [-0.35, 0, -0.4])
// 輪胎
let 邊數: Int = 18
var 輪胎材質 = PhysicallyBasedMaterial()
輪胎材質.metallic = 0.7
輪胎材質.roughness = 0.3
輪胎材質.baseColor.tint = .gray
let 材質陣列: [PhysicallyBasedMaterial] = .init(
repeating: 輪胎材質,
count: 邊數 * 2 + 2)
// 共享程式 6-9d 多角柱體
if let 外型 = try? await MeshResource.多角柱體(邊數,
底半徑: 0.2,
頂半徑: 0.2,
高: 0.1) {
// let 外型2 = MeshResource.generateCylinder(height: 0.1, radius: 0.2)
// 共享程式 6-9d 挑戰題:多角星體
// if let 外型 = try? await MeshResource.多角星體(邊數,
// 底半徑: 0.2,
// 頂半徑: 0.2,
// 次頂點比例: 0.9,
// 高: 0.1) {
// 輪胎模型母版
let 輪胎模型 = ModelEntity(mesh: 外型, materials: 材質陣列)
輪胎模型.physicsBody = .init(mode: .dynamic)
輪胎模型.generateCollisionShapes(recursive: false)
輪胎模型.physicsBody?.isAffectedByGravity = false
輪胎模型.physicsBody?.angularDamping = 3.0
輪胎模型.physicsBody?.massProperties = .init(mass: 1.0, inertia: [1, 1, 1])
輪胎模型.physicsBody?.material = .generate(friction: 0.9, restitution: 0.01)
let 輪圈 = 小球.clone(recursive: false)
輪圈.position.y = 0.05
輪胎模型.addChild(輪圈)
// 輪胎(1)左前輪
let 左前輪 = 輪胎模型.clone(recursive: true)
內容.add(左前輪)
let 左前輪接點 = 左前輪.pins.set(
named: "左前輪接點",
position: [0, 0.0, 0],
orientation: .init(angle: .pi * 0.5, axis: [0, 0, 1]))
let 左前關節 = PhysicsRevoluteJoint(
pin0: 車體左前接點,
pin1: 左前輪接點)
try? 左前關節.addToSimulation()
// 輪胎(2)右前輪
let 右前輪 = 輪胎模型.clone(recursive: true)
內容.add(右前輪)
let 右前輪接點 = 右前輪.pins.set(
named: "右前輪接點",
position: [0, 0.0, 0],
orientation: .init(angle: .pi * 0.5, axis: [0, 0, -1]))
let 右前關節 = PhysicsRevoluteJoint(
pin0: 車體右前接點,
pin1: 右前輪接點)
try? 右前關節.addToSimulation()
// 輪胎(3)左後輪
let 左後輪 = 輪胎模型.clone(recursive: true)
內容.add(左後輪)
let 左後輪接點 = 左後輪.pins.set(
named: "左後輪接點",
position: [0, 0.0, 0],
orientation: .init(angle: .pi * 0.5, axis: [0, 0, 1]))
let 左後關節 = PhysicsRevoluteJoint(
pin0: 車體左後接點,
pin1: 左後輪接點)
try? 左後關節.addToSimulation()
// 輪胎(4)右後輪
let 右後輪 = 輪胎模型.clone(recursive: true)
內容.add(右後輪)
let 右後輪接點 = 右後輪.pins.set(
named: "右後輪接點",
position: [0, 0.0, 0],
orientation: .init(angle: .pi * 0.5, axis: [0, 0, -1]))
let 右後關節 = PhysicsRevoluteJoint(
pin0: 車體右後接點,
pin1: 右後輪接點)
try? 右後關節.addToSimulation()
// 加入事件處理:SceneEvents.Update
let 強度: Float = 150
let _ = 內容.subscribe(to: SceneEvents.Update.self) { 事件 in
左前輪.addTorque([0, 強度, 0], relativeTo: 左前輪)
右前輪.addTorque([0, -強度, 0], relativeTo: 右前輪)
左後輪.addTorque([0, 強度, 0], relativeTo: 左後輪)
右後輪.addTorque([0, -強度, 0], relativeTo: 右後輪)
}
}
// 地板設定為靜態(.static)
let 地板 = MeshResource.generatePlane(width: 50.0, depth: 50.0)
let 地板材質 = SimpleMaterial(color: .gray, isMetallic: false)
let 地板模型 = ModelEntity(mesh: 地板, materials: [地板材質])
地板模型.position.y = -0.35
地板模型.physicsBody = .init(mode: .static)
地板模型.generateCollisionShapes(recursive: false, static: true)
地板模型.physicsBody?.material = .generate(friction: 0.8)
內容.add(地板模型)
if let 天空盒 = try? await EnvironmentResource(named: "威尼斯清晨") {
內容.environment = .skybox(天空盒)
}
}
.realityViewCameraControls(.orbit)
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(物理關節())