Blender4iOS/Sources/Primitives/PrimitiveGenerator.swift

516 lines
21 KiB
Swift
Raw Normal View History

2026-04-10 21:34:23 -07:00
// PrimitiveGenerator.swift
// Procedural mesh generation for all primitive types.
import simd
/// Output from a primitive generator ready for GPU upload.
struct PrimitiveMeshData {
let vertices: [MeshVertex] // Solid triangles
let wireVertices: [MeshVertex] // Wireframe lines
let faceCount: Int
let edgeCount: Int
let boundingBoxMin: SIMD3<Float>
let boundingBoxMax: SIMD3<Float>
}
enum PrimitiveGenerator {
// MARK: - Cube
static func cube(
size: Float = 2.0,
color: SIMD4<Float> = SIMD4(0.63, 0.63, 0.63, 1)
) -> PrimitiveMeshData {
let h = size * 0.5
let positions: [SIMD3<Float>] = [
.init(-h, -h, h), .init( h, -h, h), .init( h, h, h), .init(-h, h, h),
.init( h, -h, -h), .init(-h, -h, -h), .init(-h, h, -h), .init( h, h, -h),
]
let faces: [(indices: [Int], normal: SIMD3<Float>)] = [
([0,1,2,3], .init( 0, 0, 1)), // front
([4,5,6,7], .init( 0, 0,-1)), // back
([3,2,7,6], .init( 0, 1, 0)), // top
([5,4,1,0], .init( 0,-1, 0)), // bottom
([1,4,7,2], .init( 1, 0, 0)), // right
([5,0,3,6], .init(-1, 0, 0)), // left
]
var verts: [MeshVertex] = []
for face in faces {
let (idx, n) = (face.indices, face.normal)
// Two triangles per quad
for ti in [(0,1,2), (0,2,3)] {
for i in [ti.0, ti.1, ti.2] {
verts.append(MeshVertex(position: positions[idx[i]], normal: n, color: color))
}
}
}
let edges: [(Int,Int)] = [
(0,1),(1,2),(2,3),(3,0),
(4,5),(5,6),(6,7),(7,4),
(0,5),(1,4),(2,7),(3,6),
]
let wireColor = SIMD4<Float>(0, 0, 0, 0.4)
var wireVerts: [MeshVertex] = []
for (a, b) in edges {
wireVerts.append(.init(position: positions[a], normal: .zero, color: wireColor))
wireVerts.append(.init(position: positions[b], normal: .zero, color: wireColor))
}
return PrimitiveMeshData(
vertices: verts,
wireVertices: wireVerts,
faceCount: 6,
edgeCount: 12,
boundingBoxMin: .init(repeating: -h),
boundingBoxMax: .init(repeating: h)
)
}
// MARK: - Plane
static func plane(
size: Float = 2.0,
color: SIMD4<Float> = SIMD4(0.63, 0.63, 0.63, 1)
) -> PrimitiveMeshData {
let h = size * 0.5
let n: SIMD3<Float> = .init(0, 1, 0)
let p: [SIMD3<Float>] = [
.init(-h, 0, -h), .init( h, 0, -h),
.init( h, 0, h), .init(-h, 0, h),
]
let verts: [MeshVertex] = [
.init(position: p[0], normal: n, color: color),
.init(position: p[1], normal: n, color: color),
.init(position: p[2], normal: n, color: color),
.init(position: p[0], normal: n, color: color),
.init(position: p[2], normal: n, color: color),
.init(position: p[3], normal: n, color: color),
]
let wc = SIMD4<Float>(0, 0, 0, 0.4)
let wireVerts: [MeshVertex] = [
.init(position: p[0], normal: .zero, color: wc),
.init(position: p[1], normal: .zero, color: wc),
.init(position: p[1], normal: .zero, color: wc),
.init(position: p[2], normal: .zero, color: wc),
.init(position: p[2], normal: .zero, color: wc),
.init(position: p[3], normal: .zero, color: wc),
.init(position: p[3], normal: .zero, color: wc),
.init(position: p[0], normal: .zero, color: wc),
]
return PrimitiveMeshData(
vertices: verts, wireVertices: wireVerts,
faceCount: 1, edgeCount: 4,
boundingBoxMin: .init(-h, 0, -h),
boundingBoxMax: .init(h, 0, h)
)
}
// MARK: - UV Sphere
static func uvSphere(
radius: Float = 1.0,
segments: Int = 32,
rings: Int = 16,
color: SIMD4<Float> = SIMD4(0.63, 0.63, 0.63, 1)
) -> PrimitiveMeshData {
var verts: [MeshVertex] = []
var wireVerts: [MeshVertex] = []
let wc = SIMD4<Float>(0, 0, 0, 0.3)
// Generate grid of positions
var grid: [[SIMD3<Float>]] = []
for ring in 0...rings {
var row: [SIMD3<Float>] = []
let phi = Float.pi * Float(ring) / Float(rings)
for seg in 0...segments {
let theta = 2.0 * Float.pi * Float(seg) / Float(segments)
let x = radius * sinf(phi) * cosf(theta)
let y = radius * cosf(phi)
let z = radius * sinf(phi) * sinf(theta)
row.append(.init(x, y, z))
}
grid.append(row)
}
// Triangulate
for ring in 0..<rings {
for seg in 0..<segments {
let p00 = grid[ring][seg]
let p10 = grid[ring + 1][seg]
let p01 = grid[ring][seg + 1]
let p11 = grid[ring + 1][seg + 1]
let n00 = normalize(p00)
let n10 = normalize(p10)
let n01 = normalize(p01)
let n11 = normalize(p11)
if ring != 0 {
verts.append(.init(position: p00, normal: n00, color: color))
verts.append(.init(position: p10, normal: n10, color: color))
verts.append(.init(position: p01, normal: n01, color: color))
}
if ring != rings - 1 {
verts.append(.init(position: p01, normal: n01, color: color))
verts.append(.init(position: p10, normal: n10, color: color))
verts.append(.init(position: p11, normal: n11, color: color))
}
// Wireframe: horizontal + vertical lines
wireVerts.append(.init(position: p00, normal: .zero, color: wc))
wireVerts.append(.init(position: p01, normal: .zero, color: wc))
wireVerts.append(.init(position: p00, normal: .zero, color: wc))
wireVerts.append(.init(position: p10, normal: .zero, color: wc))
}
}
return PrimitiveMeshData(
vertices: verts, wireVertices: wireVerts,
faceCount: segments * rings,
edgeCount: segments * rings * 2,
boundingBoxMin: .init(repeating: -radius),
boundingBoxMax: .init(repeating: radius)
)
}
// MARK: - Cylinder
static func cylinder(
radius: Float = 1.0,
height: Float = 2.0,
segments: Int = 32,
color: SIMD4<Float> = SIMD4(0.63, 0.63, 0.63, 1)
) -> PrimitiveMeshData {
let halfH = height * 0.5
var verts: [MeshVertex] = []
var wireVerts: [MeshVertex] = []
let wc = SIMD4<Float>(0, 0, 0, 0.3)
for i in 0..<segments {
let a0 = 2.0 * Float.pi * Float(i) / Float(segments)
let a1 = 2.0 * Float.pi * Float(i + 1) / Float(segments)
let c0 = cosf(a0), s0 = sinf(a0)
let c1 = cosf(a1), s1 = sinf(a1)
let bl = SIMD3<Float>(radius * c0, -halfH, radius * s0)
let br = SIMD3<Float>(radius * c1, -halfH, radius * s1)
let tl = SIMD3<Float>(radius * c0, halfH, radius * s0)
let tr = SIMD3<Float>(radius * c1, halfH, radius * s1)
let n0 = normalize(SIMD3<Float>(c0, 0, s0))
let n1 = normalize(SIMD3<Float>(c1, 0, s1))
// Side quad (2 tris)
verts.append(.init(position: bl, normal: n0, color: color))
verts.append(.init(position: br, normal: n1, color: color))
verts.append(.init(position: tl, normal: n0, color: color))
verts.append(.init(position: tl, normal: n0, color: color))
verts.append(.init(position: br, normal: n1, color: color))
verts.append(.init(position: tr, normal: n1, color: color))
// Top cap tri
let topN: SIMD3<Float> = .init(0, 1, 0)
verts.append(.init(position: .init(0, halfH, 0), normal: topN, color: color))
verts.append(.init(position: tl, normal: topN, color: color))
verts.append(.init(position: tr, normal: topN, color: color))
// Bottom cap tri
let botN: SIMD3<Float> = .init(0, -1, 0)
verts.append(.init(position: .init(0, -halfH, 0), normal: botN, color: color))
verts.append(.init(position: br, normal: botN, color: color))
verts.append(.init(position: bl, normal: botN, color: color))
// Wireframe
wireVerts.append(.init(position: tl, normal: .zero, color: wc))
wireVerts.append(.init(position: tr, normal: .zero, color: wc))
wireVerts.append(.init(position: bl, normal: .zero, color: wc))
wireVerts.append(.init(position: br, normal: .zero, color: wc))
wireVerts.append(.init(position: bl, normal: .zero, color: wc))
wireVerts.append(.init(position: tl, normal: .zero, color: wc))
}
return PrimitiveMeshData(
vertices: verts, wireVertices: wireVerts,
faceCount: segments * 3,
edgeCount: segments * 3,
boundingBoxMin: .init(-radius, -halfH, -radius),
boundingBoxMax: .init(radius, halfH, radius)
)
}
// MARK: - Torus
static func torus(
majorRadius: Float = 1.0,
minorRadius: Float = 0.3,
majorSegments: Int = 48,
minorSegments: Int = 12,
color: SIMD4<Float> = SIMD4(0.63, 0.63, 0.63, 1)
) -> PrimitiveMeshData {
var verts: [MeshVertex] = []
var wireVerts: [MeshVertex] = []
let wc = SIMD4<Float>(0, 0, 0, 0.3)
func torusPoint(_ u: Float, _ v: Float) -> SIMD3<Float> {
let cu = cosf(u), su = sinf(u)
let cv = cosf(v), sv = sinf(v)
return SIMD3<Float>(
(majorRadius + minorRadius * cv) * cu,
minorRadius * sv,
(majorRadius + minorRadius * cv) * su
)
}
func torusNormal(_ u: Float, _ v: Float) -> SIMD3<Float> {
let cu = cosf(u), su = sinf(u)
let cv = cosf(v), sv = sinf(v)
return normalize(SIMD3<Float>(cv * cu, sv, cv * su))
}
for i in 0..<majorSegments {
let u0 = 2.0 * Float.pi * Float(i) / Float(majorSegments)
let u1 = 2.0 * Float.pi * Float(i + 1) / Float(majorSegments)
for j in 0..<minorSegments {
let v0 = 2.0 * Float.pi * Float(j) / Float(minorSegments)
let v1 = 2.0 * Float.pi * Float(j + 1) / Float(minorSegments)
let p00 = torusPoint(u0, v0); let n00 = torusNormal(u0, v0)
let p10 = torusPoint(u1, v0); let n10 = torusNormal(u1, v0)
let p01 = torusPoint(u0, v1); let n01 = torusNormal(u0, v1)
let p11 = torusPoint(u1, v1); let n11 = torusNormal(u1, v1)
verts.append(.init(position: p00, normal: n00, color: color))
verts.append(.init(position: p10, normal: n10, color: color))
verts.append(.init(position: p01, normal: n01, color: color))
verts.append(.init(position: p01, normal: n01, color: color))
verts.append(.init(position: p10, normal: n10, color: color))
verts.append(.init(position: p11, normal: n11, color: color))
// Wireframe (major + minor rings)
wireVerts.append(.init(position: p00, normal: .zero, color: wc))
wireVerts.append(.init(position: p10, normal: .zero, color: wc))
wireVerts.append(.init(position: p00, normal: .zero, color: wc))
wireVerts.append(.init(position: p01, normal: .zero, color: wc))
}
}
let ext = majorRadius + minorRadius
return PrimitiveMeshData(
vertices: verts, wireVertices: wireVerts,
faceCount: majorSegments * minorSegments,
edgeCount: majorSegments * minorSegments * 2,
boundingBoxMin: .init(-ext, -minorRadius, -ext),
boundingBoxMax: .init(ext, minorRadius, ext)
)
}
// MARK: - Cone
static func cone(
radius: Float = 1.0,
height: Float = 2.0,
segments: Int = 32,
color: SIMD4<Float> = SIMD4(0.63, 0.63, 0.63, 1)
) -> PrimitiveMeshData {
let halfH = height * 0.5
let apex = SIMD3<Float>(0, halfH, 0)
var verts: [MeshVertex] = []
var wireVerts: [MeshVertex] = []
let wc = SIMD4<Float>(0, 0, 0, 0.3)
for i in 0..<segments {
let a0 = 2.0 * Float.pi * Float(i) / Float(segments)
let a1 = 2.0 * Float.pi * Float(i + 1) / Float(segments)
let b0 = SIMD3<Float>(radius * cosf(a0), -halfH, radius * sinf(a0))
let b1 = SIMD3<Float>(radius * cosf(a1), -halfH, radius * sinf(a1))
// Side normal (approximation)
let mid = normalize((b0 + b1) * 0.5 - SIMD3<Float>(0, -halfH, 0))
let sideN = normalize(SIMD3<Float>(mid.x, radius / height, mid.z))
// Side tri
verts.append(.init(position: apex, normal: sideN, color: color))
verts.append(.init(position: b0, normal: sideN, color: color))
verts.append(.init(position: b1, normal: sideN, color: color))
// Bottom cap
let botN = SIMD3<Float>(0, -1, 0)
verts.append(.init(position: .init(0, -halfH, 0), normal: botN, color: color))
verts.append(.init(position: b1, normal: botN, color: color))
verts.append(.init(position: b0, normal: botN, color: color))
// Wire
wireVerts.append(.init(position: b0, normal: .zero, color: wc))
wireVerts.append(.init(position: b1, normal: .zero, color: wc))
wireVerts.append(.init(position: apex, normal: .zero, color: wc))
wireVerts.append(.init(position: b0, normal: .zero, color: wc))
}
return PrimitiveMeshData(
vertices: verts, wireVertices: wireVerts,
faceCount: segments * 2,
edgeCount: segments * 2,
boundingBoxMin: .init(-radius, -halfH, -radius),
boundingBoxMax: .init(radius, halfH, radius)
)
}
// MARK: - Icosphere
static func icosphere(
radius: Float = 1.0,
subdivisions: Int = 2,
color: SIMD4<Float> = SIMD4(0.63, 0.63, 0.63, 1)
) -> PrimitiveMeshData {
// Start with icosahedron
let t = (1.0 + sqrtf(5.0)) * 0.5
var positions: [SIMD3<Float>] = [
normalize(.init(-1, t, 0)), normalize(.init(1, t, 0)),
normalize(.init(-1,-t, 0)), normalize(.init(1,-t, 0)),
normalize(.init(0,-1, t)), normalize(.init(0, 1, t)),
normalize(.init(0,-1,-t)), normalize(.init(0, 1,-t)),
normalize(.init(t, 0,-1)), normalize(.init(t, 0, 1)),
normalize(.init(-t,0,-1)), normalize(.init(-t,0, 1)),
].map { $0 * radius }
var triangles: [(Int,Int,Int)] = [
(0,11,5),(0,5,1),(0,1,7),(0,7,10),(0,10,11),
(1,5,9),(5,11,4),(11,10,2),(10,7,6),(7,1,8),
(3,9,4),(3,4,2),(3,2,6),(3,6,8),(3,8,9),
(4,9,5),(2,4,11),(6,2,10),(8,6,7),(9,8,1),
]
// Subdivide
var midpointCache: [UInt64: Int] = [:]
func midpoint(_ a: Int, _ b: Int) -> Int {
let key = UInt64(min(a,b)) << 32 | UInt64(max(a,b))
if let cached = midpointCache[key] { return cached }
let mid = normalize((positions[a] + positions[b]) * 0.5) * radius
positions.append(mid)
let idx = positions.count - 1
midpointCache[key] = idx
return idx
}
for _ in 0..<subdivisions {
var newTris: [(Int,Int,Int)] = []
for (a,b,c) in triangles {
let ab = midpoint(a,b)
let bc = midpoint(b,c)
let ca = midpoint(c,a)
newTris.append((a, ab, ca))
newTris.append((b, bc, ab))
newTris.append((c, ca, bc))
newTris.append((ab, bc, ca))
}
triangles = newTris
midpointCache.removeAll()
}
var verts: [MeshVertex] = []
var wireVerts: [MeshVertex] = []
let wc = SIMD4<Float>(0, 0, 0, 0.3)
for (a,b,c) in triangles {
let pa = positions[a], pb = positions[b], pc = positions[c]
let na = normalize(pa), nb = normalize(pb), nc = normalize(pc)
verts.append(.init(position: pa, normal: na, color: color))
verts.append(.init(position: pb, normal: nb, color: color))
verts.append(.init(position: pc, normal: nc, color: color))
wireVerts.append(.init(position: pa, normal: .zero, color: wc))
wireVerts.append(.init(position: pb, normal: .zero, color: wc))
wireVerts.append(.init(position: pb, normal: .zero, color: wc))
wireVerts.append(.init(position: pc, normal: .zero, color: wc))
wireVerts.append(.init(position: pc, normal: .zero, color: wc))
wireVerts.append(.init(position: pa, normal: .zero, color: wc))
}
return PrimitiveMeshData(
vertices: verts, wireVertices: wireVerts,
faceCount: triangles.count,
edgeCount: triangles.count * 3 / 2,
boundingBoxMin: .init(repeating: -radius),
boundingBoxMax: .init(repeating: radius)
)
}
// MARK: - Camera wireframe icon
static func cameraWireframe() -> PrimitiveMeshData {
let wc = SIMD4<Float>(0.4, 0.4, 0.4, 1.0)
// Frustum shape
let body: [SIMD3<Float>] = [
.init(-0.5, -0.3, 0.5), .init(0.5, -0.3, 0.5),
.init(0.5, 0.3, 0.5), .init(-0.5, 0.3, 0.5),
.init(-0.3, -0.2, -0.5), .init(0.3, -0.2, -0.5),
.init(0.3, 0.2, -0.5), .init(-0.3, 0.2, -0.5),
]
let edges: [(Int,Int)] = [
(0,1),(1,2),(2,3),(3,0),
(4,5),(5,6),(6,7),(7,4),
(0,4),(1,5),(2,6),(3,7),
]
var wireVerts: [MeshVertex] = []
for (a,b) in edges {
wireVerts.append(.init(position: body[a], normal: .zero, color: wc))
wireVerts.append(.init(position: body[b], normal: .zero, color: wc))
}
// Up arrow
wireVerts.append(.init(position: .init(-0.3, 0.3, 0.5), normal: .zero, color: wc))
wireVerts.append(.init(position: .init(0, 0.55, 0.5), normal: .zero, color: wc))
wireVerts.append(.init(position: .init(0, 0.55, 0.5), normal: .zero, color: wc))
wireVerts.append(.init(position: .init(0.3, 0.3, 0.5), normal: .zero, color: wc))
return PrimitiveMeshData(
vertices: [], wireVertices: wireVerts,
faceCount: 0, edgeCount: edges.count + 2,
boundingBoxMin: .init(-0.5, -0.3, -0.5),
boundingBoxMax: .init(0.5, 0.55, 0.5)
)
}
// MARK: - Light point icon
static func lightIcon() -> PrimitiveMeshData {
let lc = SIMD4<Float>(1.0, 0.85, 0.4, 1.0)
var wireVerts: [MeshVertex] = []
let r: Float = 0.3
let segments = 16
// Circle on XY
for i in 0..<segments {
let a0 = 2.0 * Float.pi * Float(i) / Float(segments)
let a1 = 2.0 * Float.pi * Float(i + 1) / Float(segments)
wireVerts.append(.init(position: .init(r * cosf(a0), r * sinf(a0), 0), normal: .zero, color: lc))
wireVerts.append(.init(position: .init(r * cosf(a1), r * sinf(a1), 0), normal: .zero, color: lc))
}
// Circle on XZ
for i in 0..<segments {
let a0 = 2.0 * Float.pi * Float(i) / Float(segments)
let a1 = 2.0 * Float.pi * Float(i + 1) / Float(segments)
wireVerts.append(.init(position: .init(r * cosf(a0), 0, r * sinf(a0)), normal: .zero, color: lc))
wireVerts.append(.init(position: .init(r * cosf(a1), 0, r * sinf(a1)), normal: .zero, color: lc))
}
// Rays
let rayLen: Float = 0.15
for i in 0..<8 {
let a = 2.0 * Float.pi * Float(i) / 8.0
let inner = SIMD3<Float>(r * cosf(a), r * sinf(a), 0)
let outer = SIMD3<Float>((r + rayLen) * cosf(a), (r + rayLen) * sinf(a), 0)
wireVerts.append(.init(position: inner, normal: .zero, color: lc))
wireVerts.append(.init(position: outer, normal: .zero, color: lc))
}
return PrimitiveMeshData(
vertices: [], wireVertices: wireVerts,
faceCount: 0, edgeCount: segments * 2 + 8,
boundingBoxMin: .init(repeating: -(r + rayLen)),
boundingBoxMax: .init(repeating: r + rayLen)
)
}
}