Mastering Turing Patterns: A Guide to Reaction-Diffusion in p5.js and Processing

Turing pattern made with the 'Processing'

This article explores creative coding with p5.js and Processing to generate organic-looking, intriguing Turing patterns.

This article covers the essential concepts for creating captivating visuals, provide simple example code, and share an advanced application. With these tools, you'll be able to create your own original work.

👉 この記事は日本語でも読めます。

 

Turing Patterns: An Overview

Simply put, Turing patterns resemble the intricate patterns on tropical fish or leopards fur. These patterns emerge from a mathematical process known as a reaction-diffusion system.

The pattern that looks like the surface of tropical fish.

English mathematician Alan Turing originally developed the theory behind these systems to explain how patterns in nature form.

Wikipedia : Turing pattern

Generating Turing patterns requires intensive computation—exactly where computers excel.

Creative coding on Turing pattern.

 

Basic p5.js Implementation

The following p5.js code demonstrates the fundamentals of drawing a Turing pattern using the Gray-Scott model.


/*
 * Reaction-Diffusion system by the Gray-Scott Model.
 * basic example.
 * 
 * @author @deconbatch
 * @version 0.1
 * p5.js 1.1.3
 * license CC0
 * created 2022.03.26
 */

const w = 480;
const h = w;
const cSiz = 3;   // cell size
const pCnt = 500; // calculation count

function setup() {
  createCanvas(w, h);
  noLoop();

  const lab = new Labo(cSiz);
  lab.init();
  for (let i = 0; i < pCnt; i++) {
    lab.proceed();
  }
  lab.observe();
}

/*
 * Labo : reaction-diffusion system.
 */
class Labo {

  cellSize;
  matrixW;
  matrixH;
  diffU;
  diffV;
  cells;

  constructor(_cSiz) {
    this.cellSize = _cSiz;
    this.matrixW = floor(width / this.cellSize);
    this.matrixH = floor(height / this.cellSize);
    this.diffU = 0.9;
    this.diffV = 0.1;
    this.cells = new Array();
  }

  /*
   * init : initialize reaction-diffusion system.
   */
  init() {
    for (let x = 0; x < this.matrixW; x++) {
      this.cells[x] = [];
      for (let y = 0; y < this.matrixH; y++) {
        this.cells[x][y] = new Cell(
          map(x, 0.0, this.matrixW, 0.03, 0.12),   // feed
          map(y, 0.0, this.matrixH, 0.045, 0.055), // kill
          1,                         // u
          (random(1) < 0.1) ? 1 : 0  // v
        );
      }
    }
  }

  /*
   * proceed : proceed reaction-diffusion calculation.
   */
  proceed() {

    // calculate Laplacian
    const nD = Array(); // neighbors on diagonal
    const nH = Array(); // neighbors on vertical and horizontal
    for (let x = 0; x < this.matrixW; x++) {
      for (let y = 0; y < this.matrixH; y++) {

        // set neighbors
        nD[0] = this.cells[max(x-1,0)][max(y-1,0)];
        nD[1] = this.cells[max(x-1,0)][min(y+1,this.matrixH-1)];
        nD[2] = this.cells[min(x+1,this.matrixW-1)][max(y-1,0)];
        nD[3] = this.cells[min(x+1,this.matrixW-1)][min(y+1,this.matrixH-1)];
        nH[0] = this.cells[max(x-1,0)][y];
        nH[1] = this.cells[x][max(y-1,0)];
        nH[2] = this.cells[x][min(y+1,this.matrixH-1)];
        nH[3] = this.cells[min(x+1,this.matrixW-1)][y];

        // Laplacian
        let c = this.cells[x][y];
        let sum = 0.0;
        for (let i = 0; i < 4; i++) {
          sum += nD[i].valU * 0.05 + nH[i].valU * 0.2;
        }
        sum -= c.valU;
        c.lapU = sum;

        sum = 0.0;
        for (let i = 0; i < 4; i++) {
          sum += nD[i].valV * 0.05 + nH[i].valV * 0.2;
        }
        sum -= c.valV;
        c.lapV = sum;

      }
    }

    // reaction-diffusion
    for (let x = 0; x < this.matrixW; x++) {
      for (let y = 0; y < this.matrixH; y++) {
        let c = this.cells[x][y];
        let reaction = c.valU * c.valV * c.valV;
        let inflow   = c.feed * (1.0 - c.valU);
        let outflow  = (c.feed + c.kill) * c.valV;
        c.valU = c.valU + this.diffU * c.lapU - reaction + inflow;
        c.valV = c.valV + this.diffV * c.lapV + reaction - outflow;
        c.standardization();
      }
    }
  }

