學過第4單元「SwiftUI 動畫與繪圖」之後,我們大致可以了解什麼是動畫。本質上,動畫就是隨時間變化的動作;更抽象一點來說,動畫是物件的某個或某些「屬性值」(例如位置、顏色、透明度…等)隨時間而變的視覺效果。
「時間」是動畫最關鍵的要素。
3D 空間運算也是如此,例如,物體位置從 x = -2 變成 x = 2,即水平位移4公尺,這叫動作;若加上時間:「在5秒內完成」,這個過程就變成動畫。
在我們平常認知中,「動作」通常指位置或外形有所變化,對應空間運算中的座標變換 — 也就是位移、旋轉、縮放。但本課要學的動作,則是任何一個屬性變化,都可稱之為動作;將動作約束在一段時間內,則稱之為動畫。
例如在2秒內透明度(opacity)從100%降到0%,便產生淡出(Fade-out)的動畫效果;或是0.5秒內顏色從全黑慢慢變成明黃,就產生燈光亮起的感覺。
第4單元第1課(4-1a)曾介紹十幾種時間曲線(Timing Curve),不同的時間曲線,會產生不同的視覺效果。在第7課(4-7b)還提到,時間曲線其實就是一條三階貝茲曲線,因此,時間曲線可以有無限多種。
例如,SwiftUI 預設的時間曲線是緩進緩出(EaseInOut),如下圖,兩個控制點分別位於(0.42, 0)與(0.58, 1),接近垂直中線的上下兩端:

來源 https://www.smashingmagazine.com/2014/04/understanding-css-timing-functions/
上圖是正規化座標,橫(X)軸代表時間比例(0%~100%),縱(Y)軸代表動作完成度(0%~100%),因此,隨著時間變化,曲線上升的幅度(斜率)就代表動作相對的快慢。
預設的時間曲線,啟動時較慢,接下來逐漸加速,在中間(x=50%)速度最快,然後逐漸減速,最後緩慢結束。根據 Apple 官方文件,這樣的時間曲線,視覺上是最自然的。類似這樣的曲線,也俗稱S曲線。
6-10a 自轉(SpinAction)
在 RealityKit 中,要讓個體(Entity)做出動畫效果,需要三個步驟:
(1) 先定義動作(Action)物件
(2) 加入時間,定義動畫物件(AnimationResource)
(3) 呼叫個體的 playAnimation()
RealityKit 預設的動作有以下幾種,大多是去(2024)年才發布,本課會介紹前三種:
1. SpinAction 自轉
2. OrbitEntityAction 公轉
3. FromToByAction 一般化(任何屬性)動作
4. EmphasizeAction
5. BillboardAction
6. ImpulseAction
7. PlayAnimationAction
8. PlayAudioAction
9. SetEntityEnabledAction
定義好動作之後,再加上時間參數,納入動畫物件中,便產出 AnimationResource 物件。具體做法是呼叫類型方法 AnimationResource.makeActionAnimation(),圖解如下:

除此之外,AnimationResource 還有其他類型方法,也可以產出動畫物件,但本課並不會介紹,留給讀者自行探索:

本節先做一個最簡單的自轉動畫,借用上一課的正十二面體,讓它轉動起來。
相較於上一課客製化模型,自轉動畫簡單多了,只需幾行程式:
let 自轉 = SpinAction(revolutions: 1.0) //(1)定義動作物件其實就三句程式碼,對應上面提到的三個步驟。
if let 動畫 = try? AnimationResource.makeActionAnimation( //(2)定義動畫
for: 自轉,
duration: 3.0,
bindTarget: .transform
) {
模型.playAnimation(動畫.repeat()) //(3)執行動畫
}
第一步的 SpinAction 其實有4個參數,除了圈數(revolutions)之外,其他都有預設值,故可省略:
- revolutions (自轉圈數):無預設值
- localAxis(自轉軸):預設為個體區域座標的正Y軸
- timingFunction(時間函數):預設為緩進緩出(EaseInOut)
- isAdditive(是否疊加):預設為否 — 作用不明(註解5)
// https://developer.apple.com/documentation/realitykit/spinaction
SpinAction(
revolutions: Float,
localAxis: SIMD3<Float> = [0, 1, 0],
timingFunction: AnimationTimingFunction = .default,
isAdditive: Bool = false
)
第二步 AnimationResource.makeActionAnimation() 實際上有13個參數,除了第1個參數(動作)之外,其他都有預設值:
makeActionAnimation<T>(
for action: T,
duration: TimeInterval = 1.0,
name: String = "",
bindTarget: BindTarget? = nil,
blendLayer: Int32 = 0,
repeatMode: AnimationRepeatMode = .none,
fillMode: AnimationFillMode = [],
trimStart: TimeInterval? = nil,
trimEnd: TimeInterval? = nil,
trimDuration: TimeInterval? = nil,
offset: TimeInterval = 0,
delay: TimeInterval = 0,
speed: Float = 1.0
) throws -> AnimationResource where T : EntityAction
其中比較特殊,也最重要的是 bindTarget(綁定屬性),是與動作關聯的物件屬性,對旋轉、位移、縮放來說,屬性都是 .transform (座標變換)。動畫過程中,(預設每1/60秒)會根據時間曲線計算綁定屬性的內插值,並加以變更,進而產生動畫效果。
通常我們至少要提供動作(for)、時間長度(duration, 預設1秒)、綁定屬性(bindTarget)等三個參數。範例中設定每3秒鐘自轉一圈,並且在第3步執行動畫playAnimation()時,加上重複 repeat() 指令。
最後得到的效果如下,注意預設的時間函數「緩進緩出」的作用:

