• 5

Swift[第6單元(下)] RealityKit 空間運算

經過半年多等候,新版 Swift Playgrounds 4.6 終於發布,接下來就可繼續進行第6單元(下)課程,由於下半單元用新的 RealityView + SwiftUI 撰寫,與上半單元 SceneKit 完全不同,因此決定開新頁面來寫,不再接續原來的頁面

下載 Swift Playgrounds 4.6 之前,記得先升級到 macOS 15.x (Sequoia) 或 iPadOS 18.x。
https://www.apple.com/tw/swift/playgrounds/
Swift[第6單元(下)] RealityKit 空間運算


Swift Playgrounds 4.6 新版的起始畫面與過去完全不同,改用檔案管理員(Finder)的瀏覽介面,一開始可能要適應一下。Swift[第6單元(下)] RealityKit 空間運算
我們課程仍是開啟電子書模式 playgroundbook 的格式,而不用 App 格式,兩者的圖示稍有不同,playgroundbook 圖示中間是個空心框,如上圖右側。

新增電子書 playgroundbook 的方式,macOS 版必須從選單「檔案」→「新增書籍」。若要更改名稱,必須先「顯示於Finder」,然後再改檔名即可。
Swift[第6單元(下)] RealityKit 空間運算


經過簡單測試後,在 iMac 2019 (macOS 15.3) + Swift Playgrounds 4.6 的環境下,RealityView 完全可以正常使用,這樣就可以開始寫下半單元程式了,預計下週一開工。
Swift[第6單元(下)] RealityKit 空間運算
對空間運算剛入門,或是對 SceneKit 有興趣者,可參考上半單元:第6單元(上) SceneKit 空間運算

[2025/02/12補充]
附帶一提,Swift Playgrounds 名稱從 4.6 版之後改為 Swift Playground(但中文版App Store似乎尚未更改),遊樂場從複數改為單數,強調這個App的定位,就是寫程式的一個遊樂場,而非產出很多 .playground 程式的工具。參考資料:9to5Mac

[2025/02/20補充]
本單元同步更新在筆者的Notion網站,章節目錄比較有結構,程式碼也更容易閱讀與複製。
2025-01-31 13:41 發佈
以前為了要學swift
還把我的asus改成蘋果系統的作業系統
黑頻果喔?
現在好像快沒辦法改了說

brabus1518 wrote:
把我的asus改成蘋果系統的作業系統...(恕刪)
RealityKit 的空間運算真的很有趣!如果是新手,建議先多練習如何擺放虛擬物件,然後試試Anchor的運用。實際開發中,空間運算需要考慮環境光線和用戶交互,這些細節會大大提升體驗。
前言:RealityKit v.s. SceneKit

第1課曾介紹過 SceneKit, ARKit, RealityKit 之間的關係,其中 SceneKit 與 RealityKit 功能類似,都是用來建構3D虛擬場景,再與 ARKit 輸入的實體世界結合,達成虛實整合的擴增實境(AR)效果。

也就是說,RealityKit 可以完全取代 SceneKit 的功能,若單獨使用,可建構3D虛擬場景,做出逼真有趣的3D遊戲及各種應用,在Apple平台(macOS, iOS, iPadOS, visionOS)中執行。

若以RealityKit 加 ARKit,就能製作擴增實境(AR)應用,虛實整合。不過一旦用了ARKit,就只能在 iPadOS + Swift Playgrounds 環境中開發,無法在 macOS 上運作(原因在補充(11)提過,AR程式要搭配陀螺儀等硬體傳感器才能執行)。

下圖再次回顧 SceneKit、ARKit、RealityKit 發展歷程,值得注意的是,RealityKit 自2019年發表之後逐年更新,特別是2023年配合新產品 Vision Pro,新增 RealityView,以 “RealityView + SwiftUI + ARKit” 最佳組合來開發 visionOS 的AR應用,2024年再將 RealityView 擴展到 iOS/iPadOS/macOS。

而SceneKit自 2019年之後,除了加入SceneView(搭配SwiftUI)以及AI辨識臉型與人體動作之外(參考補充14),幾乎沒有再更新,未來3D主流明顯朝 RealityKit 發展。

至於 RealityKit 與 SceneKit 有何差別呢?主要有以下幾點:
  1. RealityKit 設計之初,就以擴增實境為優先考量,因此與 ARKit 的結合更緊密;SceneKit 設計初衷完全為了3D應用。
  2. RealityKit 採用ECS (Entity-Component-System)架構,以滿足更多樣的AR、AI、Game等需求。同樣採用 ECS 架構的,還有兩大遊戲引擎 — Unity 與 Unreal Engine (UE)。
  3. RealityKit 較 SceneKit 晚7年,有後來者優勢 — 也就是會採用更新的技術,例如預設的光照模型是更接近真實的物理材質(PhysicallyBaseMaterial)、預設的3D檔案格式為USD、非同步模式採用 async/await…等等。
  4. RealityKit 物件的命名規則與 SwiftUI 類似,不加任何縮寫字首。

