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:
|
||||
|
||||
Reference in new issue
Block a user