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() {
}