// the shape of the overworld, built the way minecraft 1.18+ (and terralith on top of it) does it: // large noises for continentalness, erosion and weirdness feed nested splines that give each column a target height, // how jagged its peaks are and how rough its ground is. a 3d density around that height decides what's solid, // and caves are cut out of it. terralith's flavor comes from the extra shapes: terraced plateaus with cliffs, // shattered hills full of overhangs, deep river valleys and gorges, jagged peaks and rare sky islands import { CHUNK_HEIGHT, SEA_LEVEL } from "$/common/constants.ts"; import { clamp, lerp, OctaveNoise2D, OctaveNoise3D, Quantiles, smoothstep } from "./noise.ts"; import { Spline, spline, SplineInputs } from "./spline.ts"; // measured with tools/noise_quantiles.ts const QUANTILES_6 = new Quantiles([ -0.833, -0.386, -0.311, -0.256, -0.21, -0.17, -0.133, -0.098, -0.065, -0.032, 0, 0.032, 0.065, 0.098, 0.133, 0.17, 0.21, 0.256, 0.311, 0.386, 0.833, ]); const QUANTILES_4 = new Quantiles([ -0.917, -0.48, -0.393, -0.328, -0.273, -0.222, -0.174, -0.13, -0.086, -0.042, 0, 0.042, 0.086, 0.13, 0.174, 0.222, 0.273, 0.328, 0.393, 0.48, 0.917, ]); const QUANTILES_3 = new Quantiles([ -0.91, -0.468, -0.377, -0.308, -0.249, -0.199, -0.154, -0.113, -0.073, -0.036, 0, 0.036, 0.073, 0.113, 0.154, 0.199, 0.249, 0.308, 0.377, 0.468, 0.91, ]); const QUANTILES_2 = new Quantiles([ -0.962, -0.536, -0.439, -0.368, -0.306, -0.248, -0.195, -0.146, -0.096, -0.046, 0, 0.046, 0.096, 0.146, 0.195, 0.248, 0.306, 0.368, 0.439, 0.536, 0.962, ]); const QUANTILES_TEMPERATURE = new Quantiles([ -0.973, -0.6, -0.509, -0.44, -0.379, -0.319, -0.254, -0.19, -0.127, -0.063, 0, 0.063, 0.127, 0.19, 0.254, 0.319, 0.379, 0.44, 0.509, 0.6, 0.973, ]); // blocks of height per unit of density, how soft the ground's surface is const THICKNESS = 20; // solid ground always ends here, and nothing reaches past the top of the world const TOP_SLIDE_START = CHUNK_HEIGHT - 24; const TOP_SLIDE_END = CHUNK_HEIGHT - 4; const MIN_CAVE_Y = 5; // the climate at a column, every value spread evenly from -1 to 1 export interface Climate { // ocean far below 0, coast around -0.2, further inland higher continentalness: number; // low is mountains, high is flat land erosion: number; // picks between variants, and its folded form pv weirdness: number; // peaks and valleys: -1 in a valley (rivers), 1 on a peak pv: number; temperature: number; humidity: number; } // everything 2d about a column that the density needs, worked out once per column export interface TerrainColumn { climate: Climate; // where the ground's surface is before 3d noise, in blocks height: number; // how much 3d noise moves the ground, in units of density roughness: number; // 0 to 1, how much of it are terraced plateaus and shattered hills plateau: number; shattered: number; // 0 to 1, how pointy its peaks are jaggedness: number; // a sky island above it, when there is one island?: { top: number; bottom: number }; } // minecraft's peaks and valleys: weirdness folded so both its ends are peaks and its middle a valley export function peaks_and_valleys(weirdness: number) { return 1 - Math.abs(3 * Math.abs(weirdness) - 2); } // the valley value holds until -0.85, so rivers have a flat bottom and some width const PV_POINTS = [-1, -0.85, -0.65, -0.35, 0, 0.45, 0.8, 1]; const EROSION_POINTS = [-1, -0.6, -0.3, 0, 0.3, 0.6, 1]; // the heights over land at one level of continentalness, relative to sea level. base lifts everything, mountains // scales how tall they get. each erosion gets a spline over peaks and valleys: a valley value, then how far above // base the ground rises towards the peaks function land(base: number, mountains: number): Spline { const row = (valley: number, rises: number[]) => spline("pv", PV_POINTS, [valley, valley, ...rises.map((rise) => base + rise * mountains)]); return spline("erosion", EROSION_POINTS, [ // barely eroded: huge mountains, their valleys are gorges high above the sea row(base + 14 * mountains, [22, 44, 66, 88, 104, 110]), row(base + 8 * mountains, [14, 28, 42, 54, 62, 66]), // hills and highlands, their valleys carry rivers row(-4, [8, 20, 28, 36, 42, 44]), row(-5, [4, 10, 15, 20, 23, 24]), row(-5, [2, 6, 9, 12, 13, 14]), // worn flat: plains and wetlands row(-4, [1, 3, 5, 7, 8, 8]), row(-3, [0, 1, 2, 3, 3, 3]), ]); } // target height above sea level const OFFSET = spline( "continentalness", [-1, -0.55, -0.3, -0.18, -0.12, -0.04, 0.2, 0.5, 1], [-46, -32, -18, -8, -2, land(1, 0.3), land(3, 0.65), land(8, 1), land(14, 1.15)], ); // how pointy peaks get, 0 to 1. only tall, barely eroded mountains have them const JAGGEDNESS = spline("erosion", [-1, -0.6, -0.3, 0], [ spline("pv", [-0.2, 0.3, 1], [0, 0.6, 1]), spline("pv", [0, 0.5, 1], [0, 0.4, 0.7]), spline("pv", [0.3, 0.8, 1], [0, 0.2, 0.3]), 0, ]); // 3d noise strength: mountains are rougher than plains const ROUGHNESS = spline("erosion", [-1, -0.5, 0, 0.5, 1], [0.32, 0.22, 0.14, 0.1, 0.06]); export class OverworldTerrain { #continentalness: OctaveNoise2D; #erosion: OctaveNoise2D; #weirdness: OctaveNoise2D; #temperature: OctaveNoise2D; #humidity: OctaveNoise2D; #warp_x: OctaveNoise2D; #warp_z: OctaveNoise2D; #jagged: OctaveNoise2D; #plateau: OctaveNoise2D; #shattered: OctaveNoise2D; #sky: OctaveNoise2D; #island_shape: OctaveNoise2D; #island_height: OctaveNoise2D; #ground: OctaveNoise3D; #island_noise: OctaveNoise3D; #cheese: OctaveNoise3D; #spaghetti_a: OctaveNoise3D; #spaghetti_b: OctaveNoise3D; #entrances: OctaveNoise2D; constructor(seed: string) { this.#continentalness = new OctaveNoise2D(seed, "continentalness", 1024, [1, 1, 2, 2, 1, 1]); this.#erosion = new OctaveNoise2D(seed, "erosion", 768, [1, 1, 0, 1, 1]); this.#weirdness = new OctaveNoise2D(seed, "weirdness", 256, [1, 2, 1]); this.#temperature = new OctaveNoise2D(seed, "temperature", 1536, [1.5, 0, 1]); this.#humidity = new OctaveNoise2D(seed, "humidity", 512, [1, 1]); this.#warp_x = new OctaveNoise2D(seed, "warp_x", 200, [1, 1]); this.#warp_z = new OctaveNoise2D(seed, "warp_z", 200, [1, 1]); this.#jagged = new OctaveNoise2D(seed, "jagged", 48, [1, 1]); this.#plateau = new OctaveNoise2D(seed, "plateau", 640, [1, 1]); this.#shattered = new OctaveNoise2D(seed, "shattered", 512, [1, 1]); this.#sky = new OctaveNoise2D(seed, "sky", 900, [1, 1]); this.#island_shape = new OctaveNoise2D(seed, "island_shape", 56, [1, 1]); this.#island_height = new OctaveNoise2D(seed, "island_height", 300, [1, 1]); this.#ground = new OctaveNoise3D(seed, "ground", 64, [1, 1, 0.5], 1.2); this.#island_noise = new OctaveNoise3D(seed, "island_noise", 24, [1, 1]); this.#cheese = new OctaveNoise3D(seed, "cheese", 80, [1, 0.5], 0.6); this.#spaghetti_a = new OctaveNoise3D(seed, "spaghetti_a", 64, [1], 0.8); this.#spaghetti_b = new OctaveNoise3D(seed, "spaghetti_b", 64, [1], 0.8); this.#entrances = new OctaveNoise2D(seed, "entrances", 90, [1, 1]); } climate(x: number, z: number): Climate { // a small warp, so coasts and biome edges aren't the noise's smooth blobs const wx = x + this.#warp_x.sample(x, z) * 24; const wz = z + this.#warp_z.sample(x, z) * 24; const weirdness = QUANTILES_3.even(this.#weirdness.sample(wx, wz)); return { continentalness: QUANTILES_6.even(this.#continentalness.sample(wx, wz)), erosion: QUANTILES_4.even(this.#erosion.sample(wx, wz)), weirdness, pv: peaks_and_valleys(weirdness), temperature: QUANTILES_TEMPERATURE.even(this.#temperature.sample(wx, wz)), humidity: QUANTILES_2.even(this.#humidity.sample(wx, wz)), }; } column(x: number, z: number): TerrainColumn { const climate = this.climate(x, z); const { continentalness: c, erosion: e } = climate; const inputs: SplineInputs = climate; const inland = smoothstep(-0.1, 0.3, c); let height = OFFSET.get(inputs); // jagged peaks: sharp ridges pushed up from the tallest mountains const jaggedness = clamp(JAGGEDNESS.get(inputs), 0, 1) * inland; if (jaggedness > 0) { const ridge = 1 - Math.abs(this.#jagged.sample(x, z)); height += jaggedness * 26 * ridge * ridge; } // terraced plateaus: raised land cut into flat benches and cliffs, like terralith's // yosemite cliffs and painted mountains const plateau = smoothstep(0.35, 0.55, QUANTILES_2.even(this.#plateau.sample(x, z))) * smoothstep(-0.75, -0.45, e) * (1 - smoothstep(0.1, 0.4, e)) * smoothstep(-0.05, 0.1, c); if (plateau > 0 && climate.pv > -0.8) { const raised = height + 20 * plateau; height = lerp(plateau, height, terrace(raised, 16)); } // shattered hills: ground broken up by strong 3d noise into overhangs, arches and spires const shattered = smoothstep(0.55, 0.75, QUANTILES_2.even(this.#shattered.sample(x, z))) * smoothstep(-0.6, -0.3, e) * (1 - smoothstep(0.3, 0.5, e)) * smoothstep(-0.05, 0.1, c); const roughness = ROUGHNESS.get(inputs) + shattered * 0.9; return { climate, height: SEA_LEVEL + height, roughness, plateau, shattered, jaggedness, island: this.#island(x, z), }; } // skylands: rare regions of floating islands, flat topped with long hanging undersides #island(x: number, z: number): TerrainColumn["island"] { const region = smoothstep(0.9, 0.97, QUANTILES_2.even(this.#sky.sample(x, z))); if (region <= 0) { return undefined; } const shape = region * smoothstep(0.05, 0.35, this.#island_shape.sample(x, z)); if (shape <= 0) { return undefined; } const center = 178 + this.#island_height.sample(x, z) * 24; return { top: center + 2 + 6 * shape, bottom: center - 4 - 34 * shape * Math.sqrt(shape) }; } // positive is solid, before caves density(column: TerrainColumn, x: number, y: number, z: number) { let density = (column.height - y) / THICKNESS + column.roughness * this.#ground.sample(x, y, z); const island = column.island; if (island && y > island.bottom - 4 && y < island.top + 4) { const solid = Math.min((island.top - y) / 3, (y - island.bottom) / 6) + 0.4 * this.#island_noise.sample(x, y, z); density = Math.max(density, solid); } if (y > TOP_SLIDE_START) { density -= smoothstep(TOP_SLIDE_START, TOP_SLIDE_END, y) * 4; } if (y < 1) { density = Math.max(density, 1); } return density; } // negative where a cave is. caves stay under the ground's skin except at entrances, and never break // the floor of oceans and rivers cave(column: TerrainColumn, x: number, y: number, z: number) { if (y < MIN_CAVE_Y) { return 1; } const depth = column.height - y; if (column.height < SEA_LEVEL + 3 && depth < 14) { return 1; } if (depth < 7 && this.#entrances.sample(x, z) < 0.5) { return 1; } // cheese caves: big open caverns const cheese = (0.52 - this.#cheese.sample(x, y, z)) * 4; // spaghetti caves: long winding tunnels where two noises are both near zero const a = Math.abs(this.#spaghetti_a.sample(x, y, z)); const b = Math.abs(this.#spaghetti_b.sample(x, y, z)); const spaghetti = (Math.max(a, b) - 0.07) * 8; return Math.min(cheese, spaghetti); } } // flat benches every step blocks, joined by steep cliffs function terrace(height: number, step: number) { const k = height / step; const floor = Math.floor(k); return (floor + smoothstep(0.4, 0.6, k - floor)) * step; }