Stop thin lines between tiles of a nearest tilemap at odd zooms
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:
1 parent
9ef360ba1b
commit
aef1eba917
2 files changed
+42
-5
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user