Auburn / FastNoiseLite

Fast Portable Noise Library - C# C++ C Java HLSL GLSL JavaScript Rust Go
http://auburn.github.io/FastNoiseLite/
MIT License
2.79k stars 327 forks source link

Implementation of multifractal noise #26

Closed thomasp85 closed 4 years ago

thomasp85 commented 6 years ago

Based on my understanding of ridge/multifractal noise, the different frequencies are combined by weighing each point based on the (shifted) values at a lower frequency - I cannot see that happening in FastNoise:

https://github.com/Auburns/FastNoise/blob/faba4443efcd85ad55bf5dd85c8ee458d427d6aa/FastNoise.cpp#L579-L596

Is this my lack of understanding of the implementation, a bug, or a different interpretation of multifractal noise?

For reference, a description of multifractal noise in line with my understanding can be found here: http://chaoscultgames.com/products/CN/CoherentNoiseManual.pdf p. 25

Cazadorro commented 6 years ago

Ridge noise is purely the effect of absolute value of the input (which at first creates a billowy effect, you can reason about this in your head easily) which is inverted in order to create a ridge. It isn't due to values shifted to lower frequency. I'm not sure why the manual you are looking at says:

multifractals have different roughness in different places. To achieve this, RidgeNoise derives weight of a given frequency from values of lower-frequency samples. Also, an offset is added at each step, and a gain value is multiplied in, creating a sort of feedback loop. This ensures that rough areas tend to become rougher, and smooth areas to become smoother. The resulting function is really great for terrain generation, creating both smooth valleys and rough mountain ridges.

This doesn't make sense, are they just describing all coherent octave noise? And gain is used here, see:

    amp *= m_gain; 
    sum -= (1 - FastAbs(SingleValue(m_perm[i], x, y, z))) * amp; 

The important part here is the absolute value, not the gain.

thomasp85 commented 6 years ago

But gain is a constant in the FastNoise implementation, whereas my understanding (and the reference) lets gain be variable contingent on the noise value at the previous octaves...

This is also how it is implemented in libnoise http://libnoise.sourceforge.net/docs/classnoise_1_1module_1_1RidgedMulti.html

Having a fractal noise type that simply inverses another type seems less useful as that is pretty easy to do in post-production, but I'm aware that this interpretation of ridged noise is also prevalent, e.g. https://stackoverflow.com/questions/36796829/procedural-terrain-with-ridged-fractal-noise

Cazadorro commented 6 years ago

I'm not seeing where you are finding variable gain:

and a gain value is multiplied in, creating a sort of feedback loop

does not state this and:

This noise module, heavily based on the Perlin-noise module, generates ridged-multifractal noise. Ridged-multifractal noise is generated in much of the same way as Perlin noise, except the output of each octave is modified by an absolute-value function. Modifying the octave values in this way produces ridge-like formations.

shows their implementation is similar to FastNoise, not what you are describing. I've never seen the type of ridge noise you are describing, nor is it apparent why it would be usefull, or how it would be implemented.

thomasp85 commented 6 years ago

From the linked libnoise documentation:

