Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 50, Simplex Noise: a still of the noise / simplex lattice plate as the atlas renders it, in the fields accent.

PL. 50  ·  FIELDS / NOISE / SIMPLEX LATTICE

Simplex Noise

Ken Perlin, 2001

OPEN THE LIVE PLATE ▸

DEFINITION

skew to the simplex lattice: F = (√3−1)/2
n(p) = 70·Σᵢ (½ − |dᵢ|²)⁴ · (gᵢ · dᵢ)

NOTES

Perlin rebuilt his own noise on a triangular lattice: three corners contribute instead of four, each through a radial kernel, so the cost grows linearly with dimension instead of exponentially. Then he patented it, and for two decades procedural graphics tiptoed around the construction — OpenSimplex exists specifically to avoid this formula. The patent lapsed in January 2022, and the original is finally free to write down.

PROVENANCE

Origin
K. Perlin, "Noise Hardware", SIGGRAPH course notes, 2001; the construction is laid out in detail by S. Gustavson, 2005
Patent
US6867776 — expired 8 January 2022
Standing
Free to use. Written from the published mathematics, not from any implementation.

TOUCHDESIGNER · GLSL

The same shader this plate runs, reframed for a GLSL TOP. Pasted bare it renders the published constants as a still frame; wire absTime.seconds into u_t on the Vectors page to animate it.

// FORMA — PL. 50 · SIMPLEX NOISE — Ken Perlin, 2001
//   skew to the simplex lattice: F = (√3−1)/2
//   n(p) = 70·Σᵢ (½ − |dᵢ|²)⁴ · (gᵢ · dᵢ)
// TouchDesigner port — paste into a GLSL TOP's pixel shader. Set the
// resolution on the TOP's Common page. As pasted it renders the published
// constants as a still frame; to animate, add a uniform named u_t on the
// GLSL TOP's Vectors 1 page with the expression absTime.seconds.
// Constants are consts — edit to tweak; comments give the measured range.
// Written from the published mathematics, not adapted from any code.

#define u_res (uTDOutputInfo.res.zw)
uniform float u_t;               // absTime.seconds on the Vectors page; unset = still

const float u_phase = 0.4827;    // this plate's own grid phase, 0..1
// FORMA's FIELDS accent as cosine-gradient coefficients
const vec3 u_pal_a = vec3(0.46, 0.3031, 0.11);
const vec3 u_pal_b = vec3(0.5, 0.3294, 0.1196);
const vec3 u_pal_c = vec3(1, 1, 1);
const vec3 u_pal_d = vec3(0, 0.05, 0.1);

const float p_scale = 6.0;         // frequency · live 2 .. 14
const float p_ridge = 0.0;         // ridged · live 0 .. 1
const float p_oct   = 1.0;         // octaves · live 1 .. 6

/* The order's ramp — the same cosine formulation the JS kit uses, so a
   plate keeps its classification colour in either language. */
vec3 ramp(float t){
  return clamp(u_pal_a + u_pal_b * cos(6.28318530718 * (u_pal_c * t + u_pal_d)), 0.0, 1.0);
}

/* Sawtooth and triangle on this plate's phase, mirroring the JS kit. */
float cycle(float t, float period){ return fract(t / period + u_phase); }
float pingpong(float t, float period){
  float u = cycle(t, period);
  return u < 0.5 ? u * 2.0 : 2.0 - u * 2.0;
}


float hash2(int x, int y){
  uint h = uint(x) * 374761393u + uint(y) * 668265263u;
  h ^= h >> 13u;
  h *= 1274126177u;
  h ^= h >> 16u;
  return float(h) / 4294967296.0;
}

float smoothCurve(float t){ return t * t * (3.0 - 2.0 * t); }
float fadeCurve(float t){ return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }

/* Value noise: bilinear interpolation of a hashed lattice. */
float valueNoise(vec2 p){
  vec2 c = floor(p), f = p - c;
  int xi = int(c.x), yi = int(c.y);
  float u = smoothCurve(f.x), v = smoothCurve(f.y);
  return mix(mix(hash2(xi, yi),     hash2(xi + 1, yi),     u),
             mix(hash2(xi, yi + 1), hash2(xi + 1, yi + 1), u), v);
}

