Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
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>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-12 22:41:14 +02:00
1 parent 6141d07d4f
commit 42cafa7543
62 files changed
+1682 -144

No files matched your search

+7 -1
View File
@@ -28,6 +28,7 @@ import {
} from '../../core/graph';
import type { GraphAsset } from '../../core/model/Asset';
import { graphLookup } from '../../core/model/graphLookup';
import { shaderLibrary } from '../../viewport/ShaderLibrary';
import { findAsset, findGraphAsset } from '../../core/model/Project';
import { type Trace, TRACE_FADE } from '../../runtime/systems/BehaviorSystem';
import { beadsInFlight, launchBead } from './beads';
@@ -218,6 +219,8 @@ export class GraphCanvas extends Widget {
}
const seen = new Set<string>();
const lookup = graphLookup(this.state.project);
// A shader node's problem is the compiler's, not a call's.
const shaderProblems = asset.type === 'shader' ? shaderLibrary.buildFor(asset).problems : null;
for (const node of asset.graph.nodes) {
const def = resolveDef(node, asset.graph, lookup);
if (!def) continue;
@@ -230,7 +233,10 @@ export class GraphCanvas extends Widget {
this.views.set(node.id, view);
this.nodeLayer.addChild(view);
}
view.sync(node, asset.graph, this.state.project, def, callProblem(node, lookup, asset.id));
const problem = shaderProblems
? (shaderProblems[node.id] ?? null)
: callProblem(node, lookup, asset.id);
view.sync(node, asset.graph, this.state.project, def, problem);
}
for (const [id, view] of this.views) {
if (!seen.has(id)) {
+4
View File
@@ -51,6 +51,8 @@ export function categoryColor(category: NodeCategory): number {
return c.categoryAction;
case 'state':
return c.categoryState;
case 'shader':
return c.categoryShader;
}
}
@@ -71,6 +73,8 @@ export function portColor(type: PortType): number {
return c.portList;
case 'struct':
return c.portStruct;
case 'vec3':
return c.portVec3;
case 'any':
return c.portAny;
}
+14 -3
View File
@@ -15,6 +15,8 @@ import type { GraphAsset } from '../../core/model/Asset';
import { graphLookup } from '../../core/model/graphLookup';
import type { PropertyBag } from '../../core/model/Properties';
import type { Project } from '../../core/model/Project';
import { nextPropertyName, PROPERTY_NAME_PARAM, SHADER_PROPERTY } from '../../core/properties/shaders';
import { SHADER_NODES, SHADER_OUTPUT } from '../../core/shader/nodes';
/**
* One row of the palette. `def` is the definition the node would actually have once placed, so the
@@ -29,17 +31,26 @@ export interface PaletteEntry {
/**
* The node types offerable in `asset`'s graph. A state machine holds states and nothing else, each
* new one named by its number. In a behavior, every other behavior appears under its own name
* instead of behind a generic "Run behavior", and the two terminals drop out once the graph has them.
* new one named by its number; a shader holds shader nodes, its one Output dropping out once
* placed. In a behavior, every other behavior appears under its own name instead of behind a
* generic "Run behavior", and the two terminals drop out once the graph has them.
*/
export function paletteEntries(project: Project, asset: GraphAsset): PaletteEntry[] {
if (asset.type === 'statemachine') {
const params = { [STATE_NAME_PARAM]: nextStateName(asset.graph) };
return [{ id: State.type, hint: State.category, def: State, params }];
}
if (asset.type === 'shader') {
const hasOutput = asset.graph.nodes.some((n) => n.type === SHADER_OUTPUT);
return SHADER_NODES.filter((def) => def.type !== SHADER_OUTPUT || !hasOutput).map((def) => {
const params: PropertyBag =
def.type === SHADER_PROPERTY ? { [PROPERTY_NAME_PARAM]: nextPropertyName(asset.graph) } : {};
return { id: def.type, hint: def.category, def, params };
});
}
const entries: PaletteEntry[] = [];
for (const def of allNodeDefs()) {
if (def.type === RUN_BEHAVIOR || def.category === 'state') continue;
if (def.type === RUN_BEHAVIOR || def.category === 'state' || def.category === 'shader') continue;
if (isTerminalType(def.type) && terminalNode(asset.graph, def.type)) continue;
entries.push({ id: def.type, hint: def.category, def, params: {} });
}