Goldilox Paradox

WebGL scrollytelling experience

Role

Designer & Developer

Stack

Astro 5, Three.js, GLSL (ES 3.0), SCSS

Year

2025–2026

Goldilox Paradox showing the ray-traced Earth with cloud layers and city lights

01/The Constraint

Tell the story of planetary habitability — why Earth sits in the Goldilocks Zone while Venus burned and Mars froze — using scroll as the only input device. No clicks, no menus, no pagination. The scroll position is the narrative cursor. A 460-line Master Design Document defined the emotional arc (curiosity, dread, wonder, melancholy, humility), the canonical script, pacing controls, and scientific accuracy guardrails sourced from NASA mission data.

02/The Architecture

Astro serves a single static HTML page. The rendering pipeline uses Three.js as a thin WebGL2 wrapper — not its scene graph. An orthographic camera renders a fullscreen quad, and all planet geometry is computed entirely inside the fragment shaders via ray-sphere intersection. Scene switching is uniform-driven: when the user scrolls into a new chapter, the renderer swaps the fragment shader and uniform set, then disposes and recreates the material. A hand-rolled scrollytelling engine uses IntersectionObserver for chapter detection and requestAnimationFrame for scroll-position-mapped animations with custom easing functions. Hash-based navigation supports deep-linking to individual chapters.

03/The Shaders

Two GLSL ES 3.0 fragment shaders handle all planet rendering. The Earth shader composites five texture layers — daytime albedo, nightside city lights emission-mapped by sun angle, cloud cover with density control, specular ocean highlights via Phong reflectance, and a bump map that perturbs normals for terrain relief. The generic planet shader handles Venus and Mars with single-texture ray tracing, S-curve color grading, and luminance-derived bump mapping. Both share atmosphere glow computed from edge proximity, an equirectangular starfield that counter-rotates at one-twelfth planet speed, Reinhard tone mapping, and vignette post-processing. Texture resolution adapts to device — 8K starfield on desktop, 4K on mobile.

Venus chapter with orange atmosphere glow and single-texture ray tracing

Source Code

earth.fragment.glslglsl

// Ray-sphere intersection — all geometry computed in the shader
float t = raySphereIntersect(ro, rd, PLANET_CENTER, PLANET_RADIUS);
if (t > 0.0) {
  vec3 pos = ro + rd * t;
  vec3 normal = normalize(pos - PLANET_CENTER);

  // Five-layer texture compositing
  vec2 uv = sphereUV(normal);
  vec3 dayColor   = texture(uEarthColor, uv).rgb;
  vec3 nightColor = texture(uEarthNight, uv).rgb;
  float clouds    = texture(uEarthClouds, uv).r;
  float specMask  = texture(uEarthSpecular, uv).r;
  float bump      = texture(uEarthBump, uv).r;

  // Perturb normal with bump map for terrain relief
  normal = perturbNormal(normal, bump, uv);

  // Blend day/night based on sun angle
  float sunDot = dot(normal, uSunDirection);
  vec3 surface = mix(nightColor * NIGHT_EMISSION,
                     dayColor, smoothstep(-0.1, 0.3, sunDot));

  // Cloud layer + ocean specular + atmosphere
  surface = mix(surface, vec3(1.0), clouds * CLOUD_DENSITY);
  surface += specMask * phongSpecular(rd, normal, uSunDirection);
}