上一節提到,擠出成型(Extrusion)就是2D形狀在空間中的運動軌跡,由此可變化出無數3D造型,可玩的花樣非常多。

變化方式一則是透過各式各樣的2D形狀 — 只要符合 Shape 規範的都可當母版;二是空間中的行進路線,例如直線位移、繞圓旋轉、大小縮放等,只要是座標變換(Transform)所控制的都行。

2D形狀是由線段、圓弧或曲線所構成,其中貝茲曲線是製作2D形狀的絕妙工具,在第4單元第7課 貝茲曲線(Bezier Curve) 曾學過,本節就結合貝茲曲線與繞圓旋轉來製作3D花瓶模型。

製作出來的花瓶如下圖,造型十分優美,即是歸功於貝茲曲線:

截圖 2025-03-23 晚上8.35.22.png

右上角的方框,顯示所用的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 視圖範圍內仍可滑動,查看花瓶的各面向。