Textures: Linear/Nearest labels; prove the filter pass does not resample pixel art (#68)
game-maker / suite (push) Successful in 8m13s
game-maker / suite (push) Successful in 8m13s
Dropdown labels are now Linear (default) and Nearest; the stored values linear/nearest are unchanged, old project files load as before. e2e 12c-ii measures in Play at camera zoom 1.37 (screen scale 1.434): a Nearest edge sampled across seven pixels is byte-identical with and without a neutral Adjust colors shader, and brightness 0.5 on the same path darkens, so the pass really runs there. New hook __gm.gameRect(id). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e99480b55
commit
06e1494f94
4 files changed
+63
-6
No files matched your search
@@ -40,7 +40,7 @@ const PASTE_SPARE = `(async () => {
|
||||
|
||||
export async function textures(t) {
|
||||
const { check, sleep, json, evaluate, shot, mutate, press } = t;
|
||||
const { api, click, clickText, doubleClick, locate, undo } = t;
|
||||
const { api, click, clickText, doubleClick, locate, undo, play, stop } = t;
|
||||
|
||||
// The previous step leaves a picker open and a text field focused, and paste is deliberately
|
||||
// ignored while an input has focus, so that Ctrl+V inside a field still edits text.
|
||||
@@ -170,7 +170,7 @@ export async function textures(t) {
|
||||
const hard = await sample('nearest');
|
||||
const pure = (c) => colourDistance(c, '#e63c3c') < 60 || colourDistance(c, '#3cc85a') < 60;
|
||||
check(
|
||||
'pixel-art filtering keeps the border between two quadrants hard',
|
||||
'nearest filtering keeps the border between two quadrants hard',
|
||||
hard.pixels.every(pure),
|
||||
JSON.stringify(hard),
|
||||
);
|
||||
@@ -180,11 +180,67 @@ export async function textures(t) {
|
||||
// a working switch drifts 425, a hung one 0, and 30 even if every channel were off by two.
|
||||
const drift = hard.pixels.reduce((sum, c, i) => sum + colourDistance(c, soft.pixels[i]), 0);
|
||||
check(
|
||||
'switching the filter back to smooth blends the border without a reload',
|
||||
'switching the filter back to linear blends the border without a reload',
|
||||
soft.pixels.some((c) => !pure(c)) && drift > 150,
|
||||
JSON.stringify({ drift, nearest: hard, linear: soft }),
|
||||
);
|
||||
await shot('33-texture-filter-smooth');
|
||||
|
||||
// 12c-ii. A shader is a filter pass: the sprite goes into an intermediate texture and back. At a
|
||||
// camera zoom that puts the picture on a fractional pixel grid, that copy must not resample the
|
||||
// nearest-filtered border. Measured in play, where the camera's zoom is the world scale; one play
|
||||
// per setting, since the play world is built from the project when it starts.
|
||||
await mutate(
|
||||
`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(texture.id)}).props.filter = 'nearest'`,
|
||||
);
|
||||
const placed = await json(
|
||||
"(() => { const o = __gm.state.activeScene.objects.find((o) => o.id === 'otex'); return { x: o.position.x, y: o.position.y }; })()",
|
||||
);
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === 'otex');
|
||||
o.position.x = 0; o.position.y = 0;
|
||||
__gm.state.project.assets.find((a) => a.id === 'cam').props.zoom = 1.37`);
|
||||
const played = async (patch) => {
|
||||
await mutate(
|
||||
`Object.assign(__gm.state.project.assets.find((a) => a.id === 'tex-sprite').props, ${JSON.stringify(patch)})`,
|
||||
);
|
||||
await play();
|
||||
await sleep(500);
|
||||
const rect = await json("__gm.gameRect('otex')");
|
||||
const y = Math.round(rect.y - rect.h * 0.25);
|
||||
const pixels = [];
|
||||
for (let dx = -3; dx <= 3; dx++)
|
||||
pixels.push(await json(`__gm.pixelAt(${Math.round(rect.x + dx)}, ${y})`));
|
||||
await stop();
|
||||
return { pixels, scale: rect.w / 320 };
|
||||
};
|
||||
const bare = await played({ shader: '' });
|
||||
const filtered = await played({ shader: 'adjust-colors' });
|
||||
const darkened = await played({ shader: 'adjust-colors', brightness: 0.5 });
|
||||
check(
|
||||
'at a fractional camera zoom the nearest border is still hard in play',
|
||||
Number.isInteger(bare.scale) === false && bare.pixels.every(pure),
|
||||
JSON.stringify(bare),
|
||||
);
|
||||
check(
|
||||
'a neutral shader draws the very same pixels there: the filter pass does not resample',
|
||||
JSON.stringify(filtered.pixels) === JSON.stringify(bare.pixels),
|
||||
JSON.stringify({ bare: bare.pixels, filtered: filtered.pixels }),
|
||||
);
|
||||
// Without this, "the same" could also mean the shader never ran on that path.
|
||||
check(
|
||||
'and the shader does run on that path: brightness 0.5 darkens every one of them',
|
||||
darkened.pixels.every((c, i) => colourDistance(c, bare.pixels[i]) > 100 && !pure(c)),
|
||||
JSON.stringify(darkened),
|
||||
);
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === 'otex');
|
||||
o.position.x = ${placed.x}; o.position.y = ${placed.y};
|
||||
__gm.state.project.assets.find((a) => a.id === 'cam').props.zoom = 1;
|
||||
const sprite = __gm.state.project.assets.find((a) => a.id === 'tex-sprite');
|
||||
sprite.props.shader = ''; delete sprite.props.brightness;
|
||||
__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(texture.id)}).props.filter = 'linear'`);
|
||||
await sleep(400);
|
||||
await mutate(enlarge(160, 120));
|
||||
await sleep(400);
|
||||
|
||||
|
||||
Reference in new issue
Block a user