This noise module, heavily based on the Perlin-noise module, generates ridged-multifractal noise. Ridged-multifractal noise is generated in much of the same way as Perlin noise, except the output of each octave is modified by an absolute-value function. Modifying the octave values in this way produces ridge-like formations. Ridged-multifractal noise does not use a persistence value. This is because the persistence values of the octaves are based on the values generated from from previous octaves, creating a feedback loop (or that's what it looks like after reading the code.) This noise module outputs ridged-multifractal-noise values that usually range from -1.0 to +1.0, but there are no guarantees that all output values will exist within that range.

Persistence is equivalent to gain in the FastNoise lib

From the manual linked earlier

To achieve this, RidgeNoise derives weight of a given frequency from values of lower-frequency samples.

The whole point is to create areas of varying frequencies so you can get both large flat plains and rugged mountain areas, something not achievable with either fbm or billow

Auburn commented 6 years ago

I've had a look at LibNoise and Accidental Noise Library which is what the fractal variants in FastNoise are based on, they both do what @thomasp85 is describing. So I guess when I originally implemented it I didn't implement it fully. It should probably be named Rigid instead of RigidMulti, the Multi seems to refer to fractals that have a feedback loop from previous octaves.

I don't think I'll change it for now, it would just cause backwards compatibility issues. But I'm working on a big new FastNoise update so I'll make sure to implement it correctly for that, probably as 2 variants Rigid and RigidMulti.

thomasp85 commented 6 years ago

It also seems the convention is ridge and not rigid...

Will the thing you’re working on be a new library or a new version?

Auburn commented 6 years ago

Yeah, Ridged actually, I can see how I mixed that up.

The new library is a complete rework of the framework, it is more a successor to FastNoise SIMD. It's designed using modules so you can combine various noise outputs in any way you like, and it's easy to extend upon with your own modules. The code is much more readable and doesn't use any raw intrinsics.

It's now at feature parity with FastNoise so I'll be releasing it in an alpha stage soon. When that gets closer to a finished state I plan to make a lighter version without any SIMD, just a single .h/.cpp for portability and the ability to port to other languages

thomasp85 commented 6 years ago

Great - I currently have an R interface to FastNoise (thomasp85/ambient) so I’ll keep an eye out for the next iteration

thomasp85 commented 6 years ago

Btw (and sorry to go out on a tangent) — is there any particular reason for not supporting fractality with cellular noise other than it not being common? I think it could generate some pretty nice patterns...

Auburn commented 6 years ago

I think it got forgotten, I agree it looks cool though.

That's another thing that makes modules great since you can use any source you want and pass it through a fractal module.

thomasp85 commented 6 years ago

I will probably do it for my own package (as well as fix ridges fractal) — let me know if you want a PR

thomasp85 commented 6 years ago

I've fixed ridged fractals and added fractal cellular noise. Just for reference, here are some examples:

Simplex, ridged: simplex-rigid

Cellular, ridged: worley-rigid

Markyparky56 commented 6 years ago

Incase it interests anyone I think I've found the original implementation of Ridged Multifractal in Texturing & Modeling: A Procedural Approach 3rd Edition. It's quite old and does some interesting precomputing of "spectral weights", plus the formatting is a bit weird.

/* Ridged multifractal terrain model.  
 *  
 * Some good parameter values to start with:  
 *  
 * H: 1.0  
 * offset: 1.0  
 * gain: 2.0  
 */  
double RidgedMultifractal( Vector point, double H, double lacunarity, double octaves, double offset, double gain )
{
  double result, frequency, signal, weight, Noise3();
  int i;
  static int first = TRUE;
  static double *exponent_array;

  /* precompute and store spectral weights */
  if( first ) {
    /* seize required memory for exponent_array */
    exponent_array = (double *)malloc( (octaves+1) * sizeof(double) );
    frequency = 1.0;
    for(i=0; i < octaves; i++) {
      /* compute weight for each frequency */
      exponent_array[i] = pow( frequency, -H );
      frequency *= lacunarity;
    }
    first = FALSE;
  }

  /* get first octave */
  signal = Noise3( point );

  /* get absolute value of signal (this creates the ridges) */
  if (signal < 0.0 ) signal = -signal;
  /* invert and translate (note that "offset" should be ~ = 1.0) */
  signal = offset - signal;
  /* square the signal, to increase "sharpness" of ridges */
  signal *= signal;
  /* assign initial values */
  result = signal;
  weight = 1.0;

  for( 1=1; i<octaves; i++) {
    /* increase the frequency */
    point.x *= lacunarity;
    point.y *= lacunarity;
    point.z *= lacunarity;

    /* weight successive contributions by previous signal */
    weight = signal * gain;
    if( weight > 1.0 ) weight = 1.0;
    if( weight < 0.0 ) weight = 0.0;
    signal = Noise3( point );
    if( signal < 0.0 ) signal = -signal;
    signal = offset - signal;
    signal *= signal;

    /* weight the contribution */
    signal *= weight;
    result += signal * exponent_array[i];
  }
  return ( result );
}

Edit: From the looks of it the AccidentalNoise Library is the original source of the old RidgedMultifractal function that FastNoise uses,

from implicitfractal.cpp:

double CImplicitFractal::RidgedMulti_get(double x, double y, double z)
{
    double sum=0;
    double amp=1.0;

    x*=m_frequency;
    y*=m_frequency;
    z*=m_frequency;

    for(unsigned int i=0; i<m_numoctaves; ++i)
    {
        double n=m_source[i]->get(x,y,z);
        n=1.0-fabs(n);
        sum+=amp*n;
        amp*=m_gain;

        x*=m_lacunarity;
        y*=m_lacunarity;
        z*=m_lacunarity;
    }
    return sum;
}

Also, @thomasp85 what parameters (octaves, gain, lacunarity) did you use to produce those images? They look great but I can't replicate them.

Auburn commented 6 years ago

@thomasp85 I would also be interested to know what settings you used to create that simplex ridgedmulti

thomasp85 commented 6 years ago

I must admit I can’t remember, but let me play a bit and see what I can come up with

thomasp85 commented 6 years ago

octaves = 8, frequency = 0.002, gain = 3, lacunarity = 2 and x,y -> [1, 500] gives pretty close results to the simplex rigid

thomasp85 commented 6 years ago

octaves = 8, frequency = 0.015, gain = 1, lacunarity = 2, value = 'distance' for the cellular rigid picture, same dimensions as simplex