這些RealityKit 與 SceneKit 的差異歸納如下表:
# 比較項目 SceneKit RealityKit
1 發表年份 2012年 2019年
2 產品定位 建構3D動畫/遊戲 建構AR應用/3D遊戲
3 主要架構 節點(樹狀結構) ECS (Entity-Component-System)
4 預設光照模型 Blinn-Phong PBR (Physically Based Rendering)
5 主要檔案格式 專屬格式(.scn) 開放格式(.usdz)
6 非同步模式 GCD (Grand Central Dispatch) async-await
7 命名規則 以SCN開頭,例如SCNNode 無加綴字首,例如Entity、Material
至於為什麼本單元執意用 RealityView 而非 ARVIew 呢?兩者都屬於 RealityKit,但 ARView 主要配合 UIKit,雖可透過 UIViewRepresentable 轉換到 SwiftUI,但在手勢觸控、語音等與使用者互動的控制方式,還是得用 UIKit 的方法,非常不方便;而 RealityView 與 SwiftUI 整合得非常好,完全符合本系列教材的需求。

下半單元大綱

接下來的課程內容,預計前面幾課先學習如何用 RealityKit 來建構3D虛擬場景,主要用 RealityView + SwiftUI,可在 macOS 15 或 iPadOS 18 配合新版 Swift Playgrounds 4.6 執行。

這部分主題大致包括:

- RealityKit ECS 架構
- 模型建構、載入外部模型檔(.usdz)
- 材質、紋理、貼圖、光照
- 動作與動畫、座標變換
- 物理模擬、碰撞偵測、虛擬力場、粒子系統

預計最後兩課用 RealityView + SwiftUI + ARKit 來開發AR程式,則只能在 iPadOS 18的 Swift Playgrounds 上執行。

這部分主題可能有:

- 錨定個體(AnchorEntity)、實體座標與虛擬座標的融合
- 錨定實體特徵點、世界追蹤(WorldTracking)
- 人臉追蹤、肢體動作追蹤
- 動作捕捉(Motion Capture)、骨骼動畫
- AR語音控制

原本構想是上、下單元各用5課的篇幅(平均每課3節)來寫,但根據上半單元經驗看來,應該會超過,索性放開來寫,能寫多少算多少,不過,課程與範例程式編號仍延續上半單元,從第6課(範例6-6a)起算。

💡 註解
  1. WWDC 2019: “RealityKit is an AR first framework”. (RealityKit 是以AR優先的框架)
  2. 若想開發「跨平台」的AR應用或3D遊戲,大多會從兩大遊戲引擎中選擇其一,即 Unity 或 Unreal Engine (UE),兩者對初學者及學生都提供免費版,但若沒有人帶,很難自學。
  3. Unity 自2005年發表以來,一直與 Apple 有合作關係,其主要程式語言是微軟的C# (唸作C-Sharp),目前支援Mac, Windows 以及20多種遊戲平台。
  4. Unreal Engine (UE)則主要用C++開發,去(2024)年中國大陸爆紅的3D遊戲《黑神話:悟空》,就採用 Unreal Engine 5 (UE5),並大量使用動作捕捉(MoCap, Motion Capture)技術,動作非常流暢逼真,但成本相當高(約6年時間、人民幣3億元)。
第6課 RealityKit ECS 簡介

什麼是ECS (Entity-Component-System)

在開始寫 RealityKit 程式之前,必須先了解RealityKit 的核心架構:ECS (Entity-Component-System),也就是「個體-元件-系統」三者之間的關係,以及為什麼需要 ECS。

上節提過,SceneKit 與 RealityKit 都是用來建構3D虛擬場景,但兩者方法有明顯區別。

第1課 SceneKit 建構場景的概念很簡單,就是用「樹狀結構」將每個節點連結在一起,節點(SCNNode)可以是3D物件、燈光或鏡頭,所以場景的主要內容就是由一個個節點組成,如下圖左。

RealityKit 的觀念很類似,場景也是由樹狀結構組成,但每個節點稱為”Entity”(個體),而最上層的根節點則由 RealityView 的「內容」取代,如上圖右。

RealityKit 個體(Entity)的樹狀結構同樣有階層關係,父個體的座標變換會影響到子個體,也就是說,子個體會跟隨父節點一起位移、旋轉、縮放、鏡像,如同綁在一起;父個體如果隱藏(hidden),子個體也會跟著不見。

那麼,個體(Entity)和節點(SCNNode)除了名稱不一樣,還有何不同呢?答案是兩者內涵大不相同。

SceneKit 有個明顯缺點,就是節點(SCNNode)屬性非常多(參考6-1c節點屬性圖),因為要將各種功能(光照、材質貼圖、動畫、物理模擬、粒子系統…)相關屬性加進來,每種功能需要若干屬性,因此SCNNode所有屬性與方法將近100個!但實際運用時,個別節點通常只用到一小部分屬性,卻讓整個物件顯得相當臃腫。

而 RealityKit 要面對更多樣的應用,需要的屬性與方法只會更多,如何避免物件過於臃腫呢?RealityKit ECS 用了一個巧妙的方法 — 將不同用途的屬性與方法加以分組,每組稱為一個元件(Component),元件視需要可以動態載入,這樣一來,Entity (個體)可依照需要加掛不同元件,個別物件就輕盈許多。

也就是說,RealityKit 的個體(Entity)是由不同元件(Component)組成,每個元件有各自的屬性與方法,有需要時再加進來。

至於系統(System),則是當一群個體(Entity)需要做同樣動作或彼此協調時,所形成的臨時群體,類似6-4c模擬車輛

ECS概念圖如下:
上圖右側標註*者,是任何個體必備的兩個元件,即 Transform 座標變換,可控制個體的位移、旋轉、縮放、鏡像;以及 SynchronizationComponent 同步元件,負責小範圍內多人遊戲的網路同步(透過”MultipeerConnectivity“框架,距離不可太遠)。

