Behind the scenes
The Science of RainStorm
Have you ever looked out a window during a storm and felt a sudden sense of calm? There is a deep, biological reason for that. Natural rain is chaotic, immersive, and constantly shifting. It is never repetitive, and it never moves in perfect sync.
Most screen-saver and background animations feel robotic because they rely on simple, repeating particle loops or uniform speeds. In RainStorm, we took a different path. We built a native Metal GPU-accelerated physics engine designed to mimic the organic complexity of real-world rainfall.
Here is a look under the hood at the mathematics, physics, and creative philosophies that bring RainStorm to life on your desktop.
Nature cannot be corralled: the scheduler philosophy
Our guiding philosophy is Aggressive Naturalism. Human brains are incredibly good at spotting patterns. If rain falls at perfectly regular intervals, your brain quickly detects the repetition, breaking the cozy ambient immersion.
Because of this, the automatic storm scheduler is built with non-exact timing:
- The ±20% timing jitter. If you set a storm interval of one hour, it will not trigger exactly on the hour. The app applies a logarithmic scale and adds a ±20% timing jitter to the center point, so your one-hour interval actually schedules the next storm anywhere between 48 minutes and 1 hour 12 minutes.
- Intensity is a guideline, not a command. Your storm intensity preference is a guideline for the atmosphere, not a rigid lock. If you set your preference to prefer downpours, a weighted random distribution makes downpours highly likely (~86%), but keeps drizzle at a 5% possibility and medium rain at a 10% wildcard. Even in a storm-heavy cycle, you will still occasionally get a light drizzle.
Dynamic intensity drift: the Cruise system
Once a storm reaches its target intensity, it does not just sit perfectly static. In the real world, a rainstorm naturally waxes and wanes. The pitter-patter gets heavier, then lighter, then picks up again as the clouds pass. We replicate this through a dynamic background drift system called Cruise:
- Random Cruise segments. While a storm is active, it constantly transitions through a sequence of random time blocks, each assigned a random duration between 90 seconds and 4 minutes.
- Squared-step math. At the start of each segment, the engine calculates a small change in intensity. To prevent sudden, jarring shifts, the step magnitude squares a random number and scales it:
stepMagnitude = random² × 0.12Squaring the random value skews the odds heavily toward micro-adjustments, so the vast majority of updates are slow, gentle drifts rather than noticeable transitions.
Continuous swells. The system randomly chooses whether to nudge the intensity up or down by this step, clamping the target between a floor of 0.01 and a ceiling of 1.0. The intensity then interpolates to the new target over the segment, creating an endless, organic swell where the storm naturally breathes.
Organic randomness & GPU-driven physics
For the rain overlay itself, we rely on GPU-driven pseudo-randomness to ensure every storm is unique:
- Hash-based PRNG on the GPU. To avoid taxing the CPU, our Metal shader implements a fast, cryptographic-style hash function operating directly on each GPU thread.
- Seed decoupling. Every single drop is spawned using a unique seed calculated from its grid index combined with a high-entropy base:
seed = seedBase ⊕ (gridID × 977 + 0x9E3779B9)The constant 0x9E3779B9 is derived from the Golden Ratio, ensuring an even distribution of random values across threads and preventing visual “grid lines” or repeating bands.
Breaking the alignment. In most particle systems, every droplet tilts at the exact same angle when the wind blows. In RainStorm, 60% of the droplets opt in to the baseline wind while the remaining 40% fall on their own custom heading. This simple gate breaks up visual uniformity, making the storm feel layered and organic rather than flat.
Atmospheric depth & 3D parallax
Rain doesn't happen on a flat pane of glass; it surrounds you. To create a three-dimensional sense of volume, RainStorm calculates a continuous depth value between 0.0 (distant background) and 1.0 (immediate foreground) for every drop.
The depth distribution curve
If we distributed drops evenly across depth, the screen would feel cluttered and unnatural. In nature, you perceive far more distant drops than foreground ones because of your field of view. We model this by skewing the depth distribution with a power curve:
depth = random1.55An exponent of 1.55 skews the curve heavily toward the background, populating a rich, soft curtain of distant rain while keeping the immediate foreground clear enough to focus on your work.
Depth scaling
A droplet's depth dynamically scales all of its physical properties:
- Foreground drops (depth → 1.0) are rendered wider, longer, faster, more opaque, and highly reactive to gusts.
- Background drops (depth → 0.0) are rendered thinner, shorter, slower, semi-transparent, and deeply stable.
Storm intensity & droplet-size skewing
To keep system resources low while maintaining high visual quality, the simulation caps active drops between roughly 35 and 950 depending on the current stage. Because we operate in this tight particle budget, we cannot rely on “mist” effects. Instead, a drizzle is modeled as a light, gentle pitter-patter of individual falling drops, whereas a downpour contains a highly diverse mix of massive, heavy drops and small ones.
To capture this, RainStorm dynamically biases the drop-size distribution based on the storm's current intensity (0.0 → 1.0):
sizeFactor = randomexponentexponent = mix(2.0, 0.7, intensity)
- Drizzle (intensity ≈ 0.1): the exponent rises toward 2.0. Squaring the random value heavily skews the distribution toward 0.0, forcing drops to be tiny — a light, scattered pitter-patter.
- Downpour (intensity ≈ 1.0): the exponent drops to 0.7. Raising a fraction to a power below 1.0 “flattens” the curve, boosting the odds of massive, heavy drops while still allowing smaller ones to co-exist.
This size factor directly determines the drop's terminal velocity. Rather than scaling linearly, the base speed uses a physics-based mapping:
terminalVelocity ∝ √sizeFactorThis mimics real-world fluid dynamics, where larger droplets fall faster because their mass overcomes air resistance.
Dynamic wind, spatial gusts & curved arcs
Wind in the real world doesn't instantly tilt the entire sky. It drifts, sweeps in pockets, and affects different drops in different ways.
The multi-sine baseline wind
To create slow, sweeping wind patterns, the CPU calculates a baseline wind using three independent sine-wave oscillators with prime-ish periods (comfortably above a four-minute floor):
drift = 0.5·sin(2πt / 240) + 0.3·sin(2πt / 521) + 0.2·sin(2πt / 743)
These three sines construct an ever-shifting baseline that never repeats. To keep the wind within realistic visual bounds even when the waves constructively peak, we pass the raw value through a hyperbolic tangent (tanh) for a smooth, asymptotic soft-clamp:
baselineWind = maxBaseline × tanh(drift / maxBaseline)
Decoupled wind and gust susceptibility
A droplet's reaction to sustained wind and sudden gusts is decoupled to maximize visual variety. Each drop receives a randomized wind susceptibility and gust susceptibility on spawn. So when a gust front rolls through, some drops lurch and dance immediately while others fall with steady drift — mimicking the complex micro-currents of air within a real storm.
Spatial gust cells & sweeping fronts
Instead of a global acceleration, gusts are calculated as local 2D noise fields in the Metal shader, combining regional cells, sweeping fronts, and micro-turbulence:
cell = sin(x · 0.0034 + t) × cos(y · 0.0042 − t)This creates regional “pockets” of wind, so drops on the left side of your screen might bend while drops on the right fall straight.
Layer-desynced pulses. The foreground and background layers pulse at different rates and phases. This creates depth parallax in the wind itself, letting foreground gusts sweep past while the background remains stable.
The curved fall: vertical recovery
In nature, when raindrops fall from a high-altitude wind layer into a sheltered area, they don't fall in straight diagonal lines. They curve. RainStorm replicates this by attenuating wind speed as a droplet approaches the bottom of your viewport:
windAttenuation = 1.0 − effectiveProgress × verticalRecoveryBecause verticalRecovery and the arc's start point are randomized per particle, some drops fall straight, some curve gradually across the whole screen, and others hold their slant most of the way down before arcing sharply near the bottom — guiding the eye along flowing curves rather than rigid angles.
Visual artistry & sub-pixel shading
To keep resource usage incredibly low, we don't draw individual lines or complex vector curves. Instead, every raindrop is rendered as an instanced quad (two triangles) textured with a custom, procedurally generated alpha streak.
Velocity-based stretch
When a droplet falls faster (or is accelerated by a gust), its quad is stretched in the vertex shader along its vector of motion:
streakLength ∝ baseLength × mix(0.86, 1.72, normalizedSpeed)This velocity-based stretching naturally mimics the motion blur of real rainfall.
The multi-layer shading model
Rather than rendering flat white or blue streaks, our fragment shader applies a multi-layered lighting model so the rain reads clearly against any background — light or dark:
- The shadow layer. A subtle, dark backing (depth-tinted between
#2E343Aand#1D2024) is rendered behind the drop, keeping it visible even when passing over bright white windows. - The core body. Rendered in a curated soft grey (
#ABABAB) rather than harsh white, mimicking the refractive nature of water inheriting the ambient shading of a storm. - The rim highlight. A bright, cool-blue edge highlight simulates light refractively bending around the outer boundary of the droplet.
- The head glint. A tight, warm light cast (
#FFFDF5) is injected at the leading tip of the streak, simulating the focal refraction of light at the bottom of a falling droplet and giving the leading edge a subtle, bright definition.
Every storm you see is generated in real time, drop by drop — no two are ever alike. See it in motion →