// 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] }))); }