/** * 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 { const groups = new Map(); for (const drawn of tiles) { const group = groups.get(drawn.tileId); if (group) group.push(drawn); else groups.set(drawn.tileId, [drawn]); } return groups; }