Worley Noise

Worley Noise

Overview

Worley noise (also called cellular or Voronoi noise) works by scattering random points across a grid and measuring how close each pixel is to its nearest neighbours. The result is that organic cell-like pattern you see in nature, skin, stone, foam, water surfaces.

This implementation runs entirely on the GPU via Blinkscript with no input required.

My first kernel that used a 3D hash for animation — Z which is time in this case makes cells evolve in place instead of sliding.


Result Preview


The Algorithm

Feature points and neighbourhood

The image is divided into a grid of unit cells. Each cell gets one feature point placed at a random but stable position via hash3(). hash3() is a pseudo random hash that chains dot → sin → fract to scramble a cell coordinate into a value that looks random but is fully deterministic, so the same cell always produces the same feature point.

For each pixel the kernel scans a 3×3×3 neighbourhood around it, 27 cells in total, and tracks the two closest distances, F1 and F2. These are the classic Worley distances to the nearest and second nearest feature points.

Initial distances and clamping

Both F1 and F2 start at 10.0f before the search begins, a value large enough that the first real distance always wins and replaces it. You could initialise with 1.0f instead, but if you push the scale low enough, you get more smaller cells and distances can exceed 1.0f. At that point F1 never updates and the output clips to white. 10.0f is the safe option, while 0.0f will just give you a black screen.

Scale control - Simillar to Nukes Noise Scale

To determine the size of our cells, we divide our pixel coordinates by the scale parameter: float3 p = float3(px, py, zmove) / scale; Higher Scale = larger, more spread. Lower Scale = smaller,denser cells.

A common pattern

This initialise and minimise pattern is not unique to Worley noise. Any algorithm that searches for a minimum needs a starting value to compare against, for example, raymarching, pathfinding, and ofc nearest neighbour.

Distance Modes

Return Types


Video explaining more about cellular noise.


This is what i’ve learnt this far does not mean its correct.- Not me in the Video either.

Blinkscript Code

// WorleyNoise — Cellular / Voronoi
// Distance Type:  0=Euclidean  1=Manhattan  2=Chebyshev
// Return Type:    0=F1  1=F2  2=F2-F1  3=F1+F2

kernel WorleyNoise : ImageComputationKernel<ePixelWise> {
  Image<eWrite, eAccessPoint> dst;

  param:
    float scale;
    float zmove;
    int   pixelSize;
    int   distanceType;
    int   returnType;

  local:
    void define() {
      defineParam(scale,        "Scale",         1.0f);
      defineParam(zmove,        "Z Move",        0.0f);
      defineParam(pixelSize,    "Pixel Size",    1);
      defineParam(distanceType, "Distance Type", 0);
      defineParam(returnType,   "Return Type",   0);
    }

    // fract does not exist in blinkscript, unless I've missed it and it has another name.
    // But we can get around it by creating it ourselves.
    float fract(float x) { return x - floor(x); }

    float3 fract3(float3 p) {
      return float3(fract(p.x), fract(p.y), fract(p.z));
    }

    // places a stable random point inside each cell
    // dot → sin → fract: same input always gives same output
    float3 hash3(float3 p) {
      float3 q = float3(
        dot(p, float3(127.1f, 311.7f,  74.7f)),
        dot(p, float3(269.5f, 183.3f, 246.1f)),
        dot(p, float3(113.5f, 271.9f, 124.6f))
      );
      return fract3(sin(q) * 43758.5453f);
    }

    // Euclidean=round, Manhattan=diamond, Chebyshev=square
    float calcDist(float3 d, int type) {
      if      (type == 0) return sqrt(d.x*d.x + d.y*d.y + d.z*d.z);
      else if (type == 1) return fabs(d.x) + fabs(d.y) + fabs(d.z);
      else                return max(max(fabs(d.x), fabs(d.y)), fabs(d.z));
    }

    // searches all 27 neighbours, keeps the two closest distances
    void cellularF(float3 p, float &F1, float &F2) {
      float3 cell   = floor(p);
      float3 frac_p = p - cell;

      F1 = 10.0f;
      F2 = 10.0f;

      for (int z = -1; z <= 1; z++) {
        for (int y = -1; y <= 1; y++) {
          for (int x = -1; x <= 1; x++) {
            float3 neighbor     = float3(x, y, z);
            float3 featurePoint = neighbor + hash3(cell + neighbor);
            float3 diff         = featurePoint - frac_p;
            float  dist         = calcDist(diff, distanceType);

            if (dist < F1) { F2 = F1; F1 = dist; }
            else if (dist < F2) { F2 = dist; }
          }
        }
      }
    }

  void process(int2 pos) {
    // pixelSize > 1 = give a pixel styled look
    //int    px = (pos.x / pixelSize) * pixelSize;
    //int    py = (pos.y / pixelSize) * pixelSize;

    int    px = pos.x; 
    int    py = pos.y;

    // Divide by scale so higher values = larger, fewer cells.
    float3 p  = float3(px, py, zmove) / scale;

    float F1, F2;
    cellularF(p, F1, F2);

    // F2-F1 gives sharp cell borders, good for cracks/veins
    float noise;
    if      (returnType == 0) noise = F1;
    else if (returnType == 1) noise = F2;
    else if (returnType == 2) noise = F2 - F1;
    else                      noise = F1 + F2;

    dst() = float4(clamp(noise, 0.0f, 1.0f));
  }
};