Gegy / noise-composer

Library for composing noise samplers by generating efficient, inlined bytecode

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Noise Composer

This is a library for efficiently and simply composing noise samplers by generating inlined bytecode at runtime. By itself, it does not provide any actual noise sampler implementations such as simplex or perlin noise, but rather provides various operators that can be used to manipulate that noise. It is trivial, however, to plug in a custom sampler.

Adding to Gradle

This library is published to Maven and can be simply added as a dependency with Gradle. VERSION should be replaced with the latest version from the Releases page.

repositories {
  // ...
  maven { url = 'https://maven.gegy.dev/' }
}

dependencies {
  // ...
  implementation 'dev.gegy:noise-composer:VERSION'
}

Using Noise Samplers

This library intends to support a wide-range of use-cases, with support for up to 5-dimensional noise as well as supporting both float and double-based samplers.

Noise sampling works through the NoiseSampler interface, which is implemented by a number of interfaces specialized for specific dimensions and data types. All double-operating samplers provide casting float samplers as default implementations, and all n-dimensional samplers provide default implementation for (n-1)-dimensional samplers.

For example, NoiseSampler3d implements NoiseSampler3f as well as NoiseSampler2d.

Noise samplers can be implemented manually, or alternatively, an implementation is generated by compiling a composition of various noise operators.

Composing Noise

Noise composition works through the Noise type. It exposes various default operators which can be used to manipulate and combine from various sources.

For example, this composition of noise:

Noise noise = AxisNoise.x();
noise = Noise.constant(2.0).pow(noise);
noise = Noise.add(AxisNoise.y());

will compile as something similar to:

result = Math.pow(2.0, x) + y;   

Compiling Noise

Once you have constructed and composed noise, it needs to be compiled before it is usable. This works through the NoiseCompiler type, which takes a ClassLoader as a parameter. This is used to instantiate new class types at runtime, so you should use the classloader from one of your own classes on the desired classloader.

NoiseCompiler compiler = NoiseCompiler.create(YourClass.class.getClassLoader());

The compiler can then be used to convert Noise to a NoiseFactory which finally instantiates a NoiseSampler.

NoiseFactory factory = compiler.compile(noise, NoiseSampler3d.TYPE);
NoiseSampler3d sampler = factory.create(seed);

To compile a Noise, the noise type must be given. That is, the dimension and data type for that noise. If the constructed Noise is not compatible with the given type, an exception will be thrown.

From the created NoiseFactory, the actual NoiseSampler must be instantiated by passing in a seed. Samplers that require a seed will make use of this value. It is also worth noting that seeds passed through various operators will mix the seed as to ensure every sample gets a unique seed.

Using a Custom Sampler

Given this library does not provide actual noise samplers, implementing a custom sampler will be needed. This can be done, simply, by implementing one of the NoiseSampler interfaces. For example:

public final class ExampleSampler implements NoiseSampler3d {
    private final double factor;

    public ExampleSampler(long seed) {
        this.factor = new Random(seed).nextDouble() * 100.0;
    }

    @Override
    public double get(double x, double y, double z) {
        return (x + y + z) * this.factor;
    }
}

This can then be constructed into a Noise object with the use of the CustomNoise type:

Noise noise = CustomNoise.of(ExampleSampler::new, NoiseSampler3d.TYPE, NoiseRange.INFINITE);

The NoiseRange given here represents the range of values that this noise sampler may output. It is important that this value is correct, as it will be used for optimization as well as forming the basis of the noise normalization operator.

NoiseRange.INFINITE represents every value, and will always be technically correct, but it is encouraged to match the actual range properly.

Helpful Noise Operators

Octaves

A common use-case for combining noise is through octaves. This involves repeating a specific noise sampler at different frequency and amplitude to create a mix of broad and finer details.

This can be done through the OctaveNoise class.

OctaveNoise octaves = OctaveNoise.builder()
    .setAmplitude(2.0) // initial amplitude
    .setScaleX(0.1).setScaleY(0.1).setScaleZ(0.1) // initial frequency
    .setPersistence(0.5) // each octave, amplitude multiplies by 0.5
    .setLacunarity(2.0); // each octave, frequency multiples by 2.0

// add 4 octaves of the given noise
octaves.add(MyNoiseSampler.create(), 4);

// build these octaves into a Noise object
Noise noise = octaves.build();

Normalization

Often adding multiple layers of noise together will result in a non-intuitive range of noise values. These ranges can be simply normalized to [-1; 1] through the use of the .normalize() operator.

For example:

Noise noise = OctaveNoise.builder().add(...).build();
noise = noise.normalize();

Debugging

To view the output that Noise Composer is producing, you can add -Dnoisecomposer.debug=true to your program launch arguments. This will put the compiled class files that the noise compiler creates in a folder called noisecomposer in your root directory. This can be useful if you're wanting to see how the compiled bytecode looks under the hood.

About

Library for composing noise samplers by generating efficient, inlined bytecode

License:MIT License


Languages

Language:Java 100.0%