Field notes 001 WebGL / generative graphics

How we made the Aurora background

No video. No giant texture. Just a field of tiny points, a handful of equations, and a GPU drawing the northern lights again every frame.

01 place the dots 02 grow the noise 03 bend the light
live shader study GPU idea / Canvas model
seed 1337 ~0 points / frame move your pointer to disturb the curtain

Chapter 01 / The display

How the dot grid is generated

A normal canvas hides its pixels. We made ours visible. Each point becomes one tiny lamp: position is fixed by a grid; colour and radius carry the scene.

01.1

Calculate every dot position

The vertex number tells us its column and row. Multiply both by the grid step and every point lands on an exact rhythm—no position buffer required.

gx=idmodcolumnsgy=id/columnsp=(gxstep,  gystep)\begin{array}{l} g_x = id \bmod columns \\[0.72em] g_y = \left\lfloor id / columns \right\rfloor \\[0.72em] \mathbf{p} = (g_x \cdot step,\; g_y \cdot step) \end{array}

Why this formula Modulo returns the column and integer division returns the row. Multiplying by step converts those grid indices into pixels. The production value is 4 px: dense enough to read as an image while keeping the dots visible.

Experiment A / spacing interactive
denseopen
Drag the slider. The production background uses a tight 4 px step.
01.2

Map brightness to dot area

A circle’s area grows with . If radius followed light directly, mid-tones would look too dark. Taking the square root makes visible dot area track the signal instead.

r=rmaxLA=πr2    AL\begin{array}{l} r = r_{\max}\sqrt{L} \\[0.72em] A = \pi r^2 \;\Longrightarrow\; A \propto L \end{array}

Why this formula Circle area is proportional to , so using √L makes visible area proportional to luminance L. rmax is the Dot size setting and limits the result to the current grid cell.

Experiment B / radius same light field
dimbright
Left: texture only changes colour. Right: the same texture changes colour and area.
01.3

Render each point as a circle

WebGL points begin as squares. Inside every square we measure distance from the centre, throw away the corners, then soften only the outer 28% to stop the edge flickering.

q=2pointCoord1d=qqα=1smoothstep(0.72,1,d)\begin{array}{l} \mathbf{q} = 2\,pointCoord - 1 \\[0.72em] d = \mathbf{q}\cdot\mathbf{q} \\[0.72em] \alpha = 1 - smoothstep(0.72,\,1,\,d) \end{array}

Why this formula Remapping point coordinates to −1…1 puts the centre at zero; the dot product gives squared distance without a costly square root. Values below 0.72 stay solid, while 0.72…1 becomes the narrow anti-aliased edge.

Experiment C / edge profile 8× microscope
The dashed circle is the hard cut. The green curve is the alpha that reaches the screen.

Bench test / Halftone settings

Adjust the halftone grid

DOT FIELD0 points

Chapter 02 / The light

How the Aurora curtain is generated

Random pixels look like television snow. Aurora needs continuity, layers and direction. We build those properties one at a time, keeping every intermediate result visible.

02.1

Generate smooth value noise

We hash the four corners of a cell, then blend between them with a smooth S-curve. The same coordinate and seed always return the same number, so the field moves without boiling.

1 hash four corners 2fade(t)=t2(32t)fade(t)=t^2(3-2t) 3 bilinear mix

Why this formula The cubic fade curve has zero slope at both ends, so neighbouring noise cells meet without seams. The coefficients 3 and −2 are the smallest cubic solution that satisfies those two flat-end conditions.

FLAT COORDINATESseed 1337
02.2 / Raw noise

Sample one noise field in screen space

We sample one octave directly in screen space. The result is coherent, but it still looks painted onto glass.

n(p)=noise(p)n(\mathbf{p})=\operatorname{noise}(\mathbf{p})

Why this formula One deterministic noise lookup is the simplest coherent field. The same position p and seed always return the same value, which prevents random flicker between frames.

02.3 / Perspective

Project the noise field toward the horizon

Move the slider from flat screen coordinates to reciprocal depth. Near the horizon, a tiny vertical move travels a huge distance across the noise plane.