完整範例程式如下:
// 6-10a 自轉(SpinAction)
// Created by Heman Lu on 2025/04/24
// Based on 6-9c 作業1 by Heman Lu, 2025/04/07
// Tested with iMac 2019 (macOS 15.4.1) + Swift Playground 4.6.4
import SwiftUI
import RealityKit
struct 顯示正十二面體 : View {
let 圖示陣列: [String] = [
"apple.logo",
"apple.intelligence",
"apple.meditate",
"appletv",
"applewatch",
"wind.snow",
"mountain.2",
"dog",
"cat.fill",
"lizard.fill",
"soccerball",
"theatermasks"
]
func 文字轉圖片(_ 文字: String = "", 圖示: String = "", 寬高: CGFloat = 100) -> CGImage {
if 圖示.count > 0 {
let 視圖 = Image(systemName: 圖示)
.font(.largeTitle)
.shadow(radius: 3)
.blur(radius: 0.2)
.foregroundStyle(.black)
.frame(width: 寬高, height: 寬高)
.background {
Color.white
}
let 圖片 = ImageRenderer(content: 視圖)
return 圖片.cgImage!
} else {
let 視圖 = Text(文字)
.font(.largeTitle)
.padding()
.overlay {
Circle()
.stroke(.black, lineWidth: 3)
}
.foregroundStyle(.black)
.frame(width: 寬高, height: 寬高)
.background {
Color.white
}
let 圖片 = ImageRenderer(content: 視圖)
return 圖片.cgImage!
}
}
var body: some View {
RealityView { 內容 in
內容.add(座標軸(0.9)) // 共享程式6-6b
var 材質陣列: [PhysicallyBasedMaterial] = []
for i in 0..<12 {
var 材質 = PhysicallyBasedMaterial()
材質.baseColor.tint = .cyan
// let 圖片 = 文字轉圖片("\(i)", 寬高: 200)
let 圖片 = 文字轉圖片(圖示: 圖示陣列[i])
if let 紋理 = try? await TextureResource(image: 圖片, options: .init(semantic: .color)) {
print("紋理匯入成功#\(i)")
材質.baseColor.texture = .init(紋理)
}
材質陣列.append(材質)
}
// 共享程式6-9c 正十二面體
if let 模型 = try? await ModelEntity(mesh: .正十二面體(外接球半徑: 0.6)) {
模型.model?.materials = 材質陣列
內容.add(模型)
let 自轉 = SpinAction(revolutions: 1.0)
if let 動畫 = try? AnimationResource.makeActionAnimation(
for: 自轉,
duration: 3.0,
bindTarget: .transform
) {
模型.playAnimation(動畫.repeat())
}
}
if let 天空盒 = try? await EnvironmentResource(named: "威尼斯清晨") {
內容.environment = .skybox(天空盒)
}
}
.realityViewCameraControls(.orbit)
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(顯示正十二面體())
💡註解
- 在第6單元第3課,SceneKit 是以動畫(Animation)組合成動作(Action),兩者作用剛好與 RealityKit 相反。
- 並非任何物件屬性都可做動畫,有一個必要條件,就是屬性須符合 AnimatableData (可動畫資料)規範,具體來說,就是實數(Float, Double)或衍伸的類型,如 simd_float3, CGFloat 等;整數(Int)或字串(String)不能當作動畫屬性。
- 為什麼整數不能當作動畫屬性?因為動畫是屬性的時間函數,在開始與結束之間,必須用內插法得出屬性值,只有實數才能算出內插值。
- 例如,一秒鐘從 x=-2 位移到 x=2 的動畫,必須切割為1/60秒一幅畫面,相當於每1/60秒就需要一個x座標的內插值,如果屬性類型是整數,就無法分割計算。
- SpinAction 的參數 isAdditive 有何作用?老實說,筆者試不出來,不管設定 true 或 false,效果看起來並無差別。原廠說明也是有看沒有懂:
A Boolean value that indicates whether the animation system additively blends the action’s output with the base value.
(簡譯:「動作的輸出是否要與基礎值混合疊加」) - 注意 RealityKit 將動畫時間拆成兩部分,時間函數(timingFunction)放在動作中;而時間長度(duration)則放在動畫資源裡。
- 作業1:請將時間函數改用線性(linear),也就是等速旋轉。
- 作業2:請將動作改為(由上往下看)順時針旋轉0.5圈。
- 作業3:請讓座標軸與正十二面體一起同步旋轉。
- 作業4:實際影響物體旋轉速度的參數,除了時間函數,還有哪些?試試看能做出最快轉速是多少?