Cross-language random numbers

I have developed a set of demo sketches that implement simple cross-language random numbers.

The goal is to make it easy to adapt a sketch between Processing (Java), p5.js (JavaScript), or Processing.py (Python) such that passing the same random seed to each version will generate the same “random” playback from a generative artwork.

This is currently not possible using the core modes / libraries, as they each use different underlying systems for generating random numbers. One solution is to implement a cross-platform pseudo-random number generator (PNRG) that always gives the same sequence of pseudo-random values based on the same seed – robustly, including differences in numeric data types between the languages.

This first test is not designed to be “high quality” random numbers (good distribution), nor fast / efficient – that hasn’t been checked. Its goal is simply to make them the same, no matter what language is being used.

Below is code for three demo sketches – Java, Javascript, and Python.

Demo sketches

RandomAnywhere.randomSeed(0)

RandAnywhere_JDDemo--screenshot(0)

I would love feedback and advice – I have been away from Python for a few months and I am particularly weak in JavaScript, so there may be a lot of things that aren’t idiomatic.

I would also welcome implementations for other modes like Processing.R, JRubyArt, or p5py et cetera.

Finally, if anyone is willing to tackle extending this to Perlin noise, that would be greatly appreciated!

Not a lot of people in Processing work on things that are coordinated cross-language, but if this experiment turns out to be useful it could turn into a library.

Java

RandAnywhere rand;

void setup() {
  size(200,200);
  background(255);

  // print a series of examples values from a specific seed
  // corresponding to epoch milliseconds, as if a seed was
  // requested on 2018-11-11 at 11:11:11.
  rand = new RandAnywhere(1541963471000L);
  println(rand.nextLong());        // 1043807117
  println(rand.nextInt());         // 935077768
  println(rand.random());          // 0.78921956
  println(rand.random(10));        // 1.1313272
  println(rand.random(0, width));  // 140.05911
  
  // reset seed
  rand.randomSeed(0);
}

/**
 * Draw a series of random lines.
 */
void draw(){
  if(frameCount>64){
    noLoop();
  }
  line(rand.random(0, width), rand.random(0, height),
       rand.random(0, width), rand.random(0, height));
}

/**
 * a simple cross-language pseudorandom number generator (PRNG)
 * based on XorShift, implemented in Processing (Java)
 * inspired by https://stackoverflow.com/a/34659107/7207622
 */
class RandAnywhere {
  long seed;
  long a;
  long c;
  long m32;
  /**
   * Constructor
   * 
   * Defaults to current system time in milliseconds.
   */
  RandAnywhere() {
    this(System.currentTimeMillis());
  }
  /** 
   * @param  seed  starting seed value 
   */
  RandAnywhere(long seed) {
    this.a = 1664525;
    this.c = 1013904223;
    this.m32 = 0xFFFFFFFFL;
    this.randomSeed(seed);
  }
  /**
   * Updates seed based on past value, returns new value.
   * 
   * @return  unsigned 32bit value stored in a long
   */
  long nextLong() {
    this.seed = this.seed * a + c & m32;
    return this.seed;
  }
  /**
   * Returns next long wrapped to the max integer value.
   * Implemented so that calls to nextInt or nextLong stay in sync,
   * the next seed internally is always updated to the next long.
   * 
   * @return  positive 
   */
  int nextInt() {
    return (int)(this.nextLong()%Integer.MAX_VALUE);
  }
  /**
   * Set the current seed.
   * 
   * @param  newSeed  the new seed value
   */
  void randomSeed(long newSeed) {
    this.seed = newSeed%Integer.MAX_VALUE;
  }
  /**
   * @return  a random float between 0 and 1
   */
  float random() {
    return random(0, 1);
  }
  /**
   * @param   max  maximum value to return
   * @return  a random float between 0 and max
   */
  float random(float max) {
    return random(0, max);
  }  
  /**
   * @param   min  minimum value to return
   * @param   max  maximum value to return
   * @return       a random float in the specified range (min, max)
   */
  float random(float min, float max) {
    return map(this.nextInt(), 0, Integer.MAX_VALUE, min, max);
  }
}

p5.js

var rand;

function setup() {
  createCanvas(200, 200);
  background(255);

  // print a series of examples values from a specific seed
  // corresponding to epoch milliseconds, as if a seed was
  // requested on 2018-11-11 at 11:11:11.
  rand = new RandAnywhere(1541963471000);
  print(rand.nextLong());        // 1043807117
  print(rand.nextInt());         // 935077768
  print(rand.random());          // 0.7892195325294601
  print(rand.random(10));        // 1.1313271294959482
  print(rand.random(0, width));  // 140.0591152441032
  
  // reset seed
  rand.randomSeed(0);
}

