Noise

Perlin/Simplex noise → GPUTexture

Quick Start

import { createNoise } from './webgpu-market/noise/noise';

const noise = createNoise(device);

const texture = device.createTexture({
  size: [512, 512],
  format: 'rgba8unorm',
  usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
});

// Per frame
noise.update(texture, { time: elapsed, scale: 2.0 });
// texture is now filled — bind it in your pipeline

noise.destroy();
Source
// Perlin & Simplex noise compute shader
// Generates animated 2D noise and writes it to a storage texture.
// Supports FBM layering and optional domain warping.

struct Uniforms {
  time: f32,
  scale: f32,
  offset_x: f32,
  offset_y: f32,
  octaves: u32,
  noise_type: u32,    // 0 = perlin, 1 = simplex
  domain_warp: u32,   // 0 = off, 1 = on
  warp_strength: f32,
  persistence: f32,
  lacunarity: f32,
  width: f32,
  height: f32,
}

@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var output: texture_storage_2d<rgba8unorm, write>;

// --- Hash functions ---
// Based on hash without sine by Dave Hoskins
// https://www.shadertoy.com/view/4djSRW

fn hash2(p: vec2f) -> vec2f {
  var p3 = fract(vec3f(p.x, p.y, p.x) * vec3f(0.1031, 0.1030, 0.0973));
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.xx + p3.yz) * p3.zy);
}

fn hash1(p: vec2f) -> f32 {
  var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}

// --- Perlin noise ---
// Classic gradient noise with quintic interpolation

fn grad2(hash: vec2f) -> vec2f {
  let angle = hash.x * 6.283185;
  return vec2f(cos(angle), sin(angle));
}

fn perlin(p: vec2f) -> f32 {
  let i = floor(p);
  let f = fract(p);

  // Quintic interpolation curve (smoother than cubic)
  let u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);

  // Four corner gradients
  let g00 = dot(grad2(hash2(i + vec2f(0.0, 0.0))), f - vec2f(0.0, 0.0));
  let g10 = dot(grad2(hash2(i + vec2f(1.0, 0.0))), f - vec2f(1.0, 0.0));
  let g01 = dot(grad2(hash2(i + vec2f(0.0, 1.0))), f - vec2f(0.0, 1.0));
  let g11 = dot(grad2(hash2(i + vec2f(1.0, 1.0))), f - vec2f(1.0, 1.0));

  let x0 = mix(g00, g10, u.x);
  let x1 = mix(g01, g11, u.x);
  return mix(x0, x1, u.y);
}

// --- Simplex noise ---
// 2D simplex noise based on the approach by Ian McEwan / Ashima Arts

const SKEW: f32 = 0.36602540378;    // (sqrt(3) - 1) / 2
const UNSKEW: f32 = 0.21132486540;  // (3 - sqrt(3)) / 6

fn simplex(p: vec2f) -> f32 {
  // Skew input space to determine which simplex cell we're in
  let s = (p.x + p.y) * SKEW;
  let i = floor(p + s);
  let t = (i.x + i.y) * UNSKEW;
  let x0 = p - (i - t);

  // Determine which simplex triangle
  var i1: vec2f;
  if (x0.x > x0.y) {
    i1 = vec2f(1.0, 0.0);
  } else {
    i1 = vec2f(0.0, 1.0);
  }

  let x1 = x0 - i1 + UNSKEW;
  let x2 = x0 - 1.0 + 2.0 * UNSKEW;

  // Gradient contributions from the three corners
  var n = vec3f(0.0);

  var d0 = 0.5 - dot(x0, x0);
  if (d0 > 0.0) {
    d0 = d0 * d0;
    n.x = d0 * d0 * dot(grad2(hash2(i)), x0);
  }

  var d1 = 0.5 - dot(x1, x1);
  if (d1 > 0.0) {
    d1 = d1 * d1;
    n.y = d1 * d1 * dot(grad2(hash2(i + i1)), x1);
  }

  var d2 = 0.5 - dot(x2, x2);
  if (d2 > 0.0) {
    d2 = d2 * d2;
    n.z = d2 * d2 * dot(grad2(hash2(i + 1.0)), x2);
  }

  // Scale to [-1, 1] range (approximately)
  return 70.0 * (n.x + n.y + n.z);
}

// --- Noise selector ---

fn noise(p: vec2f) -> f32 {
  if (u.noise_type == 0u) {
    return perlin(p);
  }
  return simplex(p);
}

// --- Fractal Brownian Motion ---
// Layers multiple octaves of noise at increasing frequency and decreasing amplitude