/* Gradient (Perlin) noise: dot products against pseudo-random unit vectors. */
float gradDot(int ix, int iy, float dx, float dy){
  float a = hash2(ix, iy) * 6.28318530718;
  return cos(a) * dx + sin(a) * dy;
}
float gradNoise(vec2 p){
  vec2 c = floor(p), f = p - c;
  int xi = int(c.x), yi = int(c.y);
  float u = fadeCurve(f.x), v = fadeCurve(f.y);
  return mix(mix(gradDot(xi,     yi,     f.x,       f.y),
                 gradDot(xi + 1, yi,     f.x - 1.0, f.y), u),
             mix(gradDot(xi,     yi + 1, f.x,       f.y - 1.0),
                 gradDot(xi + 1, yi + 1, f.x - 1.0, f.y - 1.0), u), v) * 0.7071 + 0.5;
}

/* Octaves summed at falling amplitude. GLSL has no function pointers, so the
   two bases are two functions rather than one with a noise argument. The
   three-argument forms take the per-octave gain — the Hurst roughness dial,
   mirroring the kit's fbm — and the two-argument forms keep the classic 0.5
   so existing call sites read unchanged. */
float fbmValue(vec2 p, int oct, float gain){
  float sum = 0.0, amp = 0.5, norm = 0.0;
  for (int i = 0; i < 9; i++){          // 9 is the octave slider's ceiling
    if (i >= oct) break;
    sum += amp * valueNoise(p);
    norm += amp;
    amp *= gain;
    p *= 2.0;
  }
  return sum / norm;
}
float fbmValue(vec2 p, int oct){ return fbmValue(p, oct, 0.5); }
float fbmGrad(vec2 p, int oct, float gain){
  float sum = 0.0, amp = 0.5, norm = 0.0;
  for (int i = 0; i < 9; i++){
    if (i >= oct) break;
    sum += amp * gradNoise(p);
    norm += amp;
    amp *= gain;
    p *= 2.0;
  }
  return sum / norm;
}
float fbmGrad(vec2 p, int oct){ return fbmGrad(p, oct, 0.5); }

/* One simplex evaluation — exactly the published construction. */
float snoise2(vec2 p){
  /* Gustavson's F2 = (√3−1)/2 and G2 = (3−√3)/6, under longer names: F2 and
     G2 are preprocessor macros in TouchDesigner's GLSL environment, and the
     port would not compile there under the paper's own names. */
  float skewF2 = 0.3660254, unskewG2 = 0.2113249;
  float sk = (p.x + p.y) * skewF2;
  float i = floor(p.x + sk), j = floor(p.y + sk);
  float un = (i + j) * unskewG2;
  float x0 = p.x - (i - un), y0 = p.y - (j - un);
  float i1 = x0 > y0 ? 1.0 : 0.0, j1 = 1.0 - i1;
  float x1 = x0 - i1 + unskewG2, y1 = y0 - j1 + unskewG2;
  float x2 = x0 - 1.0 + 2.0 * unskewG2, y2 = y0 - 1.0 + 2.0 * unskewG2;
  float n = 0.0, tk;
  int ii = int(i), jj = int(j);
  tk = 0.5 - x0 * x0 - y0 * y0;
  if (tk > 0.0){ tk *= tk; n += tk * tk * gradDot(ii, jj, x0, y0); }
  tk = 0.5 - x1 * x1 - y1 * y1;
  if (tk > 0.0){ tk *= tk; n += tk * tk * gradDot(ii + int(i1), jj + int(j1), x1, y1); }
  tk = 0.5 - x2 * x2 - y2 * y2;
  if (tk > 0.0){ tk *= tk; n += tk * tk * gradDot(ii + 1, jj + 1, x2, y2); }
  return clamp(n * 70.0 * 0.5 + 0.5, 0.0, 1.0);
}
vec3 plate(vec2 uv){
  float ang = u_phase * 6.283;
  vec2 p = uv * p_scale + vec2(u_t * 0.06 * cos(ang), u_t * 0.06 * sin(ang));
  float n = 0.0, amp = 0.5, norm = 0.0;
  int oct = int(floor(p_oct + 0.5));
  for (int o = 0; o < 6; o++){            // 6 is the octave slider's ceiling
    if (o >= oct) break;
    n += amp * snoise2(p);
    norm += amp; amp *= 0.5; p *= 2.0;
  }
  n /= norm;
  if (p_ridge > 0.5) n = 1.0 - abs(n * 2.0 - 1.0);
  return ramp(n * 0.7 + 0.05);
}