d(e)=se+pqpersp=(xd,  zd)q(λ)=mix(qflat,qpersp,λ)\begin{array}{l} d(e) = \dfrac{s}{e+p} \\[0.72em] \mathbf{q}_{persp} = (x\,d,\;z\,d) \\[0.72em] \mathbf{q}(\lambda) = \operatorname{mix}(\mathbf{q}_{flat},\,\mathbf{q}_{persp},\,\lambda) \end{array}

Why this formula Perspective makes apparent size inversely proportional to distance. e is elevation, s is observer-dependent plane scale and p prevents division by zero. mix(a, b, λ) is exactly (1 − λ)a + λb: 0 uses flat coordinates and 1 uses full perspective.

flat fieldhorizon depth
02.4 / fBM

Combine four noise octaves

Each octave doubles frequency and halves amplitude. Large forms survive; small structure appears.

wi=0.5ini=noise(2.05ip)fBM(p)=i=03winii=03wi\begin{array}{l} w_i = 0.5^i \\[0.72em] n_i = \operatorname{noise}(2.05^i\mathbf{p}) \\[0.72em] fBM(\mathbf{p}) = \dfrac{\sum_{i=0}^{3}w_i n_i}{\sum_{i=0}^{3}w_i} \end{array}

Why this formula The factor 2.05 adds detail at roughly double frequency without producing obvious repetition. Amplitude 0.5 gives every smaller octave half the influence. Four octaves were the best detail-to-cost balance; N normalizes the result back to 0…1.

02.5 / Domain warp

Distort the sampling coordinates

We use a slow noise field to push the x-coordinate before sampling the fast field.

b=2[F(0.35x,0.4z)0.5]rays=F(0.9x+b,0.28z)\begin{array}{l} b = 2\,[F(0.35x,\,0.4z)-0.5] \\[0.72em] rays = F(0.9x+b,\,0.28z) \end{array}

Why this formula F is the fBM function from the previous step. Frequencies 0.35 and 0.4 create broad bends, while 0.9 and 0.28 stretch the final field vertically. Subtracting 0.5 centres the offset and multiplying by 2 restores a symmetric −1…1 range.

02.6 / Threshold

Extract narrow light ribbons

smoothstep selects the bright bands; a power curve tightens or opens their edges.

r=smoothstep(l,h,raw)ribbon=rc\begin{array}{l} r = \operatorname{smoothstep}(l,\,h,\,raw) \\[0.72em] ribbon = r^c \end{array}

Why this formula l and h are the low and high thresholds that define which part of the noise becomes light. Smoothstep avoids hard pixel edges. Raising the mask to c, the Contrast setting, suppresses mid-values and leaves narrow bright ribbons.

02.7 / Light & colour

Map elevation to colour

Lower and upper hues blend by elevation. Another moving noise field makes the colour shimmer.

m=e1.15h=lerpHue(h0,h1,m)h=h+shimmer\begin{array}{l} m = e^{1.15} \\[0.72em] h = \operatorname{lerpHue}(h_0,\,h_1,\,m) \\[0.72em] h' = h + shimmer \end{array}

Why this formula e is elevation, while h₀ and h₁ are the lower and upper hue settings. The exponent 1.15 delays the upper colour slightly. lerpHue follows the shortest path around the colour wheel; shimmer adds a small animated noise offset.

Control room / Aurora settings

Adjust all Aurora parameters

Every control below maps to the production Settings panel.

Quick mixtures
FULL RECIPEt = 0.00
vertex-grid modeldrag across the light
Motion
Shape
Colour
Speed

How quickly time advances through every noise field.

Depth drift

How fast the sampled plane travels away from the viewer.

Intensity

A final multiplier: more emitted light, same geometry.

Power

Adds a second ray field and moves the ribbon thresholds.

Lower / upper hue

The two endpoints of the altitude colour ramp.

Shimmer / speed

Amplitude and velocity of moving hue noise.

Saturation / brightness

Colour purity and total light before clamping.

Ribbon contrast

The exponent that decides whether curtains are foggy or cut sharp.

Chapter 03 / The space

How the observer and atmosphere create depth

Camera height, horizon and perspective are not cosmetic transforms. They change the coordinates fed into every field. Atmosphere then decides how much of that distant light survives.