/**
 * draw a series of random lines
 */
function draw() {
  if (frameCount>64) {
    noLoop();
  }
  line(rand.random(0, width), rand.random(0, height), 
    rand.random(0, width), rand.random(0, height));
}

/**
 * a simple cross-language pseudorandom number generator (PRNG)
 * based on XorShift, implemented in p5.js (JavaScript)
 * inspired by https://stackoverflow.com/a/34659107/7207622
 */
function RandAnywhere(newSeed) {
  this.a = 1664525;
  this.c = 1013904223;
  this.Integer_MAX_VALUE = 2147483647;
  // Defaults to current system time in milliseconds.
  if (newSeed === undefined) {
    var d = new Date();
    newSeed = d.getMilliseconds()%this.Integer_MAX_VALUE;
  }
  this.randomSeed(newSeed);
}

/**
 * Updates seed based on past value, returns new value.
 * 
 * Returns unsigned 32bit value stored in a long.
 * (named for cross-language compatability with Java nextLong)
 */
RandAnywhere.prototype.nextLong = function() {
  // The intermediate result fits in 52 bits, so no overflow
  this.seed = ((this.seed * 1664525 + 1013904223) | 0) >>> 0;
  return this.seed;
}
/**
 * Returns next long wrapped to the max (Java) integer value.
 * (for cross-language compatability)
 * Implemented so that calls to nextInt or nextLong stay in sync,
 * the next seed internally is always updated to the next long.
 */
RandAnywhere.prototype.nextInt = function() {
  var next = this.nextLong();
  return next%this.Integer_MAX_VALUE;
}
/**
 * Set the current seed.
 */
RandAnywhere.prototype.randomSeed = function(newSeed) {
  this.seed = newSeed%this.Integer_MAX_VALUE;
}
/**
 * Return a random float in the specified range.
 * 
 * 0 arguments: 0-1
 * 1 argument:  0-max
 * 2 arguments: min-max
 */
RandAnywhere.prototype.random = function(a, b) {
  if(!a && !b){
    rmin = 0;
    rmax = 1;
  } else if(!b){
    rmin = 0;
    rmax = a;
  } else {
    rmin = a;
    rmax = b;
  }
  return map(this.nextInt(), 0, this.Integer_MAX_VALUE, rmin, rmax);
}

python

import time

def setup():
    size(200, 200)
    background(255)

    # print a series of examples values from a specific seed
    # corresponding to epoch milliseconds, as if a seed was
    # requested on 2018-11-11 at 11:11:11.
    global rand
    rand = RandAnywhere(1541963471000)
    println(rand.nextLong())        # 1043807117
    println(rand.nextInt())         # 935077768
    println(rand.random())          # 0.789219558239
    println(rand.random(10))        # 1.13132715225
    println(rand.random(0, width))  # 140.059112549

    # reset seed
    rand.randomSeed(0)


def draw():
    """Draw a series of random lines."""
    if(frameCount > 64):
        noLoop()
    line(rand.random(0, width), rand.random(0, height),
         rand.random(0, width), rand.random(0, height))

class RandAnywhere(object):

    """a simple cross-language pseudorandom number generator (PRNG)
       based on XorShift, implemented in Processing.py (Python)
       inspired by https://stackoverflow.com/a/34659107/7207622
    """

    def __init__(self, newSeed=None):
        """Defaults to current system time in milliseconds."""
        self.a = 1664525
        self.c = 1013904223
        self.Integer_MAX_VALUE = 2147483647
        if newSeed is None:
            newSeed = int(round(time.time() * 1000)) % self.Integer_MAX_VALUE
        self.randomSeed(newSeed)

    def nextLong(self):
        """Returns unsigned 32bit value.
        (named for cross-language compatability with Java nextLong)
        """
        # The intermediate result fits in 52 bits, so no overflow
        self.seed = (self.seed * 1664525 + 1013904223) & 0xFFFFFFFF
        return self.seed

    def nextInt(self):
        """Returns next long wrapped to the max Java integer value.
        (for cross-language compatability)
        Implemented so that calls to nextInt or nextLong stay in sync,
        the next seed internally is always updated to the next long.
        """
        next = self.nextLong()
        return next % self.Integer_MAX_VALUE

    def randomSeed(self, newSeed):
        """Set the current seed."""
        self.seed = newSeed % self.Integer_MAX_VALUE

    def random(self, *args):
        """Return a random float in the specified range.
        * 0 arguments: 0-1
        * 1 argument:  0-max
        * 2 arguments: min-max
        """
        min = 0
        max = 1
        if len(args) == 1:
            min = 0
            max = args[0]
        elif len(args) > 1:
            min = args[0]
            max = args[1]
        return map(self.nextInt(), 0, self.Integer_MAX_VALUE, min, max)
