Created
April 29, 2025 14:35
-
-
Save destefanis/9818f04c93f2bb70ab1f53f7a2f19006 to your computer and use it in GitHub Desktop.
Liquid Glass Renderer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| import MetalKit | |
| class LiquidGlassRenderer: NSObject, MTKViewDelegate { | |
| var device: MTLDevice! | |
| var commandQueue: MTLCommandQueue! | |
| var pipelineState: MTLRenderPipelineState! | |
| var vertexBuffer: MTLBuffer! | |
| var currentTime: Float = 0.0 | |
| // Match the shader's uniform structure exactly | |
| struct Uniforms { | |
| var iResolution: SIMD2<Float> | |
| var iTime: Float | |
| var patternScale: Float | |
| var waveSize: Float | |
| var refraction: Float | |
| var edge: Float | |
| var patternBlur: Float | |
| var liquid: Float | |
| var backgroundColor: SIMD3<Float> | |
| var grainIntensity: Float | |
| var grainSpeed: Float | |
| var grainMean: Float | |
| var grainVariance: Float | |
| var grainBlendMode: Int32 | |
| var rectWidth: Float | |
| var rectHeight: Float | |
| var cornerRadius: Float | |
| var edgeSoftness: Float | |
| var speed: Float | |
| } | |
| override init() { | |
| super.init() | |
| guard let device = MTLCreateSystemDefaultDevice() else { | |
| fatalError("Metal is not supported") | |
| } | |
| self.device = device | |
| self.commandQueue = device.makeCommandQueue() | |
| setupPipeline() | |
| setupVertexBuffer() | |
| } | |
| private func setupPipeline() { | |
| guard let library = device.makeDefaultLibrary() else { return } | |
| let pipelineDescriptor = MTLRenderPipelineDescriptor() | |
| pipelineDescriptor.vertexFunction = library.makeFunction(name: "liquidglass::vertex_main") | |
| pipelineDescriptor.fragmentFunction = library.makeFunction(name: "liquidglass::fragment_main") | |
| // Set up color attachment for sRGB color space | |
| let colorAttachment = pipelineDescriptor.colorAttachments[0]! | |
| colorAttachment.pixelFormat = .bgra8Unorm // Changed from bgra8Unorm_srgb to match WebGL's linear color space | |
| colorAttachment.isBlendingEnabled = true | |
| colorAttachment.sourceRGBBlendFactor = .sourceAlpha | |
| colorAttachment.sourceAlphaBlendFactor = .sourceAlpha | |
| colorAttachment.destinationRGBBlendFactor = .oneMinusSourceAlpha | |
| colorAttachment.destinationAlphaBlendFactor = .oneMinusSourceAlpha | |
| let vertexDescriptor = MTLVertexDescriptor() | |
| vertexDescriptor.attributes[0].format = .float3 | |
| vertexDescriptor.attributes[0].offset = 0 | |
| vertexDescriptor.attributes[0].bufferIndex = 0 | |
| vertexDescriptor.attributes[1].format = .float2 | |
| vertexDescriptor.attributes[1].offset = MemoryLayout<Float>.stride * 3 | |
| vertexDescriptor.attributes[1].bufferIndex = 0 | |
| vertexDescriptor.layouts[0].stride = MemoryLayout<Float>.stride * 5 | |
| vertexDescriptor.layouts[0].stepRate = 1 | |
| vertexDescriptor.layouts[0].stepFunction = .perVertex | |
| pipelineDescriptor.vertexDescriptor = vertexDescriptor | |
| do { | |
| pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) | |
| } catch { | |
| fatalError("Failed to create pipeline state: \(error)") | |
| } | |
| } | |
| private func setupVertexBuffer() { | |
| let vertices: [Float] = [ | |
| -1.0, -1.0, 0.0, 0.0, 0.0, | |
| 1.0, -1.0, 0.0, 1.0, 0.0, | |
| -1.0, 1.0, 0.0, 0.0, 1.0, | |
| 1.0, 1.0, 0.0, 1.0, 1.0 | |
| ] | |
| vertexBuffer = device.makeBuffer(bytes: vertices, | |
| length: vertices.count * MemoryLayout<Float>.stride, | |
| options: []) | |
| } | |
| func draw(in view: MTKView) { | |
| guard let drawable = view.currentDrawable, | |
| let commandBuffer = commandQueue.makeCommandBuffer(), | |
| let renderPassDescriptor = view.currentRenderPassDescriptor, | |
| let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { | |
| return | |
| } | |
| // Increment time exactly as in the WebGL version: | |
| // In WebGL: materialRef.current.u_time += 0.01 * defaultParams.speed | |
| // The key is the constant increment per frame, not tied to actual time | |
| currentTime += 0.01 * 0.1 | |
| var uniforms = Uniforms( | |
| iResolution: SIMD2<Float>( | |
| Float(view.drawableSize.width), | |
| Float(view.drawableSize.height) | |
| ), | |
| iTime: currentTime, | |
| patternScale: 2.0, | |
| waveSize: 1.8, | |
| refraction: 0.01, | |
| edge: 0.04, | |
| patternBlur: 0.0025, | |
| liquid: 0.22, | |
| backgroundColor: SIMD3<Float>(249/255.0, 249/255.0, 249/255.0), | |
| grainIntensity: 0.08, | |
| grainSpeed: 2.0, | |
| grainMean: 0.0, | |
| grainVariance: 0.5, | |
| grainBlendMode: 0, | |
| rectWidth: 1.08, | |
| rectHeight: 1.08, | |
| cornerRadius: 0.24, | |
| edgeSoftness: 0.1, | |
| speed: 0.2 | |
| ) | |
| renderEncoder.setRenderPipelineState(pipelineState) | |
| renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) | |
| renderEncoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 1) | |
| renderEncoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 0) | |
| renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) | |
| renderEncoder.endEncoding() | |
| commandBuffer.present(drawable) | |
| commandBuffer.commit() | |
| } | |
| func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { | |
| // Handle resize if needed | |
| } | |
| } | |
| struct LiquidGlassView: UIViewRepresentable { | |
| func makeCoordinator() -> LiquidGlassRenderer { | |
| LiquidGlassRenderer() | |
| } | |
| func makeUIView(context: Context) -> MTKView { | |
| let mtkView = MTKView() | |
| mtkView.device = context.coordinator.device | |
| mtkView.delegate = context.coordinator | |
| mtkView.enableSetNeedsDisplay = true | |
| mtkView.isPaused = false | |
| mtkView.framebufferOnly = true | |
| // Match WebGL animation frame rate | |
| mtkView.preferredFramesPerSecond = 60 | |
| mtkView.presentsWithTransaction = false | |
| mtkView.colorPixelFormat = .bgra8Unorm // Changed from bgra8Unorm_srgb to match WebGL's linear color space | |
| mtkView.clearColor = MTLClearColor(red: 249/255.0, green: 249/255.0, blue: 249/255.0, alpha: 1) // #f9f9f9 exact match to WebGL | |
| mtkView.translatesAutoresizingMaskIntoConstraints = false | |
| mtkView.insetsLayoutMarginsFromSafeArea = false | |
| mtkView.drawableSize = mtkView.frame.size.applying( | |
| CGAffineTransform(scaleX: UIScreen.main.scale, | |
| y: UIScreen.main.scale) | |
| ) | |
| mtkView.contentScaleFactor = UIScreen.main.scale | |
| return mtkView | |
| } | |
| func updateUIView(_ uiView: MTKView, context: Context) { | |
| if let superview = uiView.superview { | |
| uiView.frame = superview.bounds | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment