165 lines
4.7 KiB
Swift
165 lines
4.7 KiB
Swift
|
|
// 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
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|