draw images from heightmap

This commit is contained in:
Azgaar 2022-05-24 00:55:03 +03:00
parent c394534246
commit 27a045b709
3 changed files with 103 additions and 34 deletions

View file

@ -41,6 +41,8 @@ const heightmaps = [
{id: "world-from-pacific", name: "World from Pacific"} {id: "world-from-pacific", name: "World from Pacific"}
]; ];
let seed = Math.floor(Math.random() * 1e9);
appendStyleSheet(); appendStyleSheet();
insertEditorHtml(); insertEditorHtml();
addListeners(); addListeners();
@ -109,11 +111,18 @@ function appendStyleSheet() {
color: #000; color: #000;
} }
.heightmap-selection article > div > span.icon-cw:active {
color: #666;
}
.heightmap-selection article > img { .heightmap-selection article > img {
width: 100%; width: 100%;
aspect-ratio: 16/9; aspect-ratio: 16/9;
border-radius: 8px; border-radius: 8px;
object-fit: cover; object-fit: cover;
}
img.heightmap-selection_precreated {
filter: contrast(1.3); filter: contrast(1.3);
} }
`; `;
@ -126,8 +135,12 @@ function appendStyleSheet() {
function insertEditorHtml() { function insertEditorHtml() {
const templatesHtml = templates const templatesHtml = templates
.map(({id, name}) => { .map(({id, name}) => {
return /* html */ `<article data-id="${id}"> Math.random = aleaPRNG(seed);
<img src="../../heightmaps/europe.png" alt="${name}" loading="lazy" /> const heights = HeightmapGenerator.fromTemplate(id);
const dataUrl = drawHeights(heights);
return /* html */ `<article data-id="${id}" data-seed="${seed}">
<img src="${dataUrl}" alt="${name}" />
<div> <div>
${name} ${name}
<span data-tip="Regenerate preview" class="icon-cw"></span> <span data-tip="Regenerate preview" class="icon-cw"></span>
@ -139,7 +152,7 @@ function insertEditorHtml() {
const heightmapsHtml = heightmaps const heightmapsHtml = heightmaps
.map(({id, name}) => { .map(({id, name}) => {
return /* html */ `<article data-id="${id}"> return /* html */ `<article data-id="${id}">
<img src="../../heightmaps/${id}.png" alt="${name}" loading="lazy" /> <img src="../../heightmaps/${id}.png" alt="${name}" class="heightmap-selection_precreated" />
<div>${name}</div> <div>${name}</div>
</article>`; </article>`;
}) })
@ -168,7 +181,11 @@ function insertEditorHtml() {
function addListeners() { function addListeners() {
byId("heightmapSelection").on("click", event => { byId("heightmapSelection").on("click", event => {
const article = event.target.closest("#heightmapSelection article"); const article = event.target.closest("#heightmapSelection article");
if (article) setSelected(article.dataset.id); if (!article) return;
const id = article.dataset.id;
if (event.target.matches("span.icon-cw")) regeneratePreview(article, id);
else setSelected(id);
}); });
} }
@ -181,3 +198,33 @@ function setSelected(id) {
$heightmapSelection.querySelector(".selected")?.classList?.remove("selected"); $heightmapSelection.querySelector(".selected")?.classList?.remove("selected");
$heightmapSelection.querySelector(`[data-id="${id}"]`)?.classList?.add("selected"); $heightmapSelection.querySelector(`[data-id="${id}"]`)?.classList?.add("selected");
} }
function drawHeights(heights) {
const canvas = document.createElement("canvas");
canvas.width = grid.cellsX;
canvas.height = grid.cellsY;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(grid.cellsX, grid.cellsY);
heights.forEach((height, i) => {
const h = height < 20 ? Math.max(height / 1.5, 0) : height;
const v = (h / 100) * 255;
imageData.data[i * 4] = v;
imageData.data[i * 4 + 1] = v;
imageData.data[i * 4 + 2] = v;
imageData.data[i * 4 + 3] = 255;
});
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL("image/png");
}
function regeneratePreview(article, id) {
seed = Math.floor(Math.random() * 1e9);
article.dataset.seed = seed;
Math.random = aleaPRNG(seed);
const heights = HeightmapGenerator.fromTemplate(id);
const dataUrl = drawHeights(heights);
article.querySelector("img").src = dataUrl;
}

View file

@ -1,12 +1,12 @@
"use strict"; "use strict";
window.HeightmapGenerator = (function () { window.HeightmapGenerator = (function () {
let cells, p; let cells, p, heights;
const generate = async function () { const generate = async function () {
cells = grid.cells; cells = grid.cells;
p = grid.points; p = grid.points;
cells.h = new Uint8Array(grid.points.length); heights = new Uint8Array(grid.points.length);
const input = document.getElementById("templateInput"); const input = document.getElementById("templateInput");
const selectedId = input.selectedIndex >= 0 ? input.selectedIndex : 0; const selectedId = input.selectedIndex >= 0 ? input.selectedIndex : 0;
@ -32,6 +32,9 @@ window.HeightmapGenerator = (function () {
assignColorsToHeight(imageData.data); assignColorsToHeight(imageData.data);
canvas.remove(); canvas.remove();
img.remove(); img.remove();
cells.h = heights;
heights = null;
TIME && console.timeEnd("defineHeightmap"); TIME && console.timeEnd("defineHeightmap");
resolve(); resolve();
}; };
@ -52,9 +55,28 @@ window.HeightmapGenerator = (function () {
addStep(...elements); addStep(...elements);
} }
cells.h = heights;
heights = null;
TIME && console.timeEnd("generateHeightmap"); TIME && console.timeEnd("generateHeightmap");
}; };
const fromTemplate = template => {
const templateString = HeightmapTemplates[template];
const steps = templateString.split("\n");
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`);
heights = new Uint8Array(grid.points.length);
for (const step of steps) {
const elements = step.trim().split(" ");
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${template}. Step: ${elements}`);
addStep(...elements);
}
return heights;
};
function addStep(a1, a2, a3, a4, a5) { function addStep(a1, a2, a3, a4, a5) {
if (a1 === "Hill") return addHill(a2, a3, a4, a5); if (a1 === "Hill") return addHill(a2, a3, a4, a5);
if (a1 === "Pit") return addPit(a2, a3, a4, a5); if (a1 === "Pit") return addPit(a2, a3, a4, a5);
@ -110,7 +132,7 @@ window.HeightmapGenerator = (function () {
} }
function addOneHill() { function addOneHill() {
const change = new Uint8Array(cells.h.length); const change = new Uint8Array(heights.length);
let limit = 0; let limit = 0;
let start; let start;
let h = lim(getNumberInRange(height)); let h = lim(getNumberInRange(height));
@ -120,7 +142,7 @@ window.HeightmapGenerator = (function () {
const y = getPointInRange(rangeY, graphHeight); const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y); start = findGridCell(x, y);
limit++; limit++;
} while (cells.h[start] + h > 90 && limit < 50); } while (heights[start] + h > 90 && limit < 50);
change[start] = h; change[start] = h;
const queue = [start]; const queue = [start];
@ -134,7 +156,7 @@ window.HeightmapGenerator = (function () {
} }
} }
cells.h = cells.h.map((h, i) => lim(h + change[i])); heights = heights.map((h, i) => lim(h + change[i]));
} }
}; };
@ -146,7 +168,7 @@ window.HeightmapGenerator = (function () {
} }
function addOnePit() { function addOnePit() {
const used = new Uint8Array(cells.h.length); const used = new Uint8Array(heights.length);
let limit = 0, let limit = 0,
start; start;
let h = lim(getNumberInRange(height)); let h = lim(getNumberInRange(height));
@ -156,7 +178,7 @@ window.HeightmapGenerator = (function () {
const y = getPointInRange(rangeY, graphHeight); const y = getPointInRange(rangeY, graphHeight);
start = findGridCell(x, y); start = findGridCell(x, y);
limit++; limit++;
} while (cells.h[start] < 20 && limit < 50); } while (heights[start] < 20 && limit < 50);
const queue = [start]; const queue = [start];
while (queue.length) { while (queue.length) {
@ -166,7 +188,7 @@ window.HeightmapGenerator = (function () {
cells.c[q].forEach(function (c, i) { cells.c[q].forEach(function (c, i) {
if (used[c]) return; if (used[c]) return;
cells.h[c] = lim(cells.h[c] - h * (Math.random() * 0.2 + 0.9)); heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
used[c] = 1; used[c] = 1;
queue.push(c); queue.push(c);
}); });
@ -183,7 +205,7 @@ window.HeightmapGenerator = (function () {
} }
function addOneRange() { function addOneRange() {
const used = new Uint8Array(cells.h.length); const used = new Uint8Array(heights.length);
let h = lim(getNumberInRange(height)); let h = lim(getNumberInRange(height));
// find start and end points // find start and end points
@ -234,7 +256,7 @@ window.HeightmapGenerator = (function () {
const frontier = queue.slice(); const frontier = queue.slice();
(queue = []), i++; (queue = []), i++;
frontier.forEach(i => { frontier.forEach(i => {
cells.h[i] = lim(cells.h[i] + h * (Math.random() * 0.3 + 0.85)); heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85));
}); });
h = h ** power - 1; h = h ** power - 1;
if (h < 2) break; if (h < 2) break;
@ -252,8 +274,8 @@ window.HeightmapGenerator = (function () {
range.forEach((cur, d) => { range.forEach((cur, d) => {
if (d % 6 !== 0) return; if (d % 6 !== 0) return;
for (const l of d3.range(i)) { for (const l of d3.range(i)) {
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3; heights[min] = (heights[cur] * 2 + heights[min]) / 3;
cur = min; cur = min;
} }
}); });
@ -269,7 +291,7 @@ window.HeightmapGenerator = (function () {
} }
function addOneTrough() { function addOneTrough() {
const used = new Uint8Array(cells.h.length); const used = new Uint8Array(heights.length);
let h = lim(getNumberInRange(height)); let h = lim(getNumberInRange(height));
// find start and end points // find start and end points
@ -285,7 +307,7 @@ window.HeightmapGenerator = (function () {
startY = getPointInRange(rangeY, graphHeight); startY = getPointInRange(rangeY, graphHeight);
start = findGridCell(startX, startY); start = findGridCell(startX, startY);
limit++; limit++;
} while (cells.h[start] < 20 && limit < 50); } while (heights[start] < 20 && limit < 50);
limit = 0; limit = 0;
do { do {
@ -328,7 +350,7 @@ window.HeightmapGenerator = (function () {
const frontier = queue.slice(); const frontier = queue.slice();
(queue = []), i++; (queue = []), i++;
frontier.forEach(i => { frontier.forEach(i => {
cells.h[i] = lim(cells.h[i] - h * (Math.random() * 0.3 + 0.85)); heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85));
}); });
h = h ** power - 1; h = h ** power - 1;
if (h < 2) break; if (h < 2) break;
@ -346,9 +368,9 @@ window.HeightmapGenerator = (function () {
range.forEach((cur, d) => { range.forEach((cur, d) => {
if (d % 6 !== 0) return; if (d % 6 !== 0) return;
for (const l of d3.range(i)) { for (const l of d3.range(i)) {
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1); //debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3; heights[min] = (heights[cur] * 2 + heights[min]) / 3;
cur = min; cur = min;
} }
}); });
@ -358,7 +380,7 @@ window.HeightmapGenerator = (function () {
const addStrait = (width, direction = "vertical") => { const addStrait = (width, direction = "vertical") => {
width = Math.min(getNumberInRange(width), grid.cellsX / 3); width = Math.min(getNumberInRange(width), grid.cellsX / 3);
if (width < 1 && P(width)) return; if (width < 1 && P(width)) return;
const used = new Uint8Array(cells.h.length); const used = new Uint8Array(heights.length);
const vert = direction === "vertical"; const vert = direction === "vertical";
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5; 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 startY = vert ? 5 : Math.floor(Math.random() * graphHeight * 0.4 + graphHeight * 0.3);
@ -398,8 +420,8 @@ window.HeightmapGenerator = (function () {
if (used[e]) return; if (used[e]) return;
used[e] = 1; used[e] = 1;
query.push(e); query.push(e);
cells.h[e] **= exp; heights[e] **= exp;
if (cells.h[e] > 100) cells.h[e] = 5; if (heights[e] > 100) heights[e] = 5;
}); });
}); });
range = query.slice(); range = query.slice();
@ -413,7 +435,7 @@ window.HeightmapGenerator = (function () {
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1]; const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
const isLand = min === 20; const isLand = min === 20;
grid.cells.h = grid.cells.h.map(h => { heights = heights.map(h => {
if (h < min || h > max) return h; if (h < min || h > max) return h;
if (add) h = isLand ? Math.max(h + add, 20) : h + add; if (add) h = isLand ? Math.max(h + add, 20) : h + add;
@ -424,9 +446,9 @@ window.HeightmapGenerator = (function () {
}; };
const smooth = (fr = 2, add = 0) => { const smooth = (fr = 2, add = 0) => {
cells.h = cells.h.map((h, i) => { heights = heights.map((h, i) => {
const a = [h]; const a = [h];
cells.c[i].forEach(c => a.push(cells.h[c])); cells.c[i].forEach(c => a.push(heights[c]));
if (fr === 1) return d3.mean(a) + add; if (fr === 1) return d3.mean(a) + add;
return lim((h * (fr - 1) + d3.mean(a) + add) / fr); return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
}); });
@ -435,7 +457,7 @@ window.HeightmapGenerator = (function () {
const mask = (power = 1) => { const mask = (power = 1) => {
const fr = power ? Math.abs(power) : 1; const fr = power ? Math.abs(power) : 1;
cells.h = cells.h.map((h, i) => { heights = heights.map((h, i) => {
const [x, y] = p[i]; const [x, y] = p[i];
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
@ -453,17 +475,17 @@ window.HeightmapGenerator = (function () {
const invertY = axes !== "x"; const invertY = axes !== "x";
const {cellsX, cellsY} = grid; const {cellsX, cellsY} = grid;
const inverted = cells.h.map((h, i) => { const inverted = heights.map((h, i) => {
const x = i % cellsX; const x = i % cellsX;
const y = Math.floor(i / cellsX); const y = Math.floor(i / cellsX);
const nx = invertX ? cellsX - x - 1 : x; const nx = invertX ? cellsX - x - 1 : x;
const ny = invertY ? cellsY - y - 1 : y; const ny = invertY ? cellsY - y - 1 : y;
const invertedI = nx + ny * cellsX; const invertedI = nx + ny * cellsX;
return cells.h[invertedI]; return heights[invertedI];
}); });
cells.h = inverted; heights = inverted;
}; };
function getPointInRange(range, length) { function getPointInRange(range, length) {
@ -481,9 +503,9 @@ window.HeightmapGenerator = (function () {
for (let i = 0; i < cells.i.length; i++) { for (let i = 0; i < cells.i.length; i++) {
const lightness = imageData[i * 4] / 255; const lightness = imageData[i * 4] / 255;
const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8; const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
cells.h[i] = minmax(Math.floor(powered * 100), 0, 100); heights[i] = minmax(Math.floor(powered * 100), 0, 100);
} }
} }
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify, mask, invert}; return {generate, fromTemplate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify, mask, invert};
})(); })();

View file

@ -1360,7 +1360,7 @@ function editHeightmap() {
const imageData = ctx.createImageData(grid.cellsX, grid.cellsY); const imageData = ctx.createImageData(grid.cellsX, grid.cellsY);
grid.cells.h.forEach((height, i) => { grid.cells.h.forEach((height, i) => {
let h = height < 20 ? Math.max(height / 1.5, 0) : height; const h = height < 20 ? Math.max(height / 1.5, 0) : height;
const v = (h / 100) * 255; const v = (h / 100) * 255;
imageData.data[i * 4] = v; imageData.data[i * 4] = v;
imageData.data[i * 4 + 1] = v; imageData.data[i * 4 + 1] = v;