99 lines
3.6 KiB
TypeScript
99 lines
3.6 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|