Back to Experiments

Metal Fluid Simulation

GPU-accelerated warped noise background using Metal compute shaders

MetalGPUShadersSwiftUIiOS4 min

A real-time fluid-like animated background powered by Metal compute shaders. Uses fractal Brownian motion with triple domain warping to create continuously morphing patterns. Theme-aware gradient mapping pulls your app's accent colour into the animation. Runs at 60 FPS with minimal CPU overhead.

Overview

A GPU-accelerated animated background built with Metal compute shaders and domain warping. It renders a continuously morphing, fluid-like noise pattern that responds to your app's theme colour. Originally built as the loading screen for Shopa, it runs at 60 FPS with minimal CPU overhead since all computation happens on the GPU.

iOS Version (Metal)

Add both files to your Xcode project. The .metal file compiles automatically when added to a target.

WarpedNoiseShader.metalMetal

GPU compute kernel with FBM, domain warping, and theme gradient

WarpedNoiseBackground.swiftSwift

SwiftUI view, Metal pipeline setup, colour helpers, and fallback gradient

Web Version (WebGL)

A standalone HTML file that ports the Metal shader to WebGL. Drop it into any project or open it directly in a browser — no build step needed.

metal-fluid.htmlHTML

Complete WebGL port — same FBM, domain warping, and theme gradient as the Metal version

How It Works

1. Fractal Brownian Motion (FBM)

Layers multiple octaves of value noise at different frequencies and amplitudes, creating organic, cloud-like patterns. A rotation matrix between octaves prevents axis-aligned artefacts.

2. Domain Warping

The pattern function applies FBM three times in sequence — fbm(p + fbm(p + fbm(p))) — each layer warping the coordinate space of the next. This creates complex, fluid motion from simple noise.

3. Theme-Aware Gradient

The noise value (0–1) maps through a three-stop gradient: analogous/complementary colour, base accent colour, and a light variant. This means the fluid naturally incorporates your app's brand colours.

4. Metal Compute Pipeline

A compute kernel runs per-pixel on the GPU via MTKView. Time, colour, and opacity are passed as buffer uniforms. Falls back to a simple gradient on devices without Metal support.

Quick Start

Add both files to your Xcode project. The Metal shader compiles automatically when added to a target. Then use WarpedNoiseBackground() as a background layer in any view.

Usage.swiftswift
// Simple usage - pass accent color directly
struct LoadingScreen: View {
    var body: some View {
        ZStack {
            MetalNoiseBackground(
                accentColor: .blue,
                complementaryColor: SIMD3<Float>(0.2, 0.9, 1.0),
                opacity: 0.4
            )
            .ignoresSafeArea()

            VStack {
                ProgressView()
                Text("Loading...")
            }
        }
    }
}

// With theme manager (as used in Shopa)
struct ThemedLoadingScreen: View {
    @EnvironmentObject var themeManager: ThemeManager

    var body: some View {
        ZStack {
            WarpedNoiseBackground()
                .ignoresSafeArea()

            VStack(spacing: 12) {
                ProgressView()
                    .tint(.secondary)
                Text("Syncing your data...")
                    .font(.subheadline)
            }
        }
    }
}

Customisation

Change colours

Pass different SIMD3<Float> values for the three gradient stops (complementary, base, light). Each component is 0.0–1.0 RGB.

Adjust speed

Multiply the time uniform in the shader. Try time * 0.5 for slower motion or time * 2.0 for faster.

Change opacity

The opacity buffer (index 4) controls overall transparency. Default is 0.4. Increase for more vivid backgrounds, decrease for subtler effects.

Lower frame rate

Set mtkView.preferredFramesPerSecond = 30 for a more battery-friendly option. The animation still looks smooth at 30 FPS.

Remove ThemeManager dependency

Replace the @EnvironmentObject with direct colour parameters. Pass accentColor: Color and complementary RGB values as init parameters instead.

Products
Josh
Speaking
Experiments