上一節提到,擠出成型(Extrusion)就是2D形狀在空間中的運動軌跡,由此可變化出無數3D造型,可玩的花樣非常多。
變化方式一則是透過各式各樣的2D形狀 — 只要符合 Shape 規範的都可當母版;二是空間中的行進路線,例如直線位移、繞圓旋轉、大小縮放等,只要是座標變換(Transform)所控制的都行。
2D形狀是由線段、圓弧或曲線所構成,其中貝茲曲線是製作2D形狀的絕妙工具,在第4單元第7課 貝茲曲線(Bezier Curve) 曾學過,本節就結合貝茲曲線與繞圓旋轉來製作3D花瓶模型。
製作出來的花瓶如下圖,造型十分優美,即是歸功於貝茲曲線:

右上角的方框,顯示所用的2D輪廓,這個輪廓是兩條(幾乎平行的)貝茲曲線所構成,有一定的寬度(對應花瓶的厚度)。
將這個曲線輪廓在空間中繞Y軸旋轉一圈,就得到3D花瓶,原理非常簡單。程式仿照上一節範例,做一個類型方法:「MeshResource.製作花瓶()」,以下程式碼請放在共享區:
// 6-8c 共享程式:製作花瓶
// Created by Heman Lu on 2025/03/20
// Tested with iMac 2019 (macOS 15.3.2) + Swift Playground 4.6.3
import SwiftUI
import RealityKit
private enum 錯誤碼: Error {
case 參數不在有效範圍
case 其他錯誤
}
extension MeshResource {
public static func 製作花瓶(
輪廓 path: Path,
分段 n: Int = 20
) async throws -> MeshResource {
if n < 0 { throw 錯誤碼.參數不在有效範圍 }
var 變換矩陣陣列: [simd_float4x4] = []
for i in 0...n { // 邊界檢查:若 n < 0 會造成閃退
let 弧度: Float = Float(i * 2) * .pi / Float(n) // 圓心角
let 旋轉 = simd_quatf(angle: 弧度, axis: [0.0, -1.0, 0.0])
let 變換矩陣 = Transform(rotation: 旋轉)
變換矩陣陣列.append(變換矩陣.matrix)
}
var 選項 = MeshResource.ShapeExtrusionOptions()
選項.extrusionMethod = .traceTransforms(變換矩陣陣列)
選項.boundaryResolution = .uniformSegmentsPerSpan(segmentCount: n)
let 結果 = try await MeshResource(
extruding: path,
extrusionOptions: 選項)
return 結果
}
}
「製作花瓶()」與上一節的主要差異,在參數部分 — 主要參數是「輪廓」,也就是上圖右上角的形狀,取形狀的畫筆(Path)物件當參數,如此一來,透過參數送進不同輪廓,就能產出各式造型的花瓶。
由於輪廓變成參數,因此我們要在主程式中做出「花瓶曲線」,再提供給 「MeshResource.製作花瓶()」,主程式如下,注意其中有兩條相鄰的貝茲曲線(addCurve),且方向相反,第一條往上畫,第二條往下畫:
// 6-8c 貝茲曲線製作花瓶
// Created by Heman Lu on 2025/03/20
// Tested with iMac 2019 (macOS 15.3.2) + Swift Playground 4.6.3
import SwiftUI
import RealityKit
// 花瓶右半輪廓曲線
struct 花瓶曲線: Shape {
func path(in 尺寸: CGRect) -> Path {
let 寬 = 尺寸.width
let 高 = 尺寸.height
var 畫筆 = Path()
畫筆.move(to: .zero)
// 正規化尺寸
畫筆.addLine(to: CGPoint(x: 0.2, y: 0.0))
畫筆.addCurve(to: CGPoint(x: 0.3, y: 0.98),
control1: CGPoint(x: 1.0, y: 0),
control2: CGPoint(x: -0.2, y: 0.9))
畫筆.addLine(to: CGPoint(x: 0.3, y: 1.0))
畫筆.addCurve(to: CGPoint(x: 0.2, y: 0.02),
control1: CGPoint(x: -0.3, y: 1.0),
control2: CGPoint(x: 0.9, y: 0.02))
畫筆.addLine(to: CGPoint(x: 0.0, y: 0.02))
畫筆.closeSubpath()
// 恢復原尺寸
let 縮放矩陣 = CGAffineTransform(scaleX: 寬, y: 高)
return 畫筆.applying(縮放矩陣)
}
}
struct 顯示花瓶: View {
var body: some View {
ZStack(alignment: .topTrailing) {
RealityView { 內容 in
內容.add(座標軸()) // 共享程式6-6b
var 材質 = PhysicallyBasedMaterial()
材質.roughness = 0.1
材質.metallic = 0.1
材質.blending = .transparent(opacity: 0.9)
// 裂紋.jpg: <https://pixabay.com/photos/abstract-pattern-surface-texture-1867395/>
if let 紋理 = try? await TextureResource(named: "裂紋.jpg") {
材質.baseColor.texture = .init(紋理)
}
let 外框 = CGRect(x: 0.0, y: 0.0, width: 0.5, height: 0.8)
let 曲線 = 花瓶曲線().path(in: 外框)
// 共享程式6-8c:製作花瓶()
if let 花瓶 = try? await MeshResource.製作花瓶(輪廓: 曲線) {
let 花瓶模型 = ModelEntity(mesh: 花瓶, materials: [材質])
內容.add(花瓶模型)
}
// 天空盒:參考6-7c
if let 天空盒 = try? await EnvironmentResource(named: "威尼斯清晨") {
內容.environment = .skybox(天空盒)
}
}
.realityViewCameraControls(.orbit)
花瓶曲線()
.fill(.primary)
.frame(width: 100, height: 100)
.border(.gray)
.scaleEffect(y: -1.0) // X軸鏡像(上下顛倒)
.padding(5)
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(顯示花瓶())
範例程式所用的花瓶材質取自 Pixabay,請下載後更名為「裂紋.jpg」,再導入 Swift Playground,操作過程請參考第7課6-7a。
主程式中,透過 ZStack 將3D的 RealityView 與2D的「花瓶曲線()」疊在一起,並且對齊右上角(alignment: .topTrailing),這樣就完成主畫面了,其中 RealityView 視圖範圍內仍可滑動,查看花瓶的各面向。