gen-biome
Version:
Procedural generation of 2D maps with distinct biomes
139 lines (113 loc) • 3.16 kB
text/typescript
import type { WorldConfig } from '../../world/types';
import {
PERLIN_AMP_FALLOFF,
PERLIN_AVG_POWER,
PERLIN_YWRAP,
PERLIN_YWRAPB,
PERLIN_ZWRAP,
} from './const';
import type { PerlinParameters } from './types';
/**
* @internal
*/
export class Perlin {
public static generate({ x, y, seed, config }: PerlinParameters): number {
const {
frequencyChange,
borderSmoothness,
heightAveraging,
heightRedistribution,
falloff,
} = this.normalizeConfig(config);
const size = seed.length - 1;
const cx = (x / config.width) * frequencyChange;
const cy = (y / config.height) * frequencyChange;
let xi = Math.floor(cx);
let yi = Math.floor(cy);
let xf = cx - xi;
let yf = cy - yi;
let r = 0;
let ampl = 0.5;
for (let o = 0; o < borderSmoothness; o++) {
let of = xi + (yi << PERLIN_YWRAPB);
const rxf = this.scaledCosine(xf);
const ryf = this.scaledCosine(yf);
let n1 = seed[of & size];
n1 += rxf * (seed[(of + 1) & size] - n1);
let n2 = seed[(of + PERLIN_YWRAP) & size];
n2 += rxf * (seed[(of + PERLIN_YWRAP + 1) & size] - n2);
n1 += ryf * (n2 - n1);
r += n1 * ampl;
ampl *= PERLIN_AMP_FALLOFF;
of += PERLIN_ZWRAP;
xi <<= 1;
xf *= 2;
if (xf >= 1.0) {
xi++;
xf--;
}
yi <<= 1;
yf *= 2;
if (yf >= 1.0) {
yi++;
yf--;
}
}
if (heightAveraging) {
if (r > 0.5) {
r **= (1.5 - r) / PERLIN_AVG_POWER;
} else if (r < 0.5) {
r **= (1.5 - r) * PERLIN_AVG_POWER;
}
}
r **= heightRedistribution;
if (falloff > 0.0) {
r *=
this.heightFalloff(x, config.width, falloff) *
this.heightFalloff(y, config.height, falloff);
}
return r;
}
private static clamp(
value: number | undefined,
defaultValue: number,
limit: [number, number] = [0, 1],
) {
return Math.max(limit[0], Math.min(limit[1], value ?? defaultValue));
}
private static scaledCosine(i: number): number {
return 0.5 * (1.0 - Math.cos(i * Math.PI));
}
private static smootherStep(x: number) {
return 3 * x ** 2 - 2 * x ** 3;
}
private static heightFalloff(
offset: number,
length: number,
falloff: number,
) {
const radius = length / 2;
const distance = Math.abs(radius - offset);
const target = radius * (1 - falloff);
if (distance < target) {
return 1;
}
let x = (distance - target) / radius / (1 - target / radius);
x = Math.min(1, Math.max(0, x));
return 1 - this.smootherStep(x);
}
private static normalizeConfig(config: WorldConfig) {
return {
frequencyChange: Math.round(
this.clamp(config.frequencyChange, 0.3) * 31 + 1,
),
borderSmoothness: Math.round(
(1 - this.clamp(config.borderSmoothness, 0.5)) * 14 + 1,
),
heightRedistribution:
2.0 - this.clamp(config.heightRedistribution, 1.0, [0.5, 1.5]),
falloff: this.clamp(config.falloff, 0.0, [0.0, 0.9]),
heightAveraging: config.heightAveraging ?? true,
};
}
}