01 · Alive
Twenty fish, one crab, and nothing scripted.
Neon tetras school tight, angelfish drift, clownfish stay near their anemones, discus hover. Their steering is smoothed until no frame shows a jerk. Before that, 60 simulated seconds of 20 fish contained ~42 000 abrupt changes of angular velocity; afterwards, 4 minor ones.
On the bottom, a red-claw crab walks sideways on eight legs driven by inverse kinematics. Feet stay planted, steps alternate, the knees bend to the side that has room, and it runs for a hideout it found by itself under a rock overhang. Flatfish bury themselves in a puff of sand. A starfish creeps on the glass.
Below: a 2D sketch of the same steering rules, running in your browser. Move your cursor through the school; click to drop food.
02 · Light you can see
Volumetric water, ray-marched per pixel.
The water isn't a colour, it's a medium. For every pixel, a ray walks through the tank in steps of about 10 cm. At each step it asks how much of the lamp reaches that point. First, is a rock or a plant in the way, according to the lamp's shadow map? Then, what did the water surface do to that light on its way down? The answer is the same ripple pattern that paints the caustics on the floor, so the shafts and the light spots line up.
Only 4–6 steps per pixel would be far too few. The trick is to jitter them: every frame they start at a different offset, and the temporal anti-aliasing (TAA) blends frames together. A handful of long steps then looks like hundreds of short ones.
Right: a 128×72 ray-marcher in your browser, using the aquarium shader's algorithm on spheres instead of scanned rocks. Play with the controls to see why the jitter and the accumulation matter.
– samples per frame
03 · Anatomy of a frame
Where 23 milliseconds go.
Bevy's GPU timers return zero on Metal, so every effect was switched off in turn on the same frame, with baseline runs in between because a fanless Mac warms up. The water is a third of the frame. Its per-pixel set-up hid a surprise: it found where each ray leaves the box by testing six planes against each other. A classic slab test and a little hoisting gave back 1 ms, with an unchanged image (54.5 dB, run-to-run noise is 50.8 dB).
04 · The change you never see
Resolution moves only when nobody is watching.
Changing the render resolution resets the temporal filters: a burst of grain, a jump in sharpness, the screen seems to "refresh". So the aquarium decides behind its start-up curtain. It runs a few dozen uncapped frames at full resolution, scales a cost model of the scene to this particular Mac, and picks the sharpest level that fits. The first frame you see is already the right one.
It can't keep measuring afterwards, and the chart shows why. Under vsync, Apple's GPU lowers its clock to fill each frame, so the load macOS reports sits near 80 % whatever the resolution. Only frames that really miss the refresh for 4 seconds count. The level is then adjusted by the smallest step, and better levels are applied only while the wallpaper sleeps or the window is hidden.
Measured on an M4 under vsync: the GPU load reported by macOS (bars) against the frame's actual cost at full clock (line). Same reading, very different work.
05 · It knows when to rest
A wallpaper shouldn't cost like a game.
Awake at 60 fps, it uses the GPU like a 3D game. So it's only awake for 30 seconds after your mouse crosses the desktop. The rest of the time it sleeps in slow motion, and it stops drawing entirely when windows cover it or the screen locks. Measured with the chip's own energy counters:
Covered, locked or asleep
no frame drawn, clock stopped
Asleep, 9 fps slow motion
desktop alone: 0.25–0.5 W
Awake on battery, 30 fps
same definition as 60 fps
Awake at 60 fps, full screen
why it doesn't stay awake
Also: 30 fps on battery or in Low Power Mode, the thermal state tightens the budget before macOS throttles, one frame every 2 s below 30 % battery, and audio is disabled so no audio stream keeps the hardware awake.
06 · A map of every rock
One distance field for everything that moves.
At start-up the aquarium bakes the signed distance from every point of the tank to the nearest glass, sand, rock or branch: a 1 cm grid with exact point-triangle distances from the 340 000 triangles the GPU draws, in about 0.2 s. The fish avoid obstacles by following its gradient. The crab and flatfish walk on its zero surface, food sinks until it touches it, and the hideouts are found in it.
It also caught a layout mistake: three rocks sat 7–12 mm inside each other, and a quarter of the grass tufts grew inside stone. Rocks were nudged by a centimetre, and plants now slide to the nearest free spot once the field is known.
Right: the real field, exported from the app, sliced 12 cm above the bottom of the tank. Hover to read distances; click to release a fish that steers by the gradient.
07 · Tested like a physics engine
81 checks, three frame rates, zero failures.
A recorder logs every animal every frame for minutes of simulated time, with a scripted cursor, clicks and a scare every 20 seconds. A script then checks 81 criteria: never inside the decor, no teleports, no foot slipping, legs within reach, calm again within 3 s after a scare, hideout reached... It runs at 1/60 s, at 1/30 s, and with the irregular frame times of a real window. Here is what it found:
The crab's carapace slid into a rock while walking along it: only its contact point had collision.
of the time hidden, its knees poked through the ceiling of its hideout.
Crab and flatfish popped onto a pebble in a single frame.
The fish pitch limit broke when frames took twice as long.
Get it
Three commands.
macOS on Apple Silicon and Rust. The rocks and sand are CC0 scans from Poly Haven, downloaded once (~61 MB). Everything else is procedural.
Then make it your wallpaper: it sits below your icons, on every Space, lets your clicks through, and asks for no permissions.
# build and run git clone https://github.com/akaloic/aquarium.git cd aquarium && ./scripts/fetch_assets.sh cargo run --release # as a live wallpaper cargo build --release ./target/release/aquarium --wallpaper &



