game-maker / suite (push) Failing after 3m59s
A shader is an asset carrying a node graph (Texture color, Output, Property, Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite wears a shader through its Shader picker, and the rows below it are the graph's Property nodes. Property schema, resolution and deviation take the project because a sprite's rows now live in another asset. The built-in registry of the prototype is gone; "Adjust colors" ships as a graph in the fixture and the three example games. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
144 lines
5.1 KiB
TypeScript
144 lines
5.1 KiB
TypeScript
import { edgesInto, findNode, type Graph, type GraphNode } from '../graph/Graph';
|
||
import { nodeDef, type PortDef } from '../graph/NodeRegistry';
|
||
import { colorOf } from '../model/Properties';
|
||
import { isPropertyNode, isValidPropertyName, propertyName, shaderParamDefs } from '../properties/shaders';
|
||
import {
|
||
asVec3,
|
||
type Expr,
|
||
floatLiteral,
|
||
isShaderNodeDef,
|
||
SHADER_OUTPUT,
|
||
TEXEL_COLOR,
|
||
uniformName,
|
||
vec3Literal,
|
||
} from './nodes';
|
||
|
||
export interface ShaderUniform {
|
||
name: string;
|
||
default: number;
|
||
}
|
||
|
||
export interface CompiledShader {
|
||
fragment: string;
|
||
uniforms: ShaderUniform[];
|
||
}
|
||
|
||
export interface ShaderBuild {
|
||
/** Null while the graph has a problem – the sprite then draws without a filter. */
|
||
compiled: CompiledShader | null;
|
||
/** Per node id, what keeps the graph from compiling. */
|
||
problems: Record<string, string>;
|
||
/** One line for the tab: empty once compiled. */
|
||
reason: string;
|
||
}
|
||
|
||
/**
|
||
* A shader graph to a Pixi filter fragment: every node reachable from the Output becomes one
|
||
* local per output, in dependency order, and every Property a float uniform. Pixi hands a filter
|
||
* premultiplied pixels, so the prelude unpremultiplies and the epilogue multiplies back.
|
||
*/
|
||
export function compileShader(graph: Graph): ShaderBuild {
|
||
const problems: Record<string, string> = {};
|
||
const problem = (node: GraphNode, text: string) => {
|
||
if (!(node.id in problems)) problems[node.id] = text;
|
||
};
|
||
|
||
const names = new Set<string>();
|
||
for (const node of graph.nodes) {
|
||
if (!isPropertyNode(node)) continue;
|
||
const name = propertyName(node);
|
||
if (!isValidPropertyName(name)) problem(node, 'Name must be one word: letters, digits, _');
|
||
else if (names.has(name)) problem(node, `Another property is named "${name}"`);
|
||
names.add(name);
|
||
}
|
||
|
||
const lines: string[] = [];
|
||
const emitted = new Map<string, Record<string, Expr>>();
|
||
const visiting = new Set<string>();
|
||
let count = 0;
|
||
|
||
const emitNode = (node: GraphNode): Record<string, Expr> | null => {
|
||
const done = emitted.get(node.id);
|
||
if (done) return done;
|
||
if (visiting.has(node.id)) {
|
||
problem(node, 'Feeds itself');
|
||
return null;
|
||
}
|
||
const def = nodeDef(node.type);
|
||
if (!def || !isShaderNodeDef(def)) {
|
||
problem(node, 'Not a shader node');
|
||
return null;
|
||
}
|
||
visiting.add(node.id);
|
||
const inputs: Record<string, Expr> = {};
|
||
let complete = true;
|
||
for (const port of def.inputs) {
|
||
const expr = input(node, port);
|
||
if (expr) inputs[port.key] = expr;
|
||
else complete = false;
|
||
}
|
||
visiting.delete(node.id);
|
||
if (!complete) return null;
|
||
const outputs: Record<string, Expr> = {};
|
||
for (const [key, expr] of Object.entries(def.emit(inputs, node.params))) {
|
||
const local = `n${count++}_${key}`;
|
||
lines.push(`${expr.type} ${local} = ${expr.code};`);
|
||
outputs[key] = { code: local, type: expr.type };
|
||
}
|
||
emitted.set(node.id, outputs);
|
||
return outputs;
|
||
};
|
||
|
||
/** What feeds an input: the edge's source, the typed-in value, or – with nothing to type – the texel. */
|
||
const input = (node: GraphNode, port: PortDef): Expr | null => {
|
||
const edge = edgesInto(graph, node.id, port.key)[0];
|
||
if (edge) {
|
||
const source = findNode(graph, edge.from.node);
|
||
const outputs = source && emitNode(source);
|
||
const expr = outputs?.[edge.from.port];
|
||
if (!expr) return null;
|
||
return conform(node, port, expr);
|
||
}
|
||
const typed = node.params[port.key] ?? port.default;
|
||
if (port.field?.kind === 'color')
|
||
return { code: vec3Literal(colorOf(typed ?? 0xffffff, 0xffffff)), type: 'vec3' };
|
||
if (port.field) return { code: floatLiteral(Number(typed ?? 0)), type: 'float' };
|
||
return TEXEL_COLOR;
|
||
};
|
||
|
||
const conform = (node: GraphNode, port: PortDef, expr: Expr): Expr | null => {
|
||
if (port.type === 'number' && expr.type === 'vec3') {
|
||
problem(node, `"${port.key}" needs a number, not a colour`);
|
||
return null;
|
||
}
|
||
if (port.type === 'vec3' && expr.type === 'float') return { code: asVec3(expr), type: 'vec3' };
|
||
return expr;
|
||
};
|
||
|
||
const output = graph.nodes.find((n) => n.type === SHADER_OUTPUT);
|
||
if (!output) return { compiled: null, problems, reason: 'No Output node' };
|
||
const outputDef = nodeDef(SHADER_OUTPUT)!;
|
||
const color = input(output, outputDef.inputs[0]!);
|
||
|
||
const first = graph.nodes.find((n) => n.id in problems);
|
||
if (first || !color)
|
||
return { compiled: null, problems, reason: first ? problems[first.id]! : 'Cannot compile' };
|
||
|
||
const uniforms = shaderParamDefs(graph).map((row) => ({ name: row.key, default: row.default }));
|
||
const fragment = [
|
||
'in vec2 vTextureCoord;',
|
||
'out vec4 finalColor;',
|
||
'uniform sampler2D uTexture;',
|
||
...uniforms.map((u) => `uniform float ${uniformName(u.name)};`),
|
||
'void main()',
|
||
'{',
|
||
' vec4 texel = texture(uTexture, vTextureCoord);',
|
||
` vec3 ${TEXEL_COLOR.code} = texel.a > 0.0 ? texel.rgb / texel.a : texel.rgb;`,
|
||
...lines.map((line) => ` ${line}`),
|
||
` finalColor = vec4(clamp(${color.code}, 0.0, 1.0) * texel.a, texel.a);`,
|
||
'}',
|
||
'',
|
||
].join('\n');
|
||
return { compiled: { fragment, uniforms }, problems, reason: '' };
|
||
}
|