131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
// noise for world generation. everything is built from named noises, the same ones mods get through
|
|
// FeatureChunk.noise_2d and noise_3d, so a generator using them gives the same world wherever it runs
|
|
import { Alea, create_noise_2d, create_noise_3d, NoiseFunction2D, NoiseFunction3D } from "@paulaboks/rng";
|
|
|
|
// create_noise_2d(new Alea(seed + "_" + name)), cached since building the permutation tables is slow
|
|
const noise_2d_cache = new Map<string, NoiseFunction2D>();
|
|
const noise_3d_cache = new Map<string, NoiseFunction3D>();
|
|
|
|
export function named_noise_2d(seed: string, name: string): NoiseFunction2D {
|
|
const key = `${seed}_${name}`;
|
|
let noise = noise_2d_cache.get(key);
|
|
if (!noise) {
|
|
noise = create_noise_2d(new Alea(key));
|
|
noise_2d_cache.set(key, noise);
|
|
}
|
|
return noise;
|
|
}
|
|
|
|
export function named_noise_3d(seed: string, name: string): NoiseFunction3D {
|
|
const key = `${seed}_${name}`;
|
|
let noise = noise_3d_cache.get(key);
|
|
if (!noise) {
|
|
noise = create_noise_3d(new Alea(key));
|
|
noise_3d_cache.set(key, noise);
|
|
}
|
|
return noise;
|
|
}
|
|
|
|
// several octaves of simplex noise, like minecraft's NormalNoise: each octave has twice the frequency and half the
|
|
// strength of the last, times its amplitude. wavelength is the size in blocks of the first octave's features, an
|
|
// amplitude of 0 skips that octave. the result is roughly -1 to 1 but bunched in the middle, see Quantiles for an
|
|
// even spread
|
|
export class OctaveNoise2D {
|
|
#octaves: { noise: NoiseFunction2D; frequency: number; amplitude: number }[] = [];
|
|
#total: number;
|
|
|
|
constructor(seed: string, name: string, wavelength: number, amplitudes: number[]) {
|
|
amplitudes.forEach((amplitude, i) => {
|
|
if (amplitude !== 0) {
|
|
this.#octaves.push({
|
|
noise: named_noise_2d(seed, `${name}_${i}`),
|
|
frequency: 2 ** i / wavelength,
|
|
amplitude: amplitude / 2 ** i,
|
|
});
|
|
}
|
|
});
|
|
this.#total = amplitudes.reduce((sum, amplitude, i) => sum + amplitude / 2 ** i, 0);
|
|
}
|
|
|
|
sample(x: number, z: number) {
|
|
let value = 0;
|
|
for (const { noise, frequency, amplitude } of this.#octaves) {
|
|
value += noise(x * frequency, z * frequency) * amplitude;
|
|
}
|
|
return value / this.#total;
|
|
}
|
|
}
|
|
|
|
export class OctaveNoise3D {
|
|
#octaves: { noise: NoiseFunction3D; frequency: number; amplitude: number }[] = [];
|
|
#total: number;
|
|
// how much slower it changes vertically than horizontally
|
|
#vertical_stretch: number;
|
|
|
|
constructor(seed: string, name: string, wavelength: number, amplitudes: number[], vertical_stretch = 1) {
|
|
amplitudes.forEach((amplitude, i) => {
|
|
if (amplitude !== 0) {
|
|
this.#octaves.push({
|
|
noise: named_noise_3d(seed, `${name}_${i}`),
|
|
frequency: 2 ** i / wavelength,
|
|
amplitude: amplitude / 2 ** i,
|
|
});
|
|
}
|
|
});
|
|
this.#total = amplitudes.reduce((sum, amplitude, i) => sum + amplitude / 2 ** i, 0);
|
|
this.#vertical_stretch = vertical_stretch;
|
|
}
|
|
|
|
sample(x: number, y: number, z: number) {
|
|
let value = 0;
|
|
const sy = y / this.#vertical_stretch;
|
|
for (const { noise, frequency, amplitude } of this.#octaves) {
|
|
value += noise(x * frequency, sy * frequency, z * frequency) * amplitude;
|
|
}
|
|
return value / this.#total;
|
|
}
|
|
}
|
|
|
|
// maps a noise's bunched up values to an even spread from -1 to 1, so "the lowest 20%" is always below -0.6.
|
|
// built from the noise's measured percentiles (every 5%, see tools/noise_quantiles.ts), which only depend
|
|
// on its amplitudes
|
|
export class Quantiles {
|
|
#values: readonly number[];
|
|
|
|
constructor(values: readonly number[]) {
|
|
this.#values = values;
|
|
}
|
|
|
|
even(value: number) {
|
|
const values = this.#values;
|
|
const last = values.length - 1;
|
|
if (value <= values[0]) return -1;
|
|
if (value >= values[last]) return 1;
|
|
let lo = 0;
|
|
let hi = last;
|
|
while (hi - lo > 1) {
|
|
const mid = (lo + hi) >> 1;
|
|
if (values[mid] <= value) lo = mid;
|
|
else hi = mid;
|
|
}
|
|
const t = (value - values[lo]) / (values[hi] - values[lo]);
|
|
return ((lo + t) / last) * 2 - 1;
|
|
}
|
|
}
|
|
|
|
// helpers
|
|
|
|
export function clamp(value: number, min: number, max: number) {
|
|
return value < min ? min : value > max ? max : value;
|
|
}
|
|
|
|
export function lerp(t: number, from: number, to: number) {
|
|
return from + (to - from) * t;
|
|
}
|
|
|
|
// 0 below edge0, 1 above edge1, smooth in between
|
|
export function smoothstep(edge0: number, edge1: number, value: number) {
|
|
const t = clamp((value - edge0) / (edge1 - edge0), 0, 1);
|
|
return t * t * (3 - 2 * t);
|
|
}
|