diff --git a/Sources/Constraints3D/DraggableParameter.swift b/Sources/Constraints3D/DraggableParameter.swift index b3ed2b6e..4eac05d7 100644 --- a/Sources/Constraints3D/DraggableParameter.swift +++ b/Sources/Constraints3D/DraggableParameter.swift @@ -55,14 +55,13 @@ public struct DraggableParamaterViewModifier: ViewModifier { input = value.translation.height } var newValue = initialValue.unsafelyUnwrapped + input * scale - guard let range else { - fatalError("TODO") - } + if let range { switch behavior { case .clamping: newValue = newValue.clamped(to: range) case .wrapping: newValue = newValue.wrapped(to: range) + } } parameter = newValue } diff --git a/Sources/GaussianSplatShaders/include/GaussianSplatRenderShaders.h b/Sources/GaussianSplatShaders/include/GaussianSplatRenderShaders.h index e6323d34..7db2b90f 100644 --- a/Sources/GaussianSplatShaders/include/GaussianSplatRenderShaders.h +++ b/Sources/GaussianSplatShaders/include/GaussianSplatRenderShaders.h @@ -67,8 +67,8 @@ namespace GaussianSplatShaders { VertexOut out; - auto indexedDistance = indexedDistances[instance_id]; - auto splat = splats[indexedDistance.index]; + const IndexedDistance indexedDistance = indexedDistances[instance_id]; + const SplatC splat = splats[indexedDistance.index]; const float4 splatModelSpacePosition = float4(float3(splat.position), 1); const float4 splatClipSpacePosition = uniforms.modelViewProjectionMatrix * splatModelSpacePosition; @@ -80,7 +80,7 @@ namespace GaussianSplatShaders { return out; } - const float4 splatWorldSpacePosition = uniforms.modelViewMatrix * splatModelSpacePosition; + const float4 splatWorldSpacePosition = uniforms.modelViewMatrix * splatModelSpacePosition; const float3 covPosition = splatWorldSpacePosition.xyz; const Tuple2 axes = decomposedCalcCovariance2D(covPosition, splat.cov_a, splat.cov_b, uniforms.modelViewMatrix, uniforms.focalSize, uniforms.limit); diff --git a/Sources/GaussianSplatSupport/GaussianSplatConfiguration.swift b/Sources/GaussianSplatSupport/GaussianSplatConfiguration.swift new file mode 100644 index 00000000..3bc41303 --- /dev/null +++ b/Sources/GaussianSplatSupport/GaussianSplatConfiguration.swift @@ -0,0 +1,66 @@ +import Metal +import PanoramaSupport +#if !targetEnvironment(simulator) +import MetalFX +#endif +import MetalKit +import os +import simd +import SwiftUI + +public struct GaussianSplatConfiguration { + public enum SortMethod { + case gpuBitonic + case cpuRadix + } + + public var debugMode: Bool + public var metalFXRate: Float + public var discardRate: Float + public var clearColor: MTLClearColor + public var skyboxTexture: MTLTexture? + public var verticalAngleOfView: Angle + public var sortMethod: SortMethod + public var renderSkybox: Bool = true + public var renderSplats: Bool = true + + public init(debugMode: Bool = false, metalFXRate: Float = 2, discardRate: Float = 0.0, clearColor: MTLClearColor = .init(red: 0, green: 0, blue: 0, alpha: 1), skyboxTexture: MTLTexture? = nil, verticalAngleOfView: Angle = .degrees(90), sortMethod: SortMethod = .cpuRadix) { + self.debugMode = debugMode + self.metalFXRate = metalFXRate + self.discardRate = discardRate + self.clearColor = clearColor + self.skyboxTexture = skyboxTexture + self.verticalAngleOfView = verticalAngleOfView + self.sortMethod = sortMethod + } + + @MainActor + public static func defaultSkyboxTexture(device: MTLDevice) -> MTLTexture? { + let gradient = LinearGradient( + stops: [ + .init(color: .white, location: 0), + .init(color: .white, location: 0.4), + .init(color: Color(red: 135 / 255, green: 206 / 255, blue: 235 / 255), location: 0.5), + .init(color: Color(red: 135 / 255, green: 206 / 255, blue: 235 / 255), location: 1) + ], + startPoint: .init(x: 0, y: 0), + endPoint: .init(x: 0, y: 1) + ) + + guard var cgImage = ImageRenderer(content: Rectangle().fill(gradient).frame(width: 1024, height: 1024)).cgImage else { + fatalError("Could not render image.") + } + let bitmapInfo: CGBitmapInfo + if cgImage.byteOrderInfo == .order32Little { + bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) + } else { + bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) + } + cgImage = cgImage.convert(bitmapInfo: bitmapInfo)! + + let textureLoader = MTKTextureLoader(device: device) + let texture = try! textureLoader.newTexture(cgImage: cgImage, options: nil) + texture.label = "Skybox Gradient" + return texture + } +} diff --git a/Sources/GaussianSplatSupport/GaussianSplatSupport.swift b/Sources/GaussianSplatSupport/GaussianSplatSupport.swift index 99f0c53f..0c39cf6b 100644 --- a/Sources/GaussianSplatSupport/GaussianSplatSupport.swift +++ b/Sources/GaussianSplatSupport/GaussianSplatSupport.swift @@ -1,4 +1,5 @@ import BaseSupport +import CoreGraphics import Foundation import Metal import os @@ -33,47 +34,51 @@ internal func releaseAssert(_ condition: @autoclosure () -> Bool, _ message: @au } } -@dynamicMemberLookup -public struct TupleBuffered { - var keys: [String: Int] - var elements: [Element] - - public init(keys: [String], elements: [Element]) { - self.keys = Dictionary(uniqueKeysWithValues: zip(keys, keys.indices)) - self.elements = elements +public extension CGImage { + func convert(bitmapInfo: CGBitmapInfo) -> CGImage? { + let width = width + let height = height + let bitsPerComponent = 8 + let bytesPerPixel = 4 + let bytesPerRow = width * bytesPerPixel + let colorSpace = CGColorSpaceCreateDeviceRGB() + guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else { + return nil + } + context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height)) + return context.makeImage() } +} + +internal func convertCGImageEndianness2(_ inputImage: CGImage) -> CGImage? { + let width = inputImage.width + let height = inputImage.height + let bitsPerComponent = 8 + let bytesPerPixel = 4 + let bytesPerRow = width * bytesPerPixel + let colorSpace = CGColorSpaceCreateDeviceRGB() - public mutating func rotate() { - let first = elements.removeFirst() - elements.append(first) + // Choose the appropriate bitmap info for the target endianness + let bitmapInfo: CGBitmapInfo + if inputImage.byteOrderInfo == .order32Little { + bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) + } else { + bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) } - public subscript(dynamicMember key: String) -> Element { - get { - guard let index = keys[key] else { - fatalError("No index for key \(key)") - } - return elements[index] - } - set { - guard let index = keys[key] else { - fatalError("No index for key \(key)") - } - elements[index] = newValue - } + guard let context = CGContext(data: nil, + width: width, + height: height, + bitsPerComponent: bitsPerComponent, + bytesPerRow: bytesPerRow, + space: colorSpace, + bitmapInfo: bitmapInfo.rawValue) else { + return nil } -} -extension TupleBuffered: Sendable where Element: Sendable { -} + // Draw the original image into the new context + context.draw(inputImage, in: CGRect(x: 0, y: 0, width: width, height: height)) -extension OSAllocatedUnfairLock where State == Int { - func postIncrement() -> State { - withLock { state in - defer { - state += 1 - } - return state - } - } + // Create a new CGImage from the context + return context.makeImage() } diff --git a/Sources/GaussianSplatSupport/GaussianSplatViewModel.swift b/Sources/GaussianSplatSupport/GaussianSplatViewModel.swift index 0423ea2c..96307596 100644 --- a/Sources/GaussianSplatSupport/GaussianSplatViewModel.swift +++ b/Sources/GaussianSplatSupport/GaussianSplatViewModel.swift @@ -13,37 +13,6 @@ import Shapes3D import simd import SIMDSupport import SwiftUI -import SwiftUISupport -import Traces - -public struct GaussianSplatConfiguration { - public enum SortMethod { - case gpuBitonic - case cpuRadix - } - - public var debugMode: Bool - public var metalFXRate: Float - public var discardRate: Float - public var gpuCounters: GPUCounters? - public var clearColor: MTLClearColor // TODO: make this a SwiftUI Color - public var skyboxTexture: MTLTexture? - public var verticalAngleOfView: Angle - public var sortMethod: SortMethod - - public init(debugMode: Bool = false, metalFXRate: Float = 2, discardRate: Float = 0.0, gpuCounters: GPUCounters? = nil, clearColor: MTLClearColor = .init(red: 0, green: 0, blue: 0, alpha: 1), skyboxTexture: MTLTexture? = nil, verticalAngleOfView: Angle = .degrees(90), sortMethod: SortMethod = .gpuBitonic) { - self.debugMode = debugMode - self.metalFXRate = metalFXRate - self.discardRate = discardRate - self.gpuCounters = gpuCounters - self.clearColor = clearColor - self.skyboxTexture = skyboxTexture - self.verticalAngleOfView = verticalAngleOfView - self.sortMethod = sortMethod - } -} - -// MARK: - @Observable @MainActor @@ -62,8 +31,6 @@ public class GaussianSplatViewModel where Splat: SplatProtocol { } } - public var splatResource: SplatResource - public var pass: GroupPass? public var loadProgress = Progress() @@ -100,9 +67,8 @@ public class GaussianSplatViewModel where Splat: SplatProtocol { // MARK: - - public init(device: MTLDevice, splatResource: SplatResource, splatCloud: SplatCloud, configuration: GaussianSplatConfiguration, logger: Logger? = nil) throws where Splat == SplatC { + public init(device: MTLDevice, splatCloud: SplatCloud, configuration: GaussianSplatConfiguration, logger: Logger? = nil) throws where Splat == SplatC { self.device = device - self.splatResource = splatResource self.configuration = configuration self.logger = logger @@ -164,7 +130,7 @@ public class GaussianSplatViewModel where Splat: SplatProtocol { enabled: sortEnabled, splats: splats, modelMatrix: simd_float3x3(truncating: splatsNode.transform.matrix), - cameraPosition: cameraNode.transform.translation + cameraPosition: cameraNode.transform.matrix.translation ) GaussianSplatBitonicSortComputePass( id: "SplatBitonicSort", @@ -172,41 +138,41 @@ public class GaussianSplatViewModel where Splat: SplatProtocol { splats: splats ) } - PanoramaShadingPass(id: "Panorama", scene: scene) - GaussianSplatRenderPass( - id: "SplatRender", - enabled: true, - scene: scene, - discardRate: configuration.discardRate - ) - } - GroupPass(id: "GaussianSplatRenderGroup-1", enabled: fullRedraw, renderPassDescriptor: offscreenRenderPassDescriptor1) { + GroupPass(id: "Panorama Render", enabled: configuration.renderSkybox && fullRedraw, renderPassDescriptor: offscreenRenderPassDescriptor1) { PanoramaShadingPass(id: "Panorama", scene: scene) } - GroupPass(id: "GaussianSplatRenderGroup-2", enabled: fullRedraw, renderPassDescriptor: offscreenRenderPassDescriptor2) { + GroupPass(id: "Splats Render", enabled: configuration.renderSplats && fullRedraw, renderPassDescriptor: offscreenRenderPassDescriptor2) { GaussianSplatRenderPass( id: "SplatRender", enabled: true, scene: scene, discardRate: configuration.discardRate ) + } + } #if !targetEnvironment(simulator) try SpatialUpscalingPass(id: "SpatialUpscalingPass", enabled: configuration.metalFXRate > 1 && fullRedraw, device: device, source: resources.downscaledTexture, destination: resources.outputTexture, colorProcessingMode: .perceptual) + let blitTexture = resources.outputTexture + #else + let blitTexture = resources.downscaledTexture #endif - BlitTexturePass(id: "BlitTexturePass", source: resources.outputTexture, destination: nil) + BlitTexturePass(id: "BlitTexturePass", source: blitTexture, destination: nil) } } public func drawableChanged(pixelFormat: MTLPixelFormat, size: SIMD2) throws { - print("###################", #function, pixelFormat, size) try makeResources(pixelFormat: pixelFormat, size: size) } // MARK: - private func makeResources(pixelFormat: MTLPixelFormat, size: SIMD2) throws { + #if !targetEnvironment(simulator) let downscaledSize = SIMD2(ceil(size / configuration.metalFXRate)) + #else + let downscaledSize = SIMD2(size) + #endif let colorTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: pixelFormat, width: downscaledSize.x, height: downscaledSize.y, mipmapped: false) colorTextureDescriptor.storageMode = .private diff --git a/Sources/GaussianSplatSupport/SplatCloud+Support.swift b/Sources/GaussianSplatSupport/SplatCloud+Support.swift index 8dcda381..689f77d3 100644 --- a/Sources/GaussianSplatSupport/SplatCloud+Support.swift +++ b/Sources/GaussianSplatSupport/SplatCloud+Support.swift @@ -14,6 +14,7 @@ public extension SplatCloud where Splat == SplatC { convert_b_to_c(splats) } } + let splats = try device.makeTypedBuffer(data: splatArray, options: .storageModeShared).labelled("Splats") try self.init(device: device, splats: splats) } diff --git a/Sources/GaussianSplatSupport/SplatResource.swift b/Sources/GaussianSplatSupport/SplatResource.swift index 0d6c0290..092968f5 100644 --- a/Sources/GaussianSplatSupport/SplatResource.swift +++ b/Sources/GaussianSplatSupport/SplatResource.swift @@ -3,7 +3,7 @@ import Foundation import simd import SwiftUI -public struct SplatResource: Hashable { +public struct UFOSpecifier: Hashable { public var name: String public var url: URL public var bounds: ConeBounds diff --git a/Sources/GaussianSplatSupport/UFOView.swift b/Sources/GaussianSplatSupport/UFOView.swift new file mode 100644 index 00000000..9cf47af0 --- /dev/null +++ b/Sources/GaussianSplatSupport/UFOView.swift @@ -0,0 +1,35 @@ +import Foundation +import MetalKit +import Observation +import PanoramaSupport +import simd +import SwiftUI + +// swiftlint:disable force_unwrapping + +@available(iOS 17, macOS 14, visionOS 1, *) +public struct UFOView: View { + @Environment(\.metalDevice) + private var device + + @Environment(GaussianSplatViewModel.self) + private var viewModel + + @State + private var bounds: ConeBounds + + public init(bounds: ConeBounds) { + self.bounds = bounds + } + + public var body: some View { + @Bindable + var viewModel = viewModel + + return GaussianSplatRenderView() +#if os(iOS) + .ignoresSafeArea() +#endif + .modifier(CameraConeController(cameraCone: bounds, transform: $viewModel.scene.unsafeCurrentCameraNode.transform)) + } +} diff --git a/Sources/RenderKit/Passes/BlitTexturePass.swift b/Sources/RenderKit/Passes/BlitTexturePass.swift index d59ebbe8..31c00b94 100644 --- a/Sources/RenderKit/Passes/BlitTexturePass.swift +++ b/Sources/RenderKit/Passes/BlitTexturePass.swift @@ -8,8 +8,8 @@ public struct BlitTexturePass: GeneralPassProtocol { public var id: PassID public var enabled: Bool = true - public var source: Box - public var destination: Box? + internal var source: Box + internal var destination: Box? public init(id: PassID, enabled: Bool = true, source: MTLTexture, destination: MTLTexture?) { self.id = id diff --git a/Sources/RenderKit/Passes/SpatialUpscalingPass.swift b/Sources/RenderKit/Passes/SpatialUpscalingPass.swift index b654ce17..9c586748 100644 --- a/Sources/RenderKit/Passes/SpatialUpscalingPass.swift +++ b/Sources/RenderKit/Passes/SpatialUpscalingPass.swift @@ -8,12 +8,13 @@ public struct SpatialUpscalingPass: GeneralPassProtocol { public var id: PassID public var enabled: Bool = true - public var spatialScaler: Box + internal var spatialScaler: Box public init(id: PassID, enabled: Bool = true, device: MTLDevice, source: MTLTexture, destination: MTLTexture, colorProcessingMode: MTLFXSpatialScalerColorProcessingMode) throws { self.id = id self.enabled = enabled + // TODO: We are doing this in init() when it really should happen in setup() because we can't easily cause a new setup if texture size changes. let spatialScalerDescriptor = MTLFXSpatialScalerDescriptor() spatialScalerDescriptor.inputWidth = source.width spatialScalerDescriptor.inputHeight = source.height diff --git a/Sources/RenderKit/Renderer/RenderErrorHandler.swift b/Sources/RenderKit/Renderer/RenderErrorHandler.swift index 874d0a55..94d4c202 100644 --- a/Sources/RenderKit/Renderer/RenderErrorHandler.swift +++ b/Sources/RenderKit/Renderer/RenderErrorHandler.swift @@ -16,8 +16,8 @@ public struct RenderErrorHandler: Sendable { } } -public struct RenderErrorHandlerKey: EnvironmentKey { - public static let defaultValue = RenderErrorHandler() +struct RenderErrorHandlerKey: EnvironmentKey { + static let defaultValue = RenderErrorHandler() } public extension EnvironmentValues { diff --git a/Sources/RenderKitSceneGraph/Passes/DebugRenderPass.swift b/Sources/RenderKitSceneGraph/Passes/DebugRenderPass.swift index 861e4a68..a66c6118 100644 --- a/Sources/RenderKitSceneGraph/Passes/DebugRenderPass.swift +++ b/Sources/RenderKitSceneGraph/Passes/DebugRenderPass.swift @@ -33,10 +33,7 @@ public struct DebugRenderPass: RenderPassProtocol { } public func setup(device: MTLDevice, configuration: some MetalConfigurationProtocol) throws -> State { - guard let bundle = Bundle.main.bundle(forTarget: "RenderKitShaders") else { - throw BaseError.error(.missingResource) - } - let library = try device.makeDebugLibrary(bundle: bundle) + let library = try device.makeDebugLibrary(bundle: Bundle.main.bundle(forTarget: "RenderKitShaders").safelyUnwrap()) let renderPipelineDescriptor = MTLRenderPipelineDescriptor(configuration) renderPipelineDescriptor.vertexFunction = library.makeFunction(name: "DebugVertexShader") renderPipelineDescriptor.fragmentFunction = library.makeFunction(name: "DebugFragmentShader") diff --git a/Sources/RenderKitSceneGraph/Passes/DiffuseShadingRenderPass.swift b/Sources/RenderKitSceneGraph/Passes/DiffuseShadingRenderPass.swift index 8698f875..ccc1907e 100644 --- a/Sources/RenderKitSceneGraph/Passes/DiffuseShadingRenderPass.swift +++ b/Sources/RenderKitSceneGraph/Passes/DiffuseShadingRenderPass.swift @@ -39,10 +39,7 @@ public struct DiffuseShadingRenderPass: RenderPassProtocol { } public func setup(device: MTLDevice, configuration: some MetalConfigurationProtocol) throws -> State { - guard let bundle = Bundle.main.bundle(forTarget: "RenderKitShaders") else { - throw BaseError.error(.missingResource) - } - let library = try device.makeDebugLibrary(bundle: bundle) + let library = try device.makeDebugLibrary(bundle: Bundle.main.bundle(forTarget: "RenderKitShaders").safelyUnwrap()) let useFlatShading = false let constantValues = MTLFunctionConstantValues(dictionary: [0: useFlatShading]) let renderPipelineDescriptor = MTLRenderPipelineDescriptor(configuration) diff --git a/Sources/RenderKitSceneGraph/Passes/PanoramaShadingPass.swift b/Sources/RenderKitSceneGraph/Passes/PanoramaShadingPass.swift index 0cfd70db..a4107649 100644 --- a/Sources/RenderKitSceneGraph/Passes/PanoramaShadingPass.swift +++ b/Sources/RenderKitSceneGraph/Passes/PanoramaShadingPass.swift @@ -55,10 +55,7 @@ public struct PanoramaShadingPass: RenderPassProtocol { } public func setup(device: MTLDevice, configuration: some MetalConfigurationProtocol) throws -> State { - guard let bundle = Bundle.main.bundle(forTarget: "RenderKitShaders") else { - throw BaseError.error(.missingResource) - } - let library = try device.makeDebugLibrary(bundle: bundle) + let library = try device.makeDebugLibrary(bundle: .main.bundle(forTarget: "RenderKitShaders", recursive: true).safelyUnwrap()) let renderPipelineDescriptor = MTLRenderPipelineDescriptor(configuration) renderPipelineDescriptor.vertexFunction = library.makeFunction(name: "UnlitShader::unlitVertexShader") let constantValues = MTLFunctionConstantValues(dictionary: [1: UInt32(1)]) @@ -67,6 +64,14 @@ public struct PanoramaShadingPass: RenderPassProtocol { let depthStencilState = try device.makeDepthStencilState(descriptor: depthStencilDescriptor).safelyUnwrap(BaseError.resourceCreationFailure) renderPipelineDescriptor.label = "\(type(of: self))" renderPipelineDescriptor.vertexDescriptor = MTLVertexDescriptor(MDLVertexDescriptor.simpleVertexDescriptor) + renderPipelineDescriptor.colorAttachments[0].isBlendingEnabled = true + renderPipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add + renderPipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add + renderPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha + renderPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha + renderPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha + renderPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha + let (renderPipelineState, reflection) = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor, options: [.bindingInfo]) var bindings = State.Bindings() try bindings.updateBindings(with: reflection) diff --git a/Sources/RenderKitSceneGraph/Passes/UnlitShadingPass.swift b/Sources/RenderKitSceneGraph/Passes/UnlitShadingPass.swift index dbe8c5d7..3465d71a 100644 --- a/Sources/RenderKitSceneGraph/Passes/UnlitShadingPass.swift +++ b/Sources/RenderKitSceneGraph/Passes/UnlitShadingPass.swift @@ -55,10 +55,7 @@ public struct UnlitShadingPass: RenderPassProtocol { } public func setup(device: MTLDevice, configuration: some MetalConfigurationProtocol) throws -> State { - guard let bundle = Bundle.main.bundle(forTarget: "RenderKitShaders") else { - throw BaseError.error(.missingResource) - } - let library = try device.makeDebugLibrary(bundle: bundle) + let library = try device.makeDebugLibrary(bundle: .main.bundle(forTarget: "RenderKitShaders", recursive: true).safelyUnwrap()) let renderPipelineDescriptor = MTLRenderPipelineDescriptor(configuration) renderPipelineDescriptor.vertexFunction = library.makeFunction(name: "UnlitShader::unlitVertexShader") let constantValues = MTLFunctionConstantValues(dictionary: [1: UInt32(0)]) // TODO: Use name diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatConfigurationView.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatConfigurationView.swift deleted file mode 100644 index f126c3f4..00000000 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatConfigurationView.swift +++ /dev/null @@ -1,41 +0,0 @@ -import GaussianSplatSupport -import SwiftUI - -struct GaussianSplatConfigurationView: View { - @Binding - var configuration: GaussianSplatConfiguration - - var body: some View { - LabeledContent("MetalFX Rate") { - VStack(alignment: .leading) { - TextField("MetalFX Rate", value: $configuration.metalFXRate, format: .number) - .labelsHidden() - Text("This is how much to downscale the splat cloud before rendering, using MetalFX to for AI upscaling.").font(.caption) - } - } - LabeledContent("Discard Rate") { - VStack(alignment: .leading) { - TextField("Discard Rate", value: $configuration.discardRate, format: .number) - .labelsHidden() - Text("This is the minimum rate for alpha to show a splat. (Should be zero. Higher values mean more splats will be discarded as they are too transparent.)").font(.caption) - } - } - LabeledContent("Vertical Angle of View") { - VStack(alignment: .leading) { - TextField("AoV", value: $configuration.verticalAngleOfView.degrees, format: .number) - .labelsHidden() - Text("Vertical Angle of View (FOV) in degrees.").font(.caption) - } - } - LabeledContent("GPU Sort") { - VStack(alignment: .leading) { - Picker("Sort Method", selection: $configuration.sortMethod) { - Text("GPU Bitonic").tag(GaussianSplatConfiguration.SortMethod.gpuBitonic) - Text("CPU Radix").tag(GaussianSplatConfiguration.SortMethod.cpuRadix) - } - .labelsHidden() - Text("Use GPU Sorting").font(.caption) - } - } - } -} diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos+Support.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos+Support.swift index 69b5f48f..614719e1 100644 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos+Support.swift +++ b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos+Support.swift @@ -6,6 +6,7 @@ import Metal import RenderKit import RenderKitSceneGraph import UniformTypeIdentifiers +import SwiftUI // swiftlint:disable force_unwrapping @@ -40,51 +41,34 @@ extension UTType { static let splat = UTType(filenameExtension: "splat")! } -extension CGImage { - func convert(bitmapInfo: CGBitmapInfo) -> CGImage? { - let width = width - let height = height - let bitsPerComponent = 8 - let bytesPerPixel = 4 - let bytesPerRow = width * bytesPerPixel - let colorSpace = CGColorSpaceCreateDeviceRGB() - guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else { - return nil - } - context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height)) - return context.makeImage() - } -} -func convertCGImageEndianness2(_ inputImage: CGImage) -> CGImage? { - let width = inputImage.width - let height = inputImage.height - let bitsPerComponent = 8 - let bytesPerPixel = 4 - let bytesPerRow = width * bytesPerPixel - let colorSpace = CGColorSpaceCreateDeviceRGB() +struct PopupHelpButton: View { - // Choose the appropriate bitmap info for the target endianness - let bitmapInfo: CGBitmapInfo - if inputImage.byteOrderInfo == .order32Little { - bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) - } else { - bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) - } + @State + var isPresented: Bool = false - guard let context = CGContext(data: nil, - width: width, - height: height, - bitsPerComponent: bitsPerComponent, - bytesPerRow: bytesPerRow, - space: colorSpace, - bitmapInfo: bitmapInfo.rawValue) else { - return nil - } + var help: String - // Draw the original image into the new context - context.draw(inputImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + var body: some View { + + Button { + isPresented = true + } label: { + Image(systemName: "questionmark.circle") + } + #if os(macOS) + .buttonStyle(.link) + #endif + .popover(isPresented: $isPresented) { + Text(help) + .font(.caption) + .padding() + #if os(iOS) + .frame(maxHeight: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + .presentationCompactAdaptation(.popover) + #endif + } + } - // Create a new CGImage from the context - return context.makeImage() } diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos.swift index 63876898..178e00a3 100644 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos.swift +++ b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos.swift @@ -1,21 +1,16 @@ import Foundation import GaussianSplatSupport +import SwiftUI -extension GaussianSplatLobbyView: DemoView { - static let testData: [SplatResource] = [ +extension GaussianSplatLobbyView { + static let testData: [UFOSpecifier] = [ .init(name: "Steve 1", url: URL(string: "https://s.zillowstatic.com/z3d-home/models/ufo_demo/steve_1.splat")!, bounds: .init(bottomHeight: 0.085, bottomInnerRadius: 0.25, topHeight: 0.7, topInnerRadius: 0.9)), .init(name: "Test 1", url: URL(string: "https://s.zillowstatic.com/z3d-home/models/ufo_demo/test1.splat")!, bounds: .init(bottomHeight: 0.05, bottomInnerRadius: 0.4, topHeight: 0.8, topInnerRadius: 0.8)), .init(name: "Test 2", url: URL(string: "https://s.zillowstatic.com/z3d-home/models/ufo_demo/test2.splat")!, bounds: .init(bottomHeight: 0.08, bottomInnerRadius: 0.8, topHeight: 1, topInnerRadius: 0.9)), .init(name: "Test 3", url: URL(string: "https://s.zillowstatic.com/z3d-home/models/ufo_demo/test3.splat")!, bounds: .init(bottomHeight: -0.09, bottomInnerRadius: 0.6, topHeight: 0.9, topInnerRadius: 1.3)), ] - init() { - self.init(sources: Self.testData) - - // [ - // // Bundle.main.url(forResource: "vision_dr", withExtension: "splat", recursive: true)!, - // URL(string: "https://s.zillowstatic.com/z3d-home/models/ufo_demo/test1.splat")!, - // URL(string: "https://s.zillowstatic.com/z3d-home/models/ufo_demo/steve_1.splat")!, - // ]) + public init(navigationPath: Binding) { + self.init(navigationPath: navigationPath, sources: Self.testData) } } diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatExtraViews.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatExtraViews.swift new file mode 100644 index 00000000..a1fdae2a --- /dev/null +++ b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatExtraViews.swift @@ -0,0 +1,92 @@ +import GaussianSplatSupport +import SwiftUI + +struct OptionsView: View { + struct Options { + var showInfo: Bool = false + var showCounters: Bool = false + } + + @Binding + var options: Options + + @Binding + var configuration: GaussianSplatConfiguration + + var body: some View { + Toggle("Render Splats", isOn: $configuration.renderSplats) + Toggle("Render Skybox", isOn: $configuration.renderSkybox) + Toggle("Show Info", isOn: $options.showInfo) +// Toggle("Show Counters", isOn: $options.showCounters) + + LabeledContent("MetalFX Rate") { + TextField("MetalFX Rate", value: $configuration.metalFXRate, format: .number) + .multilineTextAlignment(.trailing) + } + LabeledContent("Discard Rate") { + TextField("Discard Rate", value: $configuration.discardRate, format: .number) + .multilineTextAlignment(.trailing) + } + + LabeledContent("Vertical Angle of View") { + TextField("Vertical Angle of View", value: $configuration.verticalAngleOfView.degrees, format: .number) + .multilineTextAlignment(.trailing) + } + Picker("Sort Method", selection: $configuration.sortMethod) { + Text("GPU Bitonic").tag(GaussianSplatConfiguration.SortMethod.gpuBitonic) + Text("CPU Radix").tag(GaussianSplatConfiguration.SortMethod.cpuRadix) + } + #if os(iOS) + .pickerStyle(.navigationLink) + #endif + +// @ViewBuilder +// var optionsView: some View { +// Section("Options") { +// LabeledContent("GPU Counters") { +// VStack(alignment: .leading) { +// Toggle("GPU Counters", isOn: $useGPUCounters) +// .labelsHidden() +// Text("Show info on framerate, GPU usage etc.").font(.caption) +// } +// } +// LabeledContent("Background Color") { +// VStack(alignment: .leading) { +// ColorPicker("Background Color", selection: $backgroundColor) +// .labelsHidden() +// Text("Colour of background (behind the splats)").font(.caption) +// } +// } +// LabeledContent("Skybox Gradient") { +// VStack(alignment: .leading) { +// Toggle("Skybox Gradient", isOn: $useSkyboxGradient) +// if useSkyboxGradient { +// LinearGradientEditor(value: $skyboxGradient) +// } +// Text("Use a gradient skybox (above the background color, behind the splats)").font(.caption) +// } +// } +// LabeledContent("Progressive Load") { +// VStack(alignment: .leading) { +// Toggle("Progressive Load", isOn: $progressiveLoad) +// .labelsHidden() +// Text("Stream splats in (remote splats only).").font(.caption) +// } +// } +// } + + + } +} + +struct InfoView: View { + @Environment(GaussianSplatViewModel.self) + private var viewModel + + var body: some View { + VStack { + Text("# splats: \(viewModel.splatCloud.capacity.formatted())") + Text("Sort method: \(viewModel.configuration.sortMethod)") + } + } +} diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLoadingView.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLoadingView.swift index 50dab86e..4961ce8c 100644 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLoadingView.swift +++ b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLoadingView.swift @@ -8,11 +8,8 @@ public struct GaussianSplatLoadingView: View { @Environment(\.metalDevice) private var device - let url: URL - let splatResource: SplatResource + let source: UFOSpecifier let configuration: GaussianSplatConfiguration - let progressiveLoad: Bool - let bounds: ConeBounds @State private var subtitle: String = "Processing" @@ -20,19 +17,29 @@ public struct GaussianSplatLoadingView: View { @State private var viewModel: GaussianSplatViewModel? - public init(url: URL, splatResource: SplatResource, bounds: ConeBounds, initialConfiguration: GaussianSplatConfiguration, progressiveLoad: Bool) { - self.url = url - self.splatResource = splatResource - self.bounds = bounds - self.configuration = initialConfiguration - self.progressiveLoad = progressiveLoad + @AppStorage("ufo-progressive-load") + private var progressiveLoad: Bool = true + + @AppStorage("ufo-view") + private var useUFOView = false + + public init(source: UFOSpecifier) { + self.source = source + self.configuration = GaussianSplatConfiguration(skyboxTexture: GaussianSplatConfiguration.defaultSkyboxTexture(device: MTLCreateSystemDefaultDevice()!)) } public var body: some View { ZStack { if let viewModel { - GaussianSplatView(bounds: bounds) + if useUFOView { + UFOView(bounds: source.bounds) .environment(viewModel) + + } + else { + GaussianSplatView(bounds: source.bounds) + .environment(viewModel) + } } else { VStack { @@ -44,18 +51,18 @@ public struct GaussianSplatLoadingView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .task { do { - switch (progressiveLoad, url.scheme) { + switch (progressiveLoad, source.url.scheme) { case (true, "http"), (true, "https"): subtitle = "Streaming" - viewModel = try! await GaussianSplatViewModel(device: device, splatResource: splatResource, progressiveURL: url, configuration: configuration, logger: Logger()) + viewModel = try! await GaussianSplatViewModel(device: device, source: source, progressiveURL: source.url, configuration: configuration, logger: Logger()) Task.detached { - try await viewModel?.streamingLoad(url: url) + try await viewModel?.streamingLoad(url: source.url) } case (false, "http"), (false, "https"): subtitle = "Downloading" Task.detached { let session = URLSession.shared - let request = URLRequest(url: url) + let request = await URLRequest(url: source.url) let (downloadedUrl, response) = try await session.download(for: request) guard let response = response as? HTTPURLResponse else { fatalError("Oops") @@ -67,12 +74,12 @@ public struct GaussianSplatLoadingView: View { try FileManager().createSymbolicLink(at: url, withDestinationURL: downloadedUrl) try await MainActor.run { let splatCloud = try SplatCloud(device: device, url: url) - viewModel = try! GaussianSplatViewModel(device: device, splatResource: splatResource, splatCloud: splatCloud, configuration: configuration, logger: Logger()) + viewModel = try! GaussianSplatViewModel(device: device, splatCloud: splatCloud, configuration: configuration, logger: Logger()) } } default: - let splatCloud = try SplatCloud(device: device, url: url) - viewModel = try! GaussianSplatViewModel(device: device, splatResource: splatResource, splatCloud: splatCloud, configuration: configuration, logger: Logger()) + let splatCloud = try SplatCloud(device: device, url: source.url) + viewModel = try! GaussianSplatViewModel(device: device, splatCloud: splatCloud, configuration: configuration, logger: Logger()) } } catch { @@ -83,7 +90,7 @@ public struct GaussianSplatLoadingView: View { } extension GaussianSplatViewModel where Splat == SplatC { - public convenience init(device: MTLDevice, splatResource: SplatResource, progressiveURL url: URL, configuration: GaussianSplatConfiguration, logger: Logger? = nil) async throws { + public convenience init(device: MTLDevice, source: UFOSpecifier, progressiveURL url: URL, configuration: GaussianSplatConfiguration, logger: Logger? = nil) async throws { assert(MemoryLayout.stride == MemoryLayout.size) let session = URLSession.shared // Perform a HEAD request to compute the number of splats. @@ -104,7 +111,7 @@ extension GaussianSplatViewModel where Splat == SplatC { } let splatCount = contentLength / MemoryLayout.stride - try self.init(device: device, splatResource: splatResource, splatCapacity: splatCount, configuration: configuration, logger: logger) + try self.init(device: device, splatCapacity: splatCount, configuration: configuration, logger: logger) } func streamingLoad(url: URL) async throws { diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLobbyView.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLobbyView.swift index 5f7c52a5..cfdb05c4 100644 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLobbyView.swift +++ b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLobbyView.swift @@ -5,65 +5,32 @@ import RenderKit import SwiftUI public struct GaussianSplatLobbyView: View { - @Environment(\.metalDevice) - var device + @Binding + private var navigationPath: NavigationPath - @State - private var configuration: GaussianSplatConfiguration = .init() + let sources: [UFOSpecifier] - @State + @AppStorage("gpu-counters") private var useGPUCounters = false - @State - private var progressiveLoad = true + @AppStorage("ufo-progressive-load") + private var progressiveLoad: Bool = true - @State - private var backgroundColor = Color.white + @AppStorage("ufo-view") + private var useUFOView = false - @State - private var useSkyboxGradient = true - - @State - private var skyboxGradient: LinearGradient = .init( - stops: [ - .init(color: .white, location: 0), - .init(color: .white, location: 0.4), - .init(color: Color(red: 0.5294117647058824, green: 0.807843137254902, blue: 0.9215686274509803), location: 0.5), - .init(color: Color(red: 0.5294117647058824, green: 0.807843137254902, blue: 0.9215686274509803), location: 1) - ], - startPoint: .init(x: 0, y: 0), - endPoint: .init(x: 0, y: 1) - ) - - @State - private var source: SplatResource - - let sources: [SplatResource] - - enum Mode { - case config - case render - } - - @State - private var mode: Mode = .config - - init(sources: [SplatResource]) { + init(navigationPath: Binding, sources: [UFOSpecifier]) { + self._navigationPath = navigationPath self.sources = sources - self.source = sources.first! } public var body: some View { - Group { - switch mode { - case .config: - VStack { - Form { - LabeledContent("Source") { - Picker("Source", selection: $source) { + List { + Section("UFOs") { ForEach(sources, id: \.self) { source in Label { - Text(source.name) + NavigationLink(source.name, value: NavigationAtom.ufo(source)) + .frame(maxWidth: .infinity) } icon: { switch source.url.scheme { case "file": @@ -75,124 +42,37 @@ public struct GaussianSplatLobbyView: View { } } .tag(source) - } } - .labelsHidden() } - optionsView + Section("Options") { + Toggle(isOn: $progressiveLoad) { + HStack { + Text("Progressive Load") + Spacer() + PopupHelpButton(help: "Use HTTP streaming to load splats progressively.") } - Button("Use Debug Colors") { - backgroundColor = .init(red: 1, green: 1, blue: 1) - skyboxGradient = .init(stops: [ - .init(color: .init(red: 1, green: 0, blue: 0).opacity(0), location: 1), - .init(color: .init(red: 1, green: 0, blue: 0).opacity(1), location: 0) - ], - startPoint: .init(x: 0, y: 0), - endPoint: .init(x: 0, y: 1) - ) } - Button("Go!") { - // configuration.bounds = source.bounds - mode = .render + Toggle(isOn: $useGPUCounters) { + HStack { + Text("Use GPU Counters") + Spacer() + PopupHelpButton(help: "TODO") } - .frame(maxWidth: .infinity) - .buttonStyle(.borderedProminent) } - .onChange(of: useGPUCounters, initial: true) { - if useGPUCounters { - let gpuCounters = try! GPUCounters(device: device) - configuration.gpuCounters = gpuCounters + .disabled(true) + Toggle(isOn: $useUFOView) { + HStack { + Text("UFOView") + Spacer() + PopupHelpButton(help: "Uses a reduce-functionality `UFOView` to render the splats. This view doesn't have any bells & whistles that are only useful for testing/debugging.") } - else { - configuration.gpuCounters = nil } + + } - .onChange(of: backgroundColor, initial: true) { - let color = backgroundColor.resolve(in: .init()).cgColor.converted(to: CGColorSpaceCreateDeviceRGB(), intent: .perceptual, options: nil)! - let components = color.components! - configuration.clearColor = MTLClearColor(red: components[0], green: components[1], blue: components[2], alpha: components[3]) - } - .onChange(of: skyboxGradient, initial: true) { - updateSkyboxTexture() } #if os(macOS) .frame(width: 320) #endif - case .render: - GaussianSplatLoadingView(url: source.url, splatResource: source, bounds: source.bounds, initialConfiguration: configuration, progressiveLoad: progressiveLoad) - .toolbar { - ToolbarItem(placement: .navigation) { - Button("Back") { - mode = .config - } - #if os(macOS) - .buttonStyle(.link) - #endif - } - } - .environment(\.gpuCounters, configuration.gpuCounters) - } - } - } - - func updateSkyboxTexture() { - if useSkyboxGradient { - let image = skyboxGradient.image(size: .init(width: 1024, height: 1024)) - - guard var cgImage = ImageRenderer(content: image).cgImage else { - fatalError("Could not render image.") - } - let bitmapInfo: CGBitmapInfo - if cgImage.byteOrderInfo == .order32Little { - bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) - } else { - bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) - } - cgImage = cgImage.convert(bitmapInfo: bitmapInfo)! - let textureLoader = MTKTextureLoader(device: device) - let texture = try! textureLoader.newTexture(cgImage: cgImage, options: nil) - texture.label = "Skybox Gradient" - configuration.skyboxTexture = texture - } - else { - configuration.skyboxTexture = nil - } - } - - @ViewBuilder - var optionsView: some View { - Section("Options") { - LabeledContent("GPU Counters") { - VStack(alignment: .leading) { - Toggle("GPU Counters", isOn: $useGPUCounters) - .labelsHidden() - Text("Show info on framerate, GPU usage etc.").font(.caption) - } - } - LabeledContent("Background Color") { - VStack(alignment: .leading) { - ColorPicker("Background Color", selection: $backgroundColor) - .labelsHidden() - Text("Colour of background (behind the splats)").font(.caption) - } - } - LabeledContent("Skybox Gradient") { - VStack(alignment: .leading) { - Toggle("Skybox Gradient", isOn: $useSkyboxGradient) - if useSkyboxGradient { - LinearGradientEditor(value: $skyboxGradient) - } - Text("Use a gradient skybox (above the background color, behind the splats)").font(.caption) - } - } - LabeledContent("Progressive Load") { - VStack(alignment: .leading) { - Toggle("Progressive Load", isOn: $progressiveLoad) - .labelsHidden() - Text("Stream splats in (remote splats only).").font(.caption) - } - } - GaussianSplatConfigurationView(configuration: $configuration) - } } } diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatView.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatView.swift index 1e0b0725..1a4a37ea 100644 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatView.swift +++ b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatView.swift @@ -57,14 +57,26 @@ internal struct GaussianSplatView: View { } .background(.black) .toolbar { - Button(systemImage: "gear") { + Button("Options") { showOptions.toggle() } .buttonStyle(.borderless) .padding() .popover(isPresented: $showOptions) { + NavigationStack { + Form { OptionsView(options: $options, configuration: $viewModel.configuration) + } + .toolbar { + Button("Close") { + showOptions = false + } + } + } +#if os(macOS) .padding() +#endif + } } .overlay(alignment: .top) { @@ -83,15 +95,8 @@ internal struct GaussianSplatView: View { } } } - if options.showTraces { - TracesView(traces: .shared) - .allowsHitTesting(false) - .padding() - .background(.thinMaterial) - .padding() } } - } .overlay(alignment: .bottom) { if !viewModel.loadProgress.isFinished { ProgressView(viewModel.loadProgress) @@ -100,44 +105,7 @@ internal struct GaussianSplatView: View { .background(.thinMaterial) .cornerRadius(8) .padding() - } - } - } -} - -private struct OptionsView: View { - struct Options { - var showInfo: Bool = true - var showTraces: Bool = true - var showCounters: Bool = true - } - - @Binding - var options: Options - - @Binding - var configuration: GaussianSplatConfiguration - - var body: some View { - Form { - Toggle("Show Info", isOn: $options.showInfo) - Toggle("Show Traces", isOn: $options.showTraces) - Toggle("Show Counters", isOn: $options.showCounters) - GaussianSplatConfigurationView(configuration: $configuration) - } } -} - -private struct InfoView: View { - @Environment(GaussianSplatViewModel.self) - private var viewModel - - var body: some View { - VStack { - Text(viewModel.splatResource.name).font(.title) - Link(viewModel.splatResource.url.absoluteString, destination: viewModel.splatResource.url) - Text(viewModel.splatCloud.capacity, format: .number) - Text("\(viewModel.configuration.sortMethod)") } } } diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/LinearGradientEditor.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/LinearGradientEditor.swift deleted file mode 100644 index 6f5ddebf..00000000 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/LinearGradientEditor.swift +++ /dev/null @@ -1,77 +0,0 @@ -import SwiftUI - -struct LinearGradient: Equatable { - var stops: [Gradient.Stop] = [] - var startPoint: UnitPoint - var endPoint: UnitPoint -} - -extension LinearGradient { - func shading(size: CGSize) -> GraphicsContext.Shading { - let gradient = Gradient(stops: stops.sorted { $0.location < $1.location }) - return .linearGradient(gradient, startPoint: CGPoint(x: size.width * startPoint.x, y: size.height * startPoint.y), endPoint: CGPoint(x: size.width * endPoint.x, y: size.height * endPoint.y)) - } - - func image(size: CGSize) -> Image { - Image(size: size) { context in - context.fill(Path(CGRect(origin: .zero, size: size)), with: shading(size: size)) - } - } -} - -struct LinearGradientEditor: View { - @Binding - var value: LinearGradient - - @State - private var showPopover: Bool = false - - var body: some View { - Button(role: .none) { - showPopover = true - } label: { - Canvas { context, size in - context.fill(Path(CGRect(origin: .zero, size: size)), with: value.shading(size: size)) - } - } - .frame(width: 64, height: 64) - .popover(isPresented: $showPopover) { - Form { - Canvas { context, size in - context.fill(Path(CGRect(origin: .zero, size: size)), with: value.shading(size: size)) - } - .frame(width: 100, height: 100) - Section("Start") { - TextField("X", value: $value.startPoint.x.double, format: .number) - TextField("Y", value: $value.startPoint.y.double, format: .number) - } - Section("End") { - TextField("X", value: $value.endPoint.x.double, format: .number) - TextField("Y", value: $value.endPoint.y.double, format: .number) - } - Section("Stops") { - List($value.stops.indices, id: \.self) { index in - HStack { - ColorPicker("Color \(index + 1)", selection: $value.stops[index].color) - TextField("Stop \(index + 1)", value: $value.stops[index].location.double, format: .number) - } - .labelsHidden() - } - } - } - .padding() - .frame(minWidth: 240, minHeight: 480) - } - } -} - -private extension CGFloat { - var double: Double { - get { - self - } - set { - self = newValue - } - } -} diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/SplatResource.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/SplatResource.swift new file mode 100644 index 00000000..cab4d88c --- /dev/null +++ b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/SplatResource.swift @@ -0,0 +1,16 @@ +import Foundation +import PanoramaSupport +import simd +import SwiftUI + +public struct UFOSpecifier: Hashable { + public var name: String + public var url: URL + public var bounds: ConeBounds + + public init(name: String, url: URL, bounds: ConeBounds) { + self.name = name + self.url = url + self.bounds = bounds + } +} diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/UFOView.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/UFOView.swift new file mode 100644 index 00000000..e62a58e9 --- /dev/null +++ b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/UFOView.swift @@ -0,0 +1,35 @@ +import Foundation +import GaussianSplatSupport +import MetalKit +import Observation +import PanoramaSupport +import simd +import SwiftUI + +// swiftlint:disable force_unwrapping + +public struct UFOView: View { + @Environment(\.metalDevice) + private var device + + @Environment(GaussianSplatViewModel.self) + private var viewModel + + @State + private var bounds: ConeBounds + + public init(bounds: ConeBounds) { + self.bounds = bounds + } + + public var body: some View { + @Bindable + var viewModel = viewModel + + return GaussianSplatRenderView() +#if os(iOS) + .ignoresSafeArea() +#endif + .modifier(CameraConeController(cameraCone: bounds, transform: $viewModel.scene.unsafeCurrentCameraNode.transform)) + } +}