I've created a multilinear noise generator (and need help)

I wanted to challenge myself and create my own noise generator which would work with arbitrary many dimensions, have no symmetry artifacts around the origin, etc. I also wanted it to be linear, because I wanted to know how it would look like.

The code below is the finished product, but if you run it, you can notice some significant diagonal artifacts, as if the noise was correlated across the dimensions. Does anyone know why this might be the case? I took care to randomize everything as much as possible, even the dimensions themselves, so this is a bit surprising to me.

Also, the code is quite slow, so improvements in this regard are also appreciated.

import java.util.Random;
import java.time.Instant;

class _LinearNoise {
  // the main function with which the noise is called
  public double getNoise(double... pos) {
    double sum = 0, amp = 1, scale = 1;
    for (int i = 0; i < LOD; i++) {
      sum += oneIter(scale, pos) * amp;
      scale /= scaleGain;
      amp *= ampGain;
    }
    // normalization... ensures the largest possible value is 1
    return sum * (1 - ampGain) / (1 - Math.pow(ampGain, LOD));
  }
  
  // various convenient constructors
  public _LinearNoise() {
    setInternalVariables(getRandomSeed(), 8, 0.5, 0.5);
  }
  public _LinearNoise(long seed) {
    setInternalVariables(seed, 8, 0.5, 0.5);
  }
  public _LinearNoise(long LOD, double scaleGain, double ampGain) {
    setInternalVariables(getRandomSeed(), LOD, scaleGain, ampGain);
  }
  public _LinearNoise(long seed, long LOD, double scaleGain, double ampGain) {
    setInternalVariables(seed, LOD, scaleGain, ampGain);
  }
  
  // getters and setters
  public void setSeed(long seed) { this.seed = seed; }
  public long getSeed() { return seed; }
  public void setLOD(long LOD) { this.LOD = LOD; }
  public long getLOD() { return LOD; }
  public void setScaleGain(long scaleGain) { this.scaleGain = scaleGain; }
  public double getScaleGain() { return scaleGain; }
  public void setAmpGain(long ampGain) { this.ampGain = ampGain; }
  public double getAmpGain() { return ampGain; }
  
  private long seed, LOD;
  private double scaleGain, ampGain;
  
  // called by the constructors to initialize the variables
  private void setInternalVariables(long seed, long LOD, double scaleGain, double ampGain) {
    this.seed = seed;
    this.LOD = LOD;
    this.scaleGain = scaleGain;
    this.ampGain = ampGain;
  }
  
  // generates an "actual" random seed; called if no seed is provided
  private long getRandomSeed() {
    return new Random(Instant.now().toEpochMilli()).nextLong();
  }
  
  // the actual algorithm
  private double oneIter(double scale, double... pos) {
    int dim = pos.length;
    double[] scaledPos = new double[dim]; // for LOD calculation
    long[] wholePart = new long[dim];
    double[] fracPart = new double[dim];
    long[] dimOffset = new long[dim]; // gives each coordinate term a specific offset
    Random dimOffsetGenerator = new Random(seed);
    for (int i = 0; i < dim; i++) {
      scaledPos[i] = pos[i] * scale;
      wholePart[i] = (long)Math.floor(scaledPos[i]);
      fracPart[i] = scaledPos[i] - wholePart[i];
      dimOffset[i] = dimOffsetGenerator.nextLong();
    }
    // each point in the noise space is surrounded by 2^dim grid points,
    // whose values are given randomly (all other points are interpolated from these)
    int vertCount = 2 << dim;
    double[] vertVals = new double[vertCount];
    // calculating the random value associated with each vertex:
    for (int i = 0; i < vertCount; i++) {
      long reduction = 0;
      // processing each coordinate term for this specific vertex:
      for (int j = 0; j < dim; j++) {
        // pos_j = the j-th coordinate of the i-th vertex
        long pos_j = wholePart[j] + ((i >> j) & 1);
        // pos_j and dimOffset together seed another generator
        // so basically this is like the "hash" of pos_j + dimOffset
        // ...they are then added up, because I can't come up with a better reduction scheme
        reduction += new Random(pos_j + dimOffset[j]).nextLong();
      }
      // the "hash" plus the seed value are then passed to another generator:
      vertVals[i] = new Random(reduction + seed).nextDouble();
    }
    // the linear interpolation algorithm:
    for (int i = 0; i < dim; i++) {
      for (int j = 0; j < (vertCount >> (i + 1)); j++) {
        int k = j*2;
        double t = fracPart[i];
        vertVals[j] = (1 - t) * vertVals[k] + t * vertVals[k+1];
      }
    }
    // the interpolation reduces the range [0, x) to [0, x/2),
    // so in the end, the result lies at index 0
    return vertVals[0];
  }
}

_LinearNoise L = new _LinearNoise(1, 0.5, 0.5);

void setup() {
  size(800, 800);
  for (int i = 0; i < width; i++) {
    for (int j = 0; j < height; j++) {
      double val = L.getNoise(i * 0.1, j * 0.1);
      stroke((int)(val * 255));
      point(i, j);
    }
  }
}

void draw() {
}

(There are a few small bugs still, but I’m trying to iron them out as I speak.)

