Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 111, Vicsek Fractal: a still of the fractal / distance estimate plate as the atlas renders it, in the fractals accent.

PL. 111  ·  FRACTALS / FRACTAL / DISTANCE ESTIMATE

Vicsek Fractal

Tamás Vicsek, 1989

OPEN THE LIVE PLATE ▸

DEFINITION

divide [0,1]² into 3×3, keep the four corners and the centre, recurse
N(n) = 5ⁿ cells of side 3⁻ⁿ, area(n) = (5/9)ⁿ
dim_H = log 5 / log 3 ≈ 1.4650

NOTES

Divide a square into a three-by-three grid. Keep the four corner cells and the centre cell; discard the four cells that share an edge with the centre instead of a corner. Recurse into each of the five survivors. What is left at every depth still reads as a small plus sign standing on four smaller plus signs at its own corners, standing on smaller ones again — a construction Vicsek set among the deterministic growth models in his 1989 book on fractal growth, the same year a companion paper of his worked through four such deterministic models in print. Its Hausdorff dimension, log 5 over log 3, sits below the log 8 over log 3 of the carpet at plate 107 for the reason the picture already shows: five surviving cells instead of eight leaves far less area behind at every level, so past depth five or six this figure reads less like a filled shape and more like the lace it is becoming in the limit. Where the carpet plate classifies a pixel by checking, at every level, whether both of its ternary digits read one at once, this plate instead folds the pixel through the same recursive zoom and, the moment a fold lands in one of the four discarded cells, measures how far short it fell of the two neighbouring survivors and divides that by how many folds it took to get there. That quotient is a real distance estimate rather than a flat yes or no, so the boundary of the finite-depth figure stays smooth under the same slow zoom and turn that carries the frame, instead of stair-stepping the way a hard classification would.

PROVENANCE

Origin
T. Vicsek, "Deterministic models of fractal and multifractal growth", Physica D: Nonlinear Phenomena 38(1-3), 1989, 356-361, doi:10.1016/0167-2789(89)90219-4 — a paper from the same year and the same author as the book this construction is usually pictured in, T. Vicsek, Fractal Growth Phenomena, World Scientific, Singapore, 1989 (ISBN 9789971504427). Both are cited because both are genuinely his and genuinely 1989; only the journal paper carries a DOI that Crossref confirms matches this entry on title, author and year, which is why it is the src link and the book is not
What was checked and what was not
Neither source is open access, so the exact page or figure carrying the corner-plus-centre construction could not be read directly, and this entry says so rather than pretending otherwise. What is directly verified is the bibliographic record itself against Crossref (title, author, venue, year all match), and that the object commonly called the Vicsek fractal is consistently attributed in the secondary literature to this 1989 pair rather than to anything earlier
The one it is not
Vicsek also published "Fractal models for diffusion controlled aggregation", J. Phys. A 16, 1983, L647, doi:10.1088/0305-4470/16/17/003 — and this is frequently repeated online as the source of the corner-plus-centre fractal. Read directly, its abstract describes a stochastic diffusion-limited-deposition model with no fixed self-similar construction in it anywhere. That earlier paper is not cited here
Standing
Public domain — an iterated function system over a 3x3 grid. No patent has ever applied to the construction itself
Constants
depth, zoom and spin are all left free for regenerate — see the LOCKED note above for the measured reasoning
Source
doi:10.1016/0167-2789(89)90219-4

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. 111 · VICSEK FRACTAL — Tamás Vicsek, 1989
//   divide [0,1]² into 3×3, keep the four corners and the centre, recurse
//   N(n) = 5ⁿ cells of side 3⁻ⁿ, area(n) = (5/9)ⁿ
//   dim_H = log 5 / log 3 ≈ 1.4650
// 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.9568;    // this plate's own grid phase, 0..1
// FORMA's FRACTALS accent as cosine-gradient coefficients
const vec3 u_pal_a = vec3(0.46, 0.1389, 0.1912);
const vec3 u_pal_b = vec3(0.5, 0.151, 0.2078);
const vec3 u_pal_c = vec3(1, 1, 1);
const vec3 u_pal_d = vec3(0, 0.05, 0.1);

const float p_depth = 4.0;         // recursion depth · live 2 .. 7
const float p_zoom  = 1.2;         // zoom · live 0.8 .. 3
const float p_spin  = 0.1;         // rotation speed · live 0 .. 0.5

