Earth
A procedural planet with GPU-baked albedo, night lights and clouds, a lit atmosphere, and an HDR bloom chain tuned so only the sun glows.
// Bakes a procedural cloud coverage map.
//
// Coverage is a single channel, so the target is `r8unorm`. The earth shader
// reads `.r` for opacity and takes screen-space derivatives of the same channel
// to fake cloud-top normals.
import { belt, equirectDirection, fbm3, fbmVector, ridged3, saturate, valueRemap } from "./planet-common.wgsl";
@fragment
fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
let direction = equirectDirection(uv);
let latitude = direction.y;
// A gentle latitude-dependent twist around the polar axis before sampling:
// enough zonal smearing to read as circulation, not enough to turn systems
// into stripes.
let flow = latitude * 0.35;
let sheared = vec3f(
direction.x * cos(flow) - direction.z * sin(flow),
direction.y,
direction.x * sin(flow) + direction.z * cos(flow),
);
let swirl = fbmVector(sheared * 1.9 + vec3f(17.3, -5.1, 42.7), 3) * 0.40;
let systems = fbm3(sheared * 2.8 + swirl, 6);
let wisps = fbm3(sheared * 7.5 + swirl * 1.6, 5);
// Ridged noise remapped to [-1, 1] contributes the thin filaments that make a
// cloud deck read as sheared rather than blobby.
let filaments = ridged3(sheared * 16.0 + swirl * 2.2, 4) * 2.0 - 1.0;
let field = systems * 0.70 + wisps * 0.45 + filaments * 0.16;
// Circulation cells. The latitude is jittered by a low-frequency field first so
// the bands wander instead of running dead straight around the globe.
let jitter = fbm3(direction * 1.15 + vec3f(71.0, 13.0, -29.0), 3) * 0.10;
let lane = abs(latitude) + jitter;
let weather = 0.30
+ belt(lane, 0.00, 0.15) * 0.30 // intertropical convergence
+ belt(lane, 0.64, 0.30) * 0.26 // mid-latitude storm tracks
+ belt(lane, 0.97, 0.24) * 0.16 // polar cloud
- belt(lane, 0.34, 0.20) * 0.30; // subtropical highs, where the deserts sit
// Wide remap window plus a gamma above 1: most of the sphere ends up thin and
// only the densest cores reach full opacity.
let raw = saturate(valueRemap(field + (weather - 0.46) * 0.85, -0.24, 0.62, 0.0, 1.0));
return vec4f(pow(raw, 2.4), 0.0, 0.0, 1.0);
}