mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
refactor: addLakesInDeepDepressions
This commit is contained in:
parent
b2f16c4b8f
commit
3c6da6585e
16 changed files with 250 additions and 266 deletions
|
|
@ -22,10 +22,10 @@ import {debounce} from "utils/functionUtils";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {generateSeed} from "utils/probabilityUtils";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {createGrid} from "./grid";
|
||||
import {createGrid} from "./grid/grid";
|
||||
import {createPack} from "./pack/pack";
|
||||
import {getInputValue, setInputValue} from "utils/nodeUtils";
|
||||
// import {Ruler} from "modules/measurers";
|
||||
import {calculateMapCoordinates} from "modules/coordinates";
|
||||
|
||||
const {Zoom, ThreeD} = window;
|
||||
|
||||
|
|
@ -50,6 +50,8 @@ async function generate(options?: IGenerationOptions) {
|
|||
applyMapSize();
|
||||
randomizeOptions();
|
||||
|
||||
window.mapCoordinates = calculateMapCoordinates();
|
||||
|
||||
const newGrid = await createGrid(grid, precreatedGraph);
|
||||
const newPack = createPack(newGrid);
|
||||
|
||||
|
|
@ -60,10 +62,10 @@ async function generate(options?: IGenerationOptions) {
|
|||
pack = newPack;
|
||||
|
||||
// temp rendering for debug
|
||||
renderLayer("cells");
|
||||
// renderLayer("cells");
|
||||
renderLayer("features");
|
||||
renderLayer("heightmap");
|
||||
renderLayer("rivers", pack);
|
||||
// renderLayer("heightmap");
|
||||
// renderLayer("rivers", pack);
|
||||
|
||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||
// showStatistics();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import Delaunator from "delaunator";
|
|||
|
||||
import {Voronoi} from "modules/voronoi";
|
||||
import {TIME} from "config/logging";
|
||||
// @ts-expect-error js module
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {createTypedArray} from "utils/arrayUtils";
|
||||
import {rn} from "utils/numberUtils";
|
||||
|
|
|
|||
|
|
@ -1,41 +1,59 @@
|
|||
import {calculateTemperatures} from "modules/temperature";
|
||||
import {defineMapSize} from "modules/coordinates";
|
||||
import {generateGrid} from "scripts/generation/graph";
|
||||
import {calculateMapCoordinates, defineMapSize} from "modules/coordinates";
|
||||
import {markupGridFeatures} from "modules/markup";
|
||||
// @ts-expect-error js module
|
||||
import {generatePrecipitation} from "modules/precipitation";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {markupGridFeatures} from "scripts/generation/markup";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {generatePrecipitation} from "./precipitation";
|
||||
import {calculateTemperatures} from "./temperature";
|
||||
|
||||
const {Lakes, HeightmapGenerator} = window;
|
||||
const {HeightmapGenerator} = window;
|
||||
|
||||
export async function createGrid(globalGrid: IGrid, precreatedGraph?: IGrid): Promise<IGrid> {
|
||||
const baseGrid: IGridBase = shouldRegenerateGridPoints(globalGrid)
|
||||
? (precreatedGraph && undressGrid(precreatedGraph)) || generateGrid()
|
||||
: undressGrid(globalGrid);
|
||||
export async function createGrid(globalGrid: IGrid, precreatedGrid?: IGrid): Promise<IGrid> {
|
||||
const shouldRegenerate = shouldRegenerateGridPoints(globalGrid);
|
||||
const {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices} = shouldRegenerate
|
||||
? (precreatedGrid && undressPrecreatedGrid(precreatedGrid)) || generateGrid()
|
||||
: undressPrecreatedGrid(globalGrid);
|
||||
|
||||
const heights: Uint8Array = await HeightmapGenerator.generate(baseGrid);
|
||||
const heights: Uint8Array = await HeightmapGenerator.generate({
|
||||
vertices,
|
||||
points,
|
||||
cells,
|
||||
cellsDesired,
|
||||
spacing,
|
||||
cellsX,
|
||||
cellsY
|
||||
});
|
||||
if (!heights) throw new Error("Heightmap generation failed");
|
||||
const heightsGrid = {...baseGrid, cells: {...baseGrid.cells, h: heights}};
|
||||
|
||||
const {featureIds, distanceField, features} = markupGridFeatures(heightsGrid);
|
||||
const markedGrid = {...heightsGrid, features, cells: {...heightsGrid.cells, f: featureIds, t: distanceField}};
|
||||
const {featureIds, distanceField, features} = markupGridFeatures(cells.c, cells.b, heights);
|
||||
|
||||
const touchesEdges = features.some(feature => feature && feature.land && feature.border);
|
||||
defineMapSize(touchesEdges);
|
||||
window.mapCoordinates = calculateMapCoordinates();
|
||||
|
||||
Lakes.addLakesInDeepDepressions(markedGrid);
|
||||
Lakes.openNearSeaLakes(markedGrid);
|
||||
const temp = calculateTemperatures(heights, cellsX, points);
|
||||
const prec = generatePrecipitation(heights, temp, cellsX, cellsY);
|
||||
|
||||
const temperature = calculateTemperatures(markedGrid);
|
||||
const temperatureGrid = {...markedGrid, cells: {...markedGrid.cells, temp: temperature}};
|
||||
|
||||
const prec = generatePrecipitation(temperatureGrid);
|
||||
return {...temperatureGrid, cells: {...temperatureGrid.cells, prec}};
|
||||
return {
|
||||
cellsDesired,
|
||||
cellsX,
|
||||
cellsY,
|
||||
spacing,
|
||||
boundary,
|
||||
points,
|
||||
vertices,
|
||||
cells: {
|
||||
...cells,
|
||||
h: heights,
|
||||
f: featureIds,
|
||||
t: distanceField,
|
||||
prec,
|
||||
temp
|
||||
},
|
||||
features
|
||||
};
|
||||
}
|
||||
|
||||
function undressGrid(extendedGrid: IGrid): IGridBase {
|
||||
function undressPrecreatedGrid(extendedGrid: IGrid) {
|
||||
const {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices} = extendedGrid;
|
||||
const {i, b, c, v} = cells;
|
||||
return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells: {i, b, c, v}, vertices};
|
||||
117
src/scripts/generation/grid/lakes.ts
Normal file
117
src/scripts/generation/grid/lakes.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import {TIME} from "config/logging";
|
||||
import {getInputNumber, getInputValue} from "utils/nodeUtils";
|
||||
import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation";
|
||||
import {drawPolygon} from "utils/debugUtils";
|
||||
|
||||
const {LAND_COAST, WATER_COAST} = DISTANCE_FIELD;
|
||||
|
||||
// near sea lakes usually get a lot of water inflow
|
||||
// most of them would brake threshold and flow out to sea (see Ancylus Lake)
|
||||
// connect these type of lakes to the main water body to improve the heightmap
|
||||
export 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("openNearSeaLakes");
|
||||
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("openNearSeaLakes");
|
||||
}
|
||||
|
||||
// some deeply depressed areas may not be resolved on river generation
|
||||
// this areas tend to collect precipitation, so we can add a lake there to help the resolver
|
||||
export function addLakesInDeepDepressions(
|
||||
heights: Uint8Array,
|
||||
neighbours: number[][],
|
||||
cellVertices: number[][],
|
||||
vertices: IGraphVertices,
|
||||
indexes: UintArray
|
||||
) {
|
||||
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
|
||||
if (ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT) return heights; // any depression can be resolved
|
||||
|
||||
TIME && console.time("addLakesInDeepDepressions");
|
||||
|
||||
const landCells = indexes.filter(i => heights[i] >= MIN_LAND_HEIGHT);
|
||||
landCells.sort((a, b) => heights[a] - heights[b]); // lower elevation first
|
||||
|
||||
const currentHeights = new Uint8Array(heights);
|
||||
const checkedCells: Dict<true> = {[landCells[0]]: true};
|
||||
|
||||
for (const cellId of landCells) {
|
||||
if (checkedCells[cellId]) continue;
|
||||
|
||||
const THESHOLD_HEIGHT = currentHeights[cellId] + ELEVATION_LIMIT;
|
||||
|
||||
let inDeepDepression = true;
|
||||
|
||||
const queue = [cellId];
|
||||
const checkedPaths: Dict<true> = {[cellId]: true};
|
||||
|
||||
while (queue.length) {
|
||||
const nextCellId = queue.pop()!;
|
||||
|
||||
if (currentHeights[nextCellId] < MIN_LAND_HEIGHT) {
|
||||
inDeepDepression = false;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const neibCellId of neighbours[nextCellId]) {
|
||||
if (checkedPaths[neibCellId]) continue;
|
||||
|
||||
checkedPaths[neibCellId] = true;
|
||||
checkedCells[neibCellId] = true;
|
||||
|
||||
if (currentHeights[neibCellId] < THESHOLD_HEIGHT) queue.push(neibCellId);
|
||||
}
|
||||
}
|
||||
|
||||
if (inDeepDepression) {
|
||||
currentHeights[cellId] = MIN_LAND_HEIGHT - 1;
|
||||
console.log(`ⓘ Added lake at deep depression. Cell: ${cellId}`);
|
||||
|
||||
const polygon = cellVertices[cellId].map(vertex => vertices.p[vertex]);
|
||||
drawPolygon(polygon, {stroke: "red", strokeWidth: 1, fill: "none"});
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("addLakesInDeepDepressions");
|
||||
return currentHeights;
|
||||
}
|
||||
170
src/scripts/generation/grid/precipitation.ts
Normal file
170
src/scripts/generation/grid/precipitation.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
// @ts-nocheck
|
||||
import * as d3 from "d3";
|
||||
|
||||
import {TIME} from "config/logging";
|
||||
import {minmax} from "utils/numberUtils";
|
||||
import {rand} from "utils/probabilityUtils";
|
||||
import {getInputNumber, getInputValue} from "utils/nodeUtils";
|
||||
import {byId} from "utils/shorthands";
|
||||
|
||||
// simplest precipitation model
|
||||
export function generatePrecipitation(heights: Uint8Array, temperatures: Int8Array, cellsX: number, cellsY: number) {
|
||||
TIME && console.time("generatePrecipitation");
|
||||
prec.selectAll("*").remove();
|
||||
|
||||
const precipitation = new Uint8Array(heights.length); // precipitation array
|
||||
|
||||
const cellsNumberModifier = (byId("pointsInput").dataset.cells / 10000) ** 0.25;
|
||||
const precInputModifier = getInputNumber("precInput") / 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, heights.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(heights.length - cellsX, heights.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 - heights[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 (temperatures[current] < -5) continue; // no flux in permafrost
|
||||
|
||||
if (heights[current] < 20) {
|
||||
// water cell
|
||||
if (heights[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 = heights[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(heights[i + n] - heights[i], 0); // difference in height
|
||||
const mod = (heights[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains
|
||||
return minmax(normalLoss + diff * mod, 1, humidity);
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("generatePrecipitation");
|
||||
return precipitation;
|
||||
}
|
||||
|
||||
// TODO: move to renderer
|
||||
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];
|
||||
const 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];
|
||||
const 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");
|
||||
}
|
||||
44
src/scripts/generation/grid/temperature.ts
Normal file
44
src/scripts/generation/grid/temperature.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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(heights: Uint8Array, cellsX: number, points: TPoints) {
|
||||
TIME && console.time("calculateTemperatures");
|
||||
|
||||
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;
|
||||
}
|
||||
309
src/scripts/generation/markup.ts
Normal file
309
src/scripts/generation/markup.ts
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
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(neighbors: IGraphCells["c"], borderCells: IGraphCells["b"], heights: Uint8Array) {
|
||||
TIME && console.time("markupGridFeatures");
|
||||
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
|
||||
|
||||
const gridCellsNumber = borderCells.length;
|
||||
const featureIds = new Uint16Array(gridCellsNumber); // starts from 1
|
||||
const distanceField = new Int8Array(gridCellsNumber);
|
||||
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 (borderCells[cellId]) border = true;
|
||||
|
||||
for (const neighborId of neighbors[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, 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;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {markupPackFeatures} from "modules/markup";
|
||||
import {markupPackFeatures} from "scripts/generation/markup";
|
||||
// @ts-expect-error js module
|
||||
import {drawScaleBar} from "modules/measurers";
|
||||
// @ts-expect-error js module
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {getInputNumber} from "utils/nodeUtils";
|
|||
import {pick} from "utils/functionUtils";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes";
|
||||
import {drawArrow} from "utils/debugUtils";
|
||||
|
||||
const {Rivers} = window;
|
||||
const {LAND_COAST} = DISTANCE_FIELD;
|
||||
|
|
@ -376,8 +375,7 @@ const resolveDepressions = function (
|
|||
return [initialCellHeights, {}];
|
||||
}
|
||||
|
||||
INFO &&
|
||||
console.info(`ⓘ Resolved all depressions. Depressions: ${depressions[0]}. Interations: ${depressions.length}`);
|
||||
INFO && console.info(`ⓘ Resolved all depressions. Depressions: ${depressions[0]}. Iterations: ${depressions.length}`);
|
||||
return [currentCellHeights, currentDrainableLakes];
|
||||
|
||||
// define lakes that potentially can be open (drained into another water body)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue