Swift[第6單元] AR擴增實境與空間運算

前言

2023年6月Apple發表全新系列產品 Vision Pro,以及專屬作業系統 visionOS,立刻受到全球矚目,堪稱是虛擬實境(VR)或擴增實境(AR)領域最先進的設計,Apple 特別用混合實境(MR)來定位此產品。2024年2月Vision Pro正式上市,多數買家實際體驗後,感覺令人耳目一新,從一些戴著Vision Pro逛街、買咖啡的分享影片看來,未來感十足。

Vision Pro發表的同時,Apple 特別以「空間運算(Spatial Computing)」一詞來統合AR/MR的軟體開發。什麼是「空間運算」呢?簡單地說,就是設備對於實體或虛擬空間,有一定的解析能力,因此VR/AR/MR,甚至元宇宙(Metaverse)都可算是空間運算。

不過要注意,Apple的AR/MR並不是簡單地在實景中添加3D虛擬物件而已,Vision Pro 大量運用人工智慧解析實體空間(以及人、物),讓使用者能透過瞳孔加上簡單手勢,毋須借助鍵盤、滑鼠或額外的控制器,就能完美地在虛實空間中活動。

從「空間運算」的角度來看,Apple 的發展方向和 Meta/Facebook 的路線有明顯區別,Meta 著重於虛擬實境(VR)與元宇宙(Metaverse),強調的是人工開發、完全沉浸的虛擬空間;而 Apple 的看法則是虛擬空間應該融入到人工智慧輔助的實體環境中,形成獨特的混合空間。

Apple 自2007年發表iPhone成為智慧型手機領導廠商,2017年發表ARKit,就持續發展 AR 軟體框架(包括ARKit, SceneKit, RealityKit),目前 iPhone/iPad 已成為 AR 最廣泛使用的主流平台,本單元主要目的就是學習用 Swift 語言來開發 AR App 軟體。

為什麼要學AR程式設計?

可能會有人說,AR/MR 與空間運算目前還未發展成熟,應用場合不多,而且目前 Vision Pro太貴,一般人(更何況是學生)根本買不起。

沒錯,本單元並不打算使用 Vision Pro,也不教 visionOS App 設計,而是先學習「空間運算」的基礎,從3D物件的製作、顯示、動畫開始,到虛擬物件如何融入真實環境,並與使用者互動操作等等,這些基礎不但用於AR,也同樣可發展成Vision Pro App。

AR/MR 軟體目前確實尚非主流,但是與AI一樣,3D電腦繪圖已發展超過50年,累積至今,網路上免費的3D建模、繪圖軟體以及3D虛擬物件,已經是唾手可得,非常普遍,這些會促進空間運算的成熟,對高中生而言,當他5-10年後開始工作時,說不定就像現在的AI一樣,成為主流應用,空間運算將應用於自動駕駛汽車、無人機、電影、遊戲、教育、旅遊,乃至於生活上的各方面。

想像未來有一天,騎電動機車出門,戴上一個AR頭盔,除了保護頭部之外,還具有導航及各種通訊功能,很酷吧!

本單元內容大綱

章節安排依循過去慣例,大約分為10課,每課3小節,每節各有一個完整的範例程式,由淺入深。可能會介紹的主題如下(實際章節與順序未定):

1. SceneKit, ARKit 與 RealityKit
2. SceneKit: 場景、相機鏡頭、燈光、物件節點
3. 顯示3D模型、三角形網格、材質與紋理、動態效果
4. 粒子系統(SCNParticleSystem)
5. 載入外部3D物件檔案
6. RealityKit: ECS (Entity-Component-System)
7. AR視圖、場景、座標、錨點、世界地圖
8. 錨點設定(2D圖片、平面、門窗、人體…)
9. 動畫效果(transform/skeleton animation)
10. 播放音效與影片
11. 手勢互動
12. 動作追蹤(Body Tracking)

從最簡單的3D物件開始,逐漸熟悉AR與空間運算的基本觀念與相關操作,最後會以人體的動作追蹤,做一個類似第5單元5-9提過的動作捕捉(Motion Capture)的功能,操控虛擬機器人替身(如圖)。
Swift[第6單元] AR擴增實境與空間運算

需要什麼軟硬體設備呢?

本單元開發環境仍以 Swift Playgrounds App 為主,前半部(SceneKit)介紹 3D 虛擬物件的操作,可在 macOS 或 iPadOS 上開發,後半部(RealityKit)進入 AR 虛實整合,則只能在 iPadOS 上使用,原因是 AR 需要用到陀螺儀、加速度計等傳感器,只有 iPad 或 iPhone 才具備。

附帶一提,過去以 Swift 設計 AR 軟體,大多採用 Xcode,但是 Xcode 的模擬器(Simulator)無法模擬陀螺儀和加速度計,因此必須用 Xcode (透過連接線)接上 iPhone 實機,才能開發測試 AR 軟體。我們改用Swift Playgrounds + iPad 恰可避免這個問題。

至於 iPad 的規格,只要配備仿生(Bionic)晶片即可,最好是2019年之後(5年內)的產品。

學習路線

本單元完全採用 Swift Playgrounds 作為開發環境,硬體5年內的 Mac 或 iPad 都行,請先到 App Store 下載 Swift Playgrounds App。
下載Swift Playgrounds App
第1單元 Swift 程式語言基礎
第2單元 SwiftUI 圖形介面基礎
第3單元 Swift 網路程式基礎
第4單元 SwiftUI 動畫與繪圖
第5單元 Swift AI 人工智慧基礎

