import Foundation import UIKit import Display import MetalEngine import MetalKit private final class BundleMarker: NSObject { } private var metalLibraryValue: MTLLibrary? func metalLibrary(device: MTLDevice) -> MTLLibrary? { if let metalLibraryValue { return metalLibraryValue } let mainBundle = Bundle(for: BundleMarker.self) guard let path = mainBundle.path(forResource: "DustEffectMetalSourcesBundle", ofType: "bundle") else { return nil } guard let bundle = Bundle(path: path) else { return nil } guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { return nil } metalLibraryValue = library return library } public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject { public var internalData: MetalEngineSubjectInternalData? private final class Item { let frame: CGRect let texture: MTLTexture var phase: Float = 0 var particleBufferIsInitialized: Bool = false var particleBuffer: SharedBuffer? init?(frame: CGRect, image: UIImage) { self.frame = frame guard let cgImage = image.cgImage, let texture = try? MTKTextureLoader(device: MetalEngine.shared.device).newTexture(cgImage: cgImage, options: [.SRGB: false as NSNumber]) else { return nil } self.texture = texture } } private final class RenderState: RenderToLayerState { let pipelineState: MTLRenderPipelineState init?(device: MTLDevice) { guard let library = metalLibrary(device: device) else { return nil } guard let vertexFunction = library.makeFunction(name: "dustEffectVertex"), let fragmentFunction = library.makeFunction(name: "dustEffectFragment") else { return nil } let pipelineDescriptor = MTLRenderPipelineDescriptor() pipelineDescriptor.vertexFunction = vertexFunction pipelineDescriptor.fragmentFunction = fragmentFunction pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else { return nil } self.pipelineState = pipelineState } } final class DustComputeState: ComputeState { let computePipelineStateInitializeParticle: MTLComputePipelineState let computePipelineStateUpdateParticle: MTLComputePipelineState required init?(device: MTLDevice) { guard let library = metalLibrary(device: device) else { return nil } guard let functionDustEffectInitializeParticle = library.makeFunction(name: "dustEffectInitializeParticle") else { return nil } guard let computePipelineStateInitializeParticle = try? device.makeComputePipelineState(function: functionDustEffectInitializeParticle) else { return nil } self.computePipelineStateInitializeParticle = computePipelineStateInitializeParticle guard let functionDustEffectUpdateParticle = library.makeFunction(name: "dustEffectUpdateParticle") else { return nil } guard let computePipelineStateUpdateParticle = try? device.makeComputePipelineState(function: functionDustEffectUpdateParticle) else { return nil } self.computePipelineStateUpdateParticle = computePipelineStateUpdateParticle } } private var updateLink: SharedDisplayLinkDriver.Link? private var items: [Item] = [] public var becameEmpty: (() -> Void)? override public init() { super.init() self.isOpaque = false self.backgroundColor = nil self.didEnterHierarchy = { [weak self] in guard let self else { return } self.updateNeedsAnimation() } self.didExitHierarchy = { [weak self] in guard let self else { return } self.updateNeedsAnimation() } } override public init(layer: Any) { super.init(layer: layer) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func updateItems(deltaTime: Double) { var didRemoveItems = false for i in (0 ..< self.items.count).reversed() { self.items[i].phase += (1.0 / 60.0) / Float(UIView.animationDurationFactor()) if self.items[i].phase >= 4.0 { self.items.remove(at: i) didRemoveItems = true } } self.updateNeedsAnimation() if didRemoveItems && self.items.isEmpty { self.becameEmpty?() } } private func updateNeedsAnimation() { if !self.items.isEmpty && self.isInHierarchy { if self.updateLink == nil { self.updateLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .fps(60), { [weak self] deltaTime in guard let self else { return } self.updateItems(deltaTime: deltaTime) self.setNeedsUpdate() }) } } else { if self.updateLink != nil { self.updateLink = nil } } } public func addItem(frame: CGRect, image: UIImage) { if let item = Item(frame: frame, image: image) { self.items.append(item) self.updateNeedsAnimation() self.setNeedsUpdate() } } public func update(context: MetalEngineSubjectContext) { if self.bounds.isEmpty { return } let containerSize = self.bounds.size for item in self.items { var itemFrame = item.frame itemFrame.origin.y = containerSize.height - itemFrame.maxY let particleColumnCount = Int(itemFrame.width) let particleRowCount = Int(itemFrame.height) let particleCount = particleColumnCount * particleRowCount if item.particleBuffer == nil { if let particleBuffer = MetalEngine.shared.sharedBuffer(spec: BufferSpec(length: particleCount * 4 * (4 + 1))) { item.particleBuffer = particleBuffer /*let particles = particleBuffer.buffer.contents().assumingMemoryBound(to: Float.self) for i in 0 ..< particleCount { particles[i * 5 + 0] = 0.0; particles[i * 5 + 1] = 0.0; let direction = Float.random(in: 0.0 ..< Float.pi * 2.0) let velocity = Float.random(in: 0.1 ... 0.2) * 420.0 particles[i * 5 + 2] = cos(direction) * velocity particles[i * 5 + 3] = sin(direction) * velocity particles[i * 5 + 4] = Float.random(in: 0.7 ... 1.5) }*/ } } } let _ = context.compute(state: DustComputeState.self, commands: { [weak self] commandBuffer, state in guard let self else { return } guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return } for item in self.items { guard let particleBuffer = item.particleBuffer else { continue } let itemFrame = item.frame let particleColumnCount = Int(itemFrame.width) let particleRowCount = Int(itemFrame.height) let threadgroupSize = MTLSize(width: 32, height: 1, depth: 1) let threadgroupCount = MTLSize(width: (particleRowCount * particleColumnCount + threadgroupSize.width - 1) / threadgroupSize.width, height: 1, depth: 1) computeEncoder.setBuffer(particleBuffer.buffer, offset: 0, index: 0) if !item.particleBufferIsInitialized { item.particleBufferIsInitialized = true computeEncoder.setComputePipelineState(state.computePipelineStateInitializeParticle) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) } computeEncoder.setComputePipelineState(state.computePipelineStateUpdateParticle) var particleCount = SIMD2(UInt32(particleColumnCount), UInt32(particleRowCount)) computeEncoder.setBytes(&particleCount, length: 4 * 2, index: 1) var phase = item.phase computeEncoder.setBytes(&phase, length: 4, index: 2) var timeStep: Float = (1.0 / 60.0) / Float(UIView.animationDurationFactor()) computeEncoder.setBytes(&timeStep, length: 4, index: 3) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) } computeEncoder.endEncoding() }) context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0))), state: RenderState.self, layer: self, commands: { [weak self] encoder, placement in guard let self else { return } for item in self.items { guard let particleBuffer = item.particleBuffer else { continue } var itemFrame = item.frame itemFrame.origin.y = containerSize.height - itemFrame.maxY let particleColumnCount = Int(itemFrame.width) let particleRowCount = Int(itemFrame.height) let particleCount = particleColumnCount * particleRowCount var effectiveRect = placement.effectiveRect effectiveRect.origin.x += itemFrame.minX / containerSize.width * effectiveRect.width effectiveRect.origin.y += itemFrame.minY / containerSize.height * effectiveRect.height effectiveRect.size.width = itemFrame.width / containerSize.width * effectiveRect.width effectiveRect.size.height = itemFrame.height / containerSize.height * effectiveRect.height var rect = SIMD4(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height)) encoder.setVertexBytes(&rect, length: 4 * 4, index: 0) var size = SIMD2(Float(itemFrame.width), Float(itemFrame.height)) encoder.setVertexBytes(&size, length: 4 * 2, index: 1) var particleResolution = SIMD2(UInt32(particleColumnCount), UInt32(particleRowCount)) encoder.setVertexBytes(&particleResolution, length: 4 * 2, index: 2) encoder.setVertexBuffer(particleBuffer.buffer, offset: 0, index: 3) encoder.setFragmentTexture(item.texture, index: 0) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: particleCount) } }) } }