Better world generation

This commit is contained in:
2026-09-25 17:14:26 -03:00
parent c750321ced
commit d07528bd0e
14 changed files with 1315 additions and 334 deletions
+130
View File
@@ -0,0 +1,130 @@
// 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);
}