03.1 / View geometry

Convert screen height into distance

Elevation becomes the denominator. Observer height changes the plane scale before the division.

e=hy/Hhs=1.150.75hod=se+p\begin{array}{l} e = \dfrac{h-y/H}{h} \\[1.25em] s = 1.15-0.75\,h_o \\[1.25em] d = \dfrac{s}{e+p} \end{array}

Why this formula h is the horizon, hₒ is Observer height and p is Perspective. The first line maps the top of the sky to high elevation and the horizon to zero. Constants 1.15 and 0.75 move plane scale from 1.15 down to 0.40; adding p keeps horizon distance finite.

03.2 / Atmospheric extinction

Apply haze and horizon glow

Haze falls exponentially with distance; glow is deliberately local and quadratic.

haze(d)=edhazeu(δ)=1δ0.12glow(δ)=max(0,u)2intensity\begin{array}{l} haze(d) = e^{-d\,haze} \\[0.72em] u(\delta) = 1-\dfrac{\delta}{0.12} \\[0.72em] glow(\delta) = \max(0,u)^2\cdot intensity \end{array}

Why this formula Exponential decay is the standard Beer–Lambert model for light crossing a medium. The glow uses only the lowest 12% of the sky; squaring its band value concentrates light tightly at the horizon.

Chapter 04 / The silhouette

How the mountain ridges are generated

Each ridge is a one-dimensional fBM profile with its own frequency, amplitude and seed offset. Depth makes later ridges smaller; a narrow exponential rim keeps their edges readable.

04.1 / Ridge profile

Generate each ridge from one-dimensional fBM

Roughness controls both frequency and octave count. Height sets the amplitude in screen space.

ni(x)=fBM(fix+oi)γ=1.40.5rridgei(x)=yhHai[ni(x)]γ\begin{array}{l} n_i(x) = fBM(f_i x+o_i) \\[0.72em] \gamma = 1.4-0.5r \\[0.72em] ridge_i(x) = y_h-H\,a_i\,[n_i(x)]^\gamma \end{array}

Why this formula One-dimensional fBM already has the self-similar structure of terrain. fᵢ and offset oᵢ separate the ridge layers; aᵢ makes distant layers lower. Exponent γ moves from 1.4 to 0.9 as Roughness r increases, producing sharper peaks.

04.2 / Edge light

Add rim light below each ridge

An exponential is bright at the exact ridge and quickly disappears into rock.

Δy=yyridgeL(y)=L0+ReΔy/(0.035H)\begin{array}{l} \Delta y = y-y_{ridge} \\[0.72em] L(y) = L_0+R\,e^{-\Delta y/(0.035H)} \end{array}

Why this formula L₀ is base light and R is the Rim light setting. An exponential gives maximum light exactly at the silhouette and a smooth falloff below it. The 0.035H denominator keeps the rim about 3.5% of viewport height on every screen.

Control room / Mountains

Adjust the mountain profile

RIDGE STACK3 layers
Mountains

Chapter 05 / The reflection

How the sky is reflected in the water

Water starts by sampling the completed scene above the horizon. A nonlinear vertical map preserves the distant band; moving noise shifts each reflected row sideways and fragmented sine crests add foam.

05.1 / Reflection lookup

Map each water row to a sky row

Distance from the horizon chooses a source row. Wave phase supplies the horizontal error.

ys=yhHwb(0.58+0.42b)Δx=w(b,t)csxs=xΔx\begin{array}{l} y_s = y_h-H_w\,b\,(0.58+0.42\sqrt{b}) \\[0.72em] \Delta x = w(b,t)\cdot c\cdot s \\[0.72em] x_s = x-\Delta x \end{array}

Why this formula yₛ and xₛ are the sampled sky coordinates; w is wave noise, c is cell size and s is the depth scale. Mixing 58% linear depth with 42% square-root depth preserves more detail near the horizon. The sideways offset grows toward the viewer.

05.2 / Broken crests

Generate broken foam crests

Raise it to the sixth power for thin crests, then multiply by thresholded fBM to punch gaps.

