A pair of Metal shaders inspired by Andy Weir’s Project Hail Mary:
light lanes streaking through hyperspace, and a bokeh cloud of astrophage
racing toward the camera. Packaged as two native macOS
.saver
bundles.
The screen is sliced into horizontal lanes. Each lane is an independent object with its own speed, phase and color, then four such grids are stacked at different scales to deliver the “jump to hyperspace” parallax effect.
angleRadians so lanes can travel at any angle.p.y * laneDensity — integer part is the lane ID, fractional part is position inside the lane.fract(sin(seed·k)).pow(sin(x), 8.0) produces a narrow, concentrated flash instead of a smeared wave.smoothstep(0.45, 0.15, distToCenter) keeps solid fill in the middle with a crisp fall-off.layer.sample().floor yields a unique lane ID that seeds every random property of that lane.
for(int i = 1; i <= grids; i++) {
float fi = float(i);
float scale = 1.0 + (fi - 1.0) * 1.5;
float laneDensity = layersCount * scale * 0.4;
float lane = p.y * laneDensity;
float laneIndex = floor(lane);
float laneFract = fract(lane);
float seed = laneIndex + fi * 133.33;
float rndSpeed = fract(sin(seed * 78.233) * 43758.5453);
float rndColor = fract(sin(seed * 37.719) * 43758.5453);
float rndPhase = fract(sin(seed * 93.989) * 43758.5453);
pow(sin)^8 creates a narrow flash capsule while smoothstep over the distance-to-center turns a blurry lane into a crisp laser filament.
float lineSpeed = (rndSpeed * 1.5 + 0.5) * speed;
float moveX = p.x - (time * lineSpeed) + (rndPhase * 10.0);
float pulse = pow(sin(moveX * 2.0) * 0.5 + 0.5, 8.0);
float distToCenter = abs(laneFract - 0.5);
float sharpLine = smoothstep(0.45, 0.15, distToCenter);
float glow = sharpLine * (0.2 + pulse * 1.5);
finalColor += laneColor * glow * intensity * 0.4;
smoothstep.
static half3 paletteHailMary(float index) {
half3 c1 = half3(1.0, 0.6, 0.1);
half3 c2 = half3(0.4, 0.0, 0.9);
half3 c3 = half3(1.0, 0.1, 0.7);
half3 c4 = half3(0.1, 0.3, 1.0);
half3 c5 = half3(0.0, 0.9, 0.9);
half3 c6 = half3(1.0, 0.4, 0.0);
if (index < 0.2) return mix(c1, c2, smoothstep(0.0, 0.2, index));
if (index < 0.4) return mix(c2, c3, smoothstep(0.2, 0.4, index));
if (index < 0.6) return mix(c3, c4, smoothstep(0.4, 0.6, index));
if (index < 0.8) return mix(c4, c5, smoothstep(0.6, 0.8, index));
return mix(c5, c6, smoothstep(0.8, 1.0, index));
}
spaceWaves is attached via layerEffect with the uniforms wired through: time, speed, intensity, lane density, rotation and palette index.
import SwiftUI
struct HailMaryView: View {
@State private var start = Date()
let speed: Float = 0.4
let layers: Float = 12
let angle: Float = .pi / 9
let palette: Float = 0
var body: some View {
TimelineView(.animation) { ctx in
let t = Float(ctx.date.timeIntervalSince(start))
Color.black.layerEffect(
ShaderLibrary.spaceWaves(
.boundingRect,
.float(t),
.float(speed),
.float(1.0),
.float(layers),
.float(angle),
.float(palette)
),
maxSampleOffset: .zero
)
}
}
}
Space is partitioned into a grid of cells; each cell carries at most one particle with its own lifetime. Three depth layers produce perspective: far particles are tiny and slow, near particles explode outward from the center as if you were flying into them.
fract(layerPhase + t·speed) — an infinite depth loop.1 + (1−depth)·drift·3 stretches the grid as particles approach.pow(h, sizePow)) and phase offset.smoothstep(r, r·softness, d) gives a soft disk instead of a hard one.mix(color, white, t²).static float hash(float2 p) {
float3 p3 = fract(float3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
constant float SIZE_START = 0.006;
constant float SIZE_END = 0.08;
constant float SOFTNESS = 0.35;
constant float BRIGHTNESS_MIN = 0.005;
constant float BRIGHTNESS_MAX = 3.0;
float fl = float(layer);
float seed = fl * 17.31;
float grid = gridBase * (0.85 + fl * 0.06);
float2 layerOff = float2(hash(float2(fl,1.0)),
hash(float2(fl,2.0))) * 100.0;
float layerPhase = fl / float(LAYERS);
float depth = fract(layerPhase + time * speed);
depth = max(depth, 0.05);
float perspScale = 1.0 + (1.0 - depth) * drift * 3.0;
float2 st = centered * perspScale * grid + layerOff;
float layerFade = smoothstep(0.0, 0.4, depth)
* smoothstep(1.0, 0.55, depth);
t², edges dissolve via smoothstep, color warms toward white near end-of-life.
for (int ox = -1; ox <= 1; ox++) {
for (int oy = -1; oy <= 1; oy++) {
float2 cid = cellId + float2(float(ox), float(oy));
if (hash(cid + seed) > 0.5) continue;
float h_anim = hash(cid + seed + 30.0);
float spd = speedLo + fract(h_anim * 7.13) * (speedHi - speedLo);
float t = fract(h_anim + time * spd);
float sv = pow(hash(cid + seed + 40.0), sizePow);
float r = mix(SIZE_START, SIZE_END * (0.05 + sv*0.95), t*t)
* perspScale * grid;
float d = length(f - float2(ox,oy) - pp * 0.7);
if (d > r) continue;
float bokeh = smoothstep(r, r * SOFTNESS, d);
float fade = smoothstep(0.0,0.35,t) * smoothstep(1.0,0.6,t) * layerFade;
totalGlow += pColor * bokeh * fade * bright;
}
}
colorEffect, so the output is the final pixel color. Time comes from TimelineView, tunables come from @AppStorage.
import SwiftUI
struct AstrofageView: View {
@AppStorage("density") var density: Double = 160
@AppStorage("sizePow") var sizePow: Double = 2.4
@AppStorage("speed") var speed: Double = 0.18
@AppStorage("drift") var drift: Double = 0.6
var body: some View {
TimelineView(.animation) { ctx in
GeometryReader { g in
Color(red: 0.02, green: 0.04, blue: 0.09)
.colorEffect(
ShaderLibrary.flyingParticles(
.float(Float(ctx.date.timeIntervalSinceReferenceDate)),
.float2(Float(g.size.width),
Float(g.size.height)),
.float(Float(density)),
.float(Float(sizePow)),
.float(Float(speed)),
.float(Float(drift))
)
)
.ignoresSafeArea()
}
}
}
}
Download the .saver.zip you want, then double-click the archive in Finder to extract the .saver bundle.
Double-click the extracted .saver file. macOS will prompt you to install it either for your user or for all users.
The bundle is unsigned, so macOS will block it. Open System Settings Privacy & Security and click Open Anyway next to the saver’s name.
Go to System Settings Screen Saver, scroll to Other, and pick HailMary or Astrofage.
Hit Preview to see the shader full-screen. Use the saver’s Options panel to change speed, density, palette and drift.
Both .saver bundles are unsigned (no Developer ID / notarization), because they’re distributed as open shader samples. Gatekeeper flags them on first launch — this is expected. If the “Open Anyway” button doesn’t appear, place the saver manually at
~/Library/Screen Savers/
and run
xattr -dr com.apple.quarantine ~/Library/Screen Savers/HailMaryScreenSaver.saver
in Terminal. Screen savers on macOS never get access to your files, microphone or camera — they only draw pixels.