💡 註解
  1. Vision Pro和空間運算中,並非只能開發3D物件,也可以加入傳統的平面(2D)內容,例如照片、影片、文件等等。
  2. 筆者在30年前學習「計算機圖學」時,3D繪圖非常耗費CPU計算能力,所以後來才有專門的繪圖晶片GPU出現,其中最有名的廠商,就是現在AI晶片之王的輝達(NVIDIA, 成立於1993年)— AI 所需的計算能力,恰好與3D繪圖類似。
  3. Apple 並不開發 VR 產品,只看好虛實整合的 AR 及 MR。相對的,Facebook 則是以 VR 為主(2014年併購VR廠商 Oculus)。

© 2024 Heman Lu <[email protected]>
第1課 SceneKit顯示3D模型

除了Vision Pro之外,更普及的 iPad 和 iPhone 也可以執行AR應用,想要開發AR應用,目前在Apple軟體框架上有兩種選擇,一是 SceneKit + ARKit,第二種是 RealityKit + ARKit,這其中 SceneKit 最早(2012年)出現,最初是用來開發3D遊戲,所以能夠操作各種3D物件,可說是空間運算的第一步,我們就先從SceneKit開始學起。

SceneKit, ARKit, RealityKit 三者的發布時間關係如下圖:


從上圖可以看出,SceneKit 發布時間甚至比Swift/SwiftUI還早,剛開始得用Objective-C語言來開發3D應用,後來推出 SCNView (SceneKit 物件的字首大多為 SCN 開頭)用於 Swift + UIKit,2019年之後再新增 SceneView 搭配 SwiftUI。

用 SceneKit 寫的3D遊戲,最好的例子就是Swift Playgrounds所附的官方遊戲,例如「開始編寫程式碼」、「Blu的冒險」…等等。底下我們就先用 SceneView + SwiftUI 來顯示一個簡單的3D模型。

6-1a 顯示3D幾何模型

在 SceneKit 中,所有的虛擬物件會用樹狀結構連在一起,每個虛擬物件稱為一個「節點(node)」,對應 SCNNode 物件類型,每個節點的屬性包含虛擬物件的形狀、外觀、空間中的座標…等等,所有節點合在一起稱為「場景(scene)」,對應 SCNScene 物件類型。

所以要用 SceneKit 開發3D應用程式,簡單地說,就是先設定好各式各樣的3D模型及虛擬物件,成為一個個節點,然後加入到同一場景中,就可以透過 SceneView 將整個(或部分)場景顯示出來。概念如下圖所示:


上面「場景概念圖」中,有3種特殊節點值得注意:
  1. 每個場景有個唯一的「根節點」,其屬性(變數)名稱為 rootNode。
  2. 每個場景有一個或多個「鏡頭節點」,代表使用者的視角(也就是螢幕所看到的角度),其屬性名稱為 cameraNode。注意這裡的「鏡頭(camera)」是虛擬鏡頭,並非真實設備。
  3. 每個場景會有一個或多個「燈光節點」,可透過屬性名稱 lightNode 來設定。這裡的「燈光」也是虛擬的,並非實物。

其他一般節點則可用來表示3D模型,或從外部檔案導入一個製作好的子場景。因此,一個場景可大可小,最小只顯示一個3D模型,大的場景則可包含成千上萬個節點,用來製作3D遊戲不成問題。

至於3D模型從何而來呢?得益於過去50年不斷推陳出新的3D建模與製圖工具,網路上已累積非常多3D資源,現在甚至用生成式AI也能輕易產出。歸納起來,至少有以下幾種方式獲得3D模型:
  1. 用Apple內建或官方製作的3D模型
  2. 用3D繪圖軟體手工製作,免費軟體包括 TinkerCAD, Blender, Houdini, SketchUp (學生免費)
  3. 用3D掃描軟體掃描,硬體用iPhone/iPad即可
  4. 網路3D模型商城免費下載或購買,如 Sketchfab, 3D Warehouse, GrabCAD
  5. 用生成式AI自動產出(要先經過大量機器學習,故通常只能產出特定類型,如人體、家具、建築、動植物、水果…等)

本節先從最簡單的開始,利用內建的3D幾何模型,做一個金字塔(三角錐),並在頂端放置一個圓球,看看3D顯示出來的效果。範例程式如下:
// 6-1a 3D幾何模型
// Created by Heman, 2024/02/29
import SceneKit
import SwiftUI