c=sin6(kxx+kyyωt)m=smoothstep(0.54,0.86,n)foam=cm\begin{array}{l} c = \sin^6(k_xx+k_yy-\omega t) \\[0.72em] m = \operatorname{smoothstep}(0.54,\,0.86,\,n) \\[0.72em] foam = c\cdot m \end{array}

Why this formula c is the crest, n is the fBM field and m is its thresholded mask. The sixth power turns a wide sine wave into a narrow crest without a branch. Values 0.54 and 0.86 define the soft interval where natural gaps appear.

Control room / Water

Adjust reflection and waves

REFLECTION PASSsource → warp → foam
Water

Chapter 06 / The second pass

How stars are distributed and animated

Stars render in a separate pass so they can sit behind the light but remain sharper than the sky. A stable hash chooses cells, magnitude sets size and colour, and atmospheric airmass dims the horizon.

06.1 / Stable density

Convert star count into cell probability

Multiplying by cell area keeps the apparent number of stars stable when grid density changes.

N=WHhc2p=countNP(star)=clamp(p,0,0.35)\begin{array}{l} N = \dfrac{W\,H\,h}{c^2} \\[0.72em] p = \dfrac{count}{N} \\[0.72em] P(star) = \operatorname{clamp}(p,\,0,\,0.35) \end{array}

Why this formula N is the number of sky cells: viewport area W × H times horizon fraction h, divided by cell area . Count divided by N is therefore a per-cell probability. The 0.35 cap prevents the field from becoming a solid texture.

06.2 / Twinkle + air

Animate twinkle and atmospheric fading

Hashed phase and speed prevent synchronized blinking; extinction removes weak stars near the horizon.

T(t)=0.25+0.90sin2(ωt+ϕ)E(e)=e0.11(airmass(e)1)B(t)=MT(t)E(e)\begin{array}{l} T(t) = 0.25+0.90\sin^2(\omega t+\phi) \\[0.72em] E(e) = e^{-0.11(\operatorname{airmass}(e)-1)} \\[0.72em] B(t) = M\cdot T(t)\cdot E(e) \end{array}

Why this formula Base magnitude M is multiplied by twinkle T and atmospheric transmission E. Each star hashes its own phase φ and speed ω. The 0.25 floor stops complete blackouts, while 0.11 removes dim stars as airmass rises near the horizon.

Control room / Stars

Adjust the star field

STAR PASS160 candidates
Stars

Chapter 07 / The event layer

How meteor trajectories are generated

A meteor shower does not move in arbitrary directions. Each streak begins on screen and travels away from a shared point above it; speed, light and length vary only after that direction is established.

07.1 / Radiant direction

Calculate direction from the radiant

Normalize the vector from radiant to start point, then rotate it by a small random spread.

Δp=pstartpradiantv^=ΔpΔp\begin{array}{l} \Delta\mathbf{p} = \mathbf{p}_{start}-\mathbf{p}_{radiant} \\[0.72em] \hat{\mathbf{v}} = \dfrac{\Delta\mathbf{p}}{\lVert\Delta\mathbf{p}\rVert} \end{array}

Why this formula Perspective makes shower paths appear to diverge from one radiant. Subtracting radiant position from start position gives that direction; normalization removes distance so the Speed setting can control velocity independently.

07.2 / Trail lifetime

Convert trail length into lifetime

Converting requested screen length into seconds makes trails stay consistent across velocities.

distance=trailLengthHtrailTime=distancespeedpx\begin{array}{l} distance = trailLength\cdot H \\[0.72em] trailTime = \dfrac{distance}{speed_{px}} \end{array}

Why this formula Distance equals speed multiplied by time, so time must be distance divided by speed. Trail length is stored as a fraction of viewport height H, keeping the visible streak proportional on every screen.

Control room / Meteors

Adjust the meteor shower

RADIANT GUIDEdirection before variance
Meteors

Put the engineering to work

Start with three real web tasks for free

Send us three items from your actual backlog—bugs, page changes, integrations, or performance work. We will complete them during your 7-day trial so you can judge the work on your own product.

  • 3real tasks
  • 7 daysto evaluate the work
  • $0if you cancel during the trial
Submit your 3 free tasks A $1 card verification hold is never charged. Your selected plan begins on day 8 only if you continue.