God's Eye View · random-number lab

Correlated samples · fields · time series · what the repo already has

Math.random() gives independent numbers: knowing one tells you nothing about the next. Correlated random numbers are different on purpose. Nearby values, later values, or a second variable are allowed to move together. God's Eye already uses the spatial version in shaders. It does not yet have a shared function for “two numbers with correlation ρ.”

What “correlated” actually means

Correlation is not “looks random.” It is a relationship between samples. Four different relationships show up in this project, and they need different algorithms:

Independent (white)

Each draw is a fresh coin flip. IDs, HUD flavor text, radio static, traffic speed noise, retry jitter.

Statistical pair (ρ)

Two numbers from one joint distribution. If wind is high, rain tends to be high. Not in the repo yet — Lab 1 below.

Spatial field

Neighbors on a map stay similar: clouds, thermal grain, NVG tube grain. Value noise + FBM, already in GLSL.

Temporal series

The next frame is close to this frame: drifting sensor noise, wind that does not teleport. FBM-with-time, or Ornstein–Uhlenbeck.

A hash that always returns the same value for the same ID is deterministic, not correlated. Correlation is about how different samples relate to each other.

What is already in God's Eye

Kind Where Algorithm In repo?
Spatial + slow time drift src/styles/thermal.js IQ hash → value noise → 4-octave FBM, then uv + time yes
3D cloud field src/cockpitCloudEffects.js 3D hash → trilinear value noise → 3-octave FBM + domain warp yes
Slow grain + white sparkle src/styles/surveillance.js 2D value noise, plus independent hash(uv, time) yes
Independent uniforms src/data/traffic.js, HUD, IDs Math.random() yes
Seeded independent uniforms tests only (labelArbiterDifferential.test.mjs) Numerical Recipes LCG 1664525 / 1013904223 yes
ID → stable fake random server/providers/local.js hashSeed FNV-1a 32-bit yes
Clustered positions src/data/trafficQueue.js Random platoon anchors, then 6–12 m gaps — clustered, not ρ yes
Colored audio noise src/data/radio.js White buffer, then a bandpass filter yes
Two Gaussians with correlation ρ — Box–Muller + linear mix, or Cholesky not yet

Lab 1 — two numbers with correlation ρ

Start with two independent standard normals Z₁ and Z₂ (Box–Muller turns two uniforms into a Gaussian). Then mix them:

X = Z₁
Y = ρ·Z₁ + √(1 − ρ²)·Z₂

ρ = 0 is a round cloud (independent). ρ = 1 is a diagonal line (identical). Negative ρ flips the slope. This is the missing shared helper if you want “gusty wind and heavier rain,” or “along-track GPS error and a little cross-track error.”

measured r = —

Lab 2 — spatial fields (what thermal and clouds already do)

A hash of lattice coordinates is white: adjacent pixels jump. Value noise hashes only the integer corners, then interpolates with the smoothstep curve f*f*(3-2*f) — the same formula as thermal.js. Neighbors become similar. FBM adds octaves at double frequency and half amplitude, which is how the cockpit cloud shader gets billows instead of blurry blobs.

left white hash · middle value noise · right FBM (thermal / clouds)

Independent hash — like noir grain / radio static

Value noise — like NVG tube grain

FBM — like FLIR grain and cockpit clouds

Lab 3 — time series that do not teleport

White noise is a new independent sample every tick. That looks like GPS sparkle, not wind. Two useful alternatives:

white · OU · 1D FBM

Which algorithm to use

You want nearby pixels / map cells similar

Copy value noise + FBM from thermal.js or cockpitCloudEffects.js. JS ports are at the bottom of this page.

You want two quantities to rise together

Box–Muller, then the ρ mix from Lab 1. For more than two variables, Cholesky-factor the covariance matrix and multiply by a vector of independent Gaussians.

You want a value that wanders over seconds

Ornstein–Uhlenbeck (Lab 3), or keep sampling FBM at a slowly moving time coordinate — the thermal shader’s trick.

You want the same “random” every time for an ID

FNV-1a hashSeed, or the test LCG. Do not use these when neighbors should look related.

Copyable JavaScript (ports of the repo math)

LCG from the tests

function createLcg(seed) {
  let state = seed >>> 0;
  return () => {
    state = (Math.imul(state, 1664525)
           + 1013904223) >>> 0;
    return state / 0x100000000;
  };
}

Gaussian pair with correlation ρ

function gaussian(rng) {
  const u1 = Math.max(1e-12, rng());
  const u2 = rng();
  return Math.sqrt(-2 * Math.log(u1))
       * Math.cos(2 * Math.PI * u2);
}
function correlatedPair(rho, rng) {
  const z1 = gaussian(rng);
  const z2 = gaussian(rng);
  const s = Math.sqrt(Math.max(0, 1 - rho * rho));
  return [z1, rho * z1 + s * z2];
}

Value noise + FBM (thermal.js)

function hash2(x, y) {
  let p3x = fract(x * 0.1031);
  let p3y = fract(y * 0.1031);
  let p3z = fract(x * 0.1031);
  const d = p3x*(p3y+33.33) + p3y*(p3z+33.33)
          + p3z*(p3x+33.33);
  p3x += d; p3y += d; p3z += d;
  return fract((p3x + p3y) * p3z);
}
function valueNoise(x, y) {
  const ix = Math.floor(x), iy = Math.floor(y);
  let fx = x - ix, fy = y - iy;
  fx = fx*fx*(3-2*fx); fy = fy*fy*(3-2*fy);
  const a = hash2(ix, iy), b = hash2(ix+1, iy);
  const c = hash2(ix, iy+1), d = hash2(ix+1, iy+1);
  return mix(mix(a,b,fx), mix(c,d,fx), fy);
}
function fbm(x, y, octaves = 4) {
  let v = 0, a = 0.5, px = x, py = y;
  for (let i = 0; i < octaves; i++) {
    v += a * valueNoise(px, py);
    px = px * 2 + 100; py = py * 2 + 100;
    a *= 0.5;
  }
  return v;
}

Ornstein–Uhlenbeck step

function ouStep(x, theta, mu, sigma, dt, z) {
  return x + theta * (mu - x) * dt
           + sigma * Math.sqrt(dt) * z;
}

fract is x - Math.floor(x). mix is a + (b-a)*t. These are CPU ports of the GLSL already shipping in the thermal and cloud shaders.