在本課一開始提過,粒子系統是為了模擬自然界的一些現象,例如雲霧、火焰,這些現象在似有似無之間,運動軌跡變幻莫測,非常難以計算,甚至比登月軌道還難,如今透過大量算力,總算可以完美模擬出來。

粒子系統的應用場景非常多,但都必須客製化,要熟悉60多個參數並不容易,還好 RealityKit 提供6種內建特效作為模板,幫助我們很快做出想要的視覺特效。

本課最後一節範例,想從 ParticleEmitterComponent() 預設值來客製化粒子系統,利用其中一個關鍵參數 vortexStrength (渦流力場)來做出龍捲風特效,配合前面所學的動作動畫,模擬一個行進中的龍捲風。

實際效果如下,跟預期有點落差,變成是龍捲風+暴風雪的感覺:

https://youtu.be/sBv9ervQO6U

不過實際程式倒是不難,只調整13個參數,其中幾個重要參數都有 “Variation”,能提供隨機性,讓特效看起來比較自然。

完整程式如下:

// 6-12e 客製化粒子系統:龍捲風
// Created by Heman Lu on 2025/06/23
// Minimum Requirement: macOS 15 or iPadOS 18 + Swift Playground 4.6

import SwiftUI 
import RealityKit

struct 粒子系統: View {
    var body: some View {
        RealityView { 內容 in 
            內容.add(座標軸())    // 共享程式6-6b
            
            // (1) 粒子系統
            var 龍捲風 = ParticleEmitterComponent()
            龍捲風.speed = 2.4           // ← 0.5
            龍捲風.speedVariation = 1.0  // ← 0
            龍捲風.emitterShapeSize = [0.05, 0.05, 0.05] // ← 0.1
            
            龍捲風.mainEmitter.birthRate = 50000       // ← 100
            龍捲風.mainEmitter.birthRateVariation = 10000
            龍捲風.mainEmitter.lifeSpan = 3.0          // ← 1
            龍捲風.mainEmitter.lifeSpanVariation = 2.0 // ← 0.2
            龍捲風.mainEmitter.size = 0.02             // ← 0.02
            龍捲風.mainEmitter.sizeVariation = 0.01    // ← 0
            龍捲風.mainEmitter.color = .constant(.random(a: .white, b: .black))
            
            龍捲風.fieldSimulationSpace = .local       // ← .global
            龍捲風.mainEmitter.vortexStrength = -36.0  // 逆時針旋轉
            龍捲風.mainEmitter.noiseStrength = 2.5     // ← 0
            
            // (2) 隱形個體:承載粒子系統
            let 暴風眼 = Entity()
            暴風眼.name = "暴風眼"
            暴風眼.position = [0, -1.0, 0]
            暴風眼.components.set(龍捲風)
            內容.add(暴風眼)
            
            // (3) 隱形個體:調節「暴風眼」的運動路徑
            let 圓心 = Entity()
            圓心.name = "圓心"
            圓心.position = [0, -1, -2]
            內容.add(圓心)
            
            // (4) 令「暴風眼」繞著「圓心」轉動
            let 繞圈轉 = OrbitEntityAction(
                pivotEntity: .entityNamed("圓心"), 
                revolutions: 1.0)
            if let 動畫 = try? AnimationResource.makeActionAnimation(
                for: 繞圈轉,
                duration: 51.0,
                bindTarget: .transform,
                repeatMode: .repeat
            ) {
                暴風眼.playAnimation(動畫)
            }
            
            // (5) 令「圓心」在直線上來回移動
            let 位移 = FromToByAction(by: Transform(translation: [-2, 0, -1]))
            if let 動畫2 = try? AnimationResource.makeActionAnimation(
                for: 位移,
                duration: 19.0,
                bindTarget: .transform,
                repeatMode: .autoReverse
            ) {
                圓心.playAnimation(動畫2)
            }
            
            if let 天空盒 = try? await EnvironmentResource(named: "威尼斯清晨") {
                內容.environment = .skybox(天空盒)
            }
        } 
        .realityViewCameraControls(.orbit)
    }
}

import PlaygroundSupport 
PlaygroundPage.current.setLiveView(粒子系統())

程式用到一個新的物件 Entity(),這其實是 ModelEntity 的父類別(用 class 定義,會有階層關係),是最簡單的個體,只包含 Transform 與 SynchronizationComponent 兩個必要元件。

為什麼用 Entity() 而不用 ModelEntity() 呢?

實際上,所有個體都可以從 Entity() 加上不同元件而來:加上 ModelComponent 就變成模型個體,可顯示在場景中;加上 CollisionComponent 跟 PhysicsBodyComponent 就得到物理模擬效果。因此,用 Entity() 加上元件(參考補充(23) RealityKit 元件列表),我們可以任意組合新的個體。

範例中,我們用 Entity() 加上粒子元件(ParticleEmitterComponent),就變成最簡單的粒子特效,因為沒有 ModelComponent,所以個體是隱形的,只能看到粒子特效;而且因為 Entity() 包含座標變換元件,所以可配合動作動畫。