Fantasy-Map-Generator/modules/heightmap-generator.js
2025-09-04 10:13:01 +01:00

573 lines
17 KiB
JavaScript

"use strict";
window.HeightmapGenerator = (function () {
let grid = null;
let heights = null;
let blobPower;
let linePower;
const setGraph = graph => {
const {cellsDesired, cells, points} = graph;
heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({maxValue: 100, length: points.length});
blobPower = getBlobPower(cellsDesired);
linePower = getLinePower(cellsDesired);
grid = graph;
};
const getHeights = () => heights;
const clearData = () => {
heights = null;
grid = null;
};
const fromTemplate = (graph, id) => {
const templateString = heightmapTemplates[id]?.template || "";
const steps = templateString.split("\n");
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
setGraph(graph);
for (const step of steps) {
const elements = step.trim().split(" ");
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
addStep(...elements);
}
return heights;
};
const fromPrecreated = (graph, id) => {
return new Promise(resolve => {
// create canvas where 1px corresponts to a cell
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const {cellsX, cellsY} = graph;
canvas.width = cellsX;
canvas.height = cellsY;
// load heightmap into image and render to canvas
const img = new Image();
img.src = `./heightmaps/${id}.png`;
img.onload = () => {
ctx.drawImage(img, 0, 0, cellsX, cellsY);
const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
setGraph(graph);
getHeightsFromImageData(imageData.data);
canvas.remove();
img.remove();
resolve(heights);
};
});
};
const generate = async function (graph) {
TIME && console.time("defineHeightmap");
const id = byId("templateInput").value;
Math.random = aleaPRNG(seed);
const isTemplate = id in heightmapTemplates;
const heights = isTemplate ? fromTemplate(graph, id) : await fromPrecreated(graph, id);
TIME && console.timeEnd("defineHeightmap");
clearData();
return heights;
};
function addStep(tool, a2, a3, a4, a5) {
if (tool === "Hill") return addHill(a2, a3, a4, a5);
if (tool === "Pit") return addPit(a2, a3, a4, a5);
if (tool === "Range") return addRange(a2, a3, a4, a5);
if (tool === "Trough") return addTrough(a2, a3, a4, a5);
if (tool === "Strait") return addStrait(a2, a3);
if (tool === "Mask") return mask(a2);
if (tool === "Invert") return invert(a2, a3);
if (tool === "Add") return modify(a3, +a2, 1);
if (tool === "Multiply") return modify(a3, 0, +a2);
if (tool === "Smooth") return smooth(a2);
}
function getBlobPower(cells) {
const blobPowerMap = {
1000: 0.93,
2000: 0.95,
5000: 0.97,
10000: 0.98,
20000: 0.99,
30000: 0.991,
40000: 0.993,
50000: 0.994,
60000: 0.995,
70000: 0.9955,
80000: 0.996,
90000: 0.9964,
100000: 0.9973
};
return blobPowerMap[cells] || 0.98;
}
function getLinePower() {
const linePowerMap = {
1000: 0.75,
2000: 0.77,
5000: 0.79,
10000: 0.81,
20000: 0.82,
30000: 0.83,
40000: 0.84,
50000: 0.86,
60000: 0.87,
70000: 0.88,
80000: 0.91,
90000: 0.92,
100000: 0.93
};
return linePowerMap[cells] || 0.81;
}
const addHill = (count, height, rangeX, rangeY) => {
count = getNumberInRange(count);
while (count > 0) {
addOneHill();
count--;
}
function addOneHill() {
// reuse a scratch buffer to avoid reallocations
if (!addHill._change || addHill._change.length !== heights.length) addHill._change = new Uint8Array(heights.length);
const change = addHill._change;
change.fill(0);
let limit = 0;
let start;
let h = lim(getNumberInRange(height));
do {
const x = getPointInRange(rangeX, graphWidth);
const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] + h > 90 && limit < 50);
change[start] = h;
const queue = [start];
for (let qi = 0; qi < queue.length; qi++) {
const q = queue[qi];
const neibs = grid.cells.c[q];
for (let k = 0; k < neibs.length; k++) {
const c = neibs[k];
if (change[c]) continue;
change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
if (change[c] > 1) queue.push(c);
}
}
for (let i = 0; i < heights.length; i++) heights[i] = lim(heights[i] + change[i]);
}
};
const addPit = (count, height, rangeX, rangeY) => {
count = getNumberInRange(count);
while (count > 0) {
addOnePit();
count--;
}
function addOnePit() {
if (!addPit._used || addPit._used.length !== heights.length) addPit._used = new Uint8Array(heights.length);
const used = addPit._used;
used.fill(0);
let limit = 0,
start;
let h = lim(getNumberInRange(height));
do {
const x = getPointInRange(rangeX, graphWidth);
const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] < 20 && limit < 50);
const queue = [start];
for (let qi = 0; qi < queue.length; qi++) {
const q = queue[qi];
h = h ** blobPower * (Math.random() * 0.2 + 0.9);
if (h < 1) return;
const neibs = grid.cells.c[q];
for (let k = 0; k < neibs.length; k++) {
const c = neibs[k];
if (used[c]) continue;
heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
used[c] = 1;
queue.push(c);
}
}
}
};
// fromCell, toCell are options cell ids
const addRange = (count, height, rangeX, rangeY, startCell, endCell) => {
count = getNumberInRange(count);
while (count > 0) {
addOneRange();
count--;
}
function addOneRange() {
if (!addRange._used || addRange._used.length !== heights.length) addRange._used = new Uint8Array(heights.length);
const used = addRange._used;
used.fill(0);
let h = lim(getNumberInRange(height));
if (rangeX && rangeY) {
// find start and end points
const startX = getPointInRange(rangeX, graphWidth);
const startY = getPointInRange(rangeY, graphHeight);
let dist = 0,
limit = 0,
endX,
endY;
do {
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
startCell = findGridCell(startX, startY, grid);
endCell = findGridCell(endX, endY, grid);
}
let range = getRange(startCell, endCell);
// get main ridge
function getRange(cur, end) {
const range = [cur];
const p = grid.points;
used[cur] = 1;
while (cur !== end) {
let min = Infinity;
grid.cells.c[cur].forEach(function (e) {
if (used[e]) return;
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.85) diff = diff / 2;
if (diff < min) {
min = diff;
cur = e;
}
});
if (min === Infinity) return range;
range.push(cur);
used[cur] = 1;
}
return range;
}
// add height to ridge and cells around
let queue = range.slice();
let i = 0;
while (queue.length) {
const frontier = queue.slice();
queue.length = 0; i++;
for (let fi = 0; fi < frontier.length; fi++) {
const idx = frontier[fi];
heights[idx] = lim(heights[idx] + h * (Math.random() * 0.3 + 0.85));
}
h = h ** linePower - 1;
if (h < 2) break;
for (let fi = 0; fi < frontier.length; fi++) {
const f = frontier[fi];
const neibs = grid.cells.c[f];
for (let k = 0; k < neibs.length; k++) {
const idx = neibs[k];
if (!used[idx]) { queue.push(idx); used[idx] = 1; }
}
}
}
// generate prominences
for (let d = 0; d < range.length; d++) {
if (d % 6 !== 0) continue;
let cur = range[d];
for (let l = 0; l < i; l++) {
const nbrs = grid.cells.c[cur];
let minIdx = nbrs[0];
let minVal = heights[minIdx];
for (let k = 1; k < nbrs.length; k++) {
const idk = nbrs[k];
const hv = heights[idk];
if (hv < minVal) { minVal = hv; minIdx = idk; }
}
heights[minIdx] = (heights[cur] * 2 + heights[minIdx]) / 3;
cur = minIdx;
}
}
}
};
const addTrough = (count, height, rangeX, rangeY, startCell, endCell) => {
count = getNumberInRange(count);
while (count > 0) {
addOneTrough();
count--;
}
function addOneTrough() {
if (!addTrough._used || addTrough._used.length !== heights.length) addTrough._used = new Uint8Array(heights.length);
const used = addTrough._used;
used.fill(0);
let h = lim(getNumberInRange(height));
if (rangeX && rangeY) {
// find start and end points
let limit = 0,
startX,
startY,
dist = 0,
endX,
endY;
do {
startX = getPointInRange(rangeX, graphWidth);
startY = getPointInRange(rangeY, graphHeight);
startCell = findGridCell(startX, startY, grid);
limit++;
} while (heights[startCell] < 20 && limit < 50);
limit = 0;
do {
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
endY = Math.random() * graphHeight * 0.7 + graphHeight * 0.15;
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
endCell = findGridCell(endX, endY, grid);
}
let range = getRange(startCell, endCell);
// get main ridge
function getRange(cur, end) {
const range = [cur];
const p = grid.points;
used[cur] = 1;
while (cur !== end) {
let min = Infinity;
grid.cells.c[cur].forEach(function (e) {
if (used[e]) return;
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.8) diff = diff / 2;
if (diff < min) {
min = diff;
cur = e;
}
});
if (min === Infinity) return range;
range.push(cur);
used[cur] = 1;
}
return range;
}
// add height to ridge and cells around
let queue = range.slice();
let i = 0;
while (queue.length) {
const frontier = queue.slice();
queue.length = 0; i++;
for (let fi = 0; fi < frontier.length; fi++) {
const idx = frontier[fi];
heights[idx] = lim(heights[idx] - h * (Math.random() * 0.3 + 0.85));
}
h = h ** linePower - 1;
if (h < 2) break;
for (let fi = 0; fi < frontier.length; fi++) {
const f = frontier[fi];
const neibs = grid.cells.c[f];
for (let k = 0; k < neibs.length; k++) {
const idx = neibs[k];
if (!used[idx]) { queue.push(idx); used[idx] = 1; }
}
}
}
// generate prominences
for (let d = 0; d < range.length; d++) {
if (d % 6 !== 0) continue;
let cur = range[d];
for (let l = 0; l < i; l++) {
const nbrs = grid.cells.c[cur];
let minIdx = nbrs[0];
let minVal = heights[minIdx];
for (let k = 1; k < nbrs.length; k++) {
const idk = nbrs[k];
const hv = heights[idk];
if (hv < minVal) { minVal = hv; minIdx = idk; }
}
heights[minIdx] = (heights[cur] * 2 + heights[minIdx]) / 3;
cur = minIdx;
}
}
}
};
const addStrait = (width, direction = "vertical") => {
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
if (width < 1 && P(width)) return;
const used = new Uint8Array(heights.length);
const vert = direction === "vertical";
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
const endX = vert
? Math.floor(graphWidth - startX - graphWidth * 0.1 + Math.random() * graphWidth * 0.2)
: graphWidth - 5;
const endY = vert
? graphHeight - 5
: Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
const start = findGridCell(startX, startY, grid);
const end = findGridCell(endX, endY, grid);
let range = getRange(start, end);
const query = [];
function getRange(cur, end) {
const range = [];
const p = grid.points;
while (cur !== end) {
let min = Infinity;
grid.cells.c[cur].forEach(function (e) {
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.8) diff = diff / 2;
if (diff < min) {
min = diff;
cur = e;
}
});
range.push(cur);
}
return range;
}
const step = 0.1 / width;
while (width > 0) {
const exp = 0.9 - step * width;
range.forEach(function (r) {
grid.cells.c[r].forEach(function (e) {
if (used[e]) return;
used[e] = 1;
query.push(e);
heights[e] **= exp;
if (heights[e] > 100) heights[e] = 5;
});
});
range = query.slice();
width--;
}
};
const modify = (range, add, mult, power) => {
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
const isLand = min === 20;
for (let i = 0; i < heights.length; i++) {
let h = heights[i];
if (h < min || h > max) continue;
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
heights[i] = lim(h);
}
};
const smooth = (fr = 2, add = 0) => {
for (let i = 0; i < heights.length; i++) {
const h = heights[i];
const nbrs = grid.cells.c[i];
let sum = h;
for (let k = 0; k < nbrs.length; k++) sum += heights[nbrs[k]];
const mean = sum / (nbrs.length + 1);
heights[i] = fr === 1 ? mean + add : lim((h * (fr - 1) + mean + add) / fr);
}
};
const mask = (power = 1) => {
const fr = power ? Math.abs(power) : 1;
for (let i = 0; i < heights.length; i++) {
const h = heights[i];
const [x, y] = grid.points[i];
const nx = (2 * x) / graphWidth - 1; // [-1, 1]
const ny = (2 * y) / graphHeight - 1; // [-1, 1]
let distance = (1 - nx * nx) * (1 - ny * ny);
if (power < 0) distance = 1 - distance;
const masked = h * distance;
heights[i] = lim((h * (fr - 1) + masked) / fr);
}
};
const invert = (count, axes) => {
if (!P(count)) return;
const invertX = axes !== "y";
const invertY = axes !== "x";
const {cellsX, cellsY} = grid;
const inverted = heights.map((h, i) => {
const x = i % cellsX;
const y = Math.floor(i / cellsX);
const nx = invertX ? cellsX - x - 1 : x;
const ny = invertY ? cellsY - y - 1 : y;
const invertedI = nx + ny * cellsX;
return heights[invertedI];
});
heights = inverted;
};
function getPointInRange(range, length) {
if (typeof range !== "string") {
ERROR && console.error("Range should be a string");
return;
}
const min = range.split("-")[0] / 100 || 0;
const max = range.split("-")[1] / 100 || min;
return rand(min * length, max * length);
}
function getHeightsFromImageData(imageData) {
for (let i = 0; i < heights.length; i++) {
const lightness = imageData[i * 4] / 255;
const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
heights[i] = minmax(Math.floor(powered * 100), 0, 100);
}
}
return {
setGraph,
getHeights,
generate,
fromTemplate,
fromPrecreated,
addHill,
addRange,
addTrough,
addStrait,
addPit,
smooth,
modify,
mask,
invert
};
})();