out vec4 fragColor;
void main(){
  // FORMA's uv runs y-down, matching its canvas; TD's vUV runs up
  vec2 uv = vec2(vUV.s, 1.0 - vUV.t);
  fragColor = TDOutputSwizzle(vec4(plate(uv), 1.0));
}

NUKE · BLINKSCRIPT

The same shader this plate runs, transpiled to a BlinkScript kernel. Paste it into a BlinkScript node's Kernel Source and press Recompile; every constant arrives as a knob at its published value, and u_t animates with the expression frame/24. Compiled and rendered in Nuke 17.1, then compared against this plate on the page.

// FORMA — PL. 50 · SIMPLEX NOISE — Ken Perlin, 2001
//   skew to the simplex lattice: F = (√3−1)/2
//   n(p) = 70·Σᵢ (½ − |dᵢ|²)⁴ · (gᵢ · dᵢ)
// Nuke port — a BlinkScript kernel. Paste into a BlinkScript node's Kernel
// Source and press Recompile. Every constant arrives as a knob at its published
// value (the comment gives the measured range); u_t is a knob too — animate it
// with the expression frame/24 or leave it at 0 for the still frame. Written
// from the published mathematics, not adapted from any code.
// Transpiled from the shader this plate runs on the page (GLSL ES 3.00):
// vec → float2/3/4, swizzles expanded, GLSL builtins Blink lacks written out
// as forma_ functions, float literals suffixed. Compiled and rendered in a
// real Nuke (17.1v1) and compared against this plate on the page: 34 of 34.
//
// plate() and its helpers are written to a single exit — the loop that runs
// once. That is not a style: Blink 17.1 drops a conditional early return from
// a called function while Vectorize is on, which is the node default, with no
// warning and no error. Written this way it paints correctly as pasted.