11 Likes

Thank you @jeremydouglass for your initiative :+1:

To complement your work, I have implemented a normal random number generator using Ziggurat algorithm.

I have only implemented the Java version, but since I’m using your base, it shouldn’t be hard to transpose them to the other language.

I just added a function to get some random integers between values.

Here is the code with a demo sketch:

int nbOfBar = 800;
float barWidth;
int nb[];
int total;
NormalRandAnywhere nra;
int speed = 200;

void setup() {
  size(800, 600);

  nra = new NormalRandAnywhere(0, 0.5);

  nb = new int[nbOfBar];
  total = 0;
  barWidth = width / (float)nbOfBar;

  for (int i = 0; i < nbOfBar; i++) {
    nb[i] = 0;
  }
}


void draw() {
  float r;
  int j;

  for (int k = 0; k < speed; k++) {

    r = nra.random();

    if (r < -6.0 || r > 6.0) {
      ;
    } else {

      j = (int)map(r, -6.0, 6.0, 0, nbOfBar -1);
      total++;
      nb[j] = nb[j] + 1;

      background(20);
      fill(200);
      noStroke();

      for (int i = 0; i < nbOfBar; i++) {
        rect(i * barWidth, 0, barWidth, (nb[i]/(float)total) * height * 100);
      }
    }
  }
}



class NormalRandAnywhere {

  private float mean; // Mean of the normal distribution
  private float stdDev; // Standard deviation of the normal distribution
  private float var; // Variance of the standrad diviation
  private float A, B; // Helper to compute the probability density function
  private float[][] boxParam; // Pre compute data of the boxes needed for the algorithm
  private RandAnywhere prng; // The pseudo random generator


  /**
   * Default constructor
   * 
   * @param  mean  the mean of the normal distribution
   * @param  var  the variance of the normal distribution
   */
  public NormalRandAnywhere(float mean, float var) {
    initialize(mean, var);
    prng = new RandAnywhere();
  }


  /**
   * Variant constructor with seed for the PRNG
   * 
   * @param  mean  the mean of the normal distribution
   * @param  var  the variance of the normal distribution
   * @param  seed  the seed for the PRNG
   */
  public NormalRandAnywhere(float mean, float var, long seed) {
    initialize(mean, var);
    prng = new RandAnywhere(seed);
  }


  /**
   * Initialize the variables of the object
   * 
   * @param  mean  the mean of the normal distribution
   * @param  var  the variance of the normal distribution
   */
  private void initialize(float mean, float var) {
    this.mean = mean;
    this.var = var;

    stdDev = sqrt(var);
    A = 1 / sqrt(2 * PI * var);
    B = -1 / (2*var);
    boxParam = new float[256][2];

    computeBoxParam();
  }


  /**
   * Compute all the data of the boxParam variable
   */
  private void computeBoxParam() {    
    float x, y, area;

    x = getFirstX();
    y = pdf(x);
    area = x*y + (A/2.0) * sqrt(-PI/B) * erfc(sqrt(-B*x));

    boxParam[0][0] = area/y;
    boxParam[0][1] = 0;

    for (int j = 1; j < 255; j++) {
      y += area/x;
      x = pdfInv(y);
      boxParam[j][0] = x;
      boxParam[j][1] = y;
    }

    boxParam[255][0] = 0;
    boxParam[255][1] = pdf(0);
  }


  /**
   * Get the first x so all the bar have the same area and yn = pdf(0)
   */
  private float getFirstX() {
    float maxX, minX, x1, x, pdf0, y, area, error;
    boolean x1Found;
    int i;

    error = 0.000002;
    pdf0 = pdf(0);
    maxX = 0;
    minX = 0;
    x1 = 5*stdDev;

    x1Found = false;


    while (!x1Found) { // While we haven't found a proper starting point we look for one
      x = x1;
      y = pdf(x);
      area = x*y + (A/2.0) * sqrt(-PI/B) * erfc(sqrt(-B*x));
      i = 1;

      while (i < 256 && y < pdf0) { // We compute all the table to see if the last point match yn = (0)
        i++;
        y += area/x; // The next y
        x = pdfInv(y); // The next x

        if (y > pdf0) { // If a point is higher than f(0) there is 2 cases
          if (i == 255 && (y - pdf0) < error) { // Either it is the last point and its within error acceptance
            x1Found = true; // In this case we found the point
          } else { // Or we need to try another starting point
            minX = x1;
            if (maxX == 0) {
              x1 *= 2;
            } else {
              x1 = (maxX + x1) / 2.0;
            }
          }
        } 
      }

      if (i == 256) {
        if ((pdf0 - y) < error) { // If the final y is too low to be within error acceptance
          x1Found = true;
        } else {
          maxX = x1;
          x1 = (minX + x1) / 2.0;
        }
      }
    }

    return x1;
  }


