Files
feuerwerk/src/viewport/tilemapMesh.ts
T
2026-09-18 08:01:07 +02:00

99 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The geometry a painted layer becomes: one mesh per tile instead of one sprite per cell, so a
* map of a thousand cells costs as many draw calls as it has tiles on that layer (#128). Kept a
* plain function of numbers – no Pixi, no project – so the quarter that lands in a corner can be
* checked without a browser.
*/
import type { FrameRect } from '../core/model/texture';
import type { TileDraw } from '../core/model/tilemap';
/** Where a tile's quarters are cut from, in the pixels of the texture the mesh wears. */
export interface TileSheet {
/** Side of one quarter tile in image pixels. */
quad: number;
/** The block inside that texture; the quadrant positions count from its top-left corner. */
block: FrameRect;
textureWidth: number;
textureHeight: number;
/**
* Whether the texture is sampled smoothly. It is, the uvs are pulled half a texel inwards: a
* linear sample right on the border of a quarter otherwise reaches into the one next to it and
* draws a seam along every tile.
*/
smooth: boolean;
}
export interface MeshArrays {
positions: Float32Array;
uvs: Float32Array;
indices: Uint32Array;
}
/** The corners of one quad, in reading order – the order `tileQuadrants` uses for the four quarters. */
const CORNER_OFFSETS = [
[0, 0],
[1, 0],
[0, 1],
[1, 1],
] as const;
/**
* `origin` is where cell (0, 0) starts in the tilemap's own frame, `tile` the side of a drawn cell
* in world units. Every cell contributes four quads – its four quarters – so a tile whose corners
* come from four corners of the block is one thing to draw, not four.
*/
export function tileMesh(
tiles: readonly TileDraw[],
tile: number,
origin: { x: number; y: number },
sheet: TileSheet,
): MeshArrays {
const positions = new Float32Array(tiles.length * 4 * 4 * 2);
const uvs = new Float32Array(positions.length);
const indices = new Uint32Array(tiles.length * 4 * 6);
const half = tile / 2;
const insetX = sheet.smooth ? 0.5 / sheet.textureWidth : 0;
const insetY = sheet.smooth ? 0.5 / sheet.textureHeight : 0;
let v = 0;
let i = 0;
for (const drawn of tiles) {
const left = origin.x + drawn.x * tile;
const top = origin.y + drawn.y * tile;
drawn.quadrants.forEach((position, corner) => {
const x = left + (corner % 2) * half;
const y = top + Math.floor(corner / 2) * half;
const u0 = (sheet.block.x + position.x * sheet.quad) / sheet.textureWidth + insetX;
const v0 = (sheet.block.y + position.y * sheet.quad) / sheet.textureHeight + insetY;
const u1 = (sheet.block.x + (position.x + 1) * sheet.quad) / sheet.textureWidth - insetX;
const v1 = (sheet.block.y + (position.y + 1) * sheet.quad) / sheet.textureHeight - insetY;
const base = v / 2;
for (const [dx, dy] of CORNER_OFFSETS) {
positions[v] = x + dx * half;
uvs[v] = dx ? u1 : u0;
v++;
positions[v] = y + dy * half;
uvs[v] = dy ? v1 : v0;
v++;
}
indices[i++] = base;
indices[i++] = base + 1;
indices[i++] = base + 2;
indices[i++] = base + 2;
indices[i++] = base + 1;
indices[i++] = base + 3;
});
}
return { positions, uvs, indices };
}
/** The cells of one layer grouped by tile – one group is one mesh, because it is one texture. */
export function groupByTile(tiles: readonly TileDraw[]): Map<string, TileDraw[]> {
const groups = new Map<string, TileDraw[]>();
for (const drawn of tiles) {
const group = groups.get(drawn.tileId);
if (group) group.push(drawn);
else groups.set(drawn.tileId, [drawn]);
}
return groups;
}