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

+33 -8
View File
@@ -8,9 +8,11 @@ import {
type GraphNode,
isValidPortName,
fixedPortKeys,
type NodeDef,
nodeDef,
STATE,
} from '../graph';
import { isShaderNodeDef } from '../shader/nodes';
import type { Asset, TextureFrame } from '../model/Asset';
import { isAssetType } from '../model/AssetTypes';
import { emptyEdit, type InstanceEdit, isEmptyEdit } from '../model/instance';
@@ -45,6 +47,7 @@ import {
type StructFieldDef,
variableDef,
} from '../properties/PropertySchema';
import { SHADER_KEY, shaderParamDefs } from '../properties/shaders';
/**
* Turns untrusted JSON (server file, import) into a well-formed Project: unknown keys are dropped,
@@ -57,16 +60,33 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
// Pass 1: asset ids and their effective schemas, needed to coerce overrides in pass 2.
const schemas = new Map<string, PropertySchema>();
const variables = new Map<string, PropertyDef[]>();
const rawProps = new Map<string, Record<string, unknown>>();
const shaderGraphs = new Map<string, Graph>();
const shaderRows = new Map<string, PropertyDef[]>();
for (const a of rawAssets) {
if (typeof a.id !== 'string' || !a.id || !isAssetType(a.type)) continue;
rawProps.set(a.id, asRecord(a.props));
if (a.type === 'behavior') {
const vars = normalizeVariables(a.variables);
variables.set(a.id, vars);
schemas.set(a.id, [...COMMON_PROPS, ...vars]);
} else {
if (a.type === 'shader') {
const graph = normalizeGraph(a.graph, isShaderNodeDef);
shaderGraphs.set(a.id, graph);
shaderRows.set(a.id, shaderParamDefs(graph));
}
schemas.set(a.id, schemaFor(a.type));
}
}
/** A sprite's rows end with the parameters of the shader its bag names – an override's, else the asset's. */
const schemaOf = (assetId: string, bag: Record<string, unknown>): PropertySchema | undefined => {
const schema = schemas.get(assetId);
if (!schema || schema !== schemaFor('sprite')) return schema;
const own = bag[SHADER_KEY];
const shader = typeof own === 'string' ? own : rawProps.get(assetId)?.[SHADER_KEY];
return [...schema, ...(shaderRows.get(typeof shader === 'string' ? shader : '') ?? [])];
};
// Pass 2: build assets. Frame ids share the reference space with asset ids, so `normalizeTexture`
// is handed the ids already spoken for.
@@ -74,13 +94,13 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
const takenIds = new Set(schemas.keys());
for (const a of rawAssets) {
const id = typeof a.id === 'string' ? a.id : '';
const schema = schemas.get(id);
const schema = schemaOf(id, asRecord(a.props));
if (!schema || !isAssetType(a.type)) continue;
const type = a.type;
const name = typeof a.name === 'string' ? a.name : type;
const props = normalizeBag(a.props, schema, false);
if (type === 'scene' || type === 'container') {
assets.push({ id, type, name, props, objects: normalizeObjects(a.objects, schemas) });
assets.push({ id, type, name, props, objects: normalizeObjects(a.objects, schemaOf) });
} else if (type === 'behavior') {
assets.push({
id,
@@ -91,7 +111,9 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
graph: normalizeGraph(a.graph),
});
} else if (type === 'statemachine') {
assets.push({ id, type, name, props, graph: normalizeGraph(a.graph, STATE) });
assets.push({ id, type, name, props, graph: normalizeGraph(a.graph, (def) => def.type === STATE) });
} else if (type === 'shader') {
assets.push({ id, type, name, props, graph: shaderGraphs.get(id) ?? createEmptyGraph() });
} else if (type === 'texture') {
assets.push({ id, type, name, props, ...normalizeTexture(a, takenIds) });
} else if (type === 'animation') {
@@ -130,12 +152,15 @@ export function serializeProject(project: Project): unknown {
return JSON.parse(JSON.stringify(project));
}
function normalizeObjects(raw: unknown, schemas: Map<string, PropertySchema>): SceneObject[] {
function normalizeObjects(
raw: unknown,
schemaOf: (assetId: string, overrides: Record<string, unknown>) => PropertySchema | undefined,
): SceneObject[] {
const objects: SceneObject[] = [];
for (const item of Array.isArray(raw) ? raw : []) {
const o = asRecord(item);
if (typeof o.id !== 'string' || !o.id || typeof o.assetId !== 'string') continue;
const schema = schemas.get(o.assetId);
const schema = schemaOf(o.assetId, asRecord(o.overrides));
if (!schema || schema === schemaFor('scene')) continue; // dangling reference or illegal scene-in-scene
const p = asRecord(o.position);
const offset = asRecord(o.editorOffset);
@@ -245,8 +270,8 @@ function normalizePorts(raw: unknown, fixed: readonly string[], writable: boolea
return ports;
}
/** `only` restricts the node types kept: a state machine's graph holds states and nothing else. */
function normalizeGraph(raw: unknown, only?: string): Graph {
/** `keep` restricts the node types kept: a state machine's graph holds states, a shader's shader nodes, and nothing else. */
function normalizeGraph(raw: unknown, keep?: (def: NodeDef) => boolean): Graph {
const g = asRecord(raw);
const graph = createEmptyGraph();
const nodeIds = new Set<string>();
@@ -255,7 +280,7 @@ function normalizeGraph(raw: unknown, only?: string): Graph {
const id = typeof n.id === 'string' ? n.id : '';
const type = typeof n.type === 'string' ? n.type : '';
const def = nodeDef(type);
if (!id || nodeIds.has(id) || !def || (only !== undefined && type !== only)) continue;
if (!id || nodeIds.has(id) || !def || (keep !== undefined && !keep(def))) continue;
nodeIds.add(id);
// Params carry both settings and inline input values, so there is no schema to coerce against.
const params: PropertyBag = {};