Files
bworld/common/worldgen/spline.ts
T
2026-09-25 17:14:26 -03:00

78 lines
2.6 KiB
TypeScript

// minecraft's CubicSpline: a smooth curve through points, where a point's value can itself be a spline of another
// input. that nesting is how its terrain (and terralith's) turns continentalness, erosion and peaks and valleys into
// heights: a spline over continentalness whose points are splines over erosion, whose points are splines over pv
export interface SplineInputs {
continentalness: number;
erosion: number;
pv: number;
weirdness: number;
}
export type SplineValue = number | Spline;
export interface SplinePoint {
at: number;
value: SplineValue;
// the slope there, worked out from the neighbors when not given
slope?: number;
}
export class Spline {
readonly input: keyof SplineInputs;
#locations: number[];
#values: SplineValue[];
#slopes: number[];
constructor(input: keyof SplineInputs, points: SplinePoint[]) {
this.input = input;
this.#locations = points.map((point) => point.at);
this.#values = points.map((point) => point.value);
// catmull-rom slopes between the neighbors, flat at the ends and where values are splines
this.#slopes = points.map((point, i) => {
if (point.slope !== undefined) return point.slope;
const before = points[i - 1];
const after = points[i + 1];
if (!before || !after || typeof before.value !== "number" || typeof after.value !== "number") {
return 0;
}
return (after.value - before.value) / (after.at - before.at);
});
}
get(inputs: SplineInputs): number {
const x = inputs[this.input];
const locations = this.#locations;
const last = locations.length - 1;
if (x <= locations[0]) {
return value_of(this.#values[0], inputs) + this.#slopes[0] * (x - locations[0]);
}
if (x >= locations[last]) {
return value_of(this.#values[last], inputs) + this.#slopes[last] * (x - locations[last]);
}
let i = 0;
while (locations[i + 1] < x) i++;
const x0 = locations[i];
const x1 = locations[i + 1];
const width = x1 - x0;
const t = (x - x0) / width;
const y0 = value_of(this.#values[i], inputs);
const y1 = value_of(this.#values[i + 1], inputs);
// hermite interpolation, written the way minecraft does it
const a = this.#slopes[i] * width - (y1 - y0);
const b = -this.#slopes[i + 1] * width + (y1 - y0);
return y0 + (y1 - y0) * t + t * (1 - t) * (a + (b - a) * t);
}
}
function value_of(value: SplineValue, inputs: SplineInputs) {
return typeof value === "number" ? value : value.get(inputs);
}
// a spline through evenly spaced values of one input
export function spline(input: keyof SplineInputs, at: number[], values: SplineValue[]) {
return new Spline(input, at.map((location, i) => ({ at: location, value: values[i] })));
}