/* 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 vicsek_boxDist(vec2 p, vec2 c, float r){
  vec2 d = max(abs(p - c) - vec2(r), 0.0);
  return length(d);
}
vec3 plate(vec2 uv){
  float ar = u_res.y / u_res.x;
  float ang = u_phase * 6.283185307179586 + u_t * p_spin;
  float ca = cos(ang), sa = sin(ang);
  float sc = p_zoom * (0.75 + 0.5 * pingpong(u_t * 0.04, 1.0));
  vec3 ink = vec3(4.0, 6.0, 10.0) / 255.0;
  vec3 body = ramp(0.94), edge = ramp(1.04);

  float x = (uv.x - 0.5), y = (uv.y - 0.5) * ar;
  float px = (x * ca - y * sa) * sc + 0.5;
  float py = (x * sa + y * ca) * sc + 0.5;
  vec2 c = (fract(vec2(px, py)) - 0.5) * 2.0;

  float depth = floor(p_depth + 0.5);
  int lastK = -1;
  bool survived = true;
  // Constant loop bound at the slider ceiling, break on the live value —
  // mandelbrot fashion.
  for (int i = 0; i < 7; i++){
    if (float(i) >= depth) break;
    vec2 ac = abs(c);
    bool xCentre = ac.x < (1.0 / 3.0);
    bool yCentre = ac.y < (1.0 / 3.0);
    if (xCentre == yCentre){
      float ox = xCentre ? 0.0 : (c.x >= 0.0 ? 2.0 / 3.0 : -2.0 / 3.0);
      float oy = yCentre ? 0.0 : (c.y >= 0.0 ? 2.0 / 3.0 : -2.0 / 3.0);
      c = (c - vec2(ox, oy)) * 3.0;
    } else {
      lastK = i;
      survived = false;
      break;
    }
  }

  float fill = 0.0;
  vec3 tone = body;
  if (survived){
    fill = 1.0;
  } else if (float(lastK) == depth - 1.0){
    vec2 corner = vec2(c.x >= 0.0 ? 2.0 / 3.0 : -2.0 / 3.0, c.y >= 0.0 ? 2.0 / 3.0 : -2.0 / 3.0);
    float local = min(vicsek_boxDist(c, vec2(0.0), 1.0 / 3.0),
                       vicsek_boxDist(c, corner, 1.0 / 3.0));
    fill = 1.0 - clamp(local / 0.22, 0.0, 1.0);
    tone = edge;
  }
  return mix(ink, tone, fill);
}

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. 111 · VICSEK FRACTAL — Tamás Vicsek, 1989
//   divide [0,1]² into 3×3, keep the four corners and the centre, recurse
//   N(n) = 5ⁿ cells of side 3⁻ⁿ, area(n) = (5/9)ⁿ
//   dim_H = log 5 / log 3 ≈ 1.4650
// 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_vicsek : ImageComputationKernel<ePixelWise>
{
  Image<eWrite> dst;

param:
  float u_t;             // seconds; 0 is the still frame
  float p_depth; // recursion depth · live 2 .. 7
  float p_zoom;  // zoom · live 0.8 .. 3
  float p_spin;  // rotation speed · live 0 .. 0.5

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_depth, "p_depth", 4.0f);
    defineParam(p_zoom, "p_zoom", 1.2f);
    defineParam(p_spin, "p_spin", 0.1f);
  }

  void init(){
    u_res = float2(float(dst.bounds.width()), float(dst.bounds.height()));
    u_phase = 0.9568f;    // this plate's own grid phase, 0..1
    // FORMA's FRACTALS accent as cosine-gradient coefficients
    u_pal_a = float3(0.46f, 0.1389f, 0.1912f);
    u_pal_b = float3(0.5f, 0.151f, 0.2078f);
    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 vicsek_boxDist(float2 p, float2 c, float r){
    float2 d = forma_max(fabs(p - c) - float2(r), 0.0f);
    return length(d);
  }
  float3 plate(float2 uv){
    float ar = u_res.y / u_res.x;
    float ang = u_phase * 6.283185307179586f + u_t * p_spin;
    float ca = cos(ang);
    float sa = sin(ang);
    float sc = p_zoom * (0.75f + 0.5f * pingpong(u_t * 0.04f, 1.0f));
    float3 ink = float3(4.0f, 6.0f, 10.0f) / 255.0f;
    float3 body = ramp(0.94f);
    float3 edge = ramp(1.04f);

    float x = (uv.x - 0.5f);
    float y = (uv.y - 0.5f) * ar;
    float px = (x * ca - y * sa) * sc + 0.5f;
    float py = (x * sa + y * ca) * sc + 0.5f;
    float2 c = (forma_fract(float2(px, py)) - 0.5f) * 2.0f;

    float depth = floor(p_depth + 0.5f);
    int lastK = -1;
    bool survived = true;
    // Constant loop bound at the slider ceiling, break on the live value —
    // mandelbrot fashion.
    for (int i = 0; i < 7; i++){
      if (float(i) >= depth) break;
      float2 ac = fabs(c);
      bool xCentre = ac.x < (1.0f / 3.0f);
      bool yCentre = ac.y < (1.0f / 3.0f);
      if (xCentre == yCentre){
        float ox = xCentre ? 0.0f : (c.x >= 0.0f ? 2.0f / 3.0f : -2.0f / 3.0f);
        float oy = yCentre ? 0.0f : (c.y >= 0.0f ? 2.0f / 3.0f : -2.0f / 3.0f);
        c = (c - float2(ox, oy)) * 3.0f;
      } else {
        lastK = i;
        survived = false;
        break;
      }
    }

    float fill = 0.0f;
    float3 tone = body;
    if (survived){
      fill = 1.0f;
    } else if (float(lastK) == depth - 1.0f){
      float2 corner = float2(c.x >= 0.0f ? 2.0f / 3.0f : -2.0f / 3.0f, c.y >= 0.0f ? 2.0f / 3.0f : -2.0f / 3.0f);
      float forma_local = forma_min(vicsek_boxDist(c, float2(0.0f), 1.0f / 3.0f),
                         vicsek_boxDist(c, corner, 1.0f / 3.0f));
      fill = 1.0f - forma_clamp(forma_local / 0.22f, 0.0f, 1.0f);
      tone = edge;
    }
    return lerp(ink, tone, fill);
  }

  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);
  }
};