refactor: addLakesInDeepDepressions

This commit is contained in:
max 2022-07-24 15:18:47 +03:00
parent b2f16c4b8f
commit 3c6da6585e
16 changed files with 250 additions and 266 deletions

View file

@ -9,6 +9,7 @@ import {byId} from "utils/shorthands";
import {ERROR} from "../config/logging";
import {lim, minmax} from "../utils/numberUtils";
import {aleaPRNG} from "scripts/aleaPRNG";
import {addLakesInDeepDepressions} from "scripts/generation/grid/lakes";
window.HeightmapGenerator = (function () {
let grid = null;
@ -77,7 +78,10 @@ window.HeightmapGenerator = (function () {
Math.random = aleaPRNG(seed);
const isTemplate = id in heightmapTemplates;
const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
const rawHeights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
const heights = addLakesInDeepDepressions(rawHeights, graph.cells.c, graph.cells.v, graph.vertices, graph.cells.i);
TIME && console.timeEnd("defineHeightmap");
clearData();

View file

@ -1,126 +0,0 @@
// @ts-nocheck
import * as d3 from "d3";
import {TIME} from "config/logging";
import {aleaPRNG} from "scripts/aleaPRNG";
import {getInputNumber, getInputValue} from "utils/nodeUtils";
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
import {byId} from "utils/shorthands";
window.Lakes = (function () {
const {LAND_COAST, WATER_COAST} = DISTANCE_FIELD;
function addLakesInDeepDepressions(grid: IGraph & Partial<IGrid>) {
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
if (ELEVATION_LIMIT === 80) return;
TIME && console.time("addLakesInDeepDepressions");
const {cells, features} = grid;
if (!features) throw new Error("addLakesInDeepDepressions: features are not defined");
const {c, h, b} = cells;
for (const i of cells.i) {
if (b[i] || h[i] < MIN_LAND_HEIGHT) continue;
const minHeight = d3.min(c[i].map(c => h[c])) || 0;
if (h[i] > minHeight) continue;
let deep = true;
const threshold = h[i] + ELEVATION_LIMIT;
const queue = [i];
const checked = [];
checked[i] = true;
// check if elevated cell can potentially pour to water
while (deep && queue.length) {
const q = queue.pop()!;
for (const n of c[q]) {
if (checked[n]) continue;
if (h[n] >= threshold) continue;
if (h[n] < MIN_LAND_HEIGHT) {
deep = false;
break;
}
checked[n] = true;
queue.push(n);
}
}
// if not, add a lake
if (deep) {
const lakeCells = [i].concat(c[i].filter(n => h[n] === h[i]));
addLake(lakeCells);
}
}
function addLake(lakeCells: number[]) {
const featureId = features!.length;
for (const lakeCellId of lakeCells) {
cells.h[lakeCellId] = MIN_LAND_HEIGHT - 1;
cells.t[lakeCellId] = WATER_COAST;
cells.f[lakeCellId] = featureId;
for (const neibCellId of c[lakeCellId]) {
if (!lakeCells.includes(neibCellId)) cells.t[neibCellId] = LAND_COAST;
}
}
features!.push({i: featureId, land: false, border: false, type: "lake"});
}
TIME && console.timeEnd("addLakesInDeepDepressions");
}
// near sea lakes usually get a lot of water inflow, most of them should brake threshold and flow out to sea (see Ancylus Lake)
function openNearSeaLakes(grid: IGraph & Partial<IGrid>) {
if (getInputValue("templateInput") === "Atoll") return; // no need for Atolls
const {cells, features} = grid;
if (!features?.find(f => f && f.type === "lake")) return; // no lakes
TIME && console.time("openLakes");
const LIMIT = 22; // max height that can be breached by water
const isLake = (featureId: number) => featureId && (features[featureId] as IGridFeature).type === "lake";
const isOcean = (featureId: number) => featureId && (features[featureId] as IGridFeature).type === "ocean";
for (const cellId of cells.i) {
const featureId = cells.f[cellId];
if (!isLake(featureId)) continue; // not a lake cell
check_neighbours: for (const neibCellId of cells.c[cellId]) {
// water cannot brake the barrier
if (cells.t[neibCellId] !== WATER_COAST || cells.h[neibCellId] > LIMIT) continue;
for (const neibOfNeibCellId of cells.c[neibCellId]) {
const neibOfNeibFeatureId = cells.f[neibOfNeibCellId];
if (!isOcean(neibOfNeibFeatureId)) continue; // not an ocean
removeLake(neibCellId, featureId, neibOfNeibFeatureId);
break check_neighbours;
}
}
}
function removeLake(barrierCellId: number, lakeFeatureId: number, oceanFeatureId: number) {
cells.h[barrierCellId] = MIN_LAND_HEIGHT - 1;
cells.t[barrierCellId] = WATER_COAST;
cells.f[barrierCellId] = oceanFeatureId;
for (const neibCellId of cells.c[barrierCellId]) {
if (cells.h[neibCellId] >= MIN_LAND_HEIGHT) cells.t[neibCellId] = LAND_COAST;
}
if (features && lakeFeatureId) {
// mark former lake as ocean
(features[lakeFeatureId] as IGridFeature).type = "ocean";
}
}
TIME && console.timeEnd("openLakes");
}
return {generateName, getName, addLakesInDeepDepressions, openNearSeaLakes};
})();

View file

@ -1,322 +0,0 @@
import * as d3 from "d3";
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
import {TIME} from "config/logging";
import {INT8_MAX} from "config/constants";
import {aleaPRNG} from "scripts/aleaPRNG";
import {getFeatureVertices} from "scripts/connectVertices";
import {createTypedArray, unique} from "utils/arrayUtils";
import {dist2} from "utils/functionUtils";
import {clipPoly} from "utils/lineUtils";
import {rn} from "utils/numberUtils";
const {UNMARKED, LAND_COAST, WATER_COAST, LANDLOCKED, DEEPER_WATER} = DISTANCE_FIELD;
// define features (oceans, lakes, islands)
export function markupGridFeatures(grid: IGridWithHeights) {
TIME && console.time("markupGridFeatures");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
if (!grid.cells || !grid.cells.h) {
throw new Error("markupGridFeatures: grid.cells.h is required");
}
const cells = grid.cells;
const heights = cells.h;
const n = cells.i.length;
const featureIds = new Uint16Array(n); // starts from 1
const distanceField = new Int8Array(n);
const features: TGridFeatures = [0];
const queue = [0];
for (let featureId = 1; queue[0] !== -1; featureId++) {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = heights[firstCell] >= MIN_LAND_HEIGHT;
let border = false; // set true if feature touches map edge
while (queue.length) {
const cellId = queue.pop()!;
if (cells.b[cellId]) border = true;
for (const neighborId of cells.c[cellId]) {
const isNeibLand = heights[neighborId] >= MIN_LAND_HEIGHT;
if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
featureIds[neighborId] = featureId;
queue.push(neighborId);
} else if (land && !isNeibLand) {
distanceField[cellId] = LAND_COAST;
distanceField[neighborId] = WATER_COAST;
}
}
}
const type = land ? "island" : border ? "ocean" : "lake";
features.push({i: featureId, land, border, type});
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
}
// markup deep ocean cells
const dfOceanMarked = markup({
distanceField,
neighbors: grid.cells.c,
start: DEEPER_WATER,
increment: -1,
limit: -10
});
TIME && console.timeEnd("markupGridFeatures");
return {featureIds, distanceField: dfOceanMarked, features};
}
// define features (oceans, lakes, islands) add related details
export function markupPackFeatures(
grid: IGrid,
vertices: IGraphVertices,
cells: Pick<IPack["cells"], "c" | "v" | "b" | "p" | "h">
) {
TIME && console.time("markupPackFeatures");
const gridCellsNumber = grid.cells.h.length;
const packCellsNumber = cells.c.length;
const features: TPackFeatures = [0];
const featureIds = new Uint16Array(packCellsNumber); // ids of features, starts from 1
const distanceField = new Int8Array(packCellsNumber); // distance from coast; 1 = land along coast; -1 = water along coast
const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven (opposite water cell)
const harbor = new Uint8Array(packCellsNumber); // harbor (number of adjacent water cells)
const defineHaven = (cellId: number) => {
const waterCells = cells.c[cellId].filter(c => cells.h[c] < MIN_LAND_HEIGHT);
const distances = waterCells.map(c => dist2(cells.p[cellId], cells.p[c]));
const closest = distances.indexOf(Math.min.apply(Math, distances));
haven[cellId] = waterCells[closest];
harbor[cellId] = waterCells.length;
};
const queue = [0];
for (let featureId = 1; queue[0] !== -1; featureId++) {
const firstCell = queue[0];
featureIds[firstCell] = featureId; // assign feature number
const land = cells.h[firstCell] >= MIN_LAND_HEIGHT;
let border = false; // true if feature touches map border
let cellNumber = 1; // count cells in a feature
while (queue.length) {
const cellId = queue.pop()!;
if (cells.b[cellId]) border = true;
for (const neighborId of cells.c[cellId]) {
const isNeibLand = cells.h[neighborId] >= MIN_LAND_HEIGHT;
if (land && !isNeibLand) {
distanceField[cellId] = LAND_COAST;
distanceField[neighborId] = WATER_COAST;
if (!haven[cellId]) defineHaven(cellId);
} else if (land && isNeibLand) {
if (distanceField[neighborId] === UNMARKED && distanceField[cellId] === LAND_COAST)
distanceField[neighborId] = LANDLOCKED;
else if (distanceField[cellId] === UNMARKED && distanceField[neighborId] === LAND_COAST)
distanceField[cellId] = LANDLOCKED;
}
if (!featureIds[neighborId] && land === isNeibLand) {
queue.push(neighborId);
featureIds[neighborId] = featureId;
cellNumber++;
}
}
}
const featureVertices = getFeatureVertices({firstCell, vertices, cells, featureIds, featureId});
const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
const area = d3.polygonArea(points); // feature perimiter area
const feature = addFeature({
vertices,
heights: cells.h,
features,
featureIds,
firstCell,
land,
border,
featureVertices,
featureId,
cellNumber,
gridCellsNumber,
area
});
features.push(feature);
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
}
// markup pack land cells
const dfLandMarked = markup({distanceField, neighbors: cells.c, start: LANDLOCKED + 1, increment: 1});
TIME && console.timeEnd("markupPackFeatures");
return {features, featureIds, distanceField: dfLandMarked, haven, harbor};
}
function addFeature({
vertices,
heights,
features,
featureIds,
firstCell,
land,
border,
featureVertices,
featureId,
cellNumber,
gridCellsNumber,
area
}: {
vertices: IGraphVertices;
heights: Uint8Array;
features: TPackFeatures;
featureIds: Uint16Array;
firstCell: number;
land: boolean;
border: boolean;
featureVertices: number[];
featureId: number;
cellNumber: number;
gridCellsNumber: number;
area: number;
}) {
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
const SEA_MIN_SIZE = gridCellsNumber / 1000;
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
const absArea = Math.abs(rn(area));
if (land) return addIsland();
if (border) return addOcean();
return addLake();
function addIsland() {
const group = defineIslandGroup();
const feature: IPackFeatureIsland = {
i: featureId,
type: "island",
group,
land: true,
border,
cells: cellNumber,
firstCell,
vertices: featureVertices,
area: absArea
};
return feature;
}
function addOcean() {
const group = defineOceanGroup();
const feature: IPackFeatureOcean = {
i: featureId,
type: "ocean",
group,
land: false,
border: false,
cells: cellNumber,
firstCell,
vertices: featureVertices,
area: absArea
};
return feature;
}
function addLake() {
const group = "freshwater"; // temp, to be defined later
const name = ""; // temp, to be defined later
// ensure lake ring is clockwise (to form a hole)
const lakeVertices = area > 0 ? featureVertices.reverse() : featureVertices;
const shoreline = getShoreline(); // land cells around lake
const height = getLakeElevation();
function getShoreline() {
const isLand = (cellId: number) => heights[cellId] >= MIN_LAND_HEIGHT;
const cellsAround = lakeVertices.map(vertex => vertices.c[vertex].filter(isLand)).flat();
return unique(cellsAround);
}
function getLakeElevation() {
const MIN_ELEVATION_DELTA = 0.1;
const minShoreHeight = d3.min(shoreline.map(cellId => heights[cellId])) || MIN_LAND_HEIGHT;
return rn(minShoreHeight - MIN_ELEVATION_DELTA, 2);
}
const feature: IPackFeatureLake = {
i: featureId,
type: "lake",
group,
name,
land: false,
border: false,
cells: cellNumber,
firstCell,
vertices: lakeVertices,
shoreline: shoreline,
height,
area: absArea
};
return feature;
}
function defineOceanGroup() {
if (cellNumber > OCEAN_MIN_SIZE) return "ocean";
if (cellNumber > SEA_MIN_SIZE) return "sea";
return "gulf";
}
function defineIslandGroup() {
const prevFeature = features[featureIds[firstCell - 1]];
if (prevFeature && prevFeature.type === "lake") return "lake_island";
if (cellNumber > CONTINENT_MIN_SIZE) return "continent";
if (cellNumber > ISLAND_MIN_SIZE) return "island";
return "isle";
}
}
// calculate distance to coast for every cell
function markup({
distanceField,
neighbors,
start,
increment,
limit = INT8_MAX
}: {
distanceField: Int8Array;
neighbors: number[][];
start: number;
increment: number;
limit?: number;
}) {
for (let distance = start, marked = Infinity; marked > 0 && distance > limit; distance += increment) {
marked = 0;
const prevDistance = distance - increment;
for (let cellId = 0; cellId < neighbors.length; cellId++) {
if (distanceField[cellId] !== prevDistance) continue;
for (const neighborId of neighbors[cellId]) {
if (distanceField[neighborId] !== UNMARKED) continue;
distanceField[neighborId] = distance;
marked++;
}
}
}
return distanceField;
}

View file

@ -1,167 +0,0 @@
import * as d3 from "d3";
import {TIME} from "config/logging";
import {minmax} from "utils/numberUtils";
import {rand} from "utils/probabilityUtils";
// simplest precipitation model
export function generatePrecipitation(grid) {
TIME && console.time("generatePrecipitation");
prec.selectAll("*").remove();
const {cells, cellsX, cellsY} = grid;
const precipitation = new Uint8Array(cells.i.length); // precipitation array
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const precInputModifier = precInput.value / 100;
const modifier = cellsNumberModifier * precInputModifier;
const westerly = [];
const easterly = [];
let southerly = 0;
let northerly = 0;
// precipitation modifier per latitude band
// x4 = 0-5 latitude: wet through the year (rising zone)
// x2 = 5-20 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 20-30 latitude: dry all year (sinking zone)
// x2 = 30-50 latitude: wet winter (rising zone), dry summer (sinking zone)
// x3 = 50-60 latitude: wet all year (rising zone)
// x2 = 60-70 latitude: wet summer (rising zone), dry winter (sinking zone)
// x1 = 70-85 latitude: dry all year (sinking zone)
// x0.5 = 85-90 latitude: dry all year (sinking zone)
const latitudeModifier = [4, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 1, 1, 1, 0.5];
const MAX_PASSABLE_ELEVATION = 85;
// define wind directions based on cells latitude and prevailing winds there
d3.range(0, cells.i.length, cellsX).forEach(function (c, i) {
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
const latBand = ((Math.abs(lat) - 1) / 5) | 0;
const latMod = latitudeModifier[latBand];
const windTier = (Math.abs(lat - 89) / 30) | 0; // 30d tiers from 0 to 5 from N to S
const {isWest, isEast, isNorth, isSouth} = getWindDirections(windTier);
if (isWest) westerly.push([c, latMod, windTier]);
if (isEast) easterly.push([c + cellsX - 1, latMod, windTier]);
if (isNorth) northerly++;
if (isSouth) southerly++;
});
// distribute winds by direction
if (westerly.length) passWind(westerly, 120 * modifier, 1, cellsX);
if (easterly.length) passWind(easterly, 120 * modifier, -1, cellsX);
const vertT = southerly + northerly;
if (northerly) {
const bandN = ((Math.abs(mapCoordinates.latN) - 1) / 5) | 0;
const latModN = mapCoordinates.latT > 60 ? d3.mean(latitudeModifier) : latitudeModifier[bandN];
const maxPrecN = (northerly / vertT) * 60 * modifier * latModN;
passWind(d3.range(0, cellsX, 1), maxPrecN, cellsX, cellsY);
}
if (southerly) {
const bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0;
const latModS = mapCoordinates.latT > 60 ? d3.mean(latitudeModifier) : latitudeModifier[bandS];
const maxPrecS = (southerly / vertT) * 60 * modifier * latModS;
passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY);
}
function getWindDirections(tier) {
const angle = options.winds[tier];
const isWest = angle > 40 && angle < 140;
const isEast = angle > 220 && angle < 320;
const isNorth = angle > 100 && angle < 260;
const isSouth = angle > 280 || angle < 80;
return {isWest, isEast, isNorth, isSouth};
}
function passWind(source, maxPrec, next, steps) {
const maxPrecInit = maxPrec;
for (let first of source) {
if (first[0]) {
maxPrec = Math.min(maxPrecInit * first[1], 255);
first = first[0];
}
let humidity = maxPrec - cells.h[first]; // initial water amount
if (humidity <= 0) continue; // if first cell in row is too elevated consider wind dry
for (let s = 0, current = first; s < steps; s++, current += next) {
if (cells.temp[current] < -5) continue; // no flux in permafrost
if (cells.h[current] < 20) {
// water cell
if (cells.h[current + next] >= 20) {
precipitation[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation
} else {
humidity = Math.min(humidity + 5 * modifier, maxPrec); // wind gets more humidity passing water cell
precipitation[current] += 5 * modifier; // water cells precipitation (need to correctly pour water through lakes)
}
continue;
}
// land cell
const isPassable = cells.h[current + next] <= MAX_PASSABLE_ELEVATION;
const cellPrec = isPassable ? getPrecipitation(humidity, current, next) : humidity;
precipitation[current] += cellPrec;
const evaporation = cellPrec > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere
humidity = isPassable ? minmax(humidity - cellPrec + evaporation, 0, maxPrec) : 0;
}
}
}
function getPrecipitation(humidity, i, n) {
const normalLoss = Math.max(humidity / (10 * modifier), 1); // precipitation in normal conditions
const diff = Math.max(cells.h[i + n] - cells.h[i], 0); // difference in height
const mod = (cells.h[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains
return minmax(normalLoss + diff * mod, 1, humidity);
}
void (function drawWindDirection() {
const wind = prec.append("g").attr("id", "wind");
d3.range(0, 6).forEach(function (t) {
if (westerly.length > 1) {
const west = westerly.filter(w => w[2] === t);
if (west && west.length > 3) {
const from = west[0][0],
to = west[west.length - 1][0];
const y = (grid.points[from][1] + grid.points[to][1]) / 2;
wind.append("text").attr("x", 20).attr("y", y).text("\u21C9");
}
}
if (easterly.length > 1) {
const east = easterly.filter(w => w[2] === t);
if (east && east.length > 3) {
const from = east[0][0],
to = east[east.length - 1][0];
const y = (grid.points[from][1] + grid.points[to][1]) / 2;
wind
.append("text")
.attr("x", graphWidth - 52)
.attr("y", y)
.text("\u21C7");
}
}
});
if (northerly)
wind
.append("text")
.attr("x", graphWidth / 2)
.attr("y", 42)
.text("\u21CA");
if (southerly)
wind
.append("text")
.attr("x", graphWidth / 2)
.attr("y", graphHeight - 20)
.text("\u21C8");
})();
TIME && console.timeEnd("generatePrecipitation");
return precipitation;
}

View file

@ -118,10 +118,10 @@ window.Submap = (function () {
markupGridFeatures();
// Warning: addLakesInDeepDepressions can be very slow!
if (options.addLakesInDepressions) {
Lakes.addLakesInDeepDepressions(grid);
Lakes.openNearSeaLakes(grid);
}
// if (options.addLakesInDepressions) {
// Lakes.addLakesInDeepDepressions(grid);
// Lakes.openNearSeaLakes(grid);
// }
OceanLayers(grid);

View file

@ -1,47 +0,0 @@
import * as d3 from "d3";
import {TIME} from "config/logging";
import {minmax} from "utils/numberUtils";
import {getInputNumber} from "utils/nodeUtils";
import {MIN_LAND_HEIGHT} from "config/generation";
const interpolate = d3.easePolyInOut.exponent(0.5); // interpolation function
export function calculateTemperatures(grid: IGridWithHeights) {
TIME && console.time("calculateTemperatures");
const {cells, cellsX, points} = grid;
const heights = cells.h;
const temperatures = new Int8Array(heights.length); // temperature array
// temperature decreases by 6.5 Celsius per kilometer
const heightExponent = getInputNumber("heightExponentInput");
function decreaseTempFromElevation(height: number) {
if (height < MIN_LAND_HEIGHT) return 0;
const realHeight = Math.pow(height - 18, heightExponent);
return (realHeight / 1000) * 6.5;
}
const tEq = getInputNumber("temperatureEquatorInput");
const tPole = getInputNumber("temperaturePoleInput");
const tDelta = tEq - tPole;
const {latN, latT} = window.mapCoordinates;
d3.range(0, heights.length, cellsX).forEach(rowStart => {
const y = points[rowStart][1];
const lat = Math.abs(latN - (y / graphHeight) * latT); // [0; 90]
const initTemp = tEq - interpolate(lat / 90) * tDelta;
for (let i = rowStart; i < rowStart + cellsX; i++) {
const elevationDecrease = decreaseTempFromElevation(heights[i]);
temperatures[i] = minmax(initTemp - elevationDecrease, -128, 127);
}
});
TIME && console.timeEnd("calculateTemperatures");
return temperatures;
}