目前 RealityKit 預設有9種個體,這些個體各自包含一些元件組合,也就是說,已經預先載入某種功用,其中最常用的是模型個體(ModelEntity),下一節會開始用到。

1. AnchorEntity 錨點個體
2. ModelEntity 模型個體
3. SpotLight 燈光個體1(聚光燈)
4. PointLight 燈光個體2(點光源)
5. DirectionalLight 燈光個體3(平行光)
6. PerspectiveCamera 鏡頭個體(點透視)
7. BodyTrackingEntity 人體追蹤
8. TriggerVolume 觸發空間(無形)
9. ViewAttachmentEntity 視圖附件(visionOS only)

另外,還有三種虛擬燈光,可產生不同的渲染效果;錨點個體(AnchorEntity)用來錨定虛擬物體在實體空間中的位置,人體追蹤個體(BodyTrackingEntity)可用於動作捕捉,要等到最後兩課才會遇到。

至於可選用的元件(Component),目前原廠超過50種,足以組成各式各樣的虛擬個體。此外,還能讓程式設計師自行定義新元件,因此可根據需要,無限擴充個體的屬性與方法,同時保持物件的輕巧,這就是 ECS 架構的主要優勢。

💡註解
  1. RealityView 的「內容」類似 SceneKit 的場景,但沒有單獨一個根節點,而是以錨點(AnchorEntity)當作個別場景的根節點,「內容」可包含多個錨點,相當於多個場景。
  2. ECS (Entity-Component-System)是一種程式設計模式(Design Pattern),就像 MVC (Model-View-Controller)。MVC 是在1970年代為了解決圖形操作介面(GUI)互動問題而提出的解法,至今有50年歷史;ECS 則是針對電腦遊戲軟體所遇到的困難,應運而生,有20多年歷史,採用 ECS 最好例子應該是 Unity 遊戲引擎。
  3. 參考Unity官方文件,個體(Entity)代表虛擬場景中要處理的對象(identity),元件(Component)代表要處理的資料(data),系統(System)則是一個或一群個體的行為(behavior)。
    An Entity Component System (ECS) architecture separates identity (entities), data (components), and behaviour (systems).
  4. 若想進一步了解 RealityKit ECS 用法,可參考 “WWDC 2021: Dive into RealityKit 2”。
  5. Entity 通常翻譯為「實體」,例如 Legal Entity 譯為「法律實體」或「法人」,指在法律上與自然人享同等權利的個體,如一家公司或財團法人。但實際上,Entity 是虛擬、抽象的,在本課程一律譯為「個體」。
6-6a 初次使用RealityView

上一節了解 ECS 背景知識後,要動手寫RealityKit 程式就簡單多了。

RealityKit 有兩種視圖可選,過去通常用 ARView + UIKit,不過本單元一律採用2024年發表的 RealityView (for iOS/iPadOS/macOS),對熟悉 SwiftUI 的人來說,RealityView 更容易上手,而且若考慮未來開發 Vision Pro 軟體,RealityView 也是不二選擇。

範例程式6-6a

先來看 RealityView + SwiftUI 第一個範例程式,只有7行新內容:
// 6-6a RealityView01
// Created by Heman Lu on 2025/01/31.
// Test Environment: iMac 2019 (macOS 15.3) + Swift Playground 4.6
//
import SwiftUI
import RealityKit