  /*
   * observe : display the result.
   */
  observe() {
    background(0);
    fill(255);
    noStroke();
    for (let x = 0; x < this.matrixW; x++) {
      for (let y = 0; y < this.matrixH; y++) {
        let cx = x * this.cellSize;
        let cy = y * this.cellSize;
        let cs = this.cells[x][y].valU * this.cellSize;
        rect(cx, cy, cs, cs);
      }
    }
  }
}

/*
 * Cell : holds cell informations.
 */
class Cell {

  feed; 
  kill;
  valU;
  valV;
  lapU;
  lapV;

  constructor(_f, _k, _u, _v) {
    this.feed = _f;
    this.kill = _k;
    this.valU = _u;
    this.valV = _v;
    this.lapU = 0;
    this.lapV = 0;
  }

  standardization() {
    this.valU = constrain(this.valU, 0, 1);
    this.valV = constrain(this.valV, 0, 1);
  }

}


Note: If you're running this on OpenProcessing, be mindful of "loop protection" settings due to the high computation time.

 

This implementation uses the Gray-Scott model. The resulting pattern evolves based on the reaction-diffusion parameters mapped across the XY axes.

Calculation result of reaction-diffusion equation.

 

Code Overview

The Labo class handles reaction-diffusion calculations. The calculation scope is 'matrixW x matrixH' area. The Cell class represents a grid square in the matrix. The cell[matrixW][matrixH] array represents all cells in the computational grid.

'class Labo init()' method initializes all cells in the computational grid. 'class Labo proceed()' calculates the reaction-diffusion equations once. The number of iterations is configurable.

This process is computationally expensive and can take time to render. The calculation time is proportional to pCnt and increases fourfold when cSize is halved.

With these parameters, execution took about one minute on my machine.


const cSiz = 3;   // cell size
const pCnt = 500; // calculation count


The two-dimensional array this.cells[x][y] holds the calculation results.

While this example generates monochrome images, you can customize the output as desired.

Calculation result of reaction-diffusion equation in color.

 

Tips for Creating Captivating Patterns

1.Tweak the Parameter Values

The visual characteristics depend entirely on the parameters within the reaction-diffusion equations.