struct 顯示幾何模型: View {
let 幾何場景 = SCNScene()
var body: some View {
SceneView(scene: 幾何場景, options: [.autoenablesDefaultLighting, .allowsCameraControl])
.onAppear {
// 以下長、寬、高單位:公尺
let 三角錐 = SCNPyramid(width: 1.0, height: 0.618, length: 1.0)
let 三角錐節點 = SCNNode(geometry: 三角錐)
// 以Y軸為軸心旋轉60°
三角錐節點.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: .pi/3)

let 球體 = SCNSphere(radius: 0.1)
let 球體節點 = SCNNode(geometry: 球體)
球體節點.position.y = 0.618 + 0.1

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

import PlaygroundSupport
PlaygroundPage.current.setLiveView(顯示幾何模型())

程式碼不到20行,非常簡單易懂。有關 SwiftUI 的程式碼,可參考第2單元,不再重複,以下僅說明 SceneKit 相關部分。

首先,餵給 SceneView() 一個「幾何場景」當作參數,後面接兩個選項,分別設定自動調整照明(.autoenablesDefaultLighting)、視角可移動(.allowsCameraControl),然後 SceneView 會自動將場景轉換成視圖,呈現在螢幕上。
SceneView(scene: 幾何場景, options: [.autoenablesDefaultLighting, .allowsCameraControl])

☝ options 參數都是可以省略的,可以動手將兩個選項分別刪除,看看顯示的畫面有何不同。

接下來,「幾何場景」裡面有什麼內容呢?在最後兩行,會將準備好的「三角錐節點」、「球體節點」加入幾何場景的根節點(rootNode)中,形成樹狀結構,就像上面「場景圖」所示,這樣就完成場景的佈置了。
幾何場景.rootNode.addChildNode(三角錐節點)
幾何場景.rootNode.addChildNode(球體節點)

至於三角錐與球體如何產出呢?其實也不難,SceneKit 內建12種3D幾何模型,三角錐與球體是其中兩種,只要給相關的尺寸參數即可產出。
// 以下長、寬、高單位:公尺
let 三角錐 = SCNPyramid(width: 1.0, height: 0.618, length: 1.0)
let 三角錐節點 = SCNNode(geometry: 三角錐)
// 以Y軸為軸心旋轉60°
三角錐節點.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: .pi/3)

let 球體 = SCNSphere(radius: 0.1)
let 球體節點 = SCNNode(geometry: 球體)
球體節點.position.y = 0.618 + 0.1

從這兩段程式碼可以看出規律嗎?先產出3D模型 → 加到SCNNode節點中 → 對節點做一些操作。

注意這裡的尺寸單位,是用公制(公尺),以便融入到實體空間。三角錐底部的長寬均設為1公尺,高度0.618公尺,這樣就符合「黃金比例」,也是埃及吉薩金字塔的實際比例。

「三角錐節點」之後加了一行程式碼,讓它沿Y軸旋轉60°,比較立體好看。旋轉的設定要用到空間座標,SceneKit 所用的是三維數學坐標,X軸往右為正、Y軸往上為正、Z軸則是突出螢幕,往使用者方向為正,所以向量 (0, 1, 0) 就代表 Y 軸方向。

不過 .rotation 實際給的值是四維向量 SCNVector4(x, y, z, w),最後一個參數 w 代表轉動角度,單位是弧度,弧度1𝜋等於180°,所以𝜋/3就等於60°。

球體最簡單,只要給個半徑當參數即可,這裡做一個半徑10公分的球體,不過,球體要往上移,否則會被放在座標原點,跟三角錐重疊,故在此設定 .position.y = 0.618 + 0.1(三角錐高度+圓球半徑),相當於球心往上移 0.718公尺。

最後顯示結果如下,實際畫面是立體的,可以上下左右轉動,看到不同角度:


💡 註解
  1. 相對於 SceneKit 用來開發3D遊戲,Apple 原廠還有開發2D遊戲的 SpriteKit,SpriteKit + ARKit 也可開發 AR 應用,但不在本單元討論範圍。
  2. 至於RealityKit,雖然與SwiftUI一起於2019年發布,且功能可取代 SceneKit,但並未提供SwiftUI 可用的視圖,其中的 ARView 還是遵循 UIKit 語法。本單元下半部介紹 RealityKit 時,會將 ARView 透過 UIViewRepresentable 轉譯為 SwiftUI 可用的物件類型,以便在 iPad/iPhone 上執行。
  3. RealityKit 在2023年新增 RealityView/Model3D 物件終於採行 SwiftUI 語法,但目前只支援 visionOS,iPadOS/iOS 暫時無法使用,有點可惜。
  4. Scene 中文是「場景」,在 SceneKit 或 RealityKit 套件中,場景代表多個虛擬物件組成的可視空間,透過不同視角,可以看到空間感的立體畫面。而 ARKit 則負責控制實體鏡頭與感測器,加上AI解析實體空間,因此 SceneKit + ARKit 就能虛實融合,達到AR的效果。
  5. 場景圖的參考資料:https://developer.apple.com/documentation/scenekit/scnscene
  6. SceneKit 的場景中,所有節點會形成一個樹狀結構,有點類似第2單元2-4c提到的視圖階層。樹狀結構通常會畫成倒立的樹,根(root)在上,葉(leaf)在下。
  7. 【作業】若將場景中的節點移到(樹狀結構的)不同位置,會顯示同樣結果嗎?例如,原來這兩行程式碼,三角錐節點與球體節點均位在根節點之下:
        幾何場景.rootNode.addChildNode(三角錐節點)
    幾何場景.rootNode.addChildNode(球體節點)

    若將三角錐節點移到球體節點之下,顯示畫面會一樣嗎?
        球體節點.addChildNode(三角錐節點)
    幾何場景.rootNode.addChildNode(球體節點)

