---
name: procedural-noise-background
description: Use when you want "Artistic, subtle, advanced" - Generates organic animated textures using mathematical noise.
---

# Procedural Noise Background

> **Category:** Advanced WebGL / Canvas  -  **Personality:** Artistic, subtle, advanced
>
> **Best use:** Premium backgrounds, creative UI
>
> **Ratings:** Professional 4/5 - Casual 3/5 - Exotic 4/5 - Visual impact 4/5 - Difficulty 4/5 - Performance cost 4/5

## What it is

A full-screen GPU fragment shader that synthesizes an organic, slowly-flowing texture from **mathematical noise** — no images, no video, nothing to download. The demo stacks several octaves of 2D **simplex noise** into *fractional Brownian motion* (fbm), then **domain-warps** it (feeds fbm into itself twice, the Inigo-Quilez technique) so the field churns like ink in water, and colorizes the result with an IQ **cosine palette**. Because it is procedural it never repeats and weighs almost nothing over the wire, which makes it ideal for premium hero backdrops and creative UI where you want motion that feels alive but stays out of the way.

## Dependencies / CDN

three.js (pinned, with SRI) is used purely as a thin WebGL wrapper for the full-screen quad. The shader itself is portable to raw WebGL or [ogl](https://github.com/oframe/ogl).

```html
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"
        integrity="sha384-qOkzR5Ke/XkQxuGVJ9hpFEpDlcoLtWwVYhnJf06cLIZa2vaIptSqaubivErzmD5O"
        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
```

Fonts used by the demo (optional): `Instrument Serif` (display), `Schibsted Grotesk` (body), `Space Mono` (telemetry).

## HTML

A `<canvas>` fills a contained, rounded stage; the content sits above it. A CSS gradient on the stage doubles as the WebGL-failure fallback.

```html
<div class="pn-stage" id="pnStage">
  <canvas class="pn-canvas" id="pnCanvas" aria-hidden="true"></canvas>
  <div class="pn-scrim" aria-hidden="true"></div>   <!-- darkens for legibility -->
  <div class="pn-ui">
    <h2 class="pn-h">Living surfaces, <em>grown from pure math.</em></h2>
    <p class="pn-sub">Domain-warped simplex noise, synthesized on the GPU in real time.</p>
    <!-- octave stepper re-shapes the noise live -->
    <button id="pnMinus" type="button">&minus;</button>
    <button id="pnPlus"  type="button">+</button>
    <span id="pnOct">5</span> octaves &middot; t=<span id="pnT">0.00</span>s
  </div>
</div>
```

## CSS

The stage owns a multi-stop radial-gradient that mirrors the shader palette — the canvas covers it when WebGL works, and it shows through when it does not.

```css
.pn-stage{
  position:relative; border-radius:26px; overflow:hidden; isolation:isolate;
  min-height:clamp(560px,78vh,720px); display:flex; align-items:stretch;
  background:                                            /* == WebGL-fail fallback == */
    radial-gradient(58% 80% at 18% 26%, rgba(45,212,191,.30), transparent 62%),
    radial-gradient(56% 72% at 80% 20%, rgba(139,92,246,.32), transparent 64%),
    radial-gradient(72% 82% at 66% 92%, rgba(244,114,182,.24), transparent 62%),
    #06070d;
}
.pn-canvas{position:absolute; inset:0; width:100%; height:100%; display:block; z-index:0}
.pn-scrim {position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(90deg, rgba(4,5,11,.80), rgba(4,5,11,.34) 42%, transparent 70%)}
.pn-ui    {position:relative; z-index:2; width:100%; padding:clamp(20px,3vw,34px); color:#f3efe9}

/* when JS adds .pn-fallback (no WebGL / no three.js): hide canvas, gradient remains */
.pn-stage.pn-fallback .pn-canvas{display:none}
```

## JavaScript

### Fragment shader (GLSL — verbatim, this is the whole effect)

```glsl
precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform float uAspect;
uniform vec2  uMouse;
uniform float uIntensity;
uniform int   uOctaves;

vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
vec2 mod289(vec2 x){return x-floor(x*(1.0/289.0))*289.0;}
vec3 permute(vec3 x){return mod289(((x*34.0)+1.0)*x);}
float snoise(vec2 v){
  const vec4 C = vec4(0.211324865405187, 0.366025403784439,
                     -0.577350269189626, 0.024390243902439);
  vec2 i  = floor(v + dot(v, C.yy));
  vec2 x0 = v - i + dot(i, C.xx);
  vec2 i1 = (x0.x > x0.y) ? vec2(1.0,0.0) : vec2(0.0,1.0);
  vec4 x12 = x0.xyxy + C.xxzz; x12.xy -= i1;
  i = mod289(i);
  vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0))
                          + i.x + vec3(0.0, i1.x, 1.0));
  vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
  m = m*m; m = m*m;
  vec3 x = 2.0 * fract(p * C.www) - 1.0;
  vec3 h = abs(x) - 0.5;
  vec3 ox = floor(x + 0.5);
  vec3 a0 = x - ox;
  m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
  vec3 g;
  g.x  = a0.x  * x0.x  + h.x  * x0.y;
  g.yz = a0.yz * x12.xz + h.yz * x12.yw;
  return 130.0 * dot(m, g);
}

// fractional Brownian motion: stacked octaves of simplex noise
float fbm(vec2 p){
  float v = 0.0, a = 0.5;
  mat2 rot = mat2(0.80, 0.60, -0.60, 0.80);
  for(int i = 0; i < 7; i++){
    if(i >= uOctaves) break;          // runtime octave count (fixed max bound, uniform break)
    v += a * snoise(p);
    p = rot * p * 2.0;                // rotate + double frequency each octave
    a *= 0.5;                         // halve amplitude
  }
  return v;
}

// Inigo Quilez cosine gradient palette
vec3 palette(float t){
  vec3 a = vec3(0.52, 0.45, 0.55);
  vec3 b = vec3(0.43, 0.40, 0.42);
  vec3 c = vec3(1.00, 1.00, 1.00);
  vec3 d = vec3(0.10, 0.42, 0.72);
  return a + b * cos(6.28318 * (c * t + d));
}

void main(){
  vec2 uv = vUv;
  uv.x *= uAspect;                    // keep the noise round on any aspect
  uv   *= 1.7;                        // base zoom
  float t = uTime * 0.05;

  // domain warping (fbm of fbm of fbm) -> organic flow
  vec2 q = vec2(fbm(uv + t), fbm(uv + vec2(5.2, 1.3) - t));
  vec2 r = vec2(fbm(uv + 4.0*q + vec2(1.7, 9.2) + 0.15*t),
               fbm(uv + 4.0*q + vec2(8.3, 2.8) - 0.12*t));
  float f = fbm(uv + 4.0*r);

  // pointer adds a soft warp + aurora glow
  vec2 m = vec2(uMouse.x * uAspect, uMouse.y) * 1.7;
  float md = distance(uv, m);
  float glow = exp(-md*md*1.6) * uIntensity;
  f += 0.18 * glow;

  float v = clamp(f*0.55 + 0.5, 0.0, 1.0);
  vec3 col = palette(v + 0.18*length(r));

  // sculpt depth: shade by field, keep ridges luminous
  col *= 0.45 + 0.7*v;
  col  = mix(col, col*1.25, smoothstep(0.55, 1.0, v));
  col += vec3(0.16, 0.12, 0.20) * glow;

  // vignette
  vec2 cc = vUv - 0.5; cc.x *= uAspect;
  col *= 1.0 - 0.7*dot(cc, cc);

  // film grain to kill banding
  float gr = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453 + uTime);
  col += (gr - 0.5) * 0.03;

  gl_FragColor = vec4(max(col, 0.0), 1.0);
}
```

### Vertex shader

```glsl
varying vec2 vUv;
void main(){ vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }
```

### Wiring it up (three.js)

```js
var stage = document.getElementById('pnStage'), canvas = document.getElementById('pnCanvas');
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;

function fail(){ stage.classList.add('pn-fallback'); }   // CSS gradient takes over
if(typeof THREE === 'undefined'){ fail(); return; }

var renderer;
try { renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: false, alpha: false }); }
catch(e){ fail(); return; }
if(!renderer || !renderer.getContext()){ fail(); return; }

var scene  = new THREE.Scene();
var camera = new THREE.Camera();                          // no matrices needed: clip-space quad
var uniforms = {
  uTime:{value:0}, uAspect:{value:1}, uMouse:{value:new THREE.Vector2(.5,.5)},
  uIntensity:{value:0}, uOctaves:{value:5}
};
var material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms });
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2,2), material));

function resize(){
  var w = stage.clientWidth, h = stage.clientHeight;
  renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 1.5));   // cap DPR for fill-rate
  renderer.setSize(w, h, false);
  uniforms.uAspect.value = w / Math.max(h, 1);
}
resize();
new ResizeObserver(function(){ resize(); if(reduce) renderer.render(scene, camera); }).observe(stage);

var clock = new THREE.Clock();
var mx=.5, my=.5, tmx=.5, tmy=.5, intensity=0, tIntensity=0, raf=0, running=false;

if(reduce){                                               // reduced motion = one static frame
  uniforms.uTime.value = 14.0;
  renderer.render(scene, camera);
} else {
  stage.addEventListener('pointermove', function(e){
    var b = stage.getBoundingClientRect();
    tmx = (e.clientX - b.left) / b.width;
    tmy = 1 - (e.clientY - b.top) / b.height;             // flip Y into uv space
    tIntensity = 1;
  });
  stage.addEventListener('pointerleave', function(){ tIntensity = 0; });

  (function frame(){
    raf = requestAnimationFrame(frame);
    var dt = Math.min(clock.getDelta(), 0.05);
    mx += (tmx-mx)*Math.min(1,dt*4); my += (tmy-my)*Math.min(1,dt*4);
    intensity += (tIntensity-intensity)*Math.min(1,dt*3); tIntensity *= Math.pow(0.5, dt*1.2);
    uniforms.uMouse.value.set(mx, my);
    uniforms.uIntensity.value = intensity;
    uniforms.uTime.value = clock.elapsedTime;
    renderer.render(scene, camera);
  })();

  // pause when scrolled off-screen
  new IntersectionObserver(function(es){
    es.forEach(function(en){
      if(en.isIntersecting && !running){ running = true; clock.start(); }
      else if(!en.isIntersecting){ running = false; cancelAnimationFrame(raf); clock.stop(); }
    });
  }, { threshold: 0.04 }).observe(stage);
}
```

## How it works

- **Simplex noise (`snoise`)** is the smooth, gradient-based building block — one continuous value per `(x,y)`. (This is the standard Ashima/Gustavson GLSL implementation.)
- **fbm** sums several "octaves": each octave doubles the frequency and halves the amplitude (and rotates, to hide grid alignment). Few octaves = soft blobs; more octaves = fine, cloudy detail. The loop uses a *fixed* max bound (`i < 7`) with a `break` on the `uOctaves` uniform so the count can change at runtime.
- **Domain warping** is the magic: instead of sampling `fbm(uv)`, we sample `fbm(uv + fbm(uv + fbm(uv)))`. Distorting the input coordinates with more noise produces the swirling, fluid, "alive" structure rather than flat clouds. Animating the inner offsets by `uTime` makes the whole field drift.
- **Cosine palette** (`a + b*cos(2pi*(c*t + d))`) maps the scalar noise value to a smooth, cyclic color ramp — change four `vec3`s and you have an entirely different mood with zero texture lookups.
- A full-screen **clip-space quad** (`PlaneGeometry(2,2)`, `gl_Position = vec4(position.xy,0,1)`) means the fragment shader runs once per screen pixel; no camera math.

## Customization

- **Mood / color:** edit the four `vec3`s in `palette()`. Inigo Quilez's [palette reference](https://iquilezles.org/articles/palettes/) is the cheat sheet. Shift `d` to rotate hues.
- **Detail:** `uOctaves` (3 soft -> 6 intricate). **Scale:** the `uv *= 1.7` zoom. **Speed:** the `uTime * 0.05` multiplier (keep it slow — this effect should *breathe*).
- **Turbulence:** the `4.0` multipliers on `q`/`r` control warp strength; lower = calmer, higher = chaotic.
- **Contrast/depth:** the `col *= 0.45 + 0.7*v` and `smoothstep(0.55,1.0,v)` lines. Drop the grain line if you prefer a cleaner gradient.

## Accessibility & performance

- This is **fill-rate bound**: cost scales with `pixels x octaves x (warp passes)`. Cap `devicePixelRatio` (1.5 here) and keep octaves modest — domain warping already calls `fbm` five times per pixel.
- **`prefers-reduced-motion`:** render exactly one frame and never start the rAF loop (done above).
- **Pause off-screen** with `IntersectionObserver` so a background nobody is looking at burns zero GPU.
- The canvas is decorative: `aria-hidden="true"`, and a `.pn-scrim` gradient guarantees text contrast over the brightest noise ridges.

## Gotchas

- **Always ship a non-WebGL fallback.** If `THREE` is undefined, the context can't be created, or the context is lost, add a class that reveals a static CSS gradient (the stage already paints one behind the canvas).
- **`build/three.min.js` is deprecated** in r150+ (it logs a console warning) and is gone after r160; this demo pins r0.160.0 deliberately. For new work prefer the ES-module build (`three.module.js`) — the shader code is identical.
- **`int` uniforms + dynamic loops:** WebGL1/GLSL-ES 1.00 requires a *constant* loop bound, so you must write `for(i<7){ if(i>=uOctaves) break; }`, not `for(i<uOctaves)`.
- **Aspect correction matters:** without `uv.x *= uAspect` the noise stretches into ovals on wide screens.
- **Banding** shows up on smooth gradients across 8-bit displays — the tiny `fract(sin(dot(...)))` grain hides it; don't remove it without checking.
- Keep the canvas and its CSS-gradient fallback as **siblings inside one `isolation:isolate` stage**, or `mix-blend`/`backdrop-filter` on sibling overlays can leak outside the rounded corners.
