Blender4iOS/Sources/Scene/SceneGraph.swift

165 lines
4.7 KiB
Swift
Raw Normal View History

2026-04-10 21:34:23 -07:00
// SceneGraph.swift
// Observable scene graph that owns all objects and drives the outliner + renderer.
import Observation
import Metal
import simd
@Observable
final class SceneGraph {
// All top-level objects (children are nested inside each)
var rootObjects: [SceneObject] = []
// Quick access to the selected object
var selectedObject: SceneObject? {
allObjects.first { $0.isSelected }
}
// Flattened list of every object in the hierarchy
var allObjects: [SceneObject] {
var result: [SceneObject] = []
func walk(_ objects: [SceneObject]) {
for obj in objects {
result.append(obj)
walk(obj.children)
}
}
walk(rootObjects)
return result
}
// MARK: - Stats
var totalVertexCount: Int {
allObjects.compactMap { $0.meshHandle?.vertexCount }.reduce(0, +)
}
var totalFaceCount: Int {
allObjects.compactMap { $0.meshHandle?.faceCount }.reduce(0, +)
}
// MARK: - Mutation
func addObject(_ object: SceneObject) {
rootObjects.append(object)
}
func removeObject(_ object: SceneObject) {
object.removeFromParent()
rootObjects.removeAll { $0.id == object.id }
}
func select(_ object: SceneObject?) {
for obj in allObjects {
obj.isSelected = (obj.id == object?.id)
}
}
func deselectAll() {
for obj in allObjects {
obj.isSelected = false
}
}
// MARK: - Default Scene
/// Creates the classic Blender startup scene: Cube + Camera + Light.
static func defaultScene(device: MTLDevice) -> SceneGraph {
let graph = SceneGraph()
// Default cube
let cube = SceneObject(name: "Cube", type: .mesh)
let cubeMeshData = PrimitiveGenerator.cube(size: 2.0, color: SIMD4(0.63, 0.63, 0.63, 1))
cube.meshHandle = MeshUploader.upload(
vertices: cubeMeshData.vertices,
wireVertices: cubeMeshData.wireVertices,
faceCount: cubeMeshData.faceCount,
edgeCount: cubeMeshData.edgeCount,
label: "Cube",
device: device
)
cube.isSelected = true
graph.addObject(cube)
// Camera
let camera = SceneObject(name: "Camera", type: .camera,
position: SIMD3(7.36, 4.95, -6.93))
camera.transform.rotation = SIMD3(1.11, 0.0, 2.17)
let cameraMeshData = PrimitiveGenerator.cameraWireframe()
camera.meshHandle = MeshUploader.upload(
vertices: [],
wireVertices: cameraMeshData.wireVertices,
faceCount: 0,
edgeCount: cameraMeshData.edgeCount,
label: "Camera",
device: device
)
camera.color = SIMD4(0.4, 0.4, 0.4, 1.0)
graph.addObject(camera)
// Light
let light = SceneObject(name: "Light", type: .light,
position: SIMD3(4.08, 5.90, -1.0))
let lightMeshData = PrimitiveGenerator.lightIcon()
light.meshHandle = MeshUploader.upload(
vertices: [],
wireVertices: lightMeshData.wireVertices,
faceCount: 0,
edgeCount: lightMeshData.edgeCount,
label: "Light",
device: device
)
light.color = SIMD4(1.0, 0.85, 0.4, 1.0)
graph.addObject(light)
return graph
}
}
// MARK: - Mesh Uploader Utility
enum MeshUploader {
static func upload(
vertices: [MeshVertex],
wireVertices: [MeshVertex],
faceCount: Int,
edgeCount: Int,
label: String,
device: MTLDevice
) -> MeshHandle {
let vBuf: MTLBuffer?
if !vertices.isEmpty {
vBuf = device.makeBuffer(
bytes: vertices,
length: MemoryLayout<MeshVertex>.stride * vertices.count,
options: .storageModeShared
)
} else {
// Empty placeholder buffer (1 byte) for objects with no solid geometry
vBuf = device.makeBuffer(length: 1, options: .storageModeShared)
}
let wBuf: MTLBuffer?
if !wireVertices.isEmpty {
wBuf = device.makeBuffer(
bytes: wireVertices,
length: MemoryLayout<MeshVertex>.stride * wireVertices.count,
options: .storageModeShared
)
} else {
wBuf = nil
}
return MeshHandle(
vertexBuffer: vBuf!,
vertexCount: vertices.count,
wireVertexBuffer: wBuf,
wireVertexCount: wireVertices.count,
faceCount: faceCount,
edgeCount: edgeCount,
label: label
)
}
}