// MotionLab: camera -> chain of Metal shader effects -> window, recording, virtual camera. // // swift run -c release MotionLab [options] // // --effects DIR folder of .metal effect files (default ./effects) // --only a,b,c start with exactly these effects enabled // --virtualcam also send the output to the OBS virtual camera (for Meet, Zoom, ...) // --snapshot OUT.png save one frame after --frames N camera frames (default 90) and exit // --record OUT.mp4 record --frames N camera frames (default 90) and exit // // In the window: s saves a screenshot, r starts/stops recording (both into ./captures), q or Esc quits. // Effect files are reloaded when saved. import AppKit import MetalKit func option(_ name: String) -> String? { let arguments = CommandLine.arguments guard let i = arguments.firstIndex(of: name), i + 1 < arguments.count else { return nil } return arguments[i + 1] } func fail(_ message: String) -> Never { FileHandle.standardError.write((message + "\n").data(using: .utf8)!) exit(1) } final class AppDelegate: NSObject, NSApplicationDelegate { var onQuit: (() -> Void)? func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } func applicationWillTerminate(_ notification: Notification) { onQuit?() } } setvbuf(stdout, nil, _IOLBF, 0) // show "loaded" / shader errors immediately, even when piped guard let device = MTLCreateSystemDefaultDevice() else { fail("Metal is not available on this machine.") } let effectsDirectory = URL(fileURLWithPath: option("--effects") ?? "effects") let renderer: Renderer do { renderer = try Renderer(device: device, camera: Camera(device: device), effectsDirectory: effectsDirectory) } catch { fail("\(error)") } if renderer.layers.isEmpty { print("No .metal files in \(effectsDirectory.path) - showing the plain camera.") } if let names = option("--only") { let unknown = renderer.enableOnly(names.split(separator: ",").map(String.init)) if !unknown.isEmpty { fail("Unknown effect(s): \(unknown.joined(separator: ", ")). Available: \(renderer.layers.map(\.name).joined(separator: ", "))") } } if option("--snapshot") != nil || option("--record") != nil { if !renderer.failedLayers.isEmpty { fail("shader compile failed: \(renderer.failedLayers.joined(separator: ", "))") } renderer.snapshot = (Int(option("--frames") ?? "90") ?? 90, option("--snapshot").map { URL(fileURLWithPath: $0) }) renderer.recordURL = option("--record").map { URL(fileURLWithPath: $0) } } renderer.wantsVirtualCamera = CommandLine.arguments.contains("--virtualcam") let app = NSApplication.shared let delegate = AppDelegate() delegate.onQuit = { renderer.finish() } app.delegate = delegate app.setActivationPolicy(.regular) let size = NSSize(width: 960 + Panel.width, height: 540) let window = NSWindow(contentRect: NSRect(origin: .zero, size: size), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false) let view = MTKView(frame: .zero, device: device) view.colorPixelFormat = .bgra8Unorm view.delegate = renderer let panel = Panel(renderer: renderer) renderer.onLayersChanged = { panel.rebuild() } renderer.onStatusChanged = { panel.refreshStatus() } NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in if event.modifierFlags.contains(.command) { return event } switch event.charactersIgnoringModifiers { case "s": renderer.requestScreenshot() case "r": renderer.toggleRecording() case "q", "\u{1B}": app.terminate(nil) default: return event } return nil } let content = NSView(frame: NSRect(origin: .zero, size: size)) for subview in [view, panel.view] { subview.translatesAutoresizingMaskIntoConstraints = false content.addSubview(subview) } NSLayoutConstraint.activate([ view.leadingAnchor.constraint(equalTo: content.leadingAnchor), view.topAnchor.constraint(equalTo: content.topAnchor), view.bottomAnchor.constraint(equalTo: content.bottomAnchor), view.trailingAnchor.constraint(equalTo: panel.view.leadingAnchor), panel.view.trailingAnchor.constraint(equalTo: content.trailingAnchor), panel.view.topAnchor.constraint(equalTo: content.topAnchor), panel.view.bottomAnchor.constraint(equalTo: content.bottomAnchor), ]) window.title = "MotionLab" window.contentMinSize = NSSize(width: 480 + Panel.width, height: 270) window.contentView = content window.setContentSize(size) // auto layout would otherwise pick its own window size window.center() window.makeKeyAndOrderFront(nil) app.activate(ignoringOtherApps: true) app.run()