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
@@ -24,8 +24,13 @@ export const ON_BEHAVIOR = 'OnBehavior';
|
||||
export const VARIABLES = 'Variables';
|
||||
export const PROPERTIES = 'Properties';
|
||||
const ON_START = 'OnStart';
|
||||
/** The two ends of a shader function (#303) and the node calling one, see `core/shader/functions`. */
|
||||
export const SHADER_FUNCTION_INPUT = 'ShaderFunctionInput';
|
||||
export const SHADER_FUNCTION_OUTPUT = 'ShaderFunctionOutput';
|
||||
export const SHADER_CALL = 'ShaderCall';
|
||||
|
||||
export const BEHAVIOR_PARAM = 'behavior';
|
||||
export const FUNCTION_PARAM = 'function';
|
||||
export const VARIABLES_PARAM = 'variablesAsset';
|
||||
|
||||
/** Whose declarations a `Variables` node reads; empty is the behavior the node sits in. */
|
||||
@@ -116,8 +121,19 @@ export function callEntries(graph: Graph): GraphNode[] {
|
||||
return start ? [start] : graph.nodes.filter((n) => n.type === ON_START);
|
||||
}
|
||||
|
||||
/** A graph has one of each: its ports are those of every node calling the graph. */
|
||||
export function isTerminalType(type: string): boolean {
|
||||
return type === BEHAVIOR_START || type === BEHAVIOR_END;
|
||||
return (
|
||||
type === BEHAVIOR_START ||
|
||||
type === BEHAVIOR_END ||
|
||||
type === SHADER_FUNCTION_INPUT ||
|
||||
type === SHADER_FUNCTION_OUTPUT
|
||||
);
|
||||
}
|
||||
|
||||
/** The terminal whose ports a caller passes in, rather than takes out. */
|
||||
export function isEntryTerminal(type: string): boolean {
|
||||
return type === BEHAVIOR_START || type === SHADER_FUNCTION_INPUT;
|
||||
}
|
||||
|
||||
export function isTerminalNode(node: GraphNode): boolean {
|
||||
@@ -336,6 +352,7 @@ export function ghostProblem(node: GraphNode, graph: Graph, def: NodeDef): strin
|
||||
*/
|
||||
export function callProblem(node: GraphNode, lookup: GraphLookup, ownerId = ''): string | null {
|
||||
if (node.type === ON_BEHAVIOR) return listenProblem(node, lookup, ownerId);
|
||||
if (node.type === SHADER_CALL) return functionCallProblem(node, lookup, ownerId);
|
||||
if (node.type !== RUN_BEHAVIOR) return lerpProblem(node, lookup);
|
||||
const id = textOf(node.params[BEHAVIOR_PARAM] ?? '');
|
||||
if (!id) return 'No behavior picked';
|
||||
@@ -357,6 +374,37 @@ function listenProblem(node: GraphNode, lookup: GraphLookup, ownerId: string): s
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Why a shader function's call has nothing to set in its place (#303). */
|
||||
function functionCallProblem(node: GraphNode, lookup: GraphLookup, ownerId: string): string | null {
|
||||
const id = textOf(node.params[FUNCTION_PARAM] ?? '');
|
||||
if (!id) return 'No function picked';
|
||||
if (!lookup.shaderFunction(id)) return 'That function is gone';
|
||||
if (ownerId && callsFunction(lookup, id, ownerId)) return 'A function cannot call itself';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The functions the calls in that graph name, picked or not found alike. */
|
||||
export function calledFunctions(graph: Graph): string[] {
|
||||
return graph.nodes
|
||||
.filter((n) => n.type === SHADER_CALL)
|
||||
.map((n) => textOf(n.params[FUNCTION_PARAM] ?? ''))
|
||||
.filter((id) => id !== '');
|
||||
}
|
||||
|
||||
/** Whether `from` is `target` or calls it, however many functions lie between. */
|
||||
export function callsFunction(
|
||||
lookup: GraphLookup,
|
||||
from: string,
|
||||
target: string,
|
||||
seen: Set<string> = new Set(),
|
||||
): boolean {
|
||||
if (from === target) return true;
|
||||
if (seen.has(from)) return false;
|
||||
seen.add(from);
|
||||
const called = lookup.shaderFunction(from);
|
||||
return called ? calledFunctions(called.graph).some((id) => callsFunction(lookup, id, target, seen)) : false;
|
||||
}
|
||||
|
||||
/** The behaviors the `On behavior` nodes of that graph name, picked or not found alike. */
|
||||
export function heardBehaviors(graph: Graph): string[] {
|
||||
return graph.nodes
|
||||
|
||||
Reference in new issue
Block a user