struct 空間運算01 : View {
var body: some View {
RealityView { 內容 in
let 球體 = MeshResource.generateSphere(radius: 0.5)
let 材質 = SimpleMaterial(color: .yellow, isMetallic: true)
let 模型 = ModelEntity(mesh: 球體, materials: [材質])
模型.position.z = -1.0
內容.add(模型)
}.realityViewCameraControls(.orbit)
}
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(空間運算01())

RealityView 本身是個 SwiftUI 視圖,其特殊之處是尾隨的匿名函式(Closure) { } 會帶入一個雙向參數「內容」,一開始是空的內容,之後用「內容.add()」加入3D虛擬物體,便可顯示到螢幕。

要如何用 RealityKit 建構3D模型呢?需要用到三個基本物件:

- MeshResource (網格資源)
- SimpleMaterial (簡單材質)
- ModelEntity (模型個體)

上面範例程式中,先準備好網格資源 — 由MeshResource.generateSphere() 產出一個球體,以及材質 — 用SimpleMaterial()產出黃色金屬材質,合起來構成模型個體ModelEntity,再加到 RealityView 「內容」中,即可顯示出來。物件關係圖如下:


最關鍵的一行程式就是模型個體 ModelEntity() 的產出:
// 6-6a
let 模型 = ModelEntity(mesh: 球體, materials: [材質])

ModelEntity() 需要兩個參數,一是網格資源(mesh),決定虛擬個體的造型,相當於 SCNNode 的幾何外形(geometry) ,如第1課 SceneKit顯示3D模型(6-1a)
// 6-1a
let 金字塔節點 = SCNNode(geometry: 金字塔)

第二個參數材質(materials)則影響模型的外觀、顏色、紋理,以及對光的反應(也就是光照模型)。注意參數 materials 是複數,必須用陣列導入,因為有些模型(包含多個組件)可以用多種材質。

還記得6-1d 光照模型 lightingModel提到,SceneKit 預設的光照模型主要考量環境光(ambient)、漫射(diffuse)、高光(或稱鏡面反射,specular)以及自發光(emissive)等4個因素。

相對的,RealityKit 預設是物理光照模型(Physically Based Lighting, 或稱 Physically Based Rendering — PBR),除了上述4個因素之外,還加入金屬性(metallic)與粗糙度(roughness),對環境的反射更加細膩,如下圖。
注意上圖金屬圓球的反光,可看出周圍似乎是個房間,而非只有一盞燈,這種光照技術稱為 “Image-Based Lighting (IBL)”,用來模擬 AR 的實體環境,感覺相當真實。後面我們會介紹如何更改環境圖像。

模型個體的元件

模型個體(ModelEntity)共有6個元件(如下圖左側),除了必要的同步元件與座標變換元件之外,還加入模型元件、碰撞偵測元件、物理本體元件、物理運動元件:


「元件」其實是若干物件屬性與方法的組合,「模型元件」包含兩個基本屬性 — .mesh 是外形,也就是模型的幾何外框(由三角形或四邊形網格構成),也稱為線框(Wireframe);.materials 是材質陣列,有多種預設材質可選。

RealityKit 預設的幾何模型只有6種(如上圖右側),包括3D文字、立方體、球體、平面、圓錐體、圓柱體等,均使用 MeshResource 的類型方法(type method)產出:

1. MeshResource.generateText() 3D文字
2. MeshResource.generateBox() 立方體
3. MeshResource.generateSphere() 球體
4. MeshResource.generatePlane() 平面
5. MeshResource.generateCone() 圓錐體
6. MeshResource.generateCylinder() 圓柱體

RealityKit 預設材質目前有8種,除此之外,材質也可以用貼圖、影片動畫,或另外用3D軟體(如Blender)製作好,包在 .usdz 檔案中導入。

1. SimpleMaterail 物理材質(簡單參數)
2. PhysicallyBasedMaterial 物理材質(完整參數)
3. UnlitMaterial 不反光材質(不套用任何光照模型)
4. VideoMaterial 動畫材質(用影片貼圖)
5. PortalMaterial 門戶材質(其他虛擬場景或實體場景)
6. OcclusionMaterial 遮擋材質(用來遮擋後面的物件)
7. ShaderGraphMaterial 著色器材質(要用Reality Composer Pro製作)
8. CustomMaterial 客製化材質

模型個體另外三個元件(碰撞偵測、物理本體、物理運動)都與物理模擬有關,後面課程會用到。

在範例程式產出模型之後,我們將模型位置往後挪(離開視線)1.0米:
模型.position.z = -1.0

剛產出的個體預設位置在虛擬空間座標原點,對應螢幕的中心點。這個 position 屬性是在 Transform(座標變換元件)裡面,是所有虛擬個體必備元件。position 由 x/y/z 座標構成,上面單獨將 z座標設為-1.0。

範例最後一行是 RealityView 修飾語 .realityViewCameraControls(.orbit),使用者可以用滑鼠或觸控手勢來控制虛擬鏡頭的視角,以便觀察3D物體的各個面向,這也是2024年新加入的功能。

💡註解
  1. 在語法上,RealityView 尾隨的匿名函式 { } 裡面用是命令式語法,與 SwiftUI 的 Canvas 視圖類似(參考第4單元第5課 畫布 Canvas)。
  2. RealityView 「內容」的物件類型,在 visionOS 版本是 RealityViewContent,而在 iOS/iPadOS/macOS 版本則是 RealityViewCameraContent。「內容」除了建構虛擬場景之外,還會將鏡頭實景帶進來,也就是說,RealityView 其實已加入 ARKit 功能,未來在 iPad 上只要加一行「內容.camera = .spatialTracking」就可啟用 AR。
  3. 上一節提過,AR 無法在 macOS 上執行,因此 RealityView for macOS 只會顯示虛擬場景,不會加入鏡頭實景(即使有鏡頭設備)。
  4. 材質陣列(materials 參數)可以用空陣列嗎?請動手試試看。
  5. RealityKit 預設幾何模型比 SceneKit 12種少,為什麼呢?一是因為 RealityKit 提供客製化幾何模型的方法,後面課程會介紹;二是可用3D軟體先製作好,再存成 .usdz 檔案導入到程式中。預設幾何模型只需用來製作原型(Prototype)、測試或教學即可。
  6. RealityView 匿名函式的參數「內容」,是否類似上半單元 SceneView 的參數「場景」呢?兩者類似,但同中有異,SceneView 只包含一個場景,場景的根節點納入其他所有節點;而 RealityView 會將每個模型個體各自加入一個場景,多個個體就會有多個場景,但「內容」並沒有一個「根節點」可代表所有場景。請參考Apple原廠文件
補充(17) 比較 SceneView/ARView/RealityView

與6-6a等效的程式,若用 RealityView, ARView, SceneView 分別寫出來,然後用 SwiftUI 顯示在一起,效果會如何呢?我們來試試,順便比較看看三者的差異。
// 6-6a 附錄:比較 RealityView, ARView, SceneView
// Created by Heman Lu on 2025/02/04
// Test Environment: Mac mini 2023 (macOS 15.3) + Swift Playground 4.6.1
import SwiftUI
import RealityKit
import SceneKit

// (1) RealityView
struct RealityView01 : View {
var body: some View {
RealityView { 內容 in
let 球體 = MeshResource.generateSphere(radius: 0.5)
let 材質 = SimpleMaterial(color: .yellow, isMetallic: true)
let 模型 = ModelEntity(mesh: 球體, materials: [材質])
模型.position.z = -1.0
內容.add(模型)
}.realityViewCameraControls(.orbit)
}
}

// (2) ARView
struct ARView01: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
// (1) ModelEntity 模型實體
let 球體 = MeshResource.generateSphere(radius: 0.5)
let 材質 = SimpleMaterial(color: .yellow, isMetallic: true)
let 模型 = ModelEntity(mesh: 球體, materials: [材質])
模型.position.z = -1.0

// (2) AnchorEntity 錨點實體
let 錨點 = AnchorEntity()
錨點.children.append(模型)

// (3) ARView AR視界
let 視圖 = ARView()

視圖.scene.anchors.append(錨點)
視圖.environment.background = .color(.white)
// 視圖.debugOptions = [.showWorldOrigin]

return 視圖
}