  /**
   * Probability density function 
   * 
   * @param  x  the value
   * @return the value of the probability density function with a mean of 0 (but proper variance)
   */
  private float pdf(float x) {
    return A*exp(B*x*x);
  }


  /**
   * Probability density function 
   * 
   * @param  x  the value
   * @return the value of the probability density inverse function with a mean of 0 (but proper variance)
   */
  private float pdfInv(float x) {
    return sqrt((1/B)*log(x/A));
  }


  /**
   * Error function
   * 
   * @param  x  the value
   * @return the error function value 
   */
  private float erf(float x) {
    // constants
    float a1 =  0.254829592;
    float a2 = -0.284496736;
    float a3 =  1.421413741;
    float a4 = -1.453152027;
    float a5 =  1.061405429;
    float p  =  0.3275911;

    // Save the sign of x
    int sign = 1;
    if (x < 0) {
      sign = -1;
    }
    x = abs(x);

    // A&S formula 7.1.26
    float t = 1.0/(1.0 + p*x);
    float y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*exp(-x*x);

    return sign*y;
  }


  /**
   * Complementary error function
   * 
   * @param  x  the value
   * @return the complementary error function value 
   */
  private float erfc(float x) {
    return 1 - erf(x);
  }


  /**
   * Description
   * 
   * @return a random number fitting the defined normal distribution 
   */
  public float random() {
    int i;
    float r1, r2, x, y;
    
    i = prng.randomInt(254);
    r1 = prng.random(-1, 1);
    r2 = prng.random();
    
    x = r1 * boxParam[i][0];
    
    if (abs(x) < boxParam[i+1][0]) {
      return x + mean;
    }
    
    if (i == 0) {
      x = -log(r1) / boxParam[1][0];
      y = -log(r2);
      
      if (2*y > x*x) {
        return x + boxParam[1][0] + mean;
      }
      
      return random();
    }
    
    y = boxParam[i][1] + r2 * (boxParam[i+1][1] - boxParam[i][1]);
    
    if (y < pdf(x)) {
      return x + mean;
    }
    
    return random();
  }
}



/**
 * a simple cross-language pseudorandom number generator (PRNG)
 * based on XorShift, implemented in Processing (Java)
 * inspired by https://stackoverflow.com/a/34659107/7207622
 */
class RandAnywhere {
  long seed;
  long a;
  long c;
  long m32;
  /**
   * Constructor
   * 
   * Defaults to current system time in milliseconds.
   */
  RandAnywhere() {
    this(System.currentTimeMillis());
  }
  /** 
   * @param  seed  starting seed value 
   */
  RandAnywhere(long seed) {
    this.a = 1664525;
    this.c = 1013904223;
    this.m32 = 0xFFFFFFFFL;
    this.randomSeed(seed);
  }
  /**
   * Updates seed based on past value, returns new value.
   * 
   * @return  unsigned 32bit value stored in a long
   */
  long nextLong() {
    this.seed = this.seed * a + c & m32;
    return this.seed;
  }
  /**
   * Returns next long wrapped to the max integer value.
   * Implemented so that calls to nextInt or nextLong stay in sync,
   * the next seed internally is always updated to the next long.
   * 
   * @return  positive 
   */
  int nextInt() {
    return (int)(this.nextLong()%Integer.MAX_VALUE);
  }
  /**
   * Set the current seed.
   * 
   * @param  newSeed  the new seed value
   */
  void randomSeed(long newSeed) {
    this.seed = newSeed%Integer.MAX_VALUE;
  }
  /**
   * @return  a random float between 0 and 1
   */
  float random() {
    return random(0, 1);
  }
  /**
   * @param   max  maximum value to return
   * @return  a random float between 0 and max
   */
  float random(float max) {
    return random(0, max);
  }  
  /**
   * @param   min  minimum value to return
   * @param   max  maximum value to return
   * @return       a random float in the specified range (min, max)
   */
  float random(float min, float max) {
    return map(this.nextInt(), 0, Integer.MAX_VALUE, min, max);
  }
  /**
   * @param   min  minimum value to return
   * @param   max  maximum value to return
   * @return       a random float in the specified range (min, max)
   */
  int randomInt(int max) {
    int temp = (int)random(max + 1.0);
    
    if (temp > max) {
      temp = max;
    }
    
    return temp;
  }
}
2 Likes

The sketch in JRubyArt without RandomAnywhere would typically look as follows

def setup
  sketch_title 'Random Anwhere'
  background 255
end

def draw
  no_loop if frame_count > 64
  line(rand(0..width), rand(0..height), rand(0..width), rand(0..height))
end

def settings
  size 200, 200