kernel Forma_simplex : ImageComputationKernel<ePixelWise>
{
  Image<eWrite> dst;

param:
  float u_t;             // seconds; 0 is the still frame
  float p_scale; // frequency · live 2 .. 14
  float p_ridge; // ridged · live 0 .. 1
  float p_oct;   // octaves · live 1 .. 6

local:
  float2 u_res;
  float u_phase;
  float3 u_pal_a, u_pal_b, u_pal_c, u_pal_d;

  void define(){
    defineParam(u_t, "u_t", 0.0f);
    defineParam(p_scale, "p_scale", 6.0f);
    defineParam(p_ridge, "p_ridge", 0.0f);
    defineParam(p_oct, "p_oct", 1.0f);
  }

  void init(){
    u_res = float2(float(dst.bounds.width()), float(dst.bounds.height()));
    u_phase = 0.4827f;    // this plate's own grid phase, 0..1
    // FORMA's FIELDS accent as cosine-gradient coefficients
    u_pal_a = float3(0.46f, 0.3031f, 0.11f);
    u_pal_b = float3(0.5f, 0.3294f, 0.1196f);
    u_pal_c = float3(1.0f, 1.0f, 1.0f);
    u_pal_d = float3(0.0f, 0.05f, 0.1f);
  }

  /* GLSL builtins Blink lacks, written as templates rather than overload sets.
     Blink's operators return expression templates (Swizzle<float,N>), so a call
     passing an expression cannot resolve against an overload set on float2
     against float3 — measured in Nuke 17.1: a float2 expression is ambiguous
     between the two, while scalar-against-vector resolves. A template deduces
     the expression's own type, so the ambiguity cannot arise. */
  template <class T> T forma_fract(T v){ return v - floor(v); }
  template <class T, class S> T forma_mod(T x, S y){ return x - y * floor(x / y); }
  /* Blink's own min/max/clamp take no scalar bound against a vector, which GLSL
     does; v * 0.0f + b is that bound at the vector's own width, and collapses to
     b when v is a scalar, so one template serves both. */
  template <class T, class S> T forma_min(T a, S b){ return min(a, a * 0.0f + b); }
  template <class T, class S> T forma_max(T a, S b){ return max(a, a * 0.0f + b); }
  template <class T, class S> T forma_clamp(T v, S lo, S hi){ return clamp(v, v * 0.0f + lo, v * 0.0f + hi); }
  int forma_min(int a, int b){ return min(a, b); }
  int forma_max(int a, int b){ return max(a, b); }
  /* GLSL step(edge, x) is 1 where x >= edge; floor(sign(x - e) * 0.5 + 1) is
     that exactly, equality included, out of builtins Blink does have. */
  template <class T, class S> T forma_step(S e, T x){ return floor(sign(x - e) * 0.5f + 1.0f); }
  template <class T, class S> T forma_smoothstep(S a, S b, T x){
    T t = forma_clamp((x - a) / (b - a), 0.0f, 1.0f);
    return t * t * (3.0f - 2.0f * t);
  }
  template <class T> float forma_distance(T a, T b){ return length(a - b); }
  float forma_tanh(float x){ float e = exp(2.0f * x); return (e - 1.0f) / (e + 1.0f); }
  float forma_radians(float d){ return d * 0.01745329252f; }
  // the page's hash2 is exact uint32; Blink has int, so the shifts are made
  // logical by masking and the read-back is lifted into 0 .. 2^32
  /* A uint read back as a float. Blink has no unsigned type, so a value past
     2^31 arrives as a negative int and float() of it is negative. Measured on
     gabor, whose own generator then returned uniforms in [-0.5, 0.5) and drew
     a different picture — it compiled, it rendered, and only comparing it with

  /* The order's ramp — the same cosine formulation the JS kit uses, so a
     plate keeps its classification colour in either language. */
  float3 ramp(float t){
    return forma_clamp(u_pal_a + u_pal_b * cos(6.28318530718f * (u_pal_c * t + u_pal_d)), 0.0f, 1.0f);
  }

  /* Sawtooth and triangle on this plate's phase, mirroring the JS kit. */
  float cycle(float t, float period){ return forma_fract(t / period + u_phase); }
  float pingpong(float t, float period){
    float u = cycle(t, period);
    return u < 0.5f ? u * 2.0f : 2.0f - u * 2.0f;
  }


  float hash2(int x, int y){
    uint h = uint(x) * 374761393u + uint(y) * 668265263u;
    h ^= h >> 13u;
    h *= 1274126177u;
    h ^= h >> 16u;
    return float(h) / 4294967296.0f;
  }

  float smoothCurve(float t){ return t * t * (3.0f - 2.0f * t); }
  float fadeCurve(float t){ return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); }

  /* Value noise: bilinear interpolation of a hashed lattice. */
  float valueNoise(float2 p){
    float2 c = floor(p);
    float2 f = p - c;
    int xi = int(c.x);
    int yi = int(c.y);
    float u = smoothCurve(f.x);
    float v = smoothCurve(f.y);
    return lerp(lerp(hash2(xi, yi),     hash2(xi + 1, yi),     u),
               lerp(hash2(xi, yi + 1), hash2(xi + 1, yi + 1), u), v);
  }

  /* Gradient (Perlin) noise: dot products against pseudo-random unit vectors. */
  float gradDot(int ix, int iy, float dx, float dy){
    float a = hash2(ix, iy) * 6.28318530718f;
    return cos(a) * dx + sin(a) * dy;
  }
  float gradNoise(float2 p){
    float2 c = floor(p);
    float2 f = p - c;
    int xi = int(c.x);
    int yi = int(c.y);
    float u = fadeCurve(f.x);
    float v = fadeCurve(f.y);
    return lerp(lerp(gradDot(xi,     yi,     f.x,       f.y),
                   gradDot(xi + 1, yi,     f.x - 1.0f, f.y), u),
               lerp(gradDot(xi,     yi + 1, f.x,       f.y - 1.0f),
                   gradDot(xi + 1, yi + 1, f.x - 1.0f, f.y - 1.0f), u), v) * 0.7071f + 0.5f;
  }

  /* Octaves summed at falling amplitude. GLSL has no function pointers, so the
     two bases are two functions rather than one with a noise argument. The
     three-argument forms take the per-octave gain — the Hurst roughness dial,
     mirroring the kit's fbm — and the two-argument forms keep the classic 0.5f
     so existing call sites read unchanged. */
  float fbmValue(float2 p, int oct, float gain){
    float sum = 0.0f;
    float amp = 0.5f;
    float norm = 0.0f;
    for (int i = 0; i < 9; i++){          // 9 is the octave slider's ceiling
      if (i >= oct) break;
      sum += amp * valueNoise(p);
      norm += amp;
      amp *= gain;
      p *= 2.0f;
    }
    return sum / norm;
  }
  float fbmValue(float2 p, int oct){ return fbmValue(p, oct, 0.5f); }
  float fbmGrad(float2 p, int oct, float gain){
    float sum = 0.0f;
    float amp = 0.5f;
    float norm = 0.0f;
    for (int i = 0; i < 9; i++){
      if (i >= oct) break;
      sum += amp * gradNoise(p);
      norm += amp;
      amp *= gain;
      p *= 2.0f;
    }
    return sum / norm;
  }
  float fbmGrad(float2 p, int oct){ return fbmGrad(p, oct, 0.5f); }

  /* One simplex evaluation — exactly the published construction. */
  float snoise2(float2 p){
    /* Gustavson's F2 = (√3−1)/2 and G2 = (3−√3)/6, under longer names: F2 and
       G2 are preprocessor macros in TouchDesigner's GLSL environment, and the
       port would not compile there under the paper's own names. */
    float skewF2 = 0.3660254f;
    float unskewG2 = 0.2113249f;
    float sk = (p.x + p.y) * skewF2;
    float i = floor(p.x + sk);
    float j = floor(p.y + sk);
    float un = (i + j) * unskewG2;
    float x0 = p.x - (i - un);
    float y0 = p.y - (j - un);
    float i1 = x0 > y0 ? 1.0f : 0.0f;
    float j1 = 1.0f - i1;
    float x1 = x0 - i1 + unskewG2;
    float y1 = y0 - j1 + unskewG2;
    float x2 = x0 - 1.0f + 2.0f * unskewG2;
    float y2 = y0 - 1.0f + 2.0f * unskewG2;
    float n = 0.0f;
    float tk;
    int ii = int(i);
    int jj = int(j);
    tk = 0.5f - x0 * x0 - y0 * y0;
    if (tk > 0.0f){ tk *= tk; n += tk * tk * gradDot(ii, jj, x0, y0); }
    tk = 0.5f - x1 * x1 - y1 * y1;
    if (tk > 0.0f){ tk *= tk; n += tk * tk * gradDot(ii + int(i1), jj + int(j1), x1, y1); }
    tk = 0.5f - x2 * x2 - y2 * y2;
    if (tk > 0.0f){ tk *= tk; n += tk * tk * gradDot(ii + 1, jj + 1, x2, y2); }
    return forma_clamp(n * 70.0f * 0.5f + 0.5f, 0.0f, 1.0f);
  }
  float3 plate(float2 uv){
    float ang = u_phase * 6.283f;
    float2 p = uv * p_scale + float2(u_t * 0.06f * cos(ang), u_t * 0.06f * sin(ang));
    float n = 0.0f;
    float amp = 0.5f;
    float norm = 0.0f;
    int oct = int(floor(p_oct + 0.5f));
    for (int o = 0; o < 6; o++){            // 6 is the octave slider's ceiling
      if (o >= oct) break;
      n += amp * snoise2(p);
      norm += amp; amp *= 0.5f; p *= 2.0f;
    }
    n /= norm;
    if (p_ridge > 0.5f) n = 1.0f - fabs(n * 2.0f - 1.0f);
    return ramp(n * 0.7f + 0.05f);
  }

  void process(int2 pos){
    // FORMA's uv runs y-down like its canvas; Nuke's rows run up
    float2 uv = float2((float(pos.x) + 0.5f) / u_res.x, 1.0f - (float(pos.y) + 0.5f) / u_res.y);
    float3 c = plate(uv);
    dst() = float4(c.x, c.y, c.z, 1.0f);
  }
};