func updateUIView(_ uiView: ARView, context: Context) {
}
}

// (3) SceneView
struct SceneView01: View {
let 幾何場景 = SCNScene()

var body: some View {
SceneView(scene: 幾何場景, options: [.autoenablesDefaultLighting, .allowsCameraControl])
.onAppear {
// 以下長、寬、高單位:公尺
let 球體 = SCNSphere(radius: 0.5)
let 球體節點 = SCNNode(geometry: 球體)
球體節點.position.z = -1.0

let 材質 = SCNMaterial()
材質.lightingModel = .physicallyBased
材質.metalness.intensity = 0.9
材質.roughness.intensity = 0.1
材質.diffuse.contents = UIColor.yellow
球體.materials = [材質]

幾何場景.rootNode.addChildNode(球體節點)
}
}
}

// (4) 三個視圖比較
struct 空間運算比較: View {
var body: some View {
RealityView01()
.overlay(alignment: .bottom) {
Text("RealityView (RealityKit)").padding()
}
.border(.red)
ARView01()
.overlay(alignment: .bottom) {
Text("ARView (RealityKit)").padding()
}
.border(.red)
SceneView01()
.overlay(alignment: .bottom) {
Text("SceneView (SceneKit)").padding()
}
.border(.red)
}
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(空間運算比較())

可以看出,RealityView 是三者之中最簡潔的。

RealityView01 與 SceneView01 可設定透過螢幕觸控轉動視角,不過實際操作起來,會發現並不順暢,一般來說,AR 程式通常獨佔整個螢幕畫面,很少與其他視圖並列。

注意在 ARView 中,必須有錨點個體(AnchorEntity)當作模型個體的父節點,才能加入虛擬場景中。

在擴增實境中,錨點(Anchor)通常是用來錨定虛擬物件在實體座標的位置,但對 ARView 來說,即使未開啟擴增實境模式,ARView 還是強制模型個體必須透過錨點個體才能加入場景。

ARView 的模型個體與材質等物件,和 RealityView 是共用的,所以顯示出來的效果一模一樣,從這裡可看出 RealityView 並非憑空出現,而是以 ARView 為基礎重新包裝而成,後面甚至會看到 RealityView 還留有 makeUIView()、updateUIView() 的影子。

執行結果如下:
6-6b 3D座標軸 — 設定「共享程式」

上節提到RealityKit 預設的幾何模型只有6種:包括3D文字、立方體、球體、平面、圓錐體、圓柱體等,均使用 MeshResource 的類型方法產出。

雖然預設模型種類不多,但加以組合還是可以製作不少物件,本節就利用3D文字、圓錐體、圓柱體來製作一個「3D座標軸」,以便在後續程式中當作虛擬空間的參考座標。

我們要將「3D座標軸」做成「共享程式」,這樣以後每個頁面都可用到,避免程式碼一再重複。怎麼做呢?在電子書模式(playgroudbook)下,只要將下面寫好的「座標軸()」函式,剪貼到左側欄「原始碼」之下的 “SharedCode.swift” 裡面即可。

搬移到共享程式唯一需要更改的地方,是函式必須宣告為 “public” (公開),如果沒有加 public,主程式會出現以下錯誤:”Cannot find ‘座標軸’ in scope” (有效範圍內找不到「座標軸」名稱)


這是因為主程式與共享程式現在位於不同模組,預設是不能互通的,只有宣告為 “public” 的函式、變數或物件,才能讓別的模組取用。

共享的「座標軸()」程式碼如下:
// 6-6b 3D座標系統
// Created by Heman Lu on 2025/02/03
// Test Environment: iMac 2019 (macOS 15.3) + Swift Playground 4.6.1
import RealityKit

public func 座標軸(_ 軸長: Float = 1.0) -> ModelEntity {
let 原點 = MeshResource.generateSphere(radius: 0.0)
let 軸線 = MeshResource.generateCylinder(height: 軸長 * 2.0, radius: 軸長 * 0.001)
let 箭頭 = MeshResource.generateCone(height: 軸長 * 0.07, radius: 軸長 * 0.01)

let 座標原點 = ModelEntity(mesh: 原點)

let y軸 = ModelEntity(mesh: 軸線)
let 箭頭y = ModelEntity(mesh: 箭頭)
箭頭y.position.y = 軸長
y軸.addChild(箭頭y)
座標原點.addChild(y軸)

let x軸 = ModelEntity(mesh: 軸線)
let 箭頭x = ModelEntity(mesh: 箭頭)
箭頭x.position.y = 軸長
x軸.addChild(箭頭x)
x軸.orientation = simd_quatf(angle: .pi / 2.0, axis: [0, 0, -1])

let z軸 = ModelEntity(mesh: 軸線)
let 箭頭z = ModelEntity(mesh: 箭頭)
箭頭z.position.y = 軸長
z軸.addChild(箭頭z)
z軸.orientation = simd_quatf(angle: .pi / 2.0, axis: [1, 0, 0])

let 字體比例 = 軸長 * 0.005
let y字 = ModelEntity(mesh: .generateText("Y"))
y字.scale = [字體比例, 字體比例, 字體比例]
y字.position.y = 軸長
座標原點.addChild(y字)

let x字 = ModelEntity(mesh: .generateText("X"))
x字.scale = [字體比例, 字體比例, 字體比例]
x字.position.x = 軸長
座標原點.addChild(x字)
座標原點.addChild(x軸)

let z字 = ModelEntity(mesh: .generateText("Z"))
z字.scale = [字體比例, 字體比例, 字體比例]
z字.position.z = 軸長
座標原點.addChild(z字)
座標原點.addChild(z軸)

return 座標原點
}

注意第一行 import RealityKit,每個共享程式還是要導入所需框架。

「座標軸()」會以模型個體(ModelEntity)的類型傳回一個做好的X/Y/Z座標系統,以後要用時,只要呼叫「座標軸()」並加入「內容」即可:
內容.add(座標軸())    // 預設XYZ軸長±1.0米
// 或
內容.add(座標軸(2.5)) // XYZ軸長±2.5米

實際做法,我們先產出一個隱藏的(半徑=0)小球模型「座標原點」,當作整個座標系的原點,也是所有個體的根節點,其他個體用「座標原點.addChild()」加進來。

接下來以圓柱體(generateCylinder)畫軸線、圓錐體(generateCone)畫箭頭,最後再加上X, Y, Z文字標示(generateText)放在箭頭旁。

整個座標軸並未用到任何材質,RealityKit 會顯示特殊的紫色條紋,表示沒有材質。

此3D座標軸由10個模型個體組成,由於「座標原點」是根節點,最後只要回傳「座標原點」即可。所有個體的節點關係如下:

初學空間運算時,要特別注意以下幾點:

1. RealityKit 的座標方向,往螢幕右方是正X軸,往螢幕上方是正Y軸,垂直螢幕(往眼睛方向)是正Z軸。這又稱為右手坐標系(X=大拇指、Y=食指、Z=中指)。
2. 座標的長度單位是公制1米(以配合AR實景融合)
3. 每個個體(Entity)都有自己的區域座標,區域座標原點的初始位置由上層個體決定。
4. RealityView的座標原點由上層視圖決定,預設在螢幕中心點
5. RealityKit 預設幾何模型中,球體、立方體、圓柱體、圓錐體是以幾何中心為區域座標原點,文字(generateText)的區域座標原點在左下角。

例如在產出文字時,若字體大小為12點,將會變成12米,所以函式裡面將字體模型縮小為1/200 (0.005),變成6公分。

空間旋轉與simd_quatf()
x軸、y軸、z軸的軸線都由圓柱體構成,剛產出時,圓柱體中心對齊原點,高度沿Y軸方向,因此除了y軸直接可用之外,x軸與z軸需要經過旋轉才能到達正確位置。

旋轉的程式碼如下:
x軸.orientation = simd_quatf(angle: .pi / 2.0, axis: [0, 0, -1])
// 以及
z軸.orientation = simd_quatf(angle: .pi / 2.0, axis: [1, 0, 0])

前面提過每個個體(Entity)都有座標變換元件(Transform),因此共同的屬性有 position (座標位置)、scale (縮放或鏡像)、orientation (旋轉面向)以及 transform (座標變換),這些屬性在空間運算中非常重要。

此處設定旋轉面向時,用到一個新物件 simd_quatf,這個物件是特別用來加速CPU計算的資料結構,對空間運算很有用,下一節(補充18)會詳細介紹。

上面第一行程式碼,是要依著-Z軸逆時針轉90°(弳度0.5π),等同依+Z軸順時針轉90°,將y軸軸線轉成x軸。上面 simd_quatf 的參數 axis: [0, 0, -1],axis 是軸心、轉軸的意思,參數值[0, 0, -1]是向量(而不是點座標),即-Z軸方向。

有了「座標軸()」共享函式,主程式就簡單多了,將以下幾行貼在一個新增頁面6-6b(參考下圖):
// 6-6b 3D座標系統
// Created by Heman Lu on 2025/02/03
// Test Environment: iMac 2019 (macOS 15.3) + Swift Playground 4.6
import SwiftUI
import RealityKit

struct 顯示座標軸: View {
var body: some View {
RealityView { 內容 in
內容.add(座標軸()) // 座標軸() 放在共享程式區
}.realityViewCameraControls(.orbit)
}
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(顯示座標軸())

執行結果如下:


確認程式可順利執行之後,還要進一步將共享的目錄與檔案重新命名,以後找尋比較容易。


只要出現在共享區的目錄與檔案,都會自動共享,重新命名也不影響,因此,未來我們可以給每個共享程式新增一個 .swift 檔案。

💡註解
  1. 注意Swift程式裡面,任何命名都不能以半形數字開頭,也就是說,不能取名為「3D座標軸()」,只能叫「座標軸()」或「座標軸3D()」。此外,名稱內(任何位置)也不能包含半形標點符號,唯一例外是底線 “_” (underscore)。
  2. simd_quatf 是個專業術語縮寫(用 C 語言的命名習慣),對高中生來說可能不容易理解,但又非常重要,下一節會特別介紹。
  3. simd 代表 “Single Instruction Multiple Data”,是一種CPU硬體指令,可用一個指令並行處理多筆資料(正常一個CPU指令只能處理一筆資料,如整數運算2*3,而 simd 可一次處理多筆資料運算)。
  4. quat 代表四元數(quaternion),例如(0.707, 0.0, 0.0, -0.707)。
  5. f 代表浮點數(floating-point),通常是32位元的實數,如0.707。
  6. 所以 simd_quatf 表示「32位元浮點四元數的並行運算指令」,常用於3D空間的旋轉。可參考交大周志成老師「線代啟示錄」維基百科:四元數與空間旋轉
  7. 對Swift Playground 的 App模式來說,程式碼同樣可分散在不同 .swift 檔案(放在左側欄「程式碼」區),但所有 .swift 檔案視為同一個模組,所以並不需要宣告為 public 即可互通。
  8. 作業:請將「座標軸()」加入上一節範例程式6-6a,執行後轉動畫面檢視圓球位置。
  9. 作業:請將「座標軸()」參數改為 x/y/z 軸可不同長度,例如「座標軸(x: 1.5)」。
補充(18) SIMD in RealityKit

純量(Scalar)與向量(Vector)

在 RealityKit 3D空間運算中,會經常看到 Scalar 與 SIMD 兩個名詞,Scalar 代表「純量」,就是單純的數量,例如 Int, Float, Double 等;SIMD(Single Instruction Multiple Data) 用來表示「向量」,也就是具有大小及方向的量,例如空間中的運動速度或力。

向量其實是由多個純量所組成,可以用 Swift 同類型的多元組 (1, 5, …) 或陣列 [1.0, -2.5, …] 來表示,N維空間的向量就由N個純量所組成,每個純量稱為向量的「元素」。

例如二維空間中,向量 [1.0, -1.0] 由兩個純量組成,代表從原點[0, 0]到點座標[1.0, -1.0]的方向與距離,也就是往第4象限45°的方向,大小(長度)是√2。

Swift 目前支援以下幾種維度的 SIMD 向量:

1. SIMD2(2維向量)元素由2個同類型純量(Int/Float/Double)組成
2. SIMD3(3維向量)元素由3個同類型純量(Int/Float/Double)組成
3. SIMD4(4維向量)元素由4個同類型純量(Int/Float/Double)組成
4. SIMD8(8維向量)元素由8個同類型純量(Int/Float/Double)組成
5. SIMD16(16維向量)元素由16個同類型純量(Int/Float/Double)組成
6. SIMD32(32維向量)元素由32個同類型純量(Int/Float/Double)組成
7. SIMD64(64維向量)元素由64個同類型純量(Int/Float/Double)組成
8. simd_float2 = SIMD2<Float>(2維向量/32位元浮點數)
9. simd_float3 = SIMD3<Float>(3維向量/32位元浮點數)
10. simd_float4 = SIMD4<Float>(4維向量/32位元浮點數)
11. simd_float8 = SIMD8<Float>(8維向量/32位元浮點數)
12. simd_float16 = SIMD16<Float>(16維向量/32位元浮點數)
13. simd_double2, 3, 4, 8 = SIMDn<Double>(64位元浮點數)
14. simd_int2, 3, 4, 8, 16 = SIMDn<Int32> (32位元整數)
15. simd_long2, 3, 4, 8 = SIMDn<Int>(64位元整數)
16. simd_char2, 3, 4, 8, 16, 32, 64 = SIMDn<CChar> (C語言8-bit字元)
17. simd_quatf (32位元浮點四元數)
18. simd_quatd (64位元浮點四元數)
19. simd_float4x4(32位元浮點數4x4矩陣)

注意上面有兩套命名方式,有時會混用。例如,SIMD2<float> 與 simd_float2 是等價的,都是32位元浮點數的二維向量,前者是一般化的定義,後者是特定應用。

另外,simd_float4 與 simd_quatf 都是32位元浮點數的四維向量,但兩者是不同物件,後者(四元數)除了有前者性質之外,還專門用於3D空間的旋轉,有額外的物件屬性。

用四元數取代 Transform 仿射變換(4x4矩陣)來做3D空間旋轉,好處是計算量小很多,相對速度更快。(Q: 相對於四元數,仿射變換矩陣有何優點呢?)

SIMD 能做什麼呢?

所有 CPU 最初都只針對純量計算所設計,因為向量運算(例如求內積、外積、距離…等)可由純量計算導出,所以要做向量運算也不成問題。

但問題是在3D繪圖、AI、建築或工程計算等領域,有大量的向量與矩陣運算(矩陣相當於多筆向量,如 4X4矩陣,相當於4個4維向量組成),而且需要短時間內完成(例如3D遊戲或路況辨識),傳統CPU已不堪重任,這也是GPU崛起的原因之一。

舉例來說,我們在補充(4) 變換矩陣如何轉換座標提過,對X軸旋轉𝜃角,相當於做以下座標變換:


其中 x’ = x*1 + y*0 + z*0 + 1*0,包含4次乘法運算與3次加法運算,所以要取得(x’, y’, z’)一共需要12次乘法+9次加法,一共21次純量運算,這還不包括 cos𝜃, sin𝜃 三角函數的運算次數。

在3D遊戲中,試想一個1920x1080畫面,若每點都需要一次座標變換,共需要多少次運算?1920*1080*21 = 4,354萬次 — 而且要在1/60秒內完成,相當於每秒需要約26億次純量運算,對傳統純量運算的CPU實在是很大負擔。

還好,現在不管是CPU或GPU,都會支援SIMD向量運算。SIMD 可以看作是專門為向量與矩陣運算所設計的硬體架構,讓一個硬體指令(”Single Instruction”)同時處理多筆資料(”Multiple Data”),也就是讓單核CPU也能並行處理多筆純量計算。

因此,若想發揮新一代CPU/GPU效能,就必須善用SIMD運算。SIMD 有什麼好處呢?簡單地說,就是快。

例如2024年發表的 iPhone 16 Pro 所用的 A18 Pro 晶片(以及M4晶片)中,每個單核CPU開始支援512-bit長度的並行運算(SME 512,如左下圖),一個CPU指令就可算完8筆64位元浮點數(如simd_double8)或16筆32位元浮點數(例如simd_float16)的加/減/乘/除(參考右下圖)。




左圖來源:极客湾Geekerwan
右圖來源:Denis Bakhvalov@Intel

上半單元的SceneKit 很多物件同時支援純量與SIMD向量計算,但必須由程式設計師選擇,不會自動轉換,我們的範例程式均採用純量計算,若要用SIMD則須改寫程式碼。

相對的,RealityKit 完全採用SIMD(這也是後來者優勢,時機已成熟),也更能發揮 CPU/GPU 的效能,因此我們得熟悉SIMD向量的運算法則。

SIMD程式練習

我們先用一個簡單範例來練習SIMD向量運算:
// Tested by Heman, 2025/02/07
import simd

let a = SIMD3(1.2, 0, 0)
let b = SIMD3(x: 0, y: 1.2, z: 0)
let c = SIMD3(repeating: 1.0) // SIMD3(1.0, 1.0, 1.0)
let d: SIMD3 = [0, 1.2, 0] // 可用陣列表示向量
let e: SIMD3<double> = .zero // 類型屬性
let f: simd_double3 = .zero
let g: simd_double3 = .random(in: 0.0..<1.0) // 類型方法

let 法向量1 = simd_cross(a, b) // 計算 a x b 法向量
let 法向量2 = simd_cross(b, a) // 計算 b x a 法向量
print("向量a = \(a)")
print("向量b = \(b)")
print("法向量 a x b = \(法向量1)")
print("法向量 b x a = \(法向量2)")

第一行 “import simd” 先導入 simd 框架。注意這裡用C語言命名規則,與 Swift 常用的駝峰式(大小寫混用)明顯不同,類型名稱全部小寫(如simd_float3),而非字首大寫。這其實沒什麼對錯,習慣就好。

SIMD 物件初始化,可用參數、指定陣列值、類型屬性或類型方法,上面列舉常用的7種。

SIMD 初始化之後,就可做向量運算,幾乎所有 +-*/、內積、外積、長度、距離、平方根…等,都有支援,非常方便。範例僅以外積(simd_cross)為例,注意外積不具交換律,a x b 與 b x a 得到的法向量是相反方向,外積大量應用在3D繪圖(求幾何線框中每個三角形的法向量,用來計算光照)。

上述範例執行結果如下,可以看出未宣告類型的實數,預設為Double類型(64位元):
向量a = SIMD3<double>(1.2, 0.0, 0.0)
向量b = SIMD3<double>(0.0, 1.2, 0.0)
法向量 a x b = SIMD3<double>(0.0, 0.0, 1.44)
法向量 b x a = SIMD3<double>(0.0, 0.0, -1.44)

注意上例中 d 指定初始值為 [0, 1.2, 0],SIMD向量可以用「純量陣列」來表示,但反過來,純量陣列不一定就是SIMD向量。以下範例,請猜猜看 x3, y3 會一樣嗎?
// Tested by Heman, 2025/02/07
//
import simd

let x1 = [1.0, 2.0, 3.0] // 純量陣列
let x2 = [4.0, 5.0, 6.0]
let x3 = x1 + x2
let y1: SIMD3 = [1.0, 2.0, 3.0] // 向量
let y2: SIMD3 = [4.0, 5.0, 6.0]
let y3 = y1 + y2
print("x3 = \(x3)")
print("y3 = \(y3)")

對程式設計師來說,SIMD 最方便的地方,是提供近百種數學運算,大多採用純量運算類似的函式,如絕對值 abs()、指數 pow()、三角函數 sin/cos/tan…,讓向量運算也變得跟純量運算一樣方便簡單。

不過對我們來說,目前只需要熟悉 simd_quatf 四元數,專用於虛擬空間的旋轉即可。

💡註解
  1. Swift 多元組(Tuple)可包含不同類型的元素,但用在SIMD向量時,元素類型必須相同;陣列(Array)元素一定是相同類型,用於SIMD向量時,元素個數必須固定(如三維向量一定是三個元素)。
  2. 每秒26億次純量運算,對CPU來說算多嗎?26億次運算相當於 2.6 GFLOPS,對現在的CPU/GPU 來說,其實並不多。2023年第五代樹莓派(Raspberry Pi 5) CPU效能即可達到25 GFLOPS,而Apple M4 GPU效能更高達4.6 TFLOPS(每秒46,000億次運算)— 可輕鬆運算4K螢幕動畫。
  • 5
內文搜尋
X
評分
評分
複製連結
Mobile01提醒您
您目前瀏覽的是行動版網頁
是否切換到電腦版網頁呢?