end

Sketch running from atom editor using https://atom.io/packages/atom-k9 package
random_anywhere

My preference for implementation of a common random method within JRubyArt would be to create a module say CommonRandom that would hide the implementation details and conform to expectations of a rubyist, where rand is strictly a Math module method. For seeding srand would be used.

load_library :common_random

def setup
  sketch_title 'Random Anwhere'
  background 255
  CommonRandom.srand 1541963471000
end

def draw
  no_loop if frame_count > 64
  line(CommonRand.rand(0..width), CommonRand.rand(0..height), CommonRand.rand(0..width), CommonRand.rand(0..height))
end

def settings
  size 200, 200
end

In ruby it would be also possibly to redefine Math.rand.

1 Like

Thanks for considering what this might look like in JRubyArt, @monkstone.

Just to make sure I understand the API – in your original sketch, is rand() a Ruby built-in, or is it a JRubyArt method that is the equivalent of Processing’s random()? Does JRubyArt already have a randomSeed or built-in equivalent that corresponds to Processing’s randomSeed()?

CommonRandom is nice. More accurate than Anywhere. It reminds me of projects like CommonMark.org.

Yes rand is a ruby built in. The srand function corresponds to randomSeed, there is also Random.new(seed) but I think they are equivalent.

Thank you, @jb4x! Am I right in understanding that this would be the base of a cross-language consistent version of Processing’s randomGaussian()?

So, something like:

CommonRandom rnd;
rnd = new CommonRandom();

rnd.randomSeed(0);              // seed XorShift PNRG
println(rnd.random());          // XorShift PRNG
println(rnd.randomGaussian());  // Ziggurat normal from on XorShift PRNG

That was the idea. But it would need some tweaking to be used as in your example for 2 reasons:

  • The first one is that currently the class instantiate its own XorShift PNRG (your RandAnywhere class) so it has its own seed system
  • The second one is that you need to specify the mean and variance of your distribution to create the normal/gaussian PRNG.

Great. Setting it up as an inner class is easy, I just want to make sure the arguments are right.

Processing Java’s nextGaussian() is passed through to Java’s:

https://docs.oracle.com/javase/8/docs/api/java/util/Random.html#nextGaussian--

Given that, should the arguments for a NormalRandAnywhere be (0, 1.0) – mean=0, var=1.0? (how I’m used to defining the standard normal distribution) I ask because your demo uses 0.5, which I’m assuming was for graphical display purposes.

I noticed that var generates a warning because it is stored as a property, but the property is never called – it is only used as a parameter during initialization to compute stdDev.

If you want the same behavior of the java nextGaussian() function then yes, it would give same results.

If used that way, a big performance improvement can be done by pre-computing the table needed to generate the numbers and hard code-it.
Currently it is compute during instantiation because variance can be set on will.

I was just playing around to see if the code was working properly. I was comparing the results with some Excel graphs.

It is indeed not necessary to keep it. The reason its here is because mean, standard deviation and variance are the 3 main characteristics of the distribution.

Given that the table is only computed one time per PRNG and takes milliseconds I think there is no reason to hard code it.

I like your idea about allowing for distribution flexibility, even if it isn’t in the Processing / Java API. I’m thinking a main object method like setDistribution(float mean, float variance) that either reinitializes the internal CommonNormal object or just replaces it.

I’m unfortunately very inexperienced with Ruby – I would need a volunteer to help package code up in a module and explain how it should be distributed.

I have found a reference example of a pure ruby xorshift implementation, here:

It looks like that model already uses the rand / srand methods you prefer…?

I also ported this implementation of the perlin noise to java:

Perlin perl;
float spaceScale, timeScale;

void setup() {
  size(800, 600);
  perl = new Perlin();
  spaceScale = 10;
  timeScale = 50;
}

void draw() {
  loadPixels();
  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      pixels[y * width + x] = color(map(perl.perlinNoise(spaceScale * (float)x / width, spaceScale * (float)y / height, frameCount / timeScale), 0, 1, 0, 255));
    }
  }
  updatePixels();
}


class Perlin {
  
