refactor: grid generation

This commit is contained in:
Azgaar 2022-07-11 01:38:55 +03:00
parent d18636eb8f
commit e59b536e83
15 changed files with 157 additions and 105 deletions

View file

@ -5,5 +5,6 @@ export const MAX_HEIGHT = 100;
export enum DISTANCE_FIELD {
LAND_COAST = 1,
UNMARKED = 0,
WATER_COAST = -1
WATER_COAST = -1,
DEEPER_WATER = -2
}

View file

@ -221,9 +221,9 @@ export function open(options) {
Lakes.openNearSeaLakes(grid);
}
OceanLayers(grid);
calculateTemperatures();
generatePrecipitation();
reGraph();
calculateTemperatures(grid);
generatePrecipitation(grid);
reGraph(grid);
drawCoastline();
Rivers.generate(erosionAllowed);
@ -340,9 +340,9 @@ export function open(options) {
if (erosionAllowed) addLakesInDeepDepressions();
OceanLayers(grid);
calculateTemperatures();
generatePrecipitation();
reGraph();
calculateTemperatures(grid);
generatePrecipitation(grid);
reGraph(grid);
drawCoastline();
if (erosionAllowed) Rivers.generate(true);

View file

@ -83,7 +83,7 @@ export function open() {
}
function changeHeightExponent() {
calculateTemperatures();
calculateTemperatures(grid);
if (layerIsOn("toggleTemp")) drawTemp();
}
@ -134,7 +134,7 @@ export function open() {
// height exponent
heightExponentInput.value = heightExponentOutput.value = 1.8;
localStorage.removeItem("heightExponent");
calculateTemperatures();
calculateTemperatures(grid);
// scale bar
barSizeOutput.value = barSizeInput.value = 2;

View file

@ -399,7 +399,7 @@ async function parseLoadedData(data) {
})();
void (function parsePackData() {
reGraph();
reGraph(grid);
reMarkFeatures();
pack.features = JSON.parse(data[12]);
pack.cultures = JSON.parse(data[13]);

View file

@ -7,7 +7,7 @@ import {aleaPRNG} from "scripts/aleaPRNG";
const {UNMARKED, LAND_COAST, WATER_COAST} = DISTANCE_FIELD;
// define features (grid.features: ocean, lakes, islands) and calculate distance field (cells.t)
export function markupGridFeatures(grid: IGraph & {cells: {h: UintArray}}) {
export function markupGridFeatures(grid: IGridWithHeights) {
TIME && console.time("markupGridFeatures");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
@ -173,8 +173,10 @@ export function reMarkFeatures() {
return "gulf";
}
function defineIslandGroup(cell, number) {
if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island";
function defineIslandGroup(cellId: number, number: number) {
const prevCellFeature = features[cells.f[cellId - 1]];
if (cellId && prevCellFeature && prevCellFeature.type === "lake") return "lake_island";
if (number > grid.cells.i.length / 10) return "continent";
if (number > grid.cells.i.length / 1000) return "island";
return "isle";

View file

@ -5,11 +5,12 @@ import {minmax} from "utils/numberUtils";
import {rand} from "utils/probabilityUtils";
// simplest precipitation model
export function generatePrecipitation() {
export function generatePrecipitation(grid) {
TIME && console.time("generatePrecipitation");
prec.selectAll("*").remove();
const {cells, cellsX, cellsY} = grid;
cells.prec = new Uint8Array(cells.i.length); // precipitation array
const precipitation = new Uint8Array(cells.i.length); // precipitation array
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const precInputModifier = precInput.value / 100;
@ -94,20 +95,20 @@ export function generatePrecipitation() {
if (cells.h[current] < 20) {
// water cell
if (cells.h[current + next] >= 20) {
cells.prec[current + next] += Math.max(humidity / rand(10, 20), 1); // coastal precipitation
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
cells.prec[current] += 5 * modifier; // water cells precipitation (need to correctly pour water through lakes)
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 precipitation = isPassable ? getPrecipitation(humidity, current, next) : humidity;
cells.prec[current] += precipitation;
const evaporation = precipitation > 1.5 ? 1 : 0; // some humidity evaporates back to the atmosphere
humidity = isPassable ? minmax(humidity - precipitation + evaporation, 0, maxPrec) : 0;
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;
}
}
}
@ -162,4 +163,5 @@ export function generatePrecipitation() {
})();
TIME && console.timeEnd("generatePrecipitation");
return precipitation;
}

View file

@ -125,10 +125,8 @@ window.Submap = (function () {
OceanLayers(grid);
calculateMapCoordinates();
// calculateTemperatures();
// generatePrecipitation();
stage("Cell cleanup.");
reGraph();
reGraph(grid);
// remove misclassified cells
stage("Define coastline.");

View file

@ -1,35 +0,0 @@
import * as d3 from "d3";
import {TIME} from "config/logging";
import {minmax, rn} from "utils/numberUtils";
// temperature model
export function calculateTemperatures() {
TIME && console.time("calculateTemperatures");
const cells = grid.cells;
cells.temp = new Int8Array(cells.i.length); // temperature array
const tEq = +temperatureEquatorInput.value;
const tPole = +temperaturePoleInput.value;
const tDelta = tEq - tPole;
const int = d3.easePolyInOut.exponent(0.5); // interpolation function
d3.range(0, cells.i.length, grid.cellsX).forEach(function (r) {
const y = grid.points[r][1];
const lat = Math.abs(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT); // [0; 90]
const initTemp = tEq - int(lat / 90) * tDelta;
for (let i = r; i < r + grid.cellsX; i++) {
cells.temp[i] = minmax(initTemp - convertToFriendly(cells.h[i]), -128, 127);
}
});
// temperature decreases by 6.5 degree C per 1km
function convertToFriendly(h) {
if (h < 20) return 0;
const exponent = +heightExponentInput.value;
const height = Math.pow(h - 18, exponent);
return rn((height / 1000) * 6.5);
}
TIME && console.timeEnd("calculateTemperatures");
}

View file

@ -0,0 +1,47 @@
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;
}

View file

@ -60,8 +60,8 @@ export function editWorld() {
updateGlobeTemperature();
updateGlobePosition();
calculateTemperatures();
generatePrecipitation();
calculateTemperatures(grid);
generatePrecipitation(grid);
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
Lakes.defineGroup();

View file

@ -3,16 +3,24 @@ import * as d3 from "d3";
import {ERROR, INFO, WARN} from "config/logging";
import {closeDialogs} from "dialogs/utils";
import {initLayers, renderLayer, restoreLayers} from "layers";
// @ts-expect-error js module
import {drawCoastline} from "modules/coastline";
import {calculateMapCoordinates, defineMapSize} from "modules/coordinates";
import {markupGridFeatures, markupGridOcean} from "modules/markup";
import {markupGridFeatures} from "modules/markup";
// @ts-expect-error js module
import {drawScaleBar, Rulers} from "modules/measurers";
// @ts-expect-error js module
import {generatePrecipitation} from "modules/precipitation";
import {calculateTemperatures} from "modules/temperature";
// @ts-expect-error js module
import {unfog} from "modules/ui/editors";
// @ts-expect-error js module
import {applyMapSize, randomizeOptions} from "modules/ui/options";
// @ts-expect-error js module
import {applyStyleOnLoad} from "modules/ui/stylePresets";
// @ts-expect-error js module
import {addZones} from "modules/zones";
// @ts-expect-error js module
import {aleaPRNG} from "scripts/aleaPRNG";
import {hideLoading, showLoading} from "scripts/loading";
import {clearMainTip, tip} from "scripts/tooltips";
@ -45,9 +53,9 @@ export async function generate(options?: IGenerationOptions) {
applyMapSize();
randomizeOptions();
const updatedGrid = await updateGrid(precreatedGraph);
const updatedGrid = await updateGrid(grid, precreatedGraph);
reGraph();
reGraph(updatedGrid);
drawCoastline();
Rivers.generate();
@ -75,6 +83,8 @@ export async function generate(options?: IGenerationOptions) {
Markers.generate();
addZones();
OceanLayers(updatedGrid);
drawScaleBar(scale);
Names.getMapName();
@ -110,35 +120,33 @@ export async function generate(options?: IGenerationOptions) {
}
}
async function updateGrid(precreatedGraph?: IGrid) {
const globalGrid = grid;
const updatedGrid: IGraph & Partial<IGrid> = shouldRegenerateGridPoints(globalGrid)
async function updateGrid(globalGrid: IGrid, precreatedGraph?: IGrid): Promise<IGrid> {
const baseGrid: IGridBase = shouldRegenerateGridPoints(globalGrid)
? (precreatedGraph && undressGrid(precreatedGraph)) || generateGrid()
: undressGrid(globalGrid);
const heights = await HeightmapGenerator.generate(updatedGrid);
updatedGrid.cells.h = heights;
const heights: Uint8Array = await HeightmapGenerator.generate(baseGrid);
if (!heights) throw new Error("Heightmap generation failed");
const heightsGrid = {...baseGrid, cells: {...baseGrid.cells, h: heights}};
const {featureIds, distanceField, features} = markupGridFeatures(updatedGrid);
updatedGrid.cells.f = featureIds;
updatedGrid.cells.t = distanceField;
updatedGrid.features = features;
const {featureIds, distanceField, features} = markupGridFeatures(heightsGrid);
const markedGrid = {...heightsGrid, features, cells: {...heightsGrid.cells, f: featureIds, t: distanceField}};
const touchesEdges = features.some(feature => feature && feature.land && feature.border);
defineMapSize(touchesEdges);
Lakes.addLakesInDeepDepressions(updatedGrid);
Lakes.openNearSeaLakes(updatedGrid);
OceanLayers(updatedGrid);
window.mapCoordinates = calculateMapCoordinates();
calculateTemperatures();
generatePrecipitation();
Lakes.addLakesInDeepDepressions(markedGrid);
Lakes.openNearSeaLakes(markedGrid);
const temperature = calculateTemperatures(markedGrid);
const temperatureGrid = {...markedGrid, cells: {...markedGrid.cells, temp: temperature}};
const prec = generatePrecipitation(temperatureGrid);
return {...temperatureGrid, cells: {...temperatureGrid.cells, prec}};
}
function undressGrid(extendedGrid: IGrid) {
function undressGrid(extendedGrid: IGrid): IGridBase {
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};
@ -154,12 +162,17 @@ export async function generateMapOnLoad() {
// clear the map
export function undraw() {
viewbox.selectAll("path, circle, polygon, line, text, use, #zones > g, #armies > g, #ruler > g").remove();
byId("deftemp")
.querySelectorAll("path, clipPath, svg")
?.querySelectorAll("path, clipPath, svg")
.forEach(el => el.remove());
byId("coas").innerHTML = ""; // remove auto-generated emblems
// remove auto-generated emblems
if (byId("coas")) byId("coas")!.innerHTML = "";
notes = [];
rulers = new Rulers();
unfog();
}

View file

@ -5,9 +5,12 @@ import {UINT16_MAX} from "constants";
import {createTypedArray} from "utils/arrayUtils";
import {calculateVoronoi, getPackPolygon} from "utils/graphUtils";
import {rn} from "utils/numberUtils";
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
const {LAND_COAST, WATER_COAST, DEEPER_WATER} = DISTANCE_FIELD;
// recalculate Voronoi Graph to pack cells
export function reGraph() {
export function reGraph(grid: IGrid) {
TIME && console.time("reGraph");
const {cells: gridCells, points, features} = grid;
const newCells: {p: TPoints; g: number[]; h: number[]} = {p: [], g: [], h: []}; // store new data
@ -16,14 +19,18 @@ export function reGraph() {
for (const i of gridCells.i) {
const height = gridCells.h[i];
const type = gridCells.t[i];
if (height < 20 && type !== -1 && type !== -2) continue; // exclude all deep ocean points
if (type === -2 && (i % 4 === 0 || features[gridCells.f[i]].type === "lake")) continue; // exclude non-coastal lake points
if (height < MIN_LAND_HEIGHT && type !== WATER_COAST && type !== DEEPER_WATER) continue; // exclude all deep ocean points
const feature = features[gridCells.f[i]];
const isLake = feature && feature.type === "lake";
if (type === DEEPER_WATER && (i % 4 === 0 || isLake)) continue; // exclude non-coastal lake points
const [x, y] = points[i];
addNewPoint(i, x, y, height);
// add additional points for cells along coast
if (type === 1 || type === -1) {
if (type === LAND_COAST || type === WATER_COAST) {
if (gridCells.b[i]) continue; // not for near-border cells
gridCells.c[i].forEach(e => {
if (i > e) return;
@ -55,7 +62,7 @@ export function reGraph() {
pack.cells.p = newCells.p;
pack.cells.g = createTypedArray({maxValue: grid.points.length, from: newCells.g});
pack.cells.q = d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i]));
pack.cells.h = createTypedArray({maxValue: 100, from: newCells.h});
pack.cells.h = new Uint8Array(newCells.h);
pack.cells.area = createTypedArray({maxValue: UINT16_MAX, from: pack.cells.i}).map(getCellArea);
TIME && console.timeEnd("reGraph");

20
src/types/grid.d.ts vendored
View file

@ -5,16 +5,26 @@ interface IGrid extends IGraph {
spacing: number;
boundary: TPoints;
points: TPoints;
cells: IGridCells;
cells: IGraphCells & IGridCells;
features: TGridFeatures;
}
interface IGridCells extends IGraphCells {
h: UintArray; // heights, [0, 100], see MIN_LAND_HEIGHT constant
interface IGridCells {
h: Uint8Array; // heights, [0, 100], see MIN_LAND_HEIGHT constant
t: Int8Array; // see DISTANCE_FIELD enum
f: Uint16Array; // feature id, see IGridFeature
temp: UintArray; // temparature in Celsius
prec: UintArray; // precipitation in inner units
temp: Int8Array; // temparature in Celsius
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[]];

View file

@ -6,16 +6,23 @@ interface Navigator {
interface Window {
mapCoordinates: IMapCoordinates;
$: typeof $;
$: typeof $; // jQuery
// untyped IIFE modules
Biomes: typeof Biomes;
Names: typeof Names;
ThreeD: typeof ThreeD;
ReliefIcons: typeof ReliefIcons;
Zoom: typeof Zoom;
Lakes: typeof Lakes;
HeightmapGenerator: typeof HeightmapGenerator;
OceanLayers: typeof OceanLayers;
Biomes: any;
Names: any;
ThreeD: any;
ReliefIcons: any;
Zoom: any;
Lakes: any;
HeightmapGenerator: any;
OceanLayers: any;
Rivers: any;
Cultures: any;
BurgsAndStates: any;
Religions: any;
Military: any;
Markers: any;
}
interface Node {

2
src/types/pack.d.ts vendored
View file

@ -11,7 +11,7 @@ interface IPack extends IGraph {
interface IPackCells extends IGraphCells {
p: TPoints; // cell center points
h: UintArray; // heights, [0, 100], see MIN_LAND_HEIGHT constant
h: Uint8Array; // heights, [0, 100], see MIN_LAND_HEIGHT constant
t: Int8Array; // see DISTANCE_FIELD enum
f: Uint16Array; // feature id, see TPackFeature
g: UintArray;