And I suppose a related question is, whether there’s any way of making it so that passing in coordinates (0), (0,0), (0,0,0), etc, would all yield the same values and without generating any discontinuities. For example, if you were to simply truncate the trailing zeros, then for a 2D segment of the noise, you’d get a discontinuity at y=0, since the noise would sample from a 1D variant at that point, but the 1D variant is programmed to behave completely differently.

Very well, in the end, someone else helped me with both issues. Here is the new code:

import java.util.Random;
import java.time.Instant;

class _LinearNoise {
  public double getNoise(double... pos) {
    double sum = 0, amp = 1, scale = 1;
    for (int i = 0; i < LOD; i++) {
      sum += oneIter(scale, pos) * amp;
      scale /= scaleGain;
      amp *= ampGain;
    }
    return sum * (1 - ampGain) / (1 - Math.pow(ampGain, LOD));
  }
  
  public _LinearNoise() {
    setInternalVariables(getRandomSeed(), 8, 0.5, 0.5);
  }
  public _LinearNoise(long seed) {
    setInternalVariables(seed, 8, 0.5, 0.5);
  }
  public _LinearNoise(long LOD, double scaleGain, double ampGain) {
    setInternalVariables(getRandomSeed(), LOD, scaleGain, ampGain);
  }
  public _LinearNoise(long seed, long LOD, double scaleGain, double ampGain) {
    setInternalVariables(seed, LOD, scaleGain, ampGain);
  }
  
  public void setSeed(long seed) { this.seed = seed; }
  public long getSeed() { return seed; }
  public void setLOD(long LOD) { this.LOD = LOD; }
  public long getLOD() { return LOD; }
  public void setScaleGain(long scaleGain) { this.scaleGain = scaleGain; }
  public double getScaleGain() { return scaleGain; }
  public void setAmpGain(long ampGain) { this.ampGain = ampGain; }
  public double getAmpGain() { return ampGain; }
  
  private long seed, LOD;
  private double scaleGain, ampGain;
  
  private void setInternalVariables(long seed, long LOD, double scaleGain, double ampGain) {
    this.seed = seed;
    this.LOD = LOD;
    this.scaleGain = scaleGain;
    this.ampGain = ampGain;
  }
  
  private long getRandomSeed() {
    return new Random(Instant.now().toEpochMilli()).nextLong();
  }
  
  private double oneIter(double scale, double... pos) {
    // scale the position as required by LOD
    int dim = pos.length;
    double[] scaledPos = new double[dim];
    for (int i = 0; i < dim; i++) {
      scaledPos[i] = pos[i] * scale;
    }
    // split the position into the integral and decimal part
    // probably improve this with a more precise algorithm later
    long[] intPart = new long[dim];
    double[] decPart = new double[dim];
    for (int i = 0; i < dim; i++) {
      intPart[i] = (long)Math.floor(scaledPos[i]);
      decPart[i] = scaledPos[i] - intPart[i];
    }
    // calculate the unique offsets for each dimension
    // can be any random values
    long[] dimOffset = new long[dim];
    Random dimOffsetGenerator = new Random(seed);
    for (int i = 0; i < dim; i++) {
      dimOffset[i] = dimOffsetGenerator.nextLong();
    }
    // construct the vertex coordinates
    // all of this can be optimized, but for now, I'm gonna keep it like this for clarity
    int vertCount = 2 << dim;
    long[][] vert = new long[vertCount][dim];
    for (int i = 0; i < vertCount; i++) {
      for (int j = 0; j < dim; j++) {
        vert[i][j] = intPart[j] + ((i >> j) & 1);
      }
    }
    // calculate the "true" length of each coordinate, meaning trailing zeros are ignored
    int[] trueLengths = new int[vertCount];
    for (int i = 0; i < vertCount; i++) {
      for (int j = dim - 1; j >= 0; j--) {
        if (vert[i][j] != 0) {
          trueLengths[i] = j + 1;
          break;
        }
      }
    }
    // get the random values associated with the vertices
    double[] vertRandVals = new double[vertCount];
    for (int i = 0; i < vertCount; i++) {
      long reduction = 0;
      for (int j = 0; j < trueLengths[i]; j++) {
        reduction += new Random(vert[i][j] + dimOffset[j] + reduction).nextLong();
      }
      vertRandVals[i] = new Random(reduction + seed).nextDouble();
    }
    // interpolate (the calculation is performed in-place, overwriting vertRandVals)
    for (int i = 0; i < dim; i++) {
      for (int j = 0; j < (vertCount >> (i + 1)); j++) {
        int k = j*2;
        double t = decPart[i];
        vertRandVals[j] = (1 - t) * vertRandVals[k] + t * vertRandVals[k+1];
      }
    }
    return vertRandVals[0];
  }
}

_LinearNoise L = new _LinearNoise(1, 0.5, 0.5);

void setup() {
  size(500, 500);
  for (int i = 0; i < width; i++) {
    for (int j = 0; j < height; j++) {
      double val = L.getNoise((i - width / 2) * 0.1, (j - height / 2) * 0.1);
      stroke((int)(val * 255));
      point(i, j);
    }
  }
  println("Noise at ():     ", L.getNoise());
  println("Noise at (0):    ", L.getNoise(0));
  println("Noise at (0,0):  ", L.getNoise(0,0));
  println("Noise at (0,0,0):", L.getNoise(0,0,0));
}
1 Like