From b24aebcd8aea14ba1c3519d2835398a0b8e03e3f Mon Sep 17 00:00:00 2001 From: Jonathan Wight Date: Thu, 24 Oct 2024 09:37:33 -0700 Subject: [PATCH] Merge from upstream --- Sources/BaseSupport/Errors.swift | 8 + .../Constraints3D/DraggableParameter.swift | 5 +- .../include/GaussianSplatRenderShaders.h | 6 +- .../GaussianSplatConfiguration.swift | 65 ++++++ .../GaussianSplatSupport.swift | 77 +++---- .../GaussianSplatViewModel.swift | 69 ++---- .../SplatCloud+Support.swift | 1 + .../GaussianSplatSupport/SplatResource.swift | 2 +- .../RenderKit/Passes/BlitTexturePass.swift | 4 +- .../Passes/SpatialUpscalingPass.swift | 3 +- .../Renderer/RenderErrorHandler.swift | 4 +- .../Passes/DebugRenderPass.swift | 5 +- .../Passes/DiffuseShadingRenderPass.swift | 5 +- .../Passes/PanoramaShadingPass.swift | 13 +- .../Passes/UnlitShadingPass.swift | 5 +- Sources/SwiftGraphicsDemos/DemosScene.swift | 2 +- .../GaussianSplatConfigurationView.swift | 41 ---- .../GaussianSplatDemos+Support.swift | 68 +++--- .../GaussianSplatDemos.swift | 21 -- .../GaussianSplatExtraViews.swift | 92 ++++++++ .../GaussianSplatLoadingView.swift | 135 ------------ .../GaussianSplatLobbyView.swift | 198 ------------------ .../GaussianSplatView.swift | 58 ++--- .../LinearGradientEditor.swift | 77 ------- 24 files changed, 285 insertions(+), 679 deletions(-) create mode 100644 Sources/GaussianSplatSupport/GaussianSplatConfiguration.swift delete mode 100644 Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatConfigurationView.swift delete mode 100644 Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos.swift create mode 100644 Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatExtraViews.swift delete mode 100644 Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLoadingView.swift delete mode 100644 Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLobbyView.swift delete mode 100644 Sources/SwiftGraphicsDemos/GaussianSplatDemos/LinearGradientEditor.swift diff --git a/Sources/BaseSupport/Errors.swift b/Sources/BaseSupport/Errors.swift index a4be0d67..b8a2bfc9 100644 --- a/Sources/BaseSupport/Errors.swift +++ b/Sources/BaseSupport/Errors.swift @@ -46,6 +46,14 @@ public func unreachable(_ message: @autoclosure () -> String = String(), file: S // MARK: - public extension Optional { + func safelyUnwrap() throws -> Wrapped { + // swiftlint:disable:next shorthand_optional_binding + guard let self = self else { + throw BaseError.error(BaseError.resourceCreationFailure) + } + return self + } + func safelyUnwrap(_ error: @autoclosure () -> Error) throws -> Wrapped { // swiftlint:disable:next shorthand_optional_binding guard let self = self else { 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..7e07df55 --- /dev/null +++ b/Sources/GaussianSplatSupport/GaussianSplatConfiguration.swift @@ -0,0 +1,65 @@ +import Metal +#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..8d331fd1 100644 --- a/Sources/GaussianSplatSupport/GaussianSplatViewModel.swift +++ b/Sources/GaussianSplatSupport/GaussianSplatViewModel.swift @@ -13,38 +13,8 @@ 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 public class GaussianSplatViewModel where Splat: SplatProtocol { @@ -62,8 +32,6 @@ public class GaussianSplatViewModel where Splat: SplatProtocol { } } - public var splatResource: SplatResource - public var pass: GroupPass? public var loadProgress = Progress() @@ -100,9 +68,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 +131,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 +139,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 @@ -290,11 +257,3 @@ extension MTLSize { depth == 1 ? "\(width)x\(height)" : "\(width)x\(height)x\(depth)" } } - -// MARK: - - -public extension GaussianSplatViewModel where Splat == SplatC { - convenience init(device: MTLDevice, splatResource: SplatResource, splatCapacity: Int, configuration: GaussianSplatConfiguration, logger: Logger? = nil) throws { - try self.init(device: device, splatResource: splatResource, splatCloud: SplatCloud(device: device, capacity: splatCapacity), configuration: configuration, logger: logger) - } -} 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/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/DemosScene.swift b/Sources/SwiftGraphicsDemos/DemosScene.swift index a0901ab7..86349682 100644 --- a/Sources/SwiftGraphicsDemos/DemosScene.swift +++ b/Sources/SwiftGraphicsDemos/DemosScene.swift @@ -69,7 +69,7 @@ struct DemosView: View { var body: some View { NavigationSplitView { List(selection: $currentDemo) { - row(for: GaussianSplatLobbyView.self) +// row(for: GaussianSplatLobbyView.self) row(for: LineGeometryShaderView.self) row(for: CustomStrokeEditorDemoView.self) row(for: CameraControllerDemo.self) 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 deleted file mode 100644 index 63876898..00000000 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatDemos.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import GaussianSplatSupport - -extension GaussianSplatLobbyView: DemoView { - static let testData: [SplatResource] = [ - .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")!, - // ]) - } -} 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 deleted file mode 100644 index 50dab86e..00000000 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLoadingView.swift +++ /dev/null @@ -1,135 +0,0 @@ -import BaseSupport -import Constraints3D -import GaussianSplatSupport -import os -import SwiftUI - -public struct GaussianSplatLoadingView: View { - @Environment(\.metalDevice) - private var device - - let url: URL - let splatResource: SplatResource - let configuration: GaussianSplatConfiguration - let progressiveLoad: Bool - let bounds: ConeBounds - - @State - private var subtitle: String = "Processing" - - @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 - } - - public var body: some View { - ZStack { - if let viewModel { - GaussianSplatView(bounds: bounds) - .environment(viewModel) - } - else { - VStack { - ProgressView() - Text(subtitle) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .task { - do { - switch (progressiveLoad, url.scheme) { - case (true, "http"), (true, "https"): - subtitle = "Streaming" - viewModel = try! await GaussianSplatViewModel(device: device, splatResource: splatResource, progressiveURL: url, configuration: configuration, logger: Logger()) - Task.detached { - try await viewModel?.streamingLoad(url: url) - } - case (false, "http"), (false, "https"): - subtitle = "Downloading" - Task.detached { - let session = URLSession.shared - let request = URLRequest(url: url) - let (downloadedUrl, response) = try await session.download(for: request) - guard let response = response as? HTTPURLResponse else { - fatalError("Oops") - } - guard response.statusCode == 200 else { - throw BaseError.missingResource - } - let url = downloadedUrl.appendingPathExtension("splat") - 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()) - } - } - default: - let splatCloud = try SplatCloud(device: device, url: url) - viewModel = try! GaussianSplatViewModel(device: device, splatResource: splatResource, splatCloud: splatCloud, configuration: configuration, logger: Logger()) - } - } - catch { - fatalError(error) - } - } - } -} - -extension GaussianSplatViewModel where Splat == SplatC { - public convenience init(device: MTLDevice, splatResource: SplatResource, 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. - var headRequest = URLRequest(url: url) - headRequest.httpMethod = "HEAD" - let (_, headResponse) = try await session.data(for: headRequest) - guard let headResponse = headResponse as? HTTPURLResponse else { - fatalError("Oops") - } - guard headResponse.statusCode == 200 else { - throw BaseError.missingResource - } - guard let contentLength = try (headResponse.allHeaderFields["Content-Length"] as? String).map(Int.init)?.safelyUnwrap(BaseError.optionalUnwrapFailure) else { - fatalError("Oops") - } - guard contentLength.isMultiple(of: MemoryLayout.stride) else { - fatalError("Not an even multiple of \(MemoryLayout.stride)") - } - let splatCount = contentLength / MemoryLayout.stride - - try self.init(device: device, splatResource: splatResource, splatCapacity: splatCount, configuration: configuration, logger: logger) - } - - func streamingLoad(url: URL) async throws { - let session = URLSession.shared - loadProgress.totalUnitCount = Int64(splatCloud.capacity) - let request = URLRequest(url: url) - let (byteStream, bytesResponse) = try await session.bytes(for: request) - guard let bytesResponse = bytesResponse as? HTTPURLResponse else { - fatalError("Oops") - } - guard bytesResponse.statusCode == 200 else { - throw BaseError.missingResource - } - let splatStream = byteStream.chunks(ofCount: MemoryLayout.stride).map { bytes in - bytes.withUnsafeBytes { buffer in - let splatB = buffer.load(as: SplatB.self) - return SplatC(splatB) - } - } - .chunks(ofCount: 2048) - for try await splats in splatStream { - try splatCloud.append(splats: splats) - self.requestSort() - loadProgress.completedUnitCount = Int64(splatCloud.count) - } - assert(splatCloud.count == splatCloud.capacity) - } -} diff --git a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLobbyView.swift b/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLobbyView.swift deleted file mode 100644 index 5f7c52a5..00000000 --- a/Sources/SwiftGraphicsDemos/GaussianSplatDemos/GaussianSplatLobbyView.swift +++ /dev/null @@ -1,198 +0,0 @@ -import Constraints3D -import GaussianSplatSupport -import MetalKit -import RenderKit -import SwiftUI - -public struct GaussianSplatLobbyView: View { - @Environment(\.metalDevice) - var device - - @State - private var configuration: GaussianSplatConfiguration = .init() - - @State - private var useGPUCounters = false - - @State - private var progressiveLoad = true - - @State - private var backgroundColor = Color.white - - @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]) { - 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) { - ForEach(sources, id: \.self) { source in - Label { - Text(source.name) - } icon: { - switch source.url.scheme { - case "file": - Image(systemName: "doc") - case "http", "https": - Image(systemName: "globe") - default: - EmptyView() - } - } - .tag(source) - } - } - .labelsHidden() - } - optionsView - } - 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 - } - .frame(maxWidth: .infinity) - .buttonStyle(.borderedProminent) - } - .onChange(of: useGPUCounters, initial: true) { - if useGPUCounters { - let gpuCounters = try! GPUCounters(device: device) - configuration.gpuCounters = gpuCounters - } - 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 - } - } -}