Use a shader graph as a node in other shaders (#303)
A calculation needed in several shaders (a greyscale, a noise, an outline) had to be built again in every one of them. A shader function keeps it as an asset of its own, called like a behavior is since #16. - New asset type `shaderfunction` ("Shader function"), edited in the Shader tab. The panel beside the graph shows no picture, the readout counts its inputs and outputs. - Two new nodes: `Function input` and `Function output`. Their declared ports, numbers or colours only, are the ports of every call. One of each per function, like Start and End. - The call is node `ShaderCall`, named after the function. The palette lists every function, it can be dragged in from the Assets panel, and a double-click opens it. - The compiler sets the function in where it is called (`inlineFunctions`). It renames the copied nodes `<call>/<node>` and turns an unconnected port into a constant. Passes, Time, the Edge cut and the corner walk then see a plain graph. - A problem inside a function is reported at the call, in the function's words. The function's own tab checks it on its own (`checkShaderFunction`). - `ShaderLibrary` keys a shader on its graph and on every function it calls, however deep, so a change reaches every caller. - The port commands mirror a new, renamed or removed port into every call, as for a behavior's Start and End. `NodeDef.portKinds` holds the two ends to Number and Color. - As agreed in the issue: no `Property`, `Color property`, Output or Vertex output in a function, and `Time` is the caller's clock. - Own decision: a `Pass` may stand in a function. It becomes a pass of the calling shader, so a wide blur can be a function. - A function calling itself, also through another, says so at the node and is not set in. The palette does not offer such a call. - The Shader tab showed only the compiler's problems. A call nothing reaches was never compiled and looked healthy, so a call now also shows its own problem there, as in a behavior. Tests: - Gate: 228 files, 3510 tests, CHECK EXIT 0 (+13: 11 in tests/core/shader-functions.test.ts, 2 for the new icon) - e2e 28: 100 checks, all passed (+3 in 28l-viii). Called and in place both #606060/#a2a2a2, halved in the function #303030/#515151. - Counter-check: a fingerprint without the called functions fails 1 unit test, and in the browser the change check (#606060 stays). - The first e2e runs found two faults, both fixed: a double-click only selected the function, and an unreached call of itself showed no problem. - e2e total 1728 is computed (1725 + 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e55071fa5
commit
a526e1b40f
28 files changed
+1091
-83
No files matched your search
@@ -1140,6 +1140,89 @@ export async function shader(t) {
|
||||
colourDistance(chained[0], LEFT) < 12 && colourDistance(chained[1], RIGHT) < 12,
|
||||
{ chained },
|
||||
);
|
||||
|
||||
// 28l-viii. A shader function (#303): Grayscale as an asset of its own, called from the shader,
|
||||
// draws what the same nodes draw in place. A change to the function reaches the sprite
|
||||
// without the shader being touched, and a double-click on the call opens the function.
|
||||
const FN = 'fn28-gray';
|
||||
const fnEdge = (id, from, fromPort, to, toPort) => ({
|
||||
id,
|
||||
from: { node: from, port: fromPort },
|
||||
to: { node: to, port: toPort },
|
||||
});
|
||||
await mutate(`
|
||||
__gm.state.project.assets.push({
|
||||
id: '${FN}',
|
||||
type: 'shaderfunction',
|
||||
name: 'Grayscale 28',
|
||||
props: {},
|
||||
graph: {
|
||||
nodes: ${JSON.stringify([
|
||||
{ ...node('in', 'ShaderFunctionInput', {}, 0, 0), ports: [{ key: 'color', kind: 'color' }] },
|
||||
node('lum', 'ShaderLuminance', {}, 260, 0),
|
||||
{ ...node('out', 'ShaderFunctionOutput', {}, 520, 0), ports: [{ key: 'gray', kind: 'color' }] },
|
||||
])},
|
||||
edges: ${JSON.stringify([fnEdge('e1', 'in', 'color', 'lum', 'color'), fnEdge('e2', 'lum', 'value', 'out', 'gray')])},
|
||||
},
|
||||
})`);
|
||||
const ends = [node('own', 'ShaderTextureColor', {}, 0, 0), node('out', 'ShaderOutput', {}, 520, 0)];
|
||||
await graph(
|
||||
[...ends, node('lum', 'ShaderLuminance', {}, 260, 0)],
|
||||
[edge('own', 'color', 'lum', 'color'), edge('lum', 'value', 'out', 'color')],
|
||||
);
|
||||
const inPlace = [await texel(4, 6), await texel(27, 6)];
|
||||
await graph(
|
||||
[...ends, node('call', 'ShaderCall', { function: FN }, 260, 0)],
|
||||
[edge('own', 'color', 'call', 'color'), edge('call', 'gray', 'out', 'color')],
|
||||
);
|
||||
const called = [await texel(4, 6), await texel(27, 6)];
|
||||
const isGrey = (hex) => new Set(rgb(hex)).size === 1;
|
||||
check(
|
||||
'a function called from the shader draws what its nodes draw in place, byte for byte',
|
||||
called.join() === inPlace.join() && called.every(isGrey) && called[0] !== called[1],
|
||||
{ inPlace, called },
|
||||
);
|
||||
await mutate(`
|
||||
const g = __gm.state.project.assets.find((a) => a.id === '${FN}').graph;
|
||||
g.nodes.push(${JSON.stringify(node('half', 'ShaderMultiply', { b: 0.5 }, 390, 140))});
|
||||
g.edges = g.edges.filter((e) => e.id !== 'e2');
|
||||
g.edges.push(...${JSON.stringify([fnEdge('e3', 'lum', 'value', 'half', 'a'), fnEdge('e4', 'half', 'result', 'out', 'gray')])})`);
|
||||
const fnHalved = [await texel(4, 6), await texel(27, 6)];
|
||||
check(
|
||||
'a change to the function reaches the sprite, its shader untouched',
|
||||
fnHalved.every((hex, k) => isGrey(hex) && Math.abs(rgb(hex)[0] - rgb(called[k])[0] / 2) <= 2),
|
||||
{ called, fnHalved },
|
||||
);
|
||||
await select({ kind: 'asset', assetId: made.id });
|
||||
await showTab('shader');
|
||||
await clickText('Fit');
|
||||
const callNode = await json("__gm.shader.nodeAt('call')");
|
||||
const callLabel = await json("__gm.shader.nodeLabel('call')");
|
||||
if (callNode) await doubleClick(callNode.x, callNode.y);
|
||||
await sleep(200);
|
||||
const openedFunction = {
|
||||
current: await json('__gm.shader.current()'),
|
||||
info: await json('__gm.shader.info()'),
|
||||
subject: (await json('__gm.shader.preview()')).subject,
|
||||
};
|
||||
await mutate(`
|
||||
__gm.state.project.assets.find((a) => a.id === '${FN}').graph.nodes.push(
|
||||
${JSON.stringify(node('self', 'ShaderCall', { function: FN }, 260, 260))})`);
|
||||
const selfProblem = await json("__gm.shader.nodeProblem('self')");
|
||||
check(
|
||||
'the call is named after the function, a double-click opens it, and a call of itself has a problem',
|
||||
callLabel === 'Grayscale 28' &&
|
||||
openedFunction.current === FN &&
|
||||
openedFunction.info === '1 input, 1 output' &&
|
||||
openedFunction.subject === 'A function shows in the shaders calling it' &&
|
||||
selfProblem === 'A function cannot call itself',
|
||||
JSON.stringify({ callNode, callLabel, openedFunction, selfProblem }),
|
||||
);
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => a.id !== '${FN}')`);
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await showTab('scene');
|
||||
await assetProps({ texture: picture?.id ?? '' });
|
||||
await filterOf(picture?.id, 'linear');
|
||||
|
||||
|
||||
Reference in new issue
Block a user