Skip to content

Instantly share code, notes, and snippets.

@destefanis
Created April 29, 2025 14:35
Show Gist options
  • Select an option

  • Save destefanis/9818f04c93f2bb70ab1f53f7a2f19006 to your computer and use it in GitHub Desktop.

Select an option

Save destefanis/9818f04c93f2bb70ab1f53f7a2f19006 to your computer and use it in GitHub Desktop.
Liquid Glass Renderer
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