  private int repeat;
  private int p[];
  private int permutation[] = {151, 160, 137, 91, 90, 15,                                               // Hash lookup table as defined by Ken Perlin.  This is a randomly
    131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,  // arranged array of all numbers from 0-255 inclusive.
    190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
    88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
    77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
    102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
    135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
    5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
    223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
    129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
    251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
    49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
    138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180};
  
  
  public Perlin() {
    this.repeat = -1;
    fillInP();
  }
  
  
  public Perlin(int repeat) {
    this.repeat = repeat;
    fillInP();
  }
  
  
  private void fillInP() {
    p = new int[512];
    for (int i = 0; i < 512; i++) {
      p[i] = permutation[i % 256];
      println(p[i]);
    }
  }
  
  
  public float octavePerlinNoise(float x, float y, float z, int octaves, float persistence) {
    float total = 0;
    float frequency = 1;
    float amplitude = 1;
    float maxValue = 0;
    
    for (int i = 0; i < octaves; i++) {
      total += perlinNoise(x * frequency, y * frequency, z * frequency) * amplitude;
      
      maxValue += amplitude;
      
      amplitude *= persistence;
      frequency *= 2;
    }
    
    return total / maxValue;
  }
  
  
  public float perlinNoise(float x) {
    return perlinNoise(x, 0, 0);
  }
  
  
  public float perlinNoise(float x, float y) {
    return perlinNoise(x, y, 0);
  }
  
  
  public float perlinNoise(float x, float y, float z) {
    if (repeat > 0) {
      x = x % repeat;
      y = y % repeat;
      z = z % repeat;
    }
    
    int xi = (int)x & 255;
    int yi = (int)y & 255;
    int zi = (int)z & 255;
    float xf = x - (int)x;
    float yf = y - (int)y;
    float zf = z - (int)z;
    float u = fade(xf);
    float v = fade(yf);
    float w = fade(zf);
                              
    int aaa, aba, aab, abb, baa, bba, bab, bbb;
    aaa = p[p[p[    xi ]+    yi ]+    zi ];
    aba = p[p[p[    xi ]+inc(yi)]+    zi ];
    aab = p[p[p[    xi ]+    yi ]+inc(zi)];
    abb = p[p[p[    xi ]+inc(yi)]+inc(zi)];
    baa = p[p[p[inc(xi)]+    yi ]+    zi ];
    bba = p[p[p[inc(xi)]+inc(yi)]+    zi ];
    bab = p[p[p[inc(xi)]+    yi ]+inc(zi)];
    bbb = p[p[p[inc(xi)]+inc(yi)]+inc(zi)];
  
    float x1, x2, y1, y2;
    x1 = perlinLerp(grad(aaa, xf, yf, zf), grad(baa, xf-1, yf, zf), u);
    x2 = perlinLerp(grad(aba, xf, yf-1, zf), grad(bba, xf-1, yf-1, zf), u);
    y1 = perlinLerp(x1, x2, v);

    x1 = perlinLerp(grad (aab, xf, yf, zf-1), grad(bab, xf-1, yf, zf-1), u);
    x2 = perlinLerp(grad(abb, xf  , yf-1, zf-1), grad(bbb, xf-1, yf-1, zf-1), u);
    y2 = perlinLerp (x1, x2, v);
    
    return (perlinLerp(y1, y2, w) + 1) / 2.0;
  }
  
  
  private int inc(int num) {
    num++;
    if (repeat > 0) {
      num %= repeat;
    }
    return num;
  }
  
  
  private float grad(int hash, float x, float y, float z) {
    int h = hash & 15;
    float u = h < 8 ? x : y;
    
    float v;

    if(h < 4) 
      v = y;
    else if(h == 12 || h == 14)
      v = x;
    else
      v = z;
    
    return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
  }
  
  
  private float fade(float t) {
    return t * t * t * (t * (t * 6 - 15) + 10);
  }
  
  
  private float perlinLerp(float a, float b, float x) {
    return a + x * (b - a);
  }
}
1 Like

Very exciting, thank you!

Is setting repeat the equivalent of calling noiseSeed()?

More generally, is this comparable to the Processing internal implementation of noise()?

The tricky thing about a cross-language replacement for noise() is that it would need to support noiseSeed() – and then the question is whether or not it would also need to support noiseDetail(), which (I believe?) gets heavy use by noise users.

You need not worry I’ll be pleased to prove the ruby wrapper using the java version under the hood. It is quite common with jruby to have such a wrapper.

1 Like

More general thoughts:

After taking a look at the Processing API, I am thinking that the CommonRandom outer class would contain RandomGaussian and PerlinNoise inner classes, and the outer class would match all the relevant Processing API methods in a given Processing language mode.

So, for example, in Java, these are the signatures:

float random(float low){};
float random(float low, float high){};
void randomSeed(int seed){};

float noise(float x){};
float noise(float x, float y){};
float noise(float x, float y, float z){};
void noiseDetail(int lod){};
void noiseDetail(int lod, float falloff){};
void noiseSeed(long seed){};

That would mean that switching any piece of code to CommonRandom would be as simple as creating a CommonRand rand and then prefixing the normal API calls with the object: rand.random() rand.noise() et cetera.

The only problem I see with this is that the interface / abstract base class for CommonRandom is getting large, which might be a barrier to cross-language implementation. That said, there aren’t that many language modes, and these are built on decades-old math with standard arithmetic operators, so once a mode is done, it is done (one hopes).

