Independent (white)
Each draw is a fresh coin flip. IDs, HUD flavor text, radio static, traffic speed noise, retry jitter.
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 ρ.”
Correlation is not “looks random.” It is a relationship between samples. Four different relationships show up in this project, and they need different algorithms:
Each draw is a fresh coin flip. IDs, HUD flavor text, radio static, traffic speed noise, retry jitter.
Two numbers from one joint distribution. If wind is high, rain tends to be high. Not in the repo yet — Lab 1 below.
Neighbors on a map stay similar: clouds, thermal grain, NVG tube grain. Value noise + FBM, already in GLSL.
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.
| 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 |
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.”
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.
Independent hash — like noir grain / radio static
Value noise — like NVG tube grain
FBM — like FLIR grain and cockpit clouds
White noise is a new independent sample every tick. That looks like GPS sparkle, not wind. Two useful alternatives:
(t, 0). Thermal already does this with uv + vec2(time).
x ← x + θ(μ − x)Δt + σ√Δt · Z.
Large θ snaps back to the mean; large σ makes wilder wobble. Good for wind, heading jitter, gain drift.
Copy value noise + FBM from thermal.js or cockpitCloudEffects.js. JS ports are at the bottom of this page.
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.
Ornstein–Uhlenbeck (Lab 3), or keep sampling FBM at a slowly moving time coordinate — the thermal shader’s trick.
FNV-1a hashSeed, or the test LCG. Do not use these when neighbors should look related.
function createLcg(seed) {
let state = seed >>> 0;
return () => {
state = (Math.imul(state, 1664525)
+ 1013904223) >>> 0;
return state / 0x100000000;
};
}
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];
}
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;
}
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.