78 lines
2.6 KiB
TypeScript
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] })));
|
|
}
|