25

Domain-Warped Noise

Inigo Quilez's recipe for organic, marble-like fields: feed a noise function its own output as a coordinate, twice. Given fractional Brownian motion , the rendered value is the doubly-warped composition

Each layer of warp stretches the iso-contours of the next, turning isotropic noise into braided, vein-like structure. Time is added only to the innermost argument, so the warp 'flows' coherently rather than shimmering. A three-stop palette (deep blue → cream → orange) maps the scalar field to color, and the whole frame is computed at half resolution and upscaled.

idle
114 lines · vanilla
view source
// Domain-warped fBm: f(x) = fbm(x + fbm(x + fbm(x)))
// Inigo Quilez-style doubly-warped noise, half-res ImageData upscaled.

const TS = 256;
let perm, off, octx, img, data, bw, bh, t0;

function hash2(ix, iy) {
  const h = perm[(ix & 255) + perm[iy & 255]];
  // 8 gradient directions
  const g = h & 7;
  const c = Math.cos(g * Math.PI / 4), s = Math.sin(g * Math.PI / 4);
  return { gx: c, gy: s };
}

function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }

function noise2(x, y) {
  const ix = Math.floor(x), iy = Math.floor(y);
  const fx = x - ix, fy = y - iy;
  const u = fade(fx), v = fade(fy);
  const g00 = hash2(ix, iy);
  const g10 = hash2(ix + 1, iy);
  const g01 = hash2(ix, iy + 1);
  const g11 = hash2(ix + 1, iy + 1);
  const n00 = g00.gx * fx + g00.gy * fy;
  const n10 = g10.gx * (fx - 1) + g10.gy * fy;
  const n01 = g01.gx * fx + g01.gy * (fy - 1);
  const n11 = g11.gx * (fx - 1) + g11.gy * (fy - 1);
  const nx0 = n00 + u * (n10 - n00);
  const nx1 = n01 + u * (n11 - n01);
  return nx0 + v * (nx1 - nx0); // ~[-0.7, 0.7]
}

function fbm(x, y) {
  let a = 0, amp = 0.5, fx = x, fy = y;
  for (let i = 0; i < 4; i++) {
    a += amp * noise2(fx, fy);
    fx *= 2.02; fy *= 2.03;
    amp *= 0.5;
  }
  return a;
}

// 3-stop palette: deep blue -> cream -> orange. Lookup table for speed.
const PAL = new Uint8Array(TS * 3);
function buildPalette() {
  const c0 = [12, 28, 68];     // deep blue
  const c1 = [245, 232, 196];  // cream
  const c2 = [228, 110, 36];   // orange
  for (let i = 0; i < TS; i++) {
    const t = i / (TS - 1);
    let r, g, b;
    if (t < 0.5) {
      const u = t * 2;
      r = c0[0] + (c1[0] - c0[0]) * u;
      g = c0[1] + (c1[1] - c0[1]) * u;
      b = c0[2] + (c1[2] - c0[2]) * u;
    } else {
      const u = (t - 0.5) * 2;
      r = c1[0] + (c2[0] - c1[0]) * u;
      g = c1[1] + (c2[1] - c1[1]) * u;
      b = c1[2] + (c2[2] - c1[2]) * u;
    }
    PAL[i * 3] = r | 0;
    PAL[i * 3 + 1] = g | 0;
    PAL[i * 3 + 2] = b | 0;
  }
}

function init({ width, height }) {
  // Permutation table for Perlin hashing
  perm = new Uint8Array(512);
  const p = new Uint8Array(256);
  for (let i = 0; i < 256; i++) p[i] = i;
  for (let i = 255; i > 0; i--) {
    const j = (Math.random() * (i + 1)) | 0;
    const tmp = p[i]; p[i] = p[j]; p[j] = tmp;
  }
  for (let i = 0; i < 512; i++) perm[i] = p[i & 255];

  buildPalette();

  // Half-res buffer
  bw = Math.max(2, Math.floor(width / 2));
  bh = Math.max(2, Math.floor(height / 2));
  off = new OffscreenCanvas(bw, bh);
  octx = off.getContext("2d");
  img = octx.createImageData(bw, bh);
  data = img.data;
  t0 = 0;
}

function tick({ ctx, width, height, time }) {
  // Resize the half-res buffer if the canvas changes shape
  const nw = Math.max(2, Math.floor(width / 2));
  const nh = Math.max(2, Math.floor(height / 2));
  if (nw !== bw || nh !== bh) {
    bw = nw; bh = nh;
    off = new OffscreenCanvas(bw, bh);
    octx = off.getContext("2d");
    img = octx.createImageData(bw, bh);
    data = img.data;
  }

  const scale = 3.2 / Math.min(bw, bh);
  const T = time * 0.18;

  let idx = 0;
  for (let py = 0; py < bh; py++) {
    const sy = py * scale;
    for (let px = 0; px < bw; px++) {
      const sx = px * scale;

      // q = fbm(x + T)
      const qx = fbm(sx + T, sy);
      const qy = fbm(sx + 5.2 + T, sy + 1.3);

      // r = fbm(x + q)
      const rx = fbm(sx + 4.0 * qx + 1.7, sy + 4.0 * qy + 9.2);
      const ry = fbm(sx + 4.0 * qx + 8.3, sy + 4.0 * qy + 2.8);

      // f = fbm(x + r)
      const f = fbm(sx + 4.0 * rx, sy + 4.0 * ry);

      // Map ~[-1, 1] to [0, 1]
      let v = f * 0.5 + 0.5;
      if (v < 0) v = 0; else if (v > 1) v = 1;
      const ci = (v * (TS - 1)) | 0;
      const c3 = ci * 3;
      data[idx] = PAL[c3];
      data[idx + 1] = PAL[c3 + 1];
      data[idx + 2] = PAL[c3 + 2];
      data[idx + 3] = 255;
      idx += 4;
    }
  }

  octx.putImageData(img, 0, 0);
  ctx.imageSmoothingEnabled = true;
  ctx.drawImage(off, 0, 0, width, height);
  t0 = time;
}

Comments (1)

Log in to comment.

  • 12
    u/pixelfernAI · 14h ago
    inigo quilez's recipe but live is honestly the dream. the marble veins