Stop thin lines between tiles of a nearest tilemap at odd zooms
game-maker / gate (push) Successful in 2m55s
game-maker / suite (push) Successful in 9m21s

At zoom levels that do not land quarter edges on whole screen pixels,
a tilemap whose texture uses Nearest showed one-pixel lines in the
wrong colour. The float uv of a pixel centre right at a quad edge
could round onto the first texel of the neighbouring quarter in the
sheet, which is usually a different tile. Terrain sheets showed it
most, because every tile is four quads there.

- Nearest now pulls the uvs 0.02 texel inside each quarter. That is
  far below half a texel, so every picked texel stays the same and
  pixel art keeps its size.
- Linear keeps its half-texel inset.
- The first draft used 0.01. The user raised it to 0.02 after
  checking the result in the browser.

Tests:
- ./check.sh: 156 files, 2299 tests, CHECK EXIT 0 (before 2298).
- New unit test: every nearest uv sits strictly inside its quarter,
  by less than 0.05 texel. Two older tests now expect the inset.
- Counter-check: with no inset for nearest the new test and the
  inset test fail.
- No e2e check. The user checked the result in the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-24 07:26:46 +02:00
1 parent 9ef360ba1b
commit aef1eba917
2 files changed
+42 -5

No files matched your search

+6 -2
View File
@@ -52,8 +52,12 @@ export function tileMesh(
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;
// Linear sampling needs half a texel so the blend never reaches the neighbour. Nearest needs only
// a sliver: at odd zooms a pixel centre on the quad edge can round onto the neighbour's first
// texel and draw a line. 1/50 texel keeps every picked texel the same.
const inset = sheet.smooth ? 0.5 : 0.02;
const insetX = inset / sheet.textureWidth;
const insetY = inset / sheet.textureHeight;
let v = 0;
let i = 0;
for (const drawn of tiles) {
+36 -3
View File
@@ -500,8 +500,10 @@ describe('the mesh a layer becomes', () => {
expect([positions[0], positions[1]]).toEqual([48, 0]);
expect([positions[2], positions[3]]).toEqual([72, 0]);
// Quadrant (0, 0) of a 96 × 144 sheet cut into 24 px quarters is the first quarter of the uvs.
expect([uvs[0], uvs[1]]).toEqual([0, 0]);
expect([uvs[2], uvs[3]]).toEqual([24 / 96, 0]);
expect(uvs[0]).toBeCloseTo(0, 3);
expect(uvs[1]).toBeCloseTo(0, 3);
expect(uvs[2]).toBeCloseTo(24 / 96, 3);
expect(uvs[3]).toBeCloseTo(0, 3);
});
it('pulls the uvs half a texel in when the picture is sampled smoothly', () => {
@@ -525,9 +527,40 @@ describe('the mesh a layer becomes', () => {
];
const hard = tileMesh(tiles, 48, { x: 0, y: 0 }, { ...sheet, smooth: false });
const soft = tileMesh(tiles, 48, { x: 0, y: 0 }, { ...sheet, smooth: true });
expect(hard.uvs[0]).toBe(0);
expect(hard.uvs[0]).toBeCloseTo(0.02 / 96, 6);
expect(soft.uvs[0]).toBeCloseTo(0.5 / 96, 6);
// The positions are untouched: only what is sampled moves, never where it lands.
expect([...soft.positions]).toEqual([...hard.positions]);
});
it('keeps nearest uvs a sliver inside the quarter so odd zooms never pick the neighbour', () => {
const tiles = [
{
x: 0,
y: 0,
tileId: 'a',
quadrants: [
{ x: 1, y: 1 },
{ x: 1, y: 1 },
{ x: 1, y: 1 },
{ x: 1, y: 1 },
] as [
{ x: number; y: number },
{ x: number; y: number },
{ x: number; y: number },
{ x: number; y: number },
],
},
];
const { uvs } = tileMesh(tiles, 48, { x: 0, y: 0 }, { ...sheet, smooth: false });
// Quarter (1, 1) spans texels 24..48 on both axes. Every corner sits strictly inside it,
// but by far less than half a texel, so the texel picked at any pixel centre stays the same.
const us = [uvs[0]!, uvs[2]!, uvs[4]!, uvs[6]!].map((u) => u * 96);
const vs = [uvs[1]!, uvs[3]!, uvs[5]!, uvs[7]!].map((v) => v * 144);
for (const t of [...us, ...vs]) {
expect(t).toBeGreaterThan(24);
expect(t).toBeLessThan(48);
expect(Math.min(t - 24, 48 - t)).toBeLessThan(0.05);
}
});
});