Let a shader move the corners of the picture it wears (#79)
A shader could only say what colour a pixel has. Where the picture is drawn was fixed to the four corners of its rectangle, so nothing could wave, bend or wobble. - The new node Vertex output takes x and y, in pixels of the picture, and moves every corner of the wearer's grid by them. The UVs stay with the corners, so the picture bends along. - Subdivisions on that node says how fine the grid is: cells per axis, 1 to 64, 8 by default. 1 is the plain quad. A shader without the node keeps the quad every sprite shares. - A graph holds one Vertex output at most. UV, Time, Property, Number and the math nodes may feed it, and one node may feed both outputs. - Texture color, Texture alpha, Sample texture, Texture size and Color cannot feed it. The node says "Not available per corner" and the shader does not compile. - Sprites, texts and the preview in the shader tab wear the grid. A nine-slice sprite and particles draw as if the node were not there. - Picking, the selection frame, the size and colliders stay with the rectangle. A wearer with a Vertex output is never culled, as with Padding, and with Padding the grid covers the room around the picture too. - tools/perf-shaders.mjs takes GM_PERF_SUBDIVISIONS to measure a grid against the plain quad. Measured on stein2 (Intel HD 630), frames per second, one run per row: at 800 sprites 58 with a shader on the quad, 46 with 8 cells, 41 with 32. Up to 400 sprites a grid hardly shows. Every sprite holds its own grid. Tests: - Gate on melektaus-atx: 220 files, 3416 tests, CHECK EXIT 0. - 15 new unit tests for the compiler and the grid. - Four new e2e checks in modules 28 and 30: a fixed offset moves the picture, 8 cells bend only the lower half where 1 cell shears it all, Texture alpha is refused at the node, a text moves. - e2e 28,30 on melektaus-atx: 110 checks, all pass. - The e2e total of 1699 is counted (1695 + 4), the full suite did not run. - No counter-probe, no screenshot. Not looked at: the preview, Time in the browser, flipped sprites, Padding together with the grid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
963f8a152a
commit
4716726b6b
14 files changed
+586
-75
No files matched your search
@@ -854,6 +854,77 @@ export async function shader(t) {
|
||||
);
|
||||
await shot('28-shader-padding-scene');
|
||||
|
||||
// 28l-iv. A Vertex output moves the corners of the sprite's grid (#79), in pixels of the picture:
|
||||
// 16 of this 32-px one is half the sprite. A constant moves the whole picture; a cut on v
|
||||
// bends it only where the grid has corners, and with one cell there is just a shear.
|
||||
const cornered = (cornerNodes, cornerEdges, subdivisions) =>
|
||||
graph(
|
||||
[
|
||||
node('texel', 'ShaderTextureColor'),
|
||||
node('out', 'ShaderOutput', { edge: 'transparent' }, 780, 0),
|
||||
...cornerNodes,
|
||||
node('vout', 'ShaderVertexOutput', { subdivisions }, 780, 260),
|
||||
],
|
||||
[edge('texel', 'color', 'out', 'color'), ...cornerEdges],
|
||||
);
|
||||
await cornered(
|
||||
[node('by', 'ShaderNumber', { value: 16 }, 260, 260)],
|
||||
[edge('by', 'value', 'vout', 'x')],
|
||||
8,
|
||||
);
|
||||
const moved = {
|
||||
left: await pixel(-0.3, -0.3),
|
||||
right: await pixel(0.3, -0.3),
|
||||
past: await pixel(0.8, -0.3),
|
||||
};
|
||||
check(
|
||||
'a Vertex output fed 16 moves the picture half a sprite to the right, past its rectangle',
|
||||
colourDistance(moved.left, LEFT) > 60 &&
|
||||
colourDistance(moved.right, LEFT) < 12 &&
|
||||
colourDistance(moved.past, RIGHT) < 12 &&
|
||||
colourDistance(bare.left, LEFT) < 12,
|
||||
{ moved, bare },
|
||||
);
|
||||
const lowerHalf = async (subdivisions) => {
|
||||
await cornered(
|
||||
[
|
||||
node('uv', 'ShaderUV', {}, 0, 260),
|
||||
node('cut', 'ShaderStep', { edge: 0.5 }, 260, 260),
|
||||
node('far', 'ShaderMultiply', { b: 16 }, 520, 260),
|
||||
],
|
||||
[
|
||||
edge('uv', 'v', 'cut', 'value'),
|
||||
edge('cut', 'result', 'far', 'a'),
|
||||
edge('far', 'result', 'vout', 'x'),
|
||||
],
|
||||
subdivisions,
|
||||
);
|
||||
return { upper: await pixel(0.6, -0.2), lower: await pixel(0.8, 0.35) };
|
||||
};
|
||||
const bent = await lowerHalf(8);
|
||||
const sheared = await lowerHalf(1);
|
||||
await shot('28-shader-vertex-scene');
|
||||
check(
|
||||
'with 8 subdivisions only the lower half moves; with 1 there are four corners, and the upper half leans along',
|
||||
colourDistance(bent.lower, RIGHT) < 12 &&
|
||||
colourDistance(bent.upper, RIGHT) > 60 &&
|
||||
colourDistance(sheared.upper, RIGHT) < 12,
|
||||
{ bent, sheared },
|
||||
);
|
||||
await cornered(
|
||||
[node('opacity', 'ShaderTextureAlpha', {}, 260, 260)],
|
||||
[edge('opacity', 'value', 'vout', 'x')],
|
||||
8,
|
||||
);
|
||||
const perCorner = await json("__gm.shader.nodeProblem('vout')");
|
||||
const perCornerReadout = await info();
|
||||
check(
|
||||
'what reads the picture cannot feed the Vertex output: the node says so, and the shader does not compile',
|
||||
perCorner === 'Not available per corner: Texture alpha' &&
|
||||
JSON.stringify(perCornerReadout).includes(perCorner),
|
||||
{ perCorner, perCornerReadout },
|
||||
);
|
||||
|
||||
// 28l-ii. The sprite wears a picture and a shader: both open from the object's menu, from the
|
||||
// asset's menu and from the inspector, whose Texture and Shader labels are links (#221).
|
||||
const shadedProps = await json("__gm.state.project.assets.find((a) => a.id === 'shade-sprite').props");
|
||||
|
||||
+17
-2
@@ -321,10 +321,25 @@ export async function font(t) {
|
||||
Math.abs(textRoomy.last - textTight.last - reach) <= 2,
|
||||
JSON.stringify({ textTight, textRoomy, reach }),
|
||||
);
|
||||
// 30i-iii. A shader's Vertex output moves a text's corners as it does a sprite's (#79), in the
|
||||
// text's own units: six of them to the right, and the filled quad stands six further on.
|
||||
await filled(0);
|
||||
await mutate(`
|
||||
const graph = ${shaderGraph};
|
||||
graph.nodes = graph.nodes.filter((n) => n.id !== 'ft-red-full');
|
||||
graph.edges = graph.edges.filter((e) => e.id !== 'ft-red-e2');
|
||||
graph.nodes.push(${JSON.stringify(node('ft-red-by', 'ShaderNumber', 0, 320, { value: 6 }))});
|
||||
graph.nodes.push(${JSON.stringify(node('ft-red-vout', 'ShaderVertexOutput', 260, 320, { subdivisions: 4 }))});
|
||||
graph.edges.push(${JSON.stringify(edge('ft-red-e3', 'ft-red-by', 'value', 'ft-red-vout', 'x'))});`);
|
||||
const textMoved = await redSpan();
|
||||
check(
|
||||
'a Vertex output fed 6 draws the text six units to the right of where it stands',
|
||||
Math.abs(textMoved.first - textTight.first - reach) <= 2 &&
|
||||
Math.abs(textMoved.last - textTight.last - reach) <= 2,
|
||||
JSON.stringify({ textTight, textMoved, reach }),
|
||||
);
|
||||
await mutate(`
|
||||
const graph = ${shaderGraph};
|
||||
graph.nodes = graph.nodes.filter((n) => !['ft-red-full', 'ft-red-by', 'ft-red-vout'].includes(n.id));
|
||||
graph.edges = graph.edges.filter((e) => !['ft-red-e2', 'ft-red-e3'].includes(e.id));
|
||||
Object.assign(graph.nodes.find((n) => n.id === 'ft-red-out').params, { edge: 'clamp', padding: 0 });`);
|
||||
|
||||
// 30j. The game draws a text through the same picture, shader included. In front of everything:
|
||||
|
||||
+13
-1
@@ -10,6 +10,8 @@
|
||||
*
|
||||
* Usage: node tools/seed-e2e.mjs && node tools/perf-shaders.mjs
|
||||
* GM_PERF_COUNTS=... sprite counts, GM_PERF_SIZE=... edge length of one sprite
|
||||
* GM_PERF_SUBDIVISIONS=... gives the shader a Vertex output with that grid (#79); nothing feeds
|
||||
* it, so the pixels stay the same and only the corners are more
|
||||
*
|
||||
* It is a measuring tool, not a test: frame times depend on the machine, so CI does not run it.
|
||||
*
|
||||
@@ -28,6 +30,7 @@ const { projectId, baseUrl } = cliArgs('perf-shaders.mjs', PROJECTS.fixture);
|
||||
|
||||
const COUNTS = (process.env.GM_PERF_COUNTS ?? '50,100,200,400,800').split(',').map(Number);
|
||||
const SIZE = Number(process.env.GM_PERF_SIZE ?? 48);
|
||||
const SUBDIVISIONS = Number(process.env.GM_PERF_SUBDIVISIONS ?? 0);
|
||||
const WARMUP_MS = 1500;
|
||||
const SAMPLE_MS = 4000;
|
||||
|
||||
@@ -49,6 +52,14 @@ await runChecks({ projectId, baseUrl, port: 9347, shotPrefix: 'gm-perf' }, async
|
||||
props: { width: ${SIZE}, height: ${SIZE}, color: 0x4080ff, shader: '' } });
|
||||
`);
|
||||
|
||||
await mutate(`
|
||||
const graph = __gm.state.project.assets.find((a) => a.id === 'adjust-colors').graph;
|
||||
graph.nodes = graph.nodes.filter((n) => n.id !== 'perf-vout');
|
||||
if (${SUBDIVISIONS} > 0)
|
||||
graph.nodes.push({ id: 'perf-vout', type: 'ShaderVertexOutput', x: 0, y: 600,
|
||||
params: { subdivisions: ${SUBDIVISIONS} } });
|
||||
`);
|
||||
|
||||
/** Camera-sized grid of `n` instances, all inside the view so none of them is skipped. */
|
||||
const buildScene = async (n) => {
|
||||
await mutate(`
|
||||
@@ -145,7 +156,8 @@ await runChecks({ projectId, baseUrl, port: 9347, shotPrefix: 'gm-perf' }, async
|
||||
await stop();
|
||||
|
||||
console.log(
|
||||
`\nsprite ${SIZE}x${SIZE}, ${SAMPLE_MS} ms per point, neutral values, ` +
|
||||
`\nsprite ${SIZE}x${SIZE}, ${SUBDIVISIONS > 0 ? `grid of ${SUBDIVISIONS}` : 'plain quad'}, ` +
|
||||
`${SAMPLE_MS} ms per point, neutral values, ` +
|
||||
`60 Hz cap (cannot be lifted in headless)`,
|
||||
);
|
||||
console.log('| sprites | no shader | with shader | per sprite | shaders | filters |');
|
||||
|
||||
Reference in new issue
Block a user