An interesting thing here is that Python mode and R mode run on JVM and have user-level direct native Java library import mechanism, so they could import the Java library outright – or they could import a version of it with an interface wrapper that makes it more idiomatic. Still, it would be nice if it was a pure native implementation for each – that feels like it is more in the spirit of the thing as a Rosetta stone.

Alternatives:

Another way to do this would be to actually commit a PRNG to Processing(Java) and to each language mode, migrating each mode off its native PRNG. I’m thinking that doesn’t make sense because sometimes using the native in-language PRNG has advantages – and it is professionally maintained, performance, and not the Processing Foundation’s responsibility.

Another option would be to submit a pull request adding a single method to each mode, something like setPRNG, which let you swap out Processing’s internal PRNG with a custom object but access it using the built-in methods. Then the library wouldn’t be object creation with a lot of interfaces – it would just be the PRNG itself, used with an import statement and a call to that function.

The only problem with this last approach (other than a pile of pull requests that would need to be coordinated) is it isn’t clear that this would work – that is, it isn’t clear that the various implementations of randomGaussian / noiseDetail etc. are all actually equivalent with the sole exception of the PRNG itself.

So maybe the above CommonRandom object library is the best way.

It is not, it controls the tile size. You can use the example below to try it out. Every time you hit the mouse it increase the repeat variable by one.

Perlin perl;
float spaceScale, timeScale;
int repeat;

void setup() {
  size(800, 600);
  perl = new Perlin();
  spaceScale = 100;
  timeScale = 50;
  repeat = 0;
}

void draw() {
  loadPixels(); 
  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      pixels[y * width + x] = color(map(perl.perlinNoise(spaceScale * (float)x / width, spaceScale * (float)y / height), 0, 1, 0, 255));
    }
  }
  updatePixels();
}

void mousePressed() {
  repeat++;
  perl.setRepeat(repeat);
}



class Perlin {
  
  private int repeat;
  private int p[];
  private int permutation[] = {151, 160, 137, 91, 90, 15,                                               // Hash lookup table as defined by Ken Perlin.  This is a randomly
    131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,  // arranged array of all numbers from 0-255 inclusive.
    190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
    88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
    77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
    102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
    135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
    5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
    223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
    129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
    251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
    49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
    138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180};
  
  
  public Perlin() {
    this.repeat = -1;
    fillInP();
  }
  
  
  public Perlin(int repeat) {
    this.repeat = repeat;
    fillInP();
  }
  
  
  public void setRepeat(int repeat) {
    this.repeat = repeat;
  }
  
  
  private void fillInP() {
    p = new int[512];
    for (int i = 0; i < 512; i++) {
      p[i] = permutation[i % 256];
      println(p[i]);
    }
  }
  
  
  public float octavePerlinNoise(float x, float y, float z, int octaves, float persistence) {
    float total = 0;
    float frequency = 1;
    float amplitude = 1;
    float maxValue = 0;
    
    for (int i = 0; i < octaves; i++) {
      total += perlinNoise(x * frequency, y * frequency, z * frequency) * amplitude;
      
      maxValue += amplitude;
      
      amplitude *= persistence;
      frequency *= 2;
    }
    
    return total / maxValue;
  }
  
  
  public float perlinNoise(float x) {
    return perlinNoise(x, 0, 0);
  }
  
  
  public float perlinNoise(float x, float y) {
    return perlinNoise(x, y, 0);
  }
  
  
  public float perlinNoise(float x, float y, float z) {
    if (repeat > 0) {
      x = x % repeat;
      y = y % repeat;
      z = z % repeat;
    }
    
    int xi = (int)x & 255;
    int yi = (int)y & 255;
    int zi = (int)z & 255;
    float xf = x - (int)x;
    float yf = y - (int)y;
    float zf = z - (int)z;
    float u = fade(xf);
    float v = fade(yf);
    float w = fade(zf);
                              
    int aaa, aba, aab, abb, baa, bba, bab, bbb;
    aaa = p[p[p[    xi ]+    yi ]+    zi ];
    aba = p[p[p[    xi ]+inc(yi)]+    zi ];
    aab = p[p[p[    xi ]+    yi ]+inc(zi)];
    abb = p[p[p[    xi ]+inc(yi)]+inc(zi)];
    baa = p[p[p[inc(xi)]+    yi ]+    zi ];
    bba = p[p[p[inc(xi)]+inc(yi)]+    zi ];
    bab = p[p[p[inc(xi)]+    yi ]+inc(zi)];
    bbb = p[p[p[inc(xi)]+inc(yi)]+inc(zi)];
  
    float x1, x2, y1, y2;
    x1 = perlinLerp(grad(aaa, xf, yf, zf), grad(baa, xf-1, yf, zf), u);
    x2 = perlinLerp(grad(aba, xf, yf-1, zf), grad(bba, xf-1, yf-1, zf), u);
    y1 = perlinLerp(x1, x2, v);

    x1 = perlinLerp(grad (aab, xf, yf, zf-1), grad(bab, xf-1, yf, zf-1), u);
    x2 = perlinLerp(grad(abb, xf  , yf-1, zf-1), grad(bbb, xf-1, yf-1, zf-1), u);
    y2 = perlinLerp (x1, x2, v);
    
    return (perlinLerp(y1, y2, w) + 1) / 2.0;
  }
  
  
  private int inc(int num) {
    num++;
    if (repeat > 0) {
      num %= repeat;
    }
    return num;
  }
  
  
  private float grad(int hash, float x, float y, float z) {
    int h = hash & 15;
    float u = h < 8 ? x : y;
    
    float v;

    if(h < 4) 
      v = y;
    else if(h == 12 || h == 14)
      v = x;
    else
      v = z;
    
    return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
  }
  
  
  private float fade(float t) {
    return t * t * t * (t * (t * 6 - 15) + 10);
  }
  
  
  private float perlinLerp(float a, float b, float x) {
    return a + x * (b - a);
  }
}