fn fbm(p: vec2f) -> f32 {
  var value = 0.0;
  var amplitude = 0.5;
  var frequency = 1.0;
  var coord = p;

  for (var i = 0u; i < u.octaves; i++) {
    value += amplitude * noise(coord * frequency);
    frequency *= u.lacunarity;
    amplitude *= u.persistence;
  }

  return value;
}

// --- Domain warping ---
// Distorts UV coordinates using a second noise evaluation

fn domain_warp(p: vec2f) -> vec2f {
  let warp_x = fbm(p + vec2f(0.0, 0.0));
  let warp_y = fbm(p + vec2f(5.2, 1.3));
  return p + vec2f(warp_x, warp_y) * u.warp_strength;
}

@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) id: vec3u) {
  let dims = vec2u(u32(u.width), u32(u.height));
  if (id.x >= dims.x || id.y >= dims.y) {
    return;
  }

  // Normalize pixel coordinates and apply scale, offset, and time-based animation
  var p = vec2f(f32(id.x), f32(id.y)) / vec2f(u.width, u.height);
  p = p * u.scale + vec2f(u.offset_x, u.offset_y) + vec2f(u.time * 0.1, u.time * 0.07);

  if (u.domain_warp == 1u) {
    p = domain_warp(p);
  }

  // Map noise from [-1, 1] to [0, 1]
  let value = fbm(p) * 0.5 + 0.5;

  textureStore(output, vec2i(id.xy), vec4f(value, value, value, 1.0));
}
Documentation

Noise

Perlin and Simplex noise generator. Writes animated 2D noise to a caller-provided GPUTexture via a compute shader.

API

createNoise(device)

Returns a Noise instance. No options — dimensions come from the target texture.

noise.update(target, options?)

Dispatches the compute shader to write noise into the target texture.

  • targetGPUTexture to write into (must have STORAGE_BINDING usage, format rgba8unorm)
Option Type Default Description
time number 0 Animation time (drives UV scrolling)
scale number 2.0 Noise frequency scale
offset [number, number] [0, 0] UV offset
octaves number 6 FBM octave count
persistence number 0.5 Amplitude multiplier per octave
lacunarity number 2.0 Frequency multiplier per octave
type 'perlin' | 'simplex' 'simplex' Noise algorithm
domainWarp boolean false Enable domain warping
warpStrength number 1.0 Domain warp intensity

noise.destroy()

Releases the uniform buffer. Does not destroy the target texture.

Further Reading

Further Reading

Resources on the algorithms and techniques used in this module.

Perlin Noise

  • Ken Perlin, "An Image Synthesizer" (SIGGRAPH 1985) The original paper introducing gradient noise for procedural texture generation. Perlin received an Academy Award for Technical Achievement for this work. https://dl.acm.org/doi/10.1145/325165.325247

  • Ken Perlin, "Improving Noise" (SIGGRAPH 2002) Revisits the original algorithm with improved gradient selection and interpolation (the quintic curve used in this module). https://dl.acm.org/doi/10.1145/566570.566636

Simplex Noise

  • Ken Perlin, "Noise Hardware" (Real-Time Shading course, SIGGRAPH 2001) Introduces simplex noise as a faster, lower-artifact alternative to classic Perlin noise, using a simplex grid instead of a hypercubic grid. https://www.csee.umbc.edu/~olano/s2002c36/ch02.pdf

  • Stefan Gustavson, "Simplex noise demystified" (2005) A clear, practical walkthrough of the simplex noise algorithm with implementation guidance. Excellent for understanding the skew/unskew math. https://cgvr.cs.uni-bremen.de/teaching/cg_literatur/simplexnoise.pdf

  • Ian McEwan, David Munoz, Art Tevs, Ashima Arts, "Efficient computational noise in GLSL" (2012) GPU-friendly noise implementations without lookup textures, using arithmetic hash functions — the approach this module's WGSL shader is based on. https://jcgt.org/published/0001/01/02/

Fractal Brownian Motion (FBM)

  • Benoit Mandelbrot, "The Fractal Geometry of Nature" (1982) The foundational text on fractal geometry. FBM (fractional Brownian motion) is a core concept — layering self-similar noise at multiple scales.

  • Inigo Quilez, "FBM — Fractal Brownian Motion" Practical guide to implementing FBM for procedural graphics, with interactive examples and parameter explanations. https://iquilezles.org/articles/fbm/

Domain Warping

  • Inigo Quilez, "Domain Warping" Explains and visualizes the technique of distorting noise input coordinates with another noise evaluation to create organic, swirling patterns. https://iquilezles.org/articles/warp/

Hash Functions

  • Dave Hoskins, "Hash without Sine" (Shadertoy) The arithmetic hash functions used in this module's WGSL shader. Avoids trigonometric functions for better GPU performance and portability. https://www.shadertoy.com/view/4djSRW

General References