refactor: Update relief icon handling and streamline texture atlas integration

This commit is contained in:
Azgaar 2026-03-12 21:46:22 +01:00
parent b17b417d1b
commit 256f36015b
5 changed files with 46 additions and 58 deletions

View file

@ -1122,10 +1122,11 @@ export function resolveVersionConflicts(mapVersion) {
terrainEl.querySelectorAll("use").forEach(u => { terrainEl.querySelectorAll("use").forEach(u => {
const href = u.getAttribute("href") || u.getAttribute("xlink:href") || ""; const href = u.getAttribute("href") || u.getAttribute("xlink:href") || "";
if (!href) return; if (!href) return;
const icon = href.replace("#", "");
const x = +u.getAttribute("x"); const x = +u.getAttribute("x");
const y = +u.getAttribute("y"); const y = +u.getAttribute("y");
const s = +u.getAttribute("width"); const s = +u.getAttribute("width");
relief.push({i: relief.length, href, x, y, s}); relief.push({i: relief.length, icon, x, y, s});
}); });
terrainEl.innerHTML = ""; terrainEl.innerHTML = "";
pack.relief = relief; pack.relief = relief;

View file

@ -1,11 +1,12 @@
function createAtlas(ids: string[]) { function createAtlas(set: string, ids: string[]) {
const url = `images/relief/${set}.png`;
const n = ids.length || 1; const n = ids.length || 1;
const cols = Math.ceil(Math.sqrt(n)); const cols = Math.ceil(Math.sqrt(n));
return { ids, cols, rows: Math.ceil(n / cols) }; return { url, ids, cols, rows: Math.ceil(n / cols) };
} }
export const RELIEF_SYMBOLS = { export const RELIEF_ATLASES = {
simple: createAtlas([ simple: createAtlas("simple", [
"relief-mount-1", "relief-mount-1",
"relief-hill-1", "relief-hill-1",
"relief-conifer-1", "relief-conifer-1",
@ -16,7 +17,7 @@ export const RELIEF_SYMBOLS = {
"relief-swamp-1", "relief-swamp-1",
"relief-dune-1", "relief-dune-1",
]), ]),
gray: createAtlas([ gray: createAtlas("gray", [
"relief-mount-2-bw", "relief-mount-2-bw",
"relief-mount-3-bw", "relief-mount-3-bw",
"relief-mount-4-bw", "relief-mount-4-bw",
@ -52,7 +53,7 @@ export const RELIEF_SYMBOLS = {
"relief-deciduous-2-bw", "relief-deciduous-2-bw",
"relief-deciduous-3-bw", "relief-deciduous-3-bw",
]), ]),
colored: createAtlas([ colored: createAtlas("colored", [
"relief-mount-2", "relief-mount-2",
"relief-mount-3", "relief-mount-3",
"relief-mount-4", "relief-mount-4",
@ -90,13 +91,6 @@ export const RELIEF_SYMBOLS = {
]), ]),
}; };
export const RELIEF_ATLASES = Object.fromEntries(
Object.entries(RELIEF_SYMBOLS).map(([set, { cols, rows }]) => [
set,
{ url: `images/relief/${set}.png`, cols, rows },
]),
);
export const VARIANT_RANGES: Record<string, [number, number]> = { export const VARIANT_RANGES: Record<string, [number, number]> = {
mount: [2, 7], mount: [2, 7],
mountSnow: [1, 6], mountSnow: [1, 6],

View file

@ -11,7 +11,7 @@ import {
export interface ReliefIcon { export interface ReliefIcon {
i: number; i: number;
href: string; // e.g. "#relief-mount-1" icon: string; // e.g. "relief-mount-1"
x: number; x: number;
y: number; y: number;
s: number; // size (width = height in map units) s: number; // size (width = height in map units)
@ -63,10 +63,10 @@ export function generateRelief(): ReliefIcon[] {
if (!polygonContains(polygon, [cx, cy])) continue; if (!polygonContains(polygon, [cx, cy])) continue;
let h = (4 + Math.random()) * size; let h = (4 + Math.random()) * size;
const icon = getBiomeIcon(i, biome); const icon = getBiomeIcon(i, biome);
if (icon === "#relief-grass-1") h *= 1.2; if (icon === "relief-grass-1") h *= 1.2;
reliefIcons.push({ reliefIcons.push({
i: reliefIcons.length, i: reliefIcons.length,
href: icon, icon,
x: rn(cx - h, 2), x: rn(cx - h, 2),
y: rn(cy - h, 2), y: rn(cy - h, 2),
s: rn(h * 2, 2), s: rn(h * 2, 2),
@ -85,7 +85,7 @@ export function generateRelief(): ReliefIcon[] {
if (!polygonContains(polygon, [cx, cy])) continue; if (!polygonContains(polygon, [cx, cy])) continue;
reliefIcons.push({ reliefIcons.push({
i: reliefIcons.length, i: reliefIcons.length,
href: icon, icon,
x: rn(cx - h, 2), x: rn(cx - h, 2),
y: rn(cy - h, 2), y: rn(cy - h, 2),
s: rn(h * 2, 2), s: rn(h * 2, 2),
@ -125,9 +125,9 @@ function getVariant(type: string): number {
} }
function getHref(type: string, set: string): string { function getHref(type: string, set: string): string {
if (set === "colored") return `#relief-${type}-${getVariant(type)}`; if (set === "colored") return `relief-${type}-${getVariant(type)}`;
if (set === "gray") return `#relief-${type}-${getVariant(type)}-bw`; if (set === "gray") return `relief-${type}-${getVariant(type)}-bw`;
return `#relief-${COLORED_TO_SIMPLE_MAP[type] ?? type}-1`; return `relief-${COLORED_TO_SIMPLE_MAP[type] ?? type}-1`;
} }
window.generateReliefIcons = generateRelief; window.generateReliefIcons = generateRelief;

View file

@ -14,16 +14,16 @@ import {
export interface AtlasConfig { export interface AtlasConfig {
url: string; url: string;
ids: string[];
cols: number; cols: number;
rows: number; rows: number;
} }
export interface AtlasQuad { export interface AtlasItem {
atlasId: string; icon: string;
x: number; x: number;
y: number; y: number;
s: number; s: number;
tileIndex: number;
} }
export class TextureAtlasLayer { export class TextureAtlasLayer {
@ -47,25 +47,30 @@ export class TextureAtlasLayer {
}); });
} }
draw(quads: AtlasQuad[]) { draw(items: AtlasItem[]) {
if (!this.group) return; if (!this.group) return;
this.disposeGroup(); this.disposeGroup();
const byAtlas = new Map<string, AtlasQuad[]>(); const byAtlas = new Map<string, { item: AtlasItem; tileIndex: number }[]>();
for (const q of quads) { for (const item of items) {
let arr = byAtlas.get(q.atlasId); for (const [atlasId, config] of Object.entries(this.atlases)) {
const tileIndex = config.ids.indexOf(item.icon);
if (tileIndex === -1) continue;
let arr = byAtlas.get(atlasId);
if (!arr) { if (!arr) {
arr = []; arr = [];
byAtlas.set(q.atlasId, arr); byAtlas.set(atlasId, arr);
}
arr.push({ item, tileIndex });
break;
} }
arr.push(q);
} }
for (const [atlasId, atlasQuads] of byAtlas) { for (const [atlasId, entries] of byAtlas) {
const texture = this.textureCache.get(atlasId); const texture = this.textureCache.get(atlasId);
const config = this.atlases[atlasId]; const config = this.atlases[atlasId];
if (!texture || !config) continue; if (!texture || !config) continue;
this.group.add(buildMesh(atlasQuads, config, texture)); this.group.add(buildMesh(entries, config, texture));
} }
WebGLLayer.rerender(); WebGLLayer.rerender();
} }
@ -113,20 +118,20 @@ export class TextureAtlasLayer {
} }
function buildMesh( function buildMesh(
quads: AtlasQuad[], entries: Array<{ item: AtlasItem; tileIndex: number }>,
atlas: AtlasConfig, atlas: AtlasConfig,
texture: Texture, texture: Texture,
): Mesh { ): Mesh {
const { cols, rows } = atlas; const { cols, rows } = atlas;
const positions = new Float32Array(quads.length * 4 * 3); const positions = new Float32Array(entries.length * 4 * 3);
const uvs = new Float32Array(quads.length * 4 * 2); const uvs = new Float32Array(entries.length * 4 * 2);
const indices = new Uint32Array(quads.length * 6); const indices = new Uint32Array(entries.length * 6);
let vi = 0, let vi = 0,
ii = 0; ii = 0;
for (const q of quads) { for (const { item: q, tileIndex } of entries) {
const col = q.tileIndex % cols; const col = tileIndex % cols;
const row = Math.floor(q.tileIndex / cols); const row = Math.floor(tileIndex / cols);
const u0 = col / cols, const u0 = col / cols,
u1 = (col + 1) / cols; u1 = (col + 1) / cols;
const v0 = row / rows, const v0 = row / rows,

View file

@ -1,16 +1,16 @@
import { RELIEF_ATLASES, RELIEF_SYMBOLS } from "../config/relief-config"; import { RELIEF_ATLASES } from "../config/relief-config";
import type { ReliefIcon } from "../modules/relief-generator"; import type { ReliefIcon } from "../modules/relief-generator";
import { generateRelief } from "../modules/relief-generator"; import { generateRelief } from "../modules/relief-generator";
import { TextureAtlasLayer } from "../modules/texture-atlas-layer"; import { TextureAtlasLayer } from "../modules/texture-atlas-layer";
import { byId } from "../utils"; import { byId } from "../utils";
const terrainLayer = new TextureAtlasLayer("terrain", RELIEF_ATLASES); const layer = new TextureAtlasLayer("terrain", RELIEF_ATLASES);
function drawSvg(icons: ReliefIcon[], parentEl: HTMLElement): void { function drawSvg(icons: ReliefIcon[], parentEl: HTMLElement): void {
parentEl.innerHTML = icons parentEl.innerHTML = icons
.map( .map(
(r) => (r) =>
`<use href="${r.href}" data-id="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`, `<use href="#${r.icon}" data-id="${r.i}" x="${r.x}" y="${r.y}" width="${r.s}" height="${r.s}"/>`,
) )
.join(""); .join("");
} }
@ -29,24 +29,12 @@ window.drawRelief = (
if (type === "svg") { if (type === "svg") {
drawSvg(icons, parentEl); drawSvg(icons, parentEl);
} else { } else {
terrainLayer.draw(resolveQuads(icons)); layer.draw(icons);
} }
}; };
function resolveQuads(icons: ReliefIcon[]) {
return icons.map((r) => {
const id = r.href.startsWith("#") ? r.href.slice(1) : r.href;
for (const [set, { ids }] of Object.entries(RELIEF_SYMBOLS)) {
const tileIndex = ids.indexOf(id);
if (tileIndex !== -1)
return { atlasId: set, x: r.x, y: r.y, s: r.s, tileIndex };
}
throw new Error(`Relief: unknown symbol href "${r.href}"`);
});
}
window.undrawRelief = () => { window.undrawRelief = () => {
terrainLayer.clear(); layer.clear();
const terrainEl = byId("terrain"); const terrainEl = byId("terrain");
if (terrainEl) terrainEl.innerHTML = ""; if (terrainEl) terrainEl.innerHTML = "";
}; };