The seed would correspond to the variable permutation: it is an array with the numbers from 0 to 255 rearranged. If you give it another arrangement, you get a different result.

It is not completely comparable to the processing internal implementation. You can’t directly control the octaves and falloff like with noiseDetail().

Instead there is a built in octavePerlinNoise() function that do everything for you by adding up the different octaves together. It is less flexible but really simple to use. Try the code below to see how it works. The line at the top is simple perlin noise, the one below is octave perlin noise with the same characteristic, the only difference is that multiple octaves are generating and sum up together.

Perlin perl;
float spaceScale, timeScale;
int spacing, amp, mid1, mid2;

void setup() {
  size(800, 600);
  background(20);
  stroke(200);
  strokeWeight(2);
  
  perl = new Perlin();
  spaceScale = 3;
  spacing = 10;
  amp = 200;
  mid1 = 50;
  mid2 = 350;
  
  float oldY = 0;
  for (int i = 0; i < width + spacing; i+=spacing) {
    float y = amp * perl.perlinNoise(spaceScale * (float)i / (float)width);
    line(i - spacing, oldY, i, mid1 + y);
    oldY = mid1 + y;
  }
  
  oldY = 0;
  for (int i = 0; i < width + spacing; i+=spacing) {
    float y = amp * perl.octavePerlinNoise(spaceScale * (float)i / (float)width, 0, 0, 6, 10);
    line(i - spacing, oldY, i, mid2 + y);
    oldY = mid2 + y;
  }
}

That said it is possible to add a noiseDetail() function to have full control over the output.

Would it be possible to use the above in a regular JavaScript or does it use any specifics from p5.js library? Thanks.

1 Like

Class RandAnywhere and its methods don’t rely on the p5.js library. They’re just vanilla JS.

On the other hand functions setup() & draw() are p5.js callbacks and they indeed use p5.js API inside:

2 Likes

Thanks. I know of the callbacks but it was actually the map() function that doesn’t exists in JS. A simple fix and it works great:

/* a simple cross-language pseudorandom number generator (PRNG)
 * based on XorShift, implemented in p5.js (JavaScript)
 * inspired by https://stackoverflow.com/a/34659107/7207622
 */
function RandAnywhere(newSeed) {
  this.a = 1664525;
  this.c = 1013904223;
  this.Integer_MAX_VALUE = 2147483647;
  if (newSeed === undefined) {
    var d = new Date();
    newSeed = d.getMilliseconds()%this.Integer_MAX_VALUE;
  }
  this.randomSeed(newSeed);
}
RandAnywhere.prototype.nextLong = function() {
  this.seed = ((this.seed * 1664525 + 1013904223) | 0) >>> 0;
  return this.seed;
}
RandAnywhere.prototype.nextInt = function() {
  var next = this.nextLong();
  return next%this.Integer_MAX_VALUE;
}
RandAnywhere.prototype.randomSeed = function(newSeed) {
  this.seed = newSeed%this.Integer_MAX_VALUE;
}
RandAnywhere.prototype.random = function(a, b) {
  if(!a && !b){
    var rmin = 0;
    var rmax = 1;
  } else if(!b){
    var rmin = 0;
    var rmax = a;
  } else {
    var rmin = a;
    var rmax = b;
  }
  return map_range(this.nextInt(), 0, this.Integer_MAX_VALUE, rmin, rmax);
}

function map_range(value, low1, high1, low2, high2) {
    return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
}
2 Likes