    這個結果可能會出乎很多人意料之外,而且與後續課程有關,請動手試試看!
補充(1):Swift Playgrounds 使用說明

對從未接觸過Swift Playgrounds的同學,可能擔心不會用,其實這個App非常簡單易學,只要有Mac電腦或iPad平板,按照以下教學,一步一步上手實作,10分鐘即可入門。

💡 唯一要注意的是,Swift Playgrounds會用到 iCloud 同步(同一個Apple ID會看到同樣內容),因此開始之前要確認 iCloud 有剩餘空間(10MB即可)。若 iCloud 空間已滿,將無法正常使用Swift Playgrounds。

步驟一:開啟「App Store」搜尋並下載 Swift Playgrounds



步驟二:下載後開啟Swift Playgrounds

一開始視窗上半部會一片空白,不用擔心。在底下「更多Playgrounds」有個 “+ App”,按下去就可新增一個「我的 App」:


步驟三:打開「我的App」,會看到固定的範例程式(如下圖),等一會兒讓它自動執行(不用任何操作),最右邊會出現執行結果:


步驟四:將原範例程式碼刪除,改貼上本課6-1a的程式碼(但最後兩行 import PlaygroundSupport, PlaygroundPage…. 不要複製),並將視圖名稱改為 “ContentView”(大小寫要一致),接下來就會自動執行,並在右邊出現執行結果(可轉動立體畫面):


就這麼簡單!不過以上步驟二、三、四是「App 模式」的用法,建議改用「電子書模式」較適合練習,兩種模式的差異,在第4單元4-9f曾介紹過,以下是「電子書模式」的操作步驟:

步驟二:開啟 Swift Playgrounds,新增「我的 Playground」

原來舊版4.1在「更多 Playgrounds」有個 “+ Playground” 按鍵,但新版 4.4 這個按鍵移到裡面,要先按右下角「檢視全部」:


找到「書籍」的段落,再按一次「檢視全部」:


找到「書籍」(空白電子書),按下「取得」:


就會在主畫面新增一個「我的 Playground」(空白電子書):


步驟三:開啟「我的 Playground」,會出現一個空白頁面:


步驟四:將課程內容的程式碼完整複製進去(包含最後兩行),名稱都不用改:


手動按下右下角「執行我的程式碼」,就能看到執行結果(可轉動立體畫面):


新頁面預設名稱為”My Playground”,可以按右鍵(兩指觸控)更改,之後可以按「頁面」右側的加號⊕,增加新的頁面,再將以後各節的範例程式貼進去。


這樣就可以開始寫程式了!
6-1b SceneKit內建的幾何模型

上一節(6-1a)提到SceneKit內建12種3D幾何模型,本節就每種都製作一個,放在空間中不同位置,順便藉此熟悉3D空間座標,以下是12種模型名稱以及規劃好的座標:

1. 立體文字 SCNText: (-2, 0, 2) 前面
2. 3D形狀 SCNShape: (-2, 2, 0) 左上
3. 樓板/地面 SCNFloor: (0, 0, 0)
4. 立方體 SCNBox: (0, 0, 0)
5. 膠囊體 SCNCapsule: (2, 2, 0) 右上
6. 圓錐體 SCNCone: (-2, 0, 0) 左側
7. 圓柱體 SCNCylinder: (-2, -2, 0) 左下
8. 平面/牆面 SCNPlane: (0, 0, 0)
9. 三角錐/金字塔 SCNPyramid: (2, -2, 0) 右下
10. 球體 SCNSphere: (0, -2, 0) 下方
11. 甜甜圈 SCNTorus: (0, 2, 0) 上方
12. 中空圓管 SCNTube: (2, 0, 0) 右側

最後呈現出來的結果如下,請仔細觀察每個物件的位置,其中有9個物件放在與Z軸垂直平面上(相當於2維的X-Y平面):


另外還有3個特殊的物件:

1. 垂直面(X-Y平面)是由平面SCNPlane所構成,長、寬各4米,厚度為0
2. 水平面(X-Z平面)用地面SCNFloor來做,長、寬同樣各4米,厚度為0
3. 立體文字("↙文字起點”)放在前方(-2, 0, 2)的位置

上面所有物體幾乎都以「中心點」(幾何中心)對準位置座標,只有立體文字例外,其座標位於字串的左下角,而且距離文字底線還有一點空間。

還有一個小特例是金字塔(SCNPyramid),金字塔對準位置座標的地方,並不是幾何中心,而是方形底座的中心點。在程式實際執行時,透過轉動畫面,可以仔細觀察到。

產出幾何物件的方法非常簡單,每個物件都有獨特的初始化參數,只要熟悉這些參數即可運用自如。另外,物件預設位置都在原點(0, 0, 0),可透過 position, scale, rotation 做適當的位移、縮放、旋轉等操作,甚至用變換矩陣 transform 來做仿射變換,類似操作在第4單元4-7d 仿射變換(CGAffineTransform)講解過,故不再重複。

完整範例的程式碼如下:
// 6-1b SceneKit內建12種幾何模型
// Created by Heman, 2024/03/03
import SceneKit
import SwiftUI

struct 內建幾何模型: View {
let 幾何場景 = SCNScene()
var body: some View {
SceneView(scene: 幾何場景, options: [.autoenablesDefaultLighting, .allowsCameraControl])
.onAppear {
let 總數 = 12
var 節點: [SCNNode] = []
for i in 0 ..< 總數 { // 產出12個空節點,放入陣列中
節點.append(SCNNode())
}
// (1) 立體文字
節點[0].geometry = SCNText(string: "↙文字起點", extrusionDepth: 1.0)
節點[0].scale = SCNVector3(x: 0.05, y: 0.05, z: 0.05)
節點[0].position = SCNVector3(-2, 0, 2)
// (2) 3D形狀,以UIBezierPath()繪製形狀
let 外框 = CGRect(x: -0.5, y: -0.5, width: 1.0, height: 1.0)
let 形狀 = UIBezierPath(roundedRect: 外框, cornerRadius: 0.1)
節點[1].geometry = SCNShape(path: 形狀, extrusionDepth: 0.2)
節點[1].position = SCNVector3(-2, 2, 0)
// (3) 地板、地面
let 地板 = SCNFloor()
地板.width = 2.0
地板.length = 2.0
節點[2].geometry = 地板
// (4) 立方體
節點[3].geometry = SCNBox(width: 1.0, height: 1.0, length: 1.0, chamferRadius: 0.05)
節點[3].position = SCNVector3(0, 0, 0)
// (5) 膠囊體
節點[4].geometry = SCNCapsule(capRadius: 0.2, height: 1.0)
節點[4].position = SCNVector3(2, 2, 0)
// (6) 圓錐體
節點[5].geometry = SCNCone(topRadius: 0.1, bottomRadius: 0.5, height: 0.618)
節點[5].position = SCNVector3(-2, 0, 0)
// (7) 圓柱體
節點[6].geometry = SCNCylinder(radius: 0.5, height: 1.0)
節點[6].position = SCNVector3(-2, -2, 0)
// (8) 平面、牆面(單面、無厚度)
節點[7].geometry = SCNPlane(width: 4.0, height: 4.0)
// (9) 三角錐/金字塔
節點[8].geometry = SCNPyramid(width: 1.0, height: 0.618, length: 1.0)
節點[8].position = SCNVector3(2, -2, 0)
// (10)圓球體
節點[9].geometry = SCNSphere(radius: 0.5)
節點[9].position = SCNVector3(0, -2, 0)
// (11) 甜甜圈
節點[10].geometry = SCNTorus(ringRadius: 0.4, pipeRadius: 0.2)
節點[10].position = SCNVector3(0, 2, 0)
// (12)中空圓管
節點[11].geometry = SCNTube(innerRadius: 0.4, outerRadius: 0.5, height: 1.0)
節點[11].position = SCNVector3(2, 0, 0)

for i in 0 ..< 總數 {
幾何場景.rootNode.addChildNode(節點[i])
}

幾何場景.background.contents = UIColor.green
}
}
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(內建幾何模型())

程式稍複雜一點的物件是SCNShape,這與第4單元4-8a 正多邊形 — Shape類似,只是SCNShape推出較早,用的是UIKit裡面的UIBezierPath做畫筆,畫出來的形狀是平面的,要再加上Z軸深度(extrusionDepth)後變成3D物件。

範例中,用UIBezierPath畫一個圓角正方形,加上深度後變成一個像 Mac mini 的便當盒,放在左上角(-2, 2, 0)位置:
// (2) 2D形狀轉立體,以UIBezierPath()繪製形狀
let 外框 = CGRect(x: -0.5, y: -0.5, width: 1.0, height: 1.0)
let 形狀 = UIBezierPath(roundedRect: 外框, cornerRadius: 0.1)
節點[1].geometry = SCNShape(path: 形狀, extrusionDepth: 0.2)
節點[1].position = SCNVector3(-2, 2, 0)

另一個需要注意的形狀是甜甜圈SCNTorus,torus 意思是環面,也就是一個圓在立體空間中繞一圈的軌跡所形成的幾何形狀,就像甜甜圈。
// (11) 甜甜圈
節點[10].geometry = SCNTorus(ringRadius: 0.4, pipeRadius: 0.2)
節點[10].position = SCNVector3(0, 2, 0)

甜甜圈SCNTorus的參數不是非常直觀,需要說明。ringRadius 是繞行路徑的半徑,pipeRadius 則是截面圓的半徑,兩者相加,才是甜甜圈(中心點到最外側)的半徑。參考下圖(取自Apple官方文件):


程式執行結果如下:




💡 註解

  1. 類似 torus 環面的還有克萊因瓶(Klein bottle),是空間中的特殊平面。
  2. 若有做上一節6-1a作業的話,會知道物件的預設位置並不一定是原點(0, 0, 0),而是由上一層節點(參考6-1a「場景概念圖」)所決定。本範例所有物件的上層節點均為根節點,故預設位置為場景空間的原點。
  3. 仔細觀察程式執行結果,會發現牆面SCNPlane是單面(背面透明)的,而地面SCNFloor則是雙面,而且預設會反光(產生倒影)。可以更改嗎?
  4. 用這12種基本的3D幾何模型,能夠做出什麼3D作品呢?將這些幾何模型加以組合、變形,也能做出令人驚訝的作品。網路上有不少教學影片可參考,例如:
  5. 與甜甜圈類似的食物還有油條或麻花,有吃過嗎?就是將甜甜圈麵團扭轉幾圈再油炸,其幾何結構相當類似,即一個圓在空間沿著8字型(∞)走的軌跡。這種幾何模型做得出來嗎?請用關鍵字 “twisted torus” 到Google搜尋看看。
老師你好,請問目前想學習關於AR開發,因為沒有MAC OS
初步可以買一台二手的,2018 MacBook Pro ,來當發開機使用,請問這3年內,這規格建議嗎?謝謝

查了一下,還有支持
macOS:14 以上
swift:5.9
stonetein wrote:
老師你好,請問目前想...(恕刪)


2018 Macbook Pro 應該可以。

我個人用的是 Macbook Air 2020 (8GB RAM),因為有 M1 晶片(CPU+GPU),跑起來非常順暢。本單元前半部 SceneKit 的 3D 繪圖有用到 GPU 運算,所以建議是 2020 年以後的 Macbook,M系列晶片比Intel晶片好很多。


不過若只有2018 Macbook可用,仍然可以寫AR程式,不一定要換。

本單元後半部才是真正AR開發,這時候只用 Macbook 是不夠的,因為 Macbook 上即使有鏡頭,但在AR程式裡卻看不到實景(只能看到虛擬物件),要看到真正的AR效果(也就是實景+虛景),必須搭配 iPad,也就是在 Macbook 上寫程式,然後在 iPad 上執行(或直接在iPad上搭配藍芽鍵盤寫程式)。

以上個人建議,僅供參考。
stonetein wrote:
請問目前想學習關於AR開發,因為沒有MAC OS
初步可以買一台二手的,2018 MacBook Pro ,來當發開機使用,請問這3年內,這規格建議嗎?謝謝
2018 MacBook Pro 13吋目前的二手價大概為8000~10000不等
(沒意外今年的Mac OSX 15就沒辦法升級了)
建議你找一台M1 MacBook Air二手的價格大概在一萬五附近
效能以及未來的系統支援度會比intel的機器好上許多

如果預算真的還是上不去先掛著vm虛擬機來安裝Mac OSX是我比較建議的解法
雪白西丘斯
很好的建議。
CUNNING
Vm需要有耐心,i9+32g+gen4的ssd,都配一半資源給它跑了,還是很慢,我猜是缺gpu。後來買一台m2的 mac mini,速度快多了
6-1c 燈光與材質

上一節的3D幾何模型,看起來感覺與真實物件還有點距離,差在哪裡呢?這些模型有其形而無其質,外表就像用黏土捏出來的粗胚,既未上色,又缺乏質感。如何讓3D模型具有真實的外觀呢?其中關鍵就在燈光與材質。

本節想要製作一個大理石巧克力口味的甜甜圈,先看看下圖執行的結果,不管是光影、紋理、色澤,是不是比起上一節逼真多了:


要讓3D模型展現真實質感,理論上並不容易,因為要考慮色澤、材質、表面紋理、反光特性、光源角度…等等非常多因素,想想看,若僅靠(視覺)圖片,我們如何區分同樣紅色的蘋果與籃球?當然只能靠外觀 — 兩者的形狀、圖案(紋理)、表面(光滑或粗糙、會不會反光)等不同特性。

這也是過去50年來研究3D繪圖的核心課題,所幸,經過多年的發展,困難的部分都已在 SceneKit 套件中解決了,現在很輕易就能用 Swift 程式做出逼真的3D模型。

有多容易呢?只要寫20幾行即可,完整程式碼如下:
// 6-1c 材質(SCNMaterial)與燈光(SCNLight)
// Created by Heman, 2024/03/05
import SceneKit
import SwiftUI

struct 材質與燈光: View {
let 幾何場景 = SCNScene()
var body: some View {
SceneView(scene: 幾何場景, options: [.allowsCameraControl])
.onAppear {
let 甜甜圈 = SCNTorus(ringRadius: 1.0, pipeRadius: 0.4)
let 甜甜圈節點 = SCNNode(geometry: 甜甜圈)
甜甜圈節點.rotation = SCNVector4(x: 1.0, y: 0.0, z: 1.0, w: .pi/3)

let 大理石材質 = SCNMaterial()
大理石材質.diffuse.contents = UIImage(named: "大理石紋")
大理石材質.specular.contents = UIColor.white
甜甜圈.materials = [大理石材質]

let 光源 = SCNLight()
let 燈光節點 = SCNNode()
燈光節點.light = 光源
燈光節點.position = SCNVector3(x: 0, y: 10, z: 0)

幾何場景.rootNode.addChildNode(甜甜圈節點)
幾何場景.rootNode.addChildNode(燈光節點)
幾何場景.background.contents = UIColor.green
}
}
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(材質與燈光())

這其中,讓甜甜圈改頭換面的關鍵,就是用 SceneKit 的材質物件 SCNMaterial:
let 大理石材質 = SCNMaterial()
大理石材質.diffuse.contents = UIImage(named: "大理石紋")
大理石材質.specular.contents = UIColor.white
甜甜圈.materials = [大理石材質]

SCNMaterial 物件有非常多屬性(30+個,畢竟影響質感的因素很多),其中有3個最基本,是材質外觀的主要屬性:

1. .diffuse: 模型的漫射表面,預設值為白色(如上節6-1b的幾何模型)
2. .metalness: 金屬或非金屬色澤(須設定材質.lightingModel = .physicallyBased)
3. .roughness: 光滑或粗糙程度(須設定材質.lightingModel = .physicallyBased)

其它屬性則用來修飾細部或特殊效果,例如本範例將高光部位(.specular)設為白色,讓反光更顯眼。材質的屬性內容(contents)可以設定為某個數值、顏色、圖片、影片或甚至動畫效果。

在範例中,我們將 .diffuse 表面漫射的內容(contents)設為大理石圖片 — UIImage(named: "大理石紋"),這樣的方式稱為貼皮或貼圖(map)。這是3D繪圖中最常用的手法,因為很多天然物質的紋理是無法(或很難)用程式計算的,直接用圖片貼皮方便又逼真。

至於貼圖素材從哪裡來?網路上搜尋「大理石 紋理 素材」或 “marble texture”,便可找到大量圖片樣本,我們只要剪一小塊(512x512即可),儲存為 “大理石紋.png“ 檔案,然後匯入到Swift Playgrounds裡面,如下圖:



至於燈光照明也很簡單,先產出一個 SCNLight() 物件,然後指定給某個節點的 .light 屬性即可,最後將燈光節點放到(0, 10, 0),也就原點上方10公尺位置。這段程式碼如下:
let 光源 = SCNLight()
let 燈光節點 = SCNNode()
燈光節點.light = 光源
燈光節點.position = SCNVector3(x: 0, y: 10, z: 0)

到目前為止,如果範例程式都是一行一行打字進去的(而不是拷貝-貼上),就會發現一個問題:有些屬性要在模型中設定(如長寬高),有些在節點中設定(如位置、旋轉),有些則是在材質中(如貼皮、顏色),要搞清楚這些真不容易。

還記得本課6-1a提到的「場景概念圖」嗎?每個節點裡面,還隱藏很多細節,下圖將節點、幾何模型、材質也畫成樹狀結構,這樣就比較容易看到節點的完整面貌(不過要注意,下圖並未列舉所有屬性):


從上圖可以看出,同樣一個節點(SCNNode),分別用 .geometry, .light, .camera 這三個屬性(三選一),指定不同物件後,就變成模型節點、燈光節點或鏡頭節點。不管是哪種節點,同樣都會具備 .name (名稱)、.position (位移)…等數十個屬性。大部分屬性都有預設值(或是 Optional 類型),需要時再調整即可。

💡 備註
  1. 3D模型的貼皮(texture mapping)技術相當多樣,本節示範最基本的漫射貼圖(diffuse map),SceneKit 還支援其他多種貼皮方式,包括法線貼圖(normal map)、環境光貼圖(ambient map)、發光貼圖(emission map)…等十幾種。
  2. 附帶一提,SceneKit 內部物件大多以 class 寫成,並配合 UIKit 為主,所以這裡必須用 UIColor 以及 UIImage()。
  3. UIKit (class) 與 SwiftUI (struct) 有何區別呢?UIKit 主要是命令式語法,在物件操作上,都是先產出一個基本(空白)物件,再變更物件屬性;而 SwiftUI 則是宣告式語法,在視圖物件產出後,只能透過視圖修飾語(modifier)來改變屬性,不能用指定句直接更改屬性值。
  4. 本節範例也是 SwiftUI 物件與 UIKit 物件混用的例子,在第5單元、第6單元經常需要混用 class 與 struct 兩種物件,兩者觀念不同,務必區分清楚。
  5. 記得在第5單元第9課說明過 class 與 struct 異同,這對第6單元仍是非常重要的觀念。從本節範例可以觀察到,SceneKit 物件產出時都是用 let 指定給常數,但後面卻又變更物件屬性,如果是 struct 物件,這是不允許的,但對 class 物件而言卻是正常操作。

🖖 作業
  1. 有吃過黑胡椒口味的甜甜圈嗎?請在網路上找到合適的貼圖素材,做出你最喜歡的甜甜圈口味。
  2. 執行程式時,請將甜甜圈隨意翻轉,觀察光影明暗的變化。
  3. 有沒有注意到,如果翻轉到一個角度,會看到幾乎全黑的物體,請問是為什麼(燈在動還是甜甜圈在動)?
  4. 試試將大理石材質改為金屬光澤,對模型外觀會有什麼影響?
        let 大理石材質 = SCNMaterial()
    大理石材質.lightingModel = .physicallyBased
    大理石材質.metalness.contents = UIImage(named: "大理石紋")
    甜甜圈.materials = [大理石材質]

  5. 能否設計多盞燈光,讓甜甜圈以任意角度翻轉時,任何部位都不會太黑(看得出紋理)。各盞燈光放在什麼位置最好?
  6. 參考上一節6-1b,在甜甜圈下方,加一個水平地面,觀察是否在地面上形成影子(而不是倒影)。
  7. 【有難度】請加入一個SwiftUI Slider (滑竿,之前沒教過,請自行研究),控制燈光的強度(.intensity);或是一個 SwiftUI Button(按鈕),當作燈光的開關。
6-1d 光照模型 lightingModel

上一節提到材質物件 SCNMaterial,可說是決定虛擬物體(如3D模型)外觀的最重要因素,要做出逼真的虛擬物件,就必須熟悉材質的操作。材質物件除了上一節提到的3個基本屬性(diffuse, metalness, roughness)之外,還有一個非常重要的屬性,那就是「光照模型」 lightingModel。

首先要注意,光照模型並不是燈光物件(SCNLight)的屬性,而是材質的屬性。所謂「光照模型」是指材質對光線的反應模式,大部分材質都會吸收部分光線並反射其他光線,在表面形成濃淡的光影、色澤與反光,最後呈現視覺所看到的外觀。

要計算3D物體每一點的光線強弱,實在是不容易,過去50年累積非常多理論,目前 SceneKit 支援6種光照模型:

1. 材質.lightingModel = .constant (恆定)
2. 材質.lightingModel = .shadowOnly (只計算陰影)
3. 材質.lightingModel = .lambert (蘭伯特)
4. 材質.lightingModel = .phong (裴氏)
5. 材質.lightingModel = .blinn (布林)
6. 材質.lightingModel = .physicallyBased (物理渲染)

第1種 .constant 恆定光照模型最簡單,3D模型所有地方都受同一亮度的光線照射,不會有任何陰影,這顯然不太真實,但對某些展示場合有用。

第2種 .shadowOnly 光線會穿透3D模型,只顯示影子,通常不會單獨使用。

第3種 .lambert 是最早(1960年)發展出來的光照理論之一,以數學分析光線在物體表面漫射的強弱,形成光影明暗的結果,名稱為紀念一位18世紀法國數學家Johann H. Lambert。

第4種 .phong 是越南人裴祥風(Bui Tuong Phong, 後入美國籍)於1975年發表的光照理論,其數學模型考慮環境光(ambient)、漫射(diffuse)、高光(或稱鏡面反射,specular)以及自發光(emissive)等4個因素,被後人稱為標準光照模型。

第5種 .blinn 是由Jim Blinn於1977年發表,進一步改良Phong光照的模型,又稱為 Blinn-Phong 光照模型,是目前 SceneKit 預設的光照模型。

第6種 .physicallyBased 是最近20年興起的光照模型,將材質根據金屬-非金屬(metalness)、平滑-粗糙程度(roughness)兩個面向加以分析,能夠非常真實還原材質外觀,但計算遠比上面幾種複雜,最好在繪圖晶片(GPU)設備上執行。

每種光照模型均可搭配圖片貼皮,我們同樣用上節的大理石紋貼圖,除了第2種會造成物體透明看不到之外,其他5種顯示結果,可從下圖逐一比較:

使用光照模型繪製3D物體需要相當多運算,因為要根據光線的種類、角度與距離,計算3D模型每一點的反射量與明暗,當物體有任何移動或旋轉時,就得即時重算一次。這個繪製外觀的過程,術語稱為「渲染」(Rendering),光照模型是決定渲染結果的主要因素。

那麼該如何選擇光照模型呢?在實際寫程式時,沒那麼複雜,大多採用預設的 .blinn 或 .physicallyBased 二選一,其他很少用到。

也就是說,正常情況下我們先考慮標準光照模型,用漫射屬性(.diffuse)設定主要紋理, 再對高光(.specular)、自體發光(.emission)、環境光(.ambient)屬性進一步修飾;若遇到特殊材質(如金屬、塑膠、玻璃等),再改用 .physicallyBased 光照模型,設定金屬光澤(.metalness)以及粗糙度(.roughness)。

下面的範例程式,我們將6種光照模型透過 SwiftUI 做出6個視圖,同時顯示出來,每個視圖都可以獨立旋轉操作。
// 6-1d 光照模型(lightingModel)
// Created by Heman, 2024/03/09
import SceneKit
import SwiftUI

struct 顯示光照: View {
let 幾何場景 = SCNScene()
var 模型: SCNMaterial.LightingModel = .blinn // default
var 提示 = ""

var body: some View {
SceneView(scene: 幾何場景, options: [.allowsCameraControl, .autoenablesDefaultLighting])
.overlay(alignment: .top) {
Text(提示)
.font(.title2)
.padding(5)
}
.onAppear {
let 甜甜圈 = SCNTorus(ringRadius: 1.0, pipeRadius: 0.4)
let 甜甜圈節點 = SCNNode(geometry: 甜甜圈)
甜甜圈節點.rotation = SCNVector4(x: 1.0, y: 0.0, z: 1.0, w: .pi / 4.0)

let 大理石材質 = SCNMaterial()
大理石材質.lightingModel = 模型
if 模型 == .physicallyBased {
大理石材質.metalness.contents = UIImage(named: "大理石紋")
} else if 模型 == .shadowOnly {
let 地面節點 = SCNNode(geometry: SCNFloor())
地面節點.position.y = -2
幾何場景.rootNode.addChildNode(地面節點)
} else {
大理石材質.diffuse.contents = UIImage(named: "大理石紋")
大理石材質.specular.contents = UIColor.white
}
甜甜圈.materials = [大理石材質]

幾何場景.rootNode.addChildNode(甜甜圈節點)
幾何場景.background.contents = UIColor.green
}
}
}

struct 光照模型比較: View {
var body: some View {
HStack {
顯示光照(模型: .constant, 提示: ".constant\n無死角平行光")
顯示光照(模型: .shadowOnly, 提示: ".shadowOnly\n只顯示陰影")
}
HStack {
顯示光照(模型: .lambert, 提示: ".lambert\nLambert模型(霧面)")
顯示光照(模型: .phong, 提示: ".phong\n裴祥風(Bui Tuong Phong)標準光照模型")
}
HStack {
顯示光照(模型: .blinn, 提示: ".blinn\nBlinn-Phong改良模型(預設模型)")
顯示光照(模型: .physicallyBased, 提示: ".physicallyBased\nPBR金屬光澤(物理渲染模型)")
}
}
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(光照模型比較())

在物理渲染模式下,我們改用 .metalness 大理石紋貼圖,會呈現金屬光澤;若是 .shadowOnly,則加個地面,免得完全透明看不到物體;其他4種則用 .diffuse 貼圖加 .specular 高光修飾。
if 模型 == .physicallyBased {
大理石材質.metalness.contents = UIImage(named: "大理石紋")
} else if 模型 == .shadowOnly {
let 地面節點 = SCNNode(geometry: SCNFloor())
地面節點.position.y = -2
幾何場景.rootNode.addChildNode(地面節點)
} else {
大理石材質.diffuse.contents = UIImage(named: "大理石紋")
大理石材質.specular.contents = UIColor.white
}

其他程式碼在前面都解釋過,不再重複。最後顯示結果如下:


💡 註解
  1. 裴祥風(Bui Tuong Phong, 1942-1975)出生於法國殖民時期的越南,31歲取得猶他大學電腦博士學位,是早期電腦3D繪圖的開創人物之一,可惜33歲英年早逝,令人扼腕。他的博士指導教授是被尊稱為計算機圖學之父的伊凡·蘇澤蘭(Ivan Sutherland)。
  2. 當時(1970年代)在美國鹽湖城的猶他大學是計算機圖學的發源地之一,最早的一批數位3D模型就是在猶他大學製作,流傳至今,例如猶他壺(Utah Teapot)與猶他金龜車(Utah VW Bug)。
  3. Phong Lighting Model 有些人音譯為馮氏光照模型,並不恰當,應稱為裴氏光照才對。
  4. Jim Blinn 也是伊凡·蘇澤蘭在猶他大學指導的博士生,小裴祥風7歲,後來成為計算機圖學的領軍人物。
  5. .physicallyBased 光照模型又稱為物理渲染模型(Physically Based Rendering, 簡寫為PBR),在本單元後半的 RealityKit 會再次用到此光照模型。
奇怪,預覽可以看到圖片群組(如下截圖),正式發布時版面會跑掉,有人知道怎麼編排嗎?
...
關閉廣告
文章分享
評分
評分
複製連結

今日熱門文章 網友點擊推薦!