The example code uses four parameters. It sets diffU and diffV to constant values while varying feed and kill based on XY coordinates.


  // diffU, diffV
  this.diffU = 0.9;
  this.diffV = 0.1;

  // feed, kill
  this.cells[x][y] = new Cell(
    map(x, 0.0, this.matrixW, 0.03, 0.12),   // feed
    map(y, 0.0, this.matrixH, 0.045, 0.055), // kill


For example, setting feed and kill to values A and B produces these characteristic patterns:

Some characteristic pattern examples.
Some characteristic pattern examples.
Some characteristic pattern examples.

The example code will create the patterns when the values of 'feed' and 'kill' values fall within these ranges:

feed : 0.03 - 0.12
kill : 0.045 - 0.055

You can change the values of 'diffU' and 'diffV'. However, finding interesting patterns with these parameters can be challenging.

 

Seeding the Pattern

The initial state of the "V" values acts as the seed for your pattern. While the basic example code randomly sets some V values to 1, you can achieve specific structures by manually setting seeds in Labo.init().


  this.cells[x][y] = new Cell(
    (random(1) < 0.1) ? 1 : 0    // v


You can alter the resulting pattern by changing how V values are initialized.



Try setting a single point in the center, or arranging seeds in a circular formation to see how the pattern radiates outwards.


// Example to set circular.
for (let t = 0; t < TWO_PI; t += PI * 0.2) {
  let x = floor(this.matrixW * (0.5 + 0.25 * cos(t)));
  let y = floor(this.matrixW * (0.5 + 0.25 * sin(t)));
  this.cells[x][y].valV = 1;
}

 

Resolution vs. Iteration

The relationship between the cell size (cSiz) and the iteration count (pCnt) drastically affects pattern appearance. Smaller cells provide higher resolution but require many more iterations to "grow" the pattern.


const cSiz = 3;    // cell size
const pCnt = 1000; // calculation count


 

Creative Coding: Advanced Applications

Below is an advanced application using the concepts discussed. These examples are shared under the CC0 license.

 

The 'p5.js' application example code.

Creative coding application example of the Turing pattern.

Change 'class Labo init()' below.


  /*
   * init : initialize reaction-diffusion system.
   */
  init() {
    const hW = floor(this.matrixW * 0.5);
    const hH = floor(this.matrixH * 0.5);
    for (let x = 0; x < this.matrixW; x++) {
      this.cells[x] = [];
      for (let y = 0; y < this.matrixH; y++) {
        let d = dist(x, y, hW, hH);
        let f = map(sin(TWO_PI * d * 3 / hW), -1, 1, 0.12, 0.03);
        this.cells[x][y] = new Cell(
          f,     // feed
          0.045, // kill
          1,     // u
          0      // v
        );
      }
    }

    for (let t = 0; t < TWO_PI; t += PI * 0.2) {
      for (let r = 0.1; r < 0.4; r += 0.1) {
        let x = floor(this.matrixW * (0.5 + r * cos(t)));
        let y = floor(this.matrixW * (0.5 + r * sin(t)));
        this.cells[x][y].valV = 1;
      }
    }
  }

 

The 'Processing' application example code.

It's the same application example written in 'Processing'.


/**
 * Reaction-Diffusion system by the Gray-Scott Model.
 * application example.
 *
 * @author @deconbatch
 * @version 0.1
 * Processing 3.5.3
 * license CC0
 * created 2022.03.26
 */

void setup() {
  size(480, 480);
  noLoop();

  int cSiz = 2;    // cell size
  int pCnt = 1000; // calculation count
  Labo lab = new Labo(cSiz);

  lab.init();
  for (int i = 0; i < pCnt; i++) {
    lab.proceed();
  }
  lab.observe();
}

/*
 * Labo : reaction-diffusion system.
 */
public class Labo {
  int cellSize;
  int matrixW;
  int matrixH;
  float diffU;
  float diffV;
  Cell[][] cells;

  Labo(int _cSiz) {
    cellSize = _cSiz;
    matrixW =  floor(width / cellSize);
    matrixH = floor(height / cellSize);
    diffU = 0.9;
    diffV = 0.1;
    cells = new Cell[matrixW][matrixH];
  }

  /*
   * init : initialize reaction-diffusion system.
   */
  void init() {
    float hW = matrixW * 0.5;
    float hH = matrixH * 0.5;
    for (int x = 0; x < matrixW; x++) {
      for (int y = 0; y < matrixH; y++) {
        float d = dist(x, y, hW, hH);
        float f = map(sin(TWO_PI * d * 3.0 / hW), -1.0, 1.0, 0.03, 0.12);
        cells[x][y] = new Cell(
                               f,     // feed
                               0.045, // kill
                               1.0,   // u
                               0.0    // v
                               );
      }
    }

    for (float t = 0.0; t < TWO_PI; t += PI * 0.2) {
      for (float r = 0.1; r < 0.4; r += 0.1) {
        int x = floor(matrixW * (0.5 + r * cos(t)));
        int y = floor(matrixW * (0.5 + r * sin(t)));
        cells[x][y].setV(1.0);
      }
    }
  }

  /*
   * proceed : proceed reaction-diffusion calculation.
   */
  void proceed() {
    for (int x = 0; x < matrixW; x++) {
      for (int y = 0; y < matrixH; y++) {

        // neighbors on diagonal
        Cell[] nD = new Cell[4];
        nD[0] = cells[max(x-1,0)][max(y-1,0)];
        nD[1] = cells[max(x-1,0)][min(y+1,matrixH-1)];
        nD[2] = cells[min(x+1,matrixW-1)][max(y-1,0)];
        nD[3] = cells[min(x+1,matrixW-1)][min(y+1,matrixH-1)];

        // neighbors on vertical and horizontal
        Cell[] nH = new Cell[4];
        nH[0] = cells[max(x-1,0)][y];
        nH[1] = cells[x][max(y-1,0)];
        nH[2] = cells[x][min(y+1,matrixH-1)];
        nH[3] = cells[min(x+1,matrixW-1)][y];

        // lapU
        Cell c = cells[x][y];
        float sum = 0.0;
        for (int i = 0; i < 4; i++) {
          sum += nD[i].getU() * 0.05 + nH[i].getU() * 0.2;
        }
        sum -= c.getU();
        c.setLapU(sum);

        // lapV
        sum = 0.0;
        for (int i = 0; i < 4; i++) {
          sum += nD[i].getV() * 0.05 + nH[i].getV() * 0.2;;
        }
        sum -= c.getV();
        c.setLapV(sum);

      }
    }

    // reaction-diffusion
    for (int x = 0; x < matrixW; x++) {
      for (int y = 0; y < matrixH; y++) {
        Cell c = cells[x][y];
        float reaction = c.getU() * c.getV() * c.getV();
        float inflow   = c.getFeed() * (1.0 - c.getU());
        float outflow  = (c.getFeed() + c.getKill()) * c.getV();
        c.setU(c.getU() + diffU * c.getLapU() - reaction + inflow);
        c.setV(c.getV() + diffV * c.getLapV() + reaction - outflow);
        c.standardization();
      }
    }
  }

  /*
   * observe : display the result.
   */
  void observe() {
    background(0);
    fill(255);
    noStroke();
    for (int x = 0; x < matrixW; x++) {
      for (int y = 0; y < matrixH; y++) {
        int cx = x * cellSize;
        int cy = y * cellSize;
        float cs = cells[x][y].getU() * cellSize;
        rect(cx, cy, cs, cs);
      }
    }
  }


}


/**
 * Cell : hold the information about each cell.
 */
public class Cell {

  private float feed;
  private float kill;
  private float valU;
  private float valV;
  private float lapU;
  private float lapV;

  Cell(float _f, float _k, float _u, float _v) {
    feed = _f;
    kill = _k;
    valU = _u;
    valV = _v;
    lapU = 0.0;
    lapV = 0.0;
  }

  public void setLapU(float _l) {
    lapU = _l;
  }

  public void setLapV(float _l) {
    lapV = _l;
  }

  public void setU(float _u) {
    valU = _u;
  }

  public void setV(float _v) {
    valV = _v;
  }

  public float getFeed() {
    return feed;
  }

  public float getKill() {
    return kill;
  }

  public float getU() {
    return valU;
  }

  public float getV() {
    return valV;
  }

  public float getLapU() {
    return lapU;
  }

  public float getLapV() {
    return lapV;
  }

  public void standardization() {
    valU = constrain(valU, 0.0, 1.0);
    valV = constrain(valV, 0.0, 1.0);
  }

}

 

Next Post Previous Post
No Comment
Add Comment
comment url