利用上一節所學的網格描述陣列與UV映射,就能做出很多實用的3D模型,其中關鍵之處在於如何取得頂點座標以及各面的索引順序。
還記得第4單元第8課 正多邊形 — Shape ,介紹過如何計算正多邊形的頂點座標,藉此即可做出多角柱體,本節就來試試看。
網格描述並不一定都得用三角形,實際上也可以用其他多邊形,RealityKit 會自動將多邊形轉換成三角形。上一節的正十二面體,我們其實是用五邊形來產出網格:
var 網格描述 = MeshDescriptor()
網格描述.positions = .init(單面頂點陣列)
網格描述.primitives = .polygons([5], [0, 1, 2, 3, 4]) //一個5邊形,5個頂點索引
用同樣方法來製作多角柱體就相當容易,頂面與底面各為一個正多邊形(外接圓圓心在X-Z座標原點),柱面則都是四邊形,如下圖:
頂點座標的陣列索引是必要參數,因此在計算座標時,就要規劃好順序,上圖的頂點編號就是加入陣列的順序。
範例中會將頂面稍微縮小,讓角柱體有更多變化。正多邊形可透過外接圓來控制大小,因此參數有「底半徑」及「頂半徑」。
我們希望每一面的UV映射都分開,因此 n 邊形的多角柱體就需要 n+2 個材質,仍然用上一節介紹的 ImageRenderer 來做:
// 6-9d 共享程式:多角柱體
// Created by Heman Lu on 2025/04/10
// Tested with iMac 2019 (macOS 15.4) + Swift Playground 4.6.3
import RealityKit
import CoreGraphics
private enum 錯誤碼: Error {
case 參數不在有效範圍
case 其他錯誤
}
func 正規化頂點座標(_ n: Int) -> [simd_float2] { //用來計算UV映射的正規化座標
var 座標陣列: [simd_float2] = []
for i in 0 ..< n {
let 圓心角 = Float.pi * 2 * Float(i) / Float(n)
let 頂點座標 = SIMD2( //逆時針方向
x: 0.5 - sin(圓心角) * 0.5,
y: 0.5 + cos(圓心角) * 0.5)
座標陣列.append(頂點座標)
}
return 座標陣列
}
extension MeshResource {
public static func 多角柱體(
_ n: Int = 3, // 邊數最少為3
底半徑: Float = 1.0,
頂半徑: Float = 0.4,
高: Float = 1.0) async throws -> MeshResource
{
if n < 3 { throw 錯誤碼.參數不在有效範圍 }
// (1) 計算所有(2n)頂點座標
var 頂點陣列: [simd_float3] = []
for i in 0 ..< n {
let 圓心角 = Float.pi * 2.0 * Float(i) / Float(n)
// 底部頂點 -- 索引 0, 2, 4,...
let 下頂點 = SIMD3(
x: 底半徑 * sin(圓心角),
y: -高 * 0.5,
z: 底半徑 * cos(圓心角))
頂點陣列.append(下頂點)
// 頂部頂點 -- 索引 1, 3, 5,...
let 上頂點 = SIMD3(
x: 頂半徑 * sin(圓心角),
y: 高 * 0.5,
z: 頂半徑 * cos(圓心角))
頂點陣列.append(上頂點)
}
print(頂點陣列.count, 頂點陣列)
var 網格描述陣列: [MeshDescriptor] = []
// (2) 計算「頂面」的座標、索引順序、UV映射
var 頂面網格描述 = MeshDescriptor(name: "頂面")
let 頂面頂點: [simd_float3] = (0 ..< n).map { i in
頂點陣列[i*2 + 1]
}
頂面網格描述.positions = .init(頂面頂點)
let 索引正序: [UInt32] = (0 ..< n).map { i in
UInt32(i)
}
頂面網格描述.primitives = .polygons([UInt8(n)], 索引正序)
頂面網格描述.materials = .allFaces(0)
// print(正規化頂點座標(5))
頂面網格描述.textureCoordinates = .init(正規化頂點座標(n))
網格描述陣列.append(頂面網格描述)
// (3) 計算「底面」的座標、索引順序、UV映射
var 底面網格描述 = MeshDescriptor(name: "底面")
let 底面頂點: [simd_float3] = (0 ..< n).map { i in
頂點陣列[i*2]
}
底面網格描述.positions = .init(底面頂點)
let 索引反序: [UInt32] = (0 ..< n).map { i in
UInt32(n - i - 1)
}
底面網格描述.primitives = .polygons([UInt8(n)], 索引反序)
底面網格描述.materials = .allFaces(UInt32(n+1))
底面網格描述.textureCoordinates = .init(正規化頂點座標(n))
網格描述陣列.append(底面網格描述)
// (4) 計算(n個)「柱面」的座標、索引順序、UV映射
for i in 0 ..< n {
var 柱面網格描述 = MeshDescriptor(name: "柱面\\(i)")
let 柱面頂點: [simd_float3] = [
頂點陣列[i*2],
頂點陣列[i*2 + 1],
頂點陣列[(i*2 + 2) % (n*2)],
頂點陣列[(i*2 + 3) % (n*2)]
]
柱面網格描述.positions = .init(柱面頂點)
柱面網格描述.primitives = .polygons([4], [0, 2, 3, 1])
柱面網格描述.materials = .allFaces(UInt32(i)+1)
let 比例 = 頂半徑 / 底半徑
柱面網格描述.textureCoordinates = .init([
[0, 0],
[0.5 - 比例 * 0.5, 1],
[1, 0],
[0.5 + 比例 * 0.5, 1]
])
網格描述陣列.append(柱面網格描述)
}
return try await MeshResource.generate(from: 網格描述陣列)
}
}
主程式中用 ImageRenderer 來產出 n+2 個材質,程式寫法與上一節類似:
// 6-9d 多角柱體
// Created by Heman Lu on 2025/04/10
// Tested with iMac 2019 (macOS 15.4) + Swift Playground 4.6.3
import SwiftUI
import RealityKit
struct 顯示多角柱體: View {
let 邊數 = 8
func 文字轉圖片(_ 文字: String = "", 寬高: CGFloat = 200) -> CGImage {
let 視圖 = Text(文字)
.font(.system(size: 64))
.padding()
.shadow(radius: 3)
.blur(radius: 1)
.foregroundStyle(.black)
.frame(width: 寬高, height: 寬高)
.background {
Color.white
}
let 圖片 = ImageRenderer(content: 視圖)
return 圖片.cgImage!
}
var body: some View {
RealityView { 內容 in
內容.add(座標軸())
var 材質陣列: [PhysicallyBasedMaterial] = []
let 八卦: [String] = ["☯︎", "☰", "☱", "☲", "☳", "☴", "☵", "☶", "☷", "☯︎"]
for i in 0 ..< (邊數+2) {
var 材質 = PhysicallyBasedMaterial()
材質.baseColor.tint = .orange
let 圖片 = 邊數 == 8 ? 文字轉圖片(八卦[i]) : 文字轉圖片("\\(i)")
if let 紋理 = try? await TextureResource(image: 圖片, options: .init(semantic: .color)) {
print("紋理匯入成功#\\(i)")
材質.baseColor.texture = .init(紋理)
}
材質陣列.append(材質)
}
if let 多角柱模型 = try? await ModelEntity(mesh: .多角柱體(邊數, 底半徑: 0.8, 頂半徑: 0.3, 高: 0.618)) {
多角柱模型.model?.materials = 材質陣列
內容.add(多角柱模型)
}
if let 天空盒 = try? await EnvironmentResource(named: "威尼斯清晨") {
內容.environment = .skybox(天空盒)
}
}
.realityViewCameraControls(.orbit)
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(顯示多角柱體())
對於八角柱體,我們特別用太極八卦符號,其他則用數字放在各面。實際執行的畫面如下: