Blender4iOS/Sources/Viewport/ViewportRenderer.swift
2026-04-10 21:34:23 -07:00

281 lines
11 KiB
Swift

// ViewportRenderer.swift
// Draws all objects from the SceneGraph using Metal.
// Each ViewportPane owns one instance with its own command queue + camera.
import Metal
import MetalKit
import Observation
import simd
@Observable
final class ViewportRenderer {
// MARK: - Public state
var viewportSize: SIMD2<Float> = SIMD2<Float>(1, 1)
let camera: OrbitalCamera
// Weak ref to the shared scene graph (set externally)
var sceneGraph: SceneGraph?
// MARK: - Metal objects (owned per-pane)
private(set) var device: MTLDevice?
private var commandQueue: MTLCommandQueue?
// Pipelines (shared via MetalSharedState)
private var meshPipeline: MTLRenderPipelineState?
private var wirePipeline: MTLRenderPipelineState?
private var gridPipeline: MTLRenderPipelineState?
private var axisPipeline: MTLRenderPipelineState?
private var selectionPipeline: MTLRenderPipelineState?
private var depthState: MTLDepthStencilState?
private var depthStateNoWrite: MTLDepthStencilState?
// Axis gizmo buffer
private var axisVertexBuffer: MTLBuffer?
init(camera: OrbitalCamera = OrbitalCamera()) {
self.camera = camera
}
// MARK: - Setup
func configure(device: MTLDevice, library: MTLLibrary) {
self.device = device
self.commandQueue = device.makeCommandQueue()!
// Pipelines
meshPipeline = Self.makePipeline(device: device, library: library,
vertex: "mesh_vertex", fragment: "mesh_fragment",
label: "Mesh", blending: false)
wirePipeline = Self.makePipeline(device: device, library: library,
vertex: "wire_vertex", fragment: "wire_fragment",
label: "Wire", blending: true)
gridPipeline = Self.makePipeline(device: device, library: library,
vertex: "grid_vertex", fragment: "grid_fragment",
label: "Grid", blending: true)
axisPipeline = Self.makePipeline(device: device, library: library,
vertex: "axis_vertex", fragment: "axis_fragment",
label: "Axis", blending: true)
selectionPipeline = Self.makePipeline(device: device, library: library,
vertex: "wire_vertex", fragment: "wire_fragment",
label: "Selection", blending: true)
// Depth states
let dd = MTLDepthStencilDescriptor()
dd.depthCompareFunction = .less
dd.isDepthWriteEnabled = true
depthState = device.makeDepthStencilState(descriptor: dd)
let ddNoWrite = MTLDepthStencilDescriptor()
ddNoWrite.depthCompareFunction = .lessEqual
ddNoWrite.isDepthWriteEnabled = false
depthStateNoWrite = device.makeDepthStencilState(descriptor: ddNoWrite)
// Axis gizmo
let axisVerts = Self.buildAxisVertices()
axisVertexBuffer = device.makeBuffer(
bytes: axisVerts,
length: MemoryLayout<MeshVertex>.stride * axisVerts.count,
options: .storageModeShared
)
}
// MARK: - Per-Frame Draw
func draw(drawable: CAMetalDrawable, passDescriptor: MTLRenderPassDescriptor) {
guard let commandQueue, let device,
let meshPipeline, let wirePipeline,
let gridPipeline, let axisPipeline,
let depthState, let depthStateNoWrite
else { return }
guard let buffer = commandQueue.makeCommandBuffer(),
let encoder = buffer.makeRenderCommandEncoder(descriptor: passDescriptor)
else { return }
let aspect = viewportSize.x / max(viewportSize.y, 1)
let projection = MathUtils.perspective(
fovYRadians: Float.pi / 4.0, aspect: aspect,
near: 0.1, far: 500.0
)
let view = camera.viewMatrix
var sceneConstants = SceneConstants(
cameraPosition: camera.eye,
lightDirection: normalize(SIMD3<Float>(-0.5, -1.0, -0.7)),
ambientIntensity: 0.25,
diffuseIntensity: 0.75,
time: 0, _pad: 0
)
// 1. Grid
drawGrid(encoder: encoder, pipeline: gridPipeline,
depthState: depthStateNoWrite,
view: view, projection: projection,
sceneConstants: &sceneConstants)
// 2. Axis gizmo
if let axisBuf = axisVertexBuffer {
var identity = Uniforms(
modelMatrix: matrix_identity_float4x4,
viewMatrix: view,
projectionMatrix: projection,
normalMatrix: matrix_identity_float4x4
)
encoder.setRenderPipelineState(axisPipeline)
encoder.setDepthStencilState(depthState)
encoder.setVertexBuffer(axisBuf, offset: 0, index: 0)
encoder.setVertexBytes(&identity, length: MemoryLayout<Uniforms>.size, index: 1)
encoder.drawPrimitives(type: .line, vertexStart: 0, vertexCount: 6)
}
// 3. Scene objects
if let scene = sceneGraph {
for obj in scene.allObjects where obj.isVisible {
guard let mesh = obj.meshHandle else { continue }
let model = obj.worldMatrix
var uniforms = Uniforms(
modelMatrix: model,
viewMatrix: view,
projectionMatrix: projection,
normalMatrix: MathUtils.normalMatrix(from: model)
)
// Solid geometry
if mesh.vertexCount > 0 {
encoder.setRenderPipelineState(meshPipeline)
encoder.setDepthStencilState(depthState)
encoder.setVertexBuffer(mesh.vertexBuffer, offset: 0, index: 0)
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.size, index: 1)
encoder.setFragmentBytes(&sceneConstants, length: MemoryLayout<SceneConstants>.size, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: mesh.vertexCount)
}
// Wireframe overlay
if let wireBuf = mesh.wireVertexBuffer, mesh.wireVertexCount > 0 {
encoder.setRenderPipelineState(wirePipeline)
encoder.setDepthStencilState(depthStateNoWrite)
encoder.setVertexBuffer(wireBuf, offset: 0, index: 0)
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.size, index: 1)
encoder.drawPrimitives(type: .line, vertexStart: 0, vertexCount: mesh.wireVertexCount)
}
// Selection outline (orange)
if obj.isSelected, let wireBuf = mesh.wireVertexBuffer, mesh.wireVertexCount > 0 {
// Build selection wire verts from existing wire with orange color
drawSelectionOutline(
encoder: encoder,
pipeline: wirePipeline,
depthState: depthStateNoWrite,
wireBuffer: wireBuf,
wireCount: mesh.wireVertexCount,
uniforms: &uniforms,
device: device
)
}
}
}
encoder.endEncoding()
buffer.present(drawable)
buffer.commit()
}
// MARK: - Grid
private func drawGrid(
encoder: MTLRenderCommandEncoder,
pipeline: MTLRenderPipelineState,
depthState: MTLDepthStencilState,
view: simd_float4x4,
projection: simd_float4x4,
sceneConstants: inout SceneConstants
) {
var identity = Uniforms(
modelMatrix: matrix_identity_float4x4,
viewMatrix: view,
projectionMatrix: projection,
normalMatrix: matrix_identity_float4x4
)
encoder.setRenderPipelineState(pipeline)
encoder.setDepthStencilState(depthState)
encoder.setVertexBytes(&identity, length: MemoryLayout<Uniforms>.size, index: 0)
var gridConstants = GridConstants(
color: SIMD4<Float>(0.5, 0.5, 0.5, 1),
gridSpacing: 1.0, gridLineWidth: 1.0,
gridFadeDistance: 40.0, _pad: 0
)
encoder.setFragmentBytes(&gridConstants, length: MemoryLayout<GridConstants>.size, index: 0)
encoder.setFragmentBytes(&sceneConstants, length: MemoryLayout<SceneConstants>.size, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
}
// MARK: - Selection outline
private func drawSelectionOutline(
encoder: MTLRenderCommandEncoder,
pipeline: MTLRenderPipelineState,
depthState: MTLDepthStencilState,
wireBuffer: MTLBuffer,
wireCount: Int,
uniforms: inout Uniforms,
device: MTLDevice
) {
// We re-use the wire vertex positions but override color in the shader
// by drawing the same wire buffer with a slightly expanded model matrix
// Draw selection wireframe (uses wire buffer colors as-is for now)
encoder.setRenderPipelineState(pipeline)
encoder.setDepthStencilState(depthState)
encoder.setVertexBuffer(wireBuffer, offset: 0, index: 0)
encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.size, index: 1)
encoder.drawPrimitives(type: .line, vertexStart: 0, vertexCount: wireCount)
}
// MARK: - Static helpers
private static func buildAxisVertices() -> [MeshVertex] {
let len: Float = 1.5
let red = SIMD4<Float>(0.78, 0.14, 0.22, 1.0)
let green = SIMD4<Float>(0.22, 0.68, 0.28, 1.0)
let blue = SIMD4<Float>(0.22, 0.45, 0.78, 1.0)
return [
.init(position: .zero, normal: .zero, color: red),
.init(position: .init(len, 0, 0), normal: .zero, color: red),
.init(position: .zero, normal: .zero, color: green),
.init(position: .init(0, len, 0), normal: .zero, color: green),
.init(position: .zero, normal: .zero, color: blue),
.init(position: .init(0, 0, len), normal: .zero, color: blue),
]
}
static func makePipeline(
device: MTLDevice,
library: MTLLibrary,
vertex: String,
fragment: String,
label: String,
blending: Bool
) -> MTLRenderPipelineState {
let desc = MTLRenderPipelineDescriptor()
desc.label = label
desc.vertexFunction = library.makeFunction(name: vertex)
desc.fragmentFunction = library.makeFunction(name: fragment)
desc.colorAttachments[0].pixelFormat = .bgra8Unorm_srgb
desc.depthAttachmentPixelFormat = .depth32Float
if blending {
desc.colorAttachments[0].isBlendingEnabled = true
desc.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
desc.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
desc.colorAttachments[0].sourceAlphaBlendFactor = .one
desc.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
}
return try! device.makeRenderPipelineState(descriptor: desc)
}
}