mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 01:41:22 +01:00
refactor: addLakesInDeepDepressions
This commit is contained in:
parent
b2f16c4b8f
commit
3c6da6585e
16 changed files with 250 additions and 266 deletions
|
|
@ -7642,7 +7642,6 @@
|
||||||
<script type="module" src="/src/modules/heightmap-generator.js"></script>
|
<script type="module" src="/src/modules/heightmap-generator.js"></script>
|
||||||
<script type="module" src="/src/modules/ocean-layers.js"></script>
|
<script type="module" src="/src/modules/ocean-layers.js"></script>
|
||||||
<script type="module" src="/src/modules/rivers.js"></script>
|
<script type="module" src="/src/modules/rivers.js"></script>
|
||||||
<script type="module" src="/src/modules/lakes.ts"></script>
|
|
||||||
<script type="module" src="/src/modules/names-generator.js"></script>
|
<script type="module" src="/src/modules/names-generator.js"></script>
|
||||||
<script type="module" src="/src/modules/biomes.js"></script>
|
<script type="module" src="/src/modules/biomes.js"></script>
|
||||||
<script type="module" src="/src/modules/cultures-generator.js"></script>
|
<script type="module" src="/src/modules/cultures-generator.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -215,10 +215,10 @@ export function open(options) {
|
||||||
const erosionAllowed = allowErosion.checked;
|
const erosionAllowed = allowErosion.checked;
|
||||||
markupGridFeatures();
|
markupGridFeatures();
|
||||||
|
|
||||||
if (erosionAllowed) {
|
// if (erosionAllowed) {
|
||||||
Lakes.addLakesInDeepDepressions(grid);
|
// Lakes.addLakesInDeepDepressions(grid);
|
||||||
Lakes.openNearSeaLakes(grid);
|
// Lakes.openNearSeaLakes(grid);
|
||||||
}
|
// }
|
||||||
OceanLayers(grid);
|
OceanLayers(grid);
|
||||||
calculateTemperatures(grid);
|
calculateTemperatures(grid);
|
||||||
generatePrecipitation(grid);
|
generatePrecipitation(grid);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {byId} from "utils/shorthands";
|
||||||
import {ERROR} from "../config/logging";
|
import {ERROR} from "../config/logging";
|
||||||
import {lim, minmax} from "../utils/numberUtils";
|
import {lim, minmax} from "../utils/numberUtils";
|
||||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||||
|
import {addLakesInDeepDepressions} from "scripts/generation/grid/lakes";
|
||||||
|
|
||||||
window.HeightmapGenerator = (function () {
|
window.HeightmapGenerator = (function () {
|
||||||
let grid = null;
|
let grid = null;
|
||||||
|
|
@ -77,7 +78,10 @@ window.HeightmapGenerator = (function () {
|
||||||
|
|
||||||
Math.random = aleaPRNG(seed);
|
Math.random = aleaPRNG(seed);
|
||||||
const isTemplate = id in heightmapTemplates;
|
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");
|
TIME && console.timeEnd("defineHeightmap");
|
||||||
|
|
||||||
clearData();
|
clearData();
|
||||||
|
|
|
||||||
|
|
@ -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};
|
|
||||||
})();
|
|
||||||
|
|
@ -118,10 +118,10 @@ window.Submap = (function () {
|
||||||
markupGridFeatures();
|
markupGridFeatures();
|
||||||
|
|
||||||
// Warning: addLakesInDeepDepressions can be very slow!
|
// Warning: addLakesInDeepDepressions can be very slow!
|
||||||
if (options.addLakesInDepressions) {
|
// if (options.addLakesInDepressions) {
|
||||||
Lakes.addLakesInDeepDepressions(grid);
|
// Lakes.addLakesInDeepDepressions(grid);
|
||||||
Lakes.openNearSeaLakes(grid);
|
// Lakes.openNearSeaLakes(grid);
|
||||||
}
|
// }
|
||||||
|
|
||||||
OceanLayers(grid);
|
OceanLayers(grid);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ import {debounce} from "utils/functionUtils";
|
||||||
import {rn} from "utils/numberUtils";
|
import {rn} from "utils/numberUtils";
|
||||||
import {generateSeed} from "utils/probabilityUtils";
|
import {generateSeed} from "utils/probabilityUtils";
|
||||||
import {byId} from "utils/shorthands";
|
import {byId} from "utils/shorthands";
|
||||||
import {createGrid} from "./grid";
|
import {createGrid} from "./grid/grid";
|
||||||
import {createPack} from "./pack/pack";
|
import {createPack} from "./pack/pack";
|
||||||
import {getInputValue, setInputValue} from "utils/nodeUtils";
|
import {getInputValue, setInputValue} from "utils/nodeUtils";
|
||||||
// import {Ruler} from "modules/measurers";
|
import {calculateMapCoordinates} from "modules/coordinates";
|
||||||
|
|
||||||
const {Zoom, ThreeD} = window;
|
const {Zoom, ThreeD} = window;
|
||||||
|
|
||||||
|
|
@ -50,6 +50,8 @@ async function generate(options?: IGenerationOptions) {
|
||||||
applyMapSize();
|
applyMapSize();
|
||||||
randomizeOptions();
|
randomizeOptions();
|
||||||
|
|
||||||
|
window.mapCoordinates = calculateMapCoordinates();
|
||||||
|
|
||||||
const newGrid = await createGrid(grid, precreatedGraph);
|
const newGrid = await createGrid(grid, precreatedGraph);
|
||||||
const newPack = createPack(newGrid);
|
const newPack = createPack(newGrid);
|
||||||
|
|
||||||
|
|
@ -60,10 +62,10 @@ async function generate(options?: IGenerationOptions) {
|
||||||
pack = newPack;
|
pack = newPack;
|
||||||
|
|
||||||
// temp rendering for debug
|
// temp rendering for debug
|
||||||
renderLayer("cells");
|
// renderLayer("cells");
|
||||||
renderLayer("features");
|
renderLayer("features");
|
||||||
renderLayer("heightmap");
|
// renderLayer("heightmap");
|
||||||
renderLayer("rivers", pack);
|
// renderLayer("rivers", pack);
|
||||||
|
|
||||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||||
// showStatistics();
|
// showStatistics();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import Delaunator from "delaunator";
|
||||||
|
|
||||||
import {Voronoi} from "modules/voronoi";
|
import {Voronoi} from "modules/voronoi";
|
||||||
import {TIME} from "config/logging";
|
import {TIME} from "config/logging";
|
||||||
// @ts-expect-error js module
|
|
||||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||||
import {createTypedArray} from "utils/arrayUtils";
|
import {createTypedArray} from "utils/arrayUtils";
|
||||||
import {rn} from "utils/numberUtils";
|
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 {generateGrid} from "scripts/generation/graph";
|
||||||
import {calculateMapCoordinates, defineMapSize} from "modules/coordinates";
|
import {markupGridFeatures} from "scripts/generation/markup";
|
||||||
import {markupGridFeatures} from "modules/markup";
|
|
||||||
// @ts-expect-error js module
|
|
||||||
import {generatePrecipitation} from "modules/precipitation";
|
|
||||||
import {byId} from "utils/shorthands";
|
|
||||||
import {rn} from "utils/numberUtils";
|
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> {
|
export async function createGrid(globalGrid: IGrid, precreatedGrid?: IGrid): Promise<IGrid> {
|
||||||
const baseGrid: IGridBase = shouldRegenerateGridPoints(globalGrid)
|
const shouldRegenerate = shouldRegenerateGridPoints(globalGrid);
|
||||||
? (precreatedGraph && undressGrid(precreatedGraph)) || generateGrid()
|
const {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices} = shouldRegenerate
|
||||||
: undressGrid(globalGrid);
|
? (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");
|
if (!heights) throw new Error("Heightmap generation failed");
|
||||||
const heightsGrid = {...baseGrid, cells: {...baseGrid.cells, h: heights}};
|
|
||||||
|
|
||||||
const {featureIds, distanceField, features} = markupGridFeatures(heightsGrid);
|
const {featureIds, distanceField, features} = markupGridFeatures(cells.c, cells.b, heights);
|
||||||
const markedGrid = {...heightsGrid, features, cells: {...heightsGrid.cells, f: featureIds, t: distanceField}};
|
|
||||||
|
|
||||||
const touchesEdges = features.some(feature => feature && feature.land && feature.border);
|
const touchesEdges = features.some(feature => feature && feature.land && feature.border);
|
||||||
defineMapSize(touchesEdges);
|
defineMapSize(touchesEdges);
|
||||||
window.mapCoordinates = calculateMapCoordinates();
|
|
||||||
|
|
||||||
Lakes.addLakesInDeepDepressions(markedGrid);
|
const temp = calculateTemperatures(heights, cellsX, points);
|
||||||
Lakes.openNearSeaLakes(markedGrid);
|
const prec = generatePrecipitation(heights, temp, cellsX, cellsY);
|
||||||
|
|
||||||
const temperature = calculateTemperatures(markedGrid);
|
return {
|
||||||
const temperatureGrid = {...markedGrid, cells: {...markedGrid.cells, temp: temperature}};
|
cellsDesired,
|
||||||
|
cellsX,
|
||||||
const prec = generatePrecipitation(temperatureGrid);
|
cellsY,
|
||||||
return {...temperatureGrid, cells: {...temperatureGrid.cells, prec}};
|
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 {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices} = extendedGrid;
|
||||||
const {i, b, c, v} = cells;
|
const {i, b, c, v} = cells;
|
||||||
return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells: {i, b, c, v}, vertices};
|
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;
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
|
// @ts-nocheck
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import {TIME} from "config/logging";
|
import {TIME} from "config/logging";
|
||||||
import {minmax} from "utils/numberUtils";
|
import {minmax} from "utils/numberUtils";
|
||||||
import {rand} from "utils/probabilityUtils";
|
import {rand} from "utils/probabilityUtils";
|
||||||
|
import {getInputNumber, getInputValue} from "utils/nodeUtils";
|
||||||
|
import {byId} from "utils/shorthands";
|
||||||
|
|
||||||
// simplest precipitation model
|
// simplest precipitation model
|
||||||
export function generatePrecipitation(grid) {
|
export function generatePrecipitation(heights: Uint8Array, temperatures: Int8Array, cellsX: number, cellsY: number) {
|
||||||
TIME && console.time("generatePrecipitation");
|
TIME && console.time("generatePrecipitation");
|
||||||
prec.selectAll("*").remove();
|
prec.selectAll("*").remove();
|
||||||
|
|
||||||
const {cells, cellsX, cellsY} = grid;
|
const precipitation = new Uint8Array(heights.length); // precipitation array
|
||||||
const precipitation = new Uint8Array(cells.i.length); // precipitation array
|
|
||||||
|
|
||||||
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
|
const cellsNumberModifier = (byId("pointsInput").dataset.cells / 10000) ** 0.25;
|
||||||
const precInputModifier = precInput.value / 100;
|
const precInputModifier = getInputNumber("precInput") / 100;
|
||||||
const modifier = cellsNumberModifier * precInputModifier;
|
const modifier = cellsNumberModifier * precInputModifier;
|
||||||
|
|
||||||
const westerly = [];
|
const westerly = [];
|
||||||
|
|
@ -34,7 +36,7 @@ export function generatePrecipitation(grid) {
|
||||||
const MAX_PASSABLE_ELEVATION = 85;
|
const MAX_PASSABLE_ELEVATION = 85;
|
||||||
|
|
||||||
// define wind directions based on cells latitude and prevailing winds there
|
// define wind directions based on cells latitude and prevailing winds there
|
||||||
d3.range(0, cells.i.length, cellsX).forEach(function (c, i) {
|
d3.range(0, heights.length, cellsX).forEach(function (c, i) {
|
||||||
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
|
const lat = mapCoordinates.latN - (i / cellsY) * mapCoordinates.latT;
|
||||||
const latBand = ((Math.abs(lat) - 1) / 5) | 0;
|
const latBand = ((Math.abs(lat) - 1) / 5) | 0;
|
||||||
const latMod = latitudeModifier[latBand];
|
const latMod = latitudeModifier[latBand];
|
||||||
|
|
@ -63,7 +65,7 @@ export function generatePrecipitation(grid) {
|
||||||
const bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0;
|
const bandS = ((Math.abs(mapCoordinates.latS) - 1) / 5) | 0;
|
||||||
const latModS = mapCoordinates.latT > 60 ? d3.mean(latitudeModifier) : latitudeModifier[bandS];
|
const latModS = mapCoordinates.latT > 60 ? d3.mean(latitudeModifier) : latitudeModifier[bandS];
|
||||||
const maxPrecS = (southerly / vertT) * 60 * modifier * latModS;
|
const maxPrecS = (southerly / vertT) * 60 * modifier * latModS;
|
||||||
passWind(d3.range(cells.i.length - cellsX, cells.i.length, 1), maxPrecS, -cellsX, cellsY);
|
passWind(d3.range(heights.length - cellsX, heights.length, 1), maxPrecS, -cellsX, cellsY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWindDirections(tier) {
|
function getWindDirections(tier) {
|
||||||
|
|
@ -86,15 +88,15 @@ export function generatePrecipitation(grid) {
|
||||||
first = first[0];
|
first = first[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
let humidity = maxPrec - cells.h[first]; // initial water amount
|
let humidity = maxPrec - heights[first]; // initial water amount
|
||||||
if (humidity <= 0) continue; // if first cell in row is too elevated consider wind dry
|
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) {
|
for (let s = 0, current = first; s < steps; s++, current += next) {
|
||||||
if (cells.temp[current] < -5) continue; // no flux in permafrost
|
if (temperatures[current] < -5) continue; // no flux in permafrost
|
||||||
|
|
||||||
if (cells.h[current] < 20) {
|
if (heights[current] < 20) {
|
||||||
// water cell
|
// water cell
|
||||||
if (cells.h[current + next] >= 20) {
|
if (heights[current + next] >= 20) {
|
||||||
precipitation[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation
|
precipitation[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation
|
||||||
} else {
|
} else {
|
||||||
humidity = Math.min(humidity + 5 * modifier, maxPrec); // wind gets more humidity passing water cell
|
humidity = Math.min(humidity + 5 * modifier, maxPrec); // wind gets more humidity passing water cell
|
||||||
|
|
@ -104,7 +106,7 @@ export function generatePrecipitation(grid) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// land cell
|
// land cell
|
||||||
const isPassable = cells.h[current + next] <= MAX_PASSABLE_ELEVATION;
|
const isPassable = heights[current + next] <= MAX_PASSABLE_ELEVATION;
|
||||||
const cellPrec = isPassable ? getPrecipitation(humidity, current, next) : humidity;
|
const cellPrec = isPassable ? getPrecipitation(humidity, current, next) : humidity;
|
||||||
precipitation[current] += cellPrec;
|
precipitation[current] += cellPrec;
|
||||||
const evaporation = cellPrec > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere
|
const evaporation = cellPrec > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere
|
||||||
|
|
@ -115,53 +117,54 @@ export function generatePrecipitation(grid) {
|
||||||
|
|
||||||
function getPrecipitation(humidity, i, n) {
|
function getPrecipitation(humidity, i, n) {
|
||||||
const normalLoss = Math.max(humidity / (10 * modifier), 1); // precipitation in normal conditions
|
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 diff = Math.max(heights[i + n] - heights[i], 0); // difference in height
|
||||||
const mod = (cells.h[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains
|
const mod = (heights[i + n] / 70) ** 2; // 50 stands for hills, 70 for mountains
|
||||||
return minmax(normalLoss + diff * mod, 1, humidity);
|
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");
|
TIME && console.timeEnd("generatePrecipitation");
|
||||||
return precipitation;
|
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");
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,9 @@ import {MIN_LAND_HEIGHT} from "config/generation";
|
||||||
|
|
||||||
const interpolate = d3.easePolyInOut.exponent(0.5); // interpolation function
|
const interpolate = d3.easePolyInOut.exponent(0.5); // interpolation function
|
||||||
|
|
||||||
export function calculateTemperatures(grid: IGridWithHeights) {
|
export function calculateTemperatures(heights: Uint8Array, cellsX: number, points: TPoints) {
|
||||||
TIME && console.time("calculateTemperatures");
|
TIME && console.time("calculateTemperatures");
|
||||||
|
|
||||||
const {cells, cellsX, points} = grid;
|
|
||||||
const heights = cells.h;
|
|
||||||
|
|
||||||
const temperatures = new Int8Array(heights.length); // temperature array
|
const temperatures = new Int8Array(heights.length); // temperature array
|
||||||
|
|
||||||
// temperature decreases by 6.5 Celsius per kilometer
|
// temperature decreases by 6.5 Celsius per kilometer
|
||||||
|
|
@ -13,20 +13,13 @@ import {rn} from "utils/numberUtils";
|
||||||
const {UNMARKED, LAND_COAST, WATER_COAST, LANDLOCKED, DEEPER_WATER} = DISTANCE_FIELD;
|
const {UNMARKED, LAND_COAST, WATER_COAST, LANDLOCKED, DEEPER_WATER} = DISTANCE_FIELD;
|
||||||
|
|
||||||
// define features (oceans, lakes, islands)
|
// define features (oceans, lakes, islands)
|
||||||
export function markupGridFeatures(grid: IGridWithHeights) {
|
export function markupGridFeatures(neighbors: IGraphCells["c"], borderCells: IGraphCells["b"], heights: Uint8Array) {
|
||||||
TIME && console.time("markupGridFeatures");
|
TIME && console.time("markupGridFeatures");
|
||||||
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
|
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
|
||||||
|
|
||||||
if (!grid.cells || !grid.cells.h) {
|
const gridCellsNumber = borderCells.length;
|
||||||
throw new Error("markupGridFeatures: grid.cells.h is required");
|
const featureIds = new Uint16Array(gridCellsNumber); // starts from 1
|
||||||
}
|
const distanceField = new Int8Array(gridCellsNumber);
|
||||||
|
|
||||||
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 features: TGridFeatures = [0];
|
||||||
|
|
||||||
const queue = [0];
|
const queue = [0];
|
||||||
|
|
@ -39,9 +32,9 @@ export function markupGridFeatures(grid: IGridWithHeights) {
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const cellId = queue.pop()!;
|
const cellId = queue.pop()!;
|
||||||
if (cells.b[cellId]) border = true;
|
if (borderCells[cellId]) border = true;
|
||||||
|
|
||||||
for (const neighborId of cells.c[cellId]) {
|
for (const neighborId of neighbors[cellId]) {
|
||||||
const isNeibLand = heights[neighborId] >= MIN_LAND_HEIGHT;
|
const isNeibLand = heights[neighborId] >= MIN_LAND_HEIGHT;
|
||||||
|
|
||||||
if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
|
if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
|
||||||
|
|
@ -61,13 +54,7 @@ export function markupGridFeatures(grid: IGridWithHeights) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// markup deep ocean cells
|
// markup deep ocean cells
|
||||||
const dfOceanMarked = markup({
|
const dfOceanMarked = markup({distanceField, neighbors, start: DEEPER_WATER, increment: -1, limit: -10});
|
||||||
distanceField,
|
|
||||||
neighbors: grid.cells.c,
|
|
||||||
start: DEEPER_WATER,
|
|
||||||
increment: -1,
|
|
||||||
limit: -10
|
|
||||||
});
|
|
||||||
|
|
||||||
TIME && console.timeEnd("markupGridFeatures");
|
TIME && console.timeEnd("markupGridFeatures");
|
||||||
return {featureIds, distanceField: dfOceanMarked, features};
|
return {featureIds, distanceField: dfOceanMarked, features};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import {markupPackFeatures} from "modules/markup";
|
import {markupPackFeatures} from "scripts/generation/markup";
|
||||||
// @ts-expect-error js module
|
// @ts-expect-error js module
|
||||||
import {drawScaleBar} from "modules/measurers";
|
import {drawScaleBar} from "modules/measurers";
|
||||||
// @ts-expect-error js module
|
// @ts-expect-error js module
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {getInputNumber} from "utils/nodeUtils";
|
||||||
import {pick} from "utils/functionUtils";
|
import {pick} from "utils/functionUtils";
|
||||||
import {byId} from "utils/shorthands";
|
import {byId} from "utils/shorthands";
|
||||||
import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes";
|
import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes";
|
||||||
import {drawArrow} from "utils/debugUtils";
|
|
||||||
|
|
||||||
const {Rivers} = window;
|
const {Rivers} = window;
|
||||||
const {LAND_COAST} = DISTANCE_FIELD;
|
const {LAND_COAST} = DISTANCE_FIELD;
|
||||||
|
|
@ -376,8 +375,7 @@ const resolveDepressions = function (
|
||||||
return [initialCellHeights, {}];
|
return [initialCellHeights, {}];
|
||||||
}
|
}
|
||||||
|
|
||||||
INFO &&
|
INFO && console.info(`ⓘ Resolved all depressions. Depressions: ${depressions[0]}. Iterations: ${depressions.length}`);
|
||||||
console.info(`ⓘ Resolved all depressions. Depressions: ${depressions[0]}. Interations: ${depressions.length}`);
|
|
||||||
return [currentCellHeights, currentDrainableLakes];
|
return [currentCellHeights, currentDrainableLakes];
|
||||||
|
|
||||||
// define lakes that potentially can be open (drained into another water body)
|
// define lakes that potentially can be open (drained into another water body)
|
||||||
|
|
|
||||||
10
src/types/grid.d.ts
vendored
10
src/types/grid.d.ts
vendored
|
|
@ -17,16 +17,6 @@ interface IGridCells {
|
||||||
prec: Uint8Array; // precipitation in inner units
|
prec: Uint8Array; // precipitation in inner units
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGridBase extends IGrid {
|
|
||||||
cells: IGraphCells & Partial<IGridCells>;
|
|
||||||
features?: TGridFeatures;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IGridWithHeights extends IGrid {
|
|
||||||
cells: IGraphCells & Partial<IGridCells> & {h: Uint8Array};
|
|
||||||
features?: TGridFeatures;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TGridFeatures = [0, ...IGridFeature[]];
|
type TGridFeatures = [0, ...IGridFeature[]];
|
||||||
|
|
||||||
interface IGridFeature {
|
interface IGridFeature {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||||
// @ts-expect-error js module
|
|
||||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
|
||||||
|
|
||||||
// return cell index on a regular square grid
|
// return cell index on a regular square grid
|
||||||
export function findGridCell(x: number, y: number, grid: IGrid) {
|
export function findGridCell(x: number, y: number, grid: IGrid) {
|
||||||
return (
|
/* use */ const {spacing, cellsX, cellsY} = grid;
|
||||||
Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX +
|
return Math.floor(Math.min(y / spacing, cellsY - 1)) * cellsX + Math.floor(Math.min(x / spacing, cellsX - 1));
|
||||||
Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// return array of cell indexes in radius on a regular square grid
|
// return array of cell indexes in radius on a regular square grid
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue