Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
game-maker / suite (push) Failing after 3m59s
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:
1 parent
6141d07d4f
commit
42cafa7543
62 files changed
+1682
-144
No files matched your search
@@ -19,7 +19,10 @@ import {
|
||||
type ValueOf,
|
||||
type VariableKind,
|
||||
} from './kinds';
|
||||
import { findAssetOf, type Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { flatListOf, listItemOf } from './listItems';
|
||||
import { SHADER_KEY, shaderOf, shaderParamDefs } from './shaders';
|
||||
|
||||
/**
|
||||
* What a kind's definition carries beyond key, label and default. `decimals` belongs only on
|
||||
@@ -124,6 +127,8 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
||||
// The fill while there is no picture, and a tint multiplier once there is one: white = no tint.
|
||||
{ key: 'color', label: 'Color', kind: 'color', default: 0xffffff },
|
||||
{ key: 'texture', label: 'Texture', kind: 'texture', default: '' },
|
||||
// The shader's own parameters follow this row – see `schemaForAsset`. Empty is no shader at all.
|
||||
{ key: SHADER_KEY, label: 'Shader', kind: 'asset', default: '', assetTypes: ['shader'] },
|
||||
],
|
||||
// The picture itself is intrinsic data on the asset (see TextureAsset), not a property.
|
||||
texture: [
|
||||
@@ -157,6 +162,8 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
||||
behavior: [{ key: 'runInEditor', label: 'Run in editor', kind: 'boolean', default: false }],
|
||||
// The states are the asset's graph (see StateMachineAsset), not a property.
|
||||
statemachine: [],
|
||||
// The nodes are the asset's graph (see ShaderAsset); its parameters are rows of the sprites wearing it.
|
||||
shader: [],
|
||||
collider: [
|
||||
{
|
||||
key: 'shape',
|
||||
@@ -204,15 +211,43 @@ export function schemaFor(type: AssetType): PropertySchema {
|
||||
return FULL_SCHEMAS[type];
|
||||
}
|
||||
|
||||
/** The type schema, for behaviors followed by their declared variables. */
|
||||
export function schemaForAsset(asset: Asset): PropertySchema {
|
||||
return asset.type === 'behavior'
|
||||
? [...FULL_SCHEMAS.behavior, ...asset.variables]
|
||||
: FULL_SCHEMAS[asset.type];
|
||||
/**
|
||||
* The type schema, for behaviors followed by their declared variables, for sprites by the
|
||||
* parameters of the shader asset their bag names – which is why the project is needed.
|
||||
*/
|
||||
export function schemaForAsset(project: Project, asset: Asset): PropertySchema {
|
||||
return schemaForBag(project, asset, asset.props);
|
||||
}
|
||||
|
||||
export function propDef(asset: Asset, key: string): PropertyDef | undefined {
|
||||
return schemaForAsset(asset).find((d) => d.key === key);
|
||||
/** A placed object's rows: a sprite overriding its shader has that shader's parameters. */
|
||||
export function schemaForObject(project: Project, asset: Asset, object: SceneObject): PropertySchema {
|
||||
const shader = object.overrides[SHADER_KEY];
|
||||
return schemaForBag(project, asset, shader === undefined ? asset.props : { [SHADER_KEY]: shader });
|
||||
}
|
||||
|
||||
function schemaForBag(project: Project, asset: Asset, bag: PropertyBag): PropertySchema {
|
||||
if (asset.type === 'behavior') return [...FULL_SCHEMAS.behavior, ...asset.variables];
|
||||
if (asset.type === 'sprite') return [...FULL_SCHEMAS.sprite, ...shaderParamRows(project, shaderOf(bag))];
|
||||
return FULL_SCHEMAS[asset.type];
|
||||
}
|
||||
|
||||
/** The rows a shader asset adds to the sprites wearing it; a dangling id adds none. */
|
||||
export function shaderParamRows(project: Project, shaderId: string): PropertyDef[] {
|
||||
const shader = shaderId ? findAssetOf(project, shaderId, 'shader') : undefined;
|
||||
return shader ? shaderParamDefs(shader.graph) : [];
|
||||
}
|
||||
|
||||
/** The parameters of every shader in the project, one row per name – what a sprite *may* have. */
|
||||
export function allShaderParamRows(project: Project): PropertyDef[] {
|
||||
const rows = new Map<string, PropertyDef>();
|
||||
for (const asset of project.assets)
|
||||
if (asset.type === 'shader')
|
||||
for (const row of shaderParamDefs(asset.graph)) if (!rows.has(row.key)) rows.set(row.key, row);
|
||||
return [...rows.values()];
|
||||
}
|
||||
|
||||
export function propDef(project: Project, asset: Asset, key: string): PropertyDef | undefined {
|
||||
return schemaForAsset(project, asset).find((d) => d.key === key);
|
||||
}
|
||||
|
||||
/** A fresh bag of the type's default values (callers may mutate it). */
|
||||
|
||||
@@ -3,20 +3,20 @@ import { ancestorIdsOf, type ObjectTree } from '../model/hierarchy';
|
||||
import { instanceTargetOf, isEmptyEdit, resolveObjects } from '../model/instance';
|
||||
import { findAsset, type Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { type PropertySchema, schemaFor, schemaForAsset } from './PropertySchema';
|
||||
import { type PropertySchema, schemaFor, schemaForAsset, schemaForObject } from './PropertySchema';
|
||||
import { deviatesFromAsset, isModified } from './resolve';
|
||||
|
||||
/**
|
||||
* The rows the asset inspector marks as modified. A behavior's variables are left out: their
|
||||
* default is declared on the variable itself, so the asset value cannot differ from it.
|
||||
*/
|
||||
export function markedAssetSchema(asset: Asset): PropertySchema {
|
||||
return asset.type === 'behavior' ? schemaFor('behavior') : schemaForAsset(asset);
|
||||
export function markedAssetSchema(project: Project, asset: Asset): PropertySchema {
|
||||
return asset.type === 'behavior' ? schemaFor('behavior') : schemaForAsset(project, asset);
|
||||
}
|
||||
|
||||
/** Whether any asset property the inspector would mark gold differs from its default. */
|
||||
export function assetDeviates(asset: Asset): boolean {
|
||||
return markedAssetSchema(asset).some((def) => isModified(asset, def.key));
|
||||
export function assetDeviates(project: Project, asset: Asset): boolean {
|
||||
return markedAssetSchema(project, asset).some((def) => isModified(project, asset, def.key));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,8 +33,9 @@ export function objectDeviates(project: Project, holder: ObjectTree, object: Sce
|
||||
}
|
||||
if (object.editorOffset.x !== 0 || object.editorOffset.y !== 0) return true;
|
||||
const asset = findAsset(project, object.assetId);
|
||||
return (
|
||||
asset !== undefined && schemaForAsset(asset).some((def) => deviatesFromAsset(object, asset, def.key))
|
||||
if (!asset) return false;
|
||||
return schemaForObject(project, asset, object).some((def) =>
|
||||
deviatesFromAsset(project, object, asset, def.key),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,6 @@ export function runsInEditor(props: PropertyBag): boolean {
|
||||
export function hasEditorRun(project: Project, holder: ObjectTree): boolean {
|
||||
return resolveObjects(project, holder).some((object) => {
|
||||
const asset = findAsset(project, object.assetId);
|
||||
return asset?.type === 'behavior' && runsInEditor(resolveSceneObjectProps(asset, object));
|
||||
return asset?.type === 'behavior' && runsInEditor(resolveSceneObjectProps(project, asset, object));
|
||||
});
|
||||
}
|
||||
@@ -1,48 +1,60 @@
|
||||
import type { Asset } from '../model/Asset';
|
||||
import { type PropertyBag, type PropertyValue, sameValue } from '../model/Properties';
|
||||
import type { Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { propDef, schemaForAsset } from './PropertySchema';
|
||||
import { propDef, schemaForAsset, schemaForObject } from './PropertySchema';
|
||||
|
||||
/**
|
||||
* Schema defaults filled with the asset's own values. Keys unknown to the schema are ignored, so
|
||||
* stale data never leaks into the editor.
|
||||
*/
|
||||
export function resolveAssetProps(asset: Asset): PropertyBag {
|
||||
const bag: PropertyBag = {};
|
||||
for (const def of schemaForAsset(asset)) {
|
||||
const own = asset.props[def.key];
|
||||
bag[def.key] = own !== undefined ? own : def.default;
|
||||
}
|
||||
return bag;
|
||||
export function resolveAssetProps(project: Project, asset: Asset): PropertyBag {
|
||||
return valuesOver(schemaForAsset(project, asset), asset.props);
|
||||
}
|
||||
|
||||
/** The only place where overrides are merged onto asset values. */
|
||||
export function resolveSceneObjectProps(asset: Asset, object: SceneObject): PropertyBag {
|
||||
const bag = resolveAssetProps(asset);
|
||||
for (const def of schemaForAsset(asset)) {
|
||||
/**
|
||||
* The only place where overrides are merged onto asset values. The rows are the object's: a
|
||||
* sprite overriding its shader has that shader's parameters, not the asset's.
|
||||
*/
|
||||
export function resolveSceneObjectProps(project: Project, asset: Asset, object: SceneObject): PropertyBag {
|
||||
const schema = schemaForObject(project, asset, object);
|
||||
const bag = valuesOver(schema, asset.props);
|
||||
for (const def of schema) {
|
||||
const override = object.overrides[def.key];
|
||||
if (override !== undefined) bag[def.key] = override;
|
||||
}
|
||||
return bag;
|
||||
}
|
||||
|
||||
function valuesOver(
|
||||
schema: readonly { key: string; default: PropertyValue }[],
|
||||
values: PropertyBag,
|
||||
): PropertyBag {
|
||||
const bag: PropertyBag = {};
|
||||
for (const def of schema) {
|
||||
const own = values[def.key];
|
||||
bag[def.key] = own !== undefined ? own : def.default;
|
||||
}
|
||||
return bag;
|
||||
}
|
||||
|
||||
/** Whether the asset's own value differs from the schema default – the asset-level override. */
|
||||
export function isModified(asset: Asset, key: string): boolean {
|
||||
const def = propDef(asset, key);
|
||||
export function isModified(project: Project, asset: Asset, key: string): boolean {
|
||||
const def = propDef(project, asset, key);
|
||||
if (!def) return false;
|
||||
const own = asset.props[key];
|
||||
return own !== undefined && !sameValue(own, def.default);
|
||||
}
|
||||
|
||||
/** The value the object would have without its override for `key`. */
|
||||
export function inheritedValue(asset: Asset, key: string): PropertyValue | undefined {
|
||||
return resolveAssetProps(asset)[key];
|
||||
export function inheritedValue(project: Project, asset: Asset, key: string): PropertyValue | undefined {
|
||||
return resolveAssetProps(project, asset)[key];
|
||||
}
|
||||
|
||||
/** An override that happens to carry the asset's own value does not count as a deviation. */
|
||||
export function deviatesFromAsset(object: SceneObject, asset: Asset, key: string): boolean {
|
||||
export function deviatesFromAsset(project: Project, object: SceneObject, asset: Asset, key: string): boolean {
|
||||
const own = object.overrides[key];
|
||||
return own !== undefined && !sameValue(own, inheritedValue(asset, key));
|
||||
return own !== undefined && !sameValue(own, inheritedValue(project, asset, key));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { Graph, GraphNode } from '../graph/Graph';
|
||||
import { type PropertyBag, textOf } from '../model/Properties';
|
||||
import { type FlatDef, isValidFieldName } from './PropertySchema';
|
||||
|
||||
/** The sprite property naming the shader asset it wears; `''` is no shader and no render pass. */
|
||||
export const SHADER_KEY = 'shader';
|
||||
|
||||
/**
|
||||
* The node of a shader graph that declares a parameter: its `name` is a sprite property and a
|
||||
* GLSL uniform, `default`/`min`/`max` are the row the inspector shows.
|
||||
*/
|
||||
export const SHADER_PROPERTY = 'ShaderProperty';
|
||||
export const PROPERTY_NAME_PARAM = 'name';
|
||||
export const PROPERTY_DEFAULT_PARAM = 'default';
|
||||
export const PROPERTY_MIN_PARAM = 'min';
|
||||
export const PROPERTY_MAX_PARAM = 'max';
|
||||
|
||||
export function shaderOf(bag: PropertyBag): string {
|
||||
return textOf(bag[SHADER_KEY]);
|
||||
}
|
||||
|
||||
export function isPropertyNode(node: GraphNode): boolean {
|
||||
return node.type === SHADER_PROPERTY;
|
||||
}
|
||||
|
||||
export function propertyName(node: GraphNode): string {
|
||||
return textOf(node.params[PROPERTY_NAME_PARAM] ?? '');
|
||||
}
|
||||
|
||||
/** A property's name is a bag key and a uniform, so it has to be an identifier. */
|
||||
export function isValidPropertyName(name: string): boolean {
|
||||
return isValidFieldName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties a shader graph declares, in node order: one number row per `Property` node with
|
||||
* a valid name. A second node with the same name declares nothing new – both read one uniform.
|
||||
*/
|
||||
export function shaderParamDefs(graph: Graph): FlatDef<'number'>[] {
|
||||
const rows: FlatDef<'number'>[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const node of graph.nodes) {
|
||||
if (!isPropertyNode(node)) continue;
|
||||
const name = propertyName(node);
|
||||
if (!isValidPropertyName(name) || seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
rows.push(propertyRow(name, node.params));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function propertyRow(name: string, params: PropertyBag): FlatDef<'number'> {
|
||||
const min = numberParam(params, PROPERTY_MIN_PARAM, 0);
|
||||
const max = numberParam(params, PROPERTY_MAX_PARAM, 1);
|
||||
const row: FlatDef<'number'> = {
|
||||
key: name,
|
||||
label: name,
|
||||
kind: 'number',
|
||||
default: numberParam(params, PROPERTY_DEFAULT_PARAM, 0),
|
||||
min,
|
||||
step: niceStep(max > min ? max - min : 1),
|
||||
};
|
||||
// A max at or below the min would pin the value; treated as "no max" instead.
|
||||
return max > min ? { ...row, max } : row;
|
||||
}
|
||||
|
||||
function numberParam(params: PropertyBag, key: string, fallback: number): number {
|
||||
const n = Number(params[key]);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
/** A twentieth of the range's power of ten: 0.05 for 0-4, 0.05 for 0-1, 5 for 0-100. */
|
||||
function niceStep(range: number): number {
|
||||
return 10 ** Math.floor(Math.log10(range)) / 20;
|
||||
}
|
||||
|
||||
/** What a property placed from the palette is called: `value`, then `value2`, `value3` … */
|
||||
export function nextPropertyName(graph: Graph): string {
|
||||
const taken = new Set(graph.nodes.filter(isPropertyNode).map(propertyName));
|
||||
if (!taken.has('value')) return 'value';
|
||||
for (let n = 2; ; n++) if (!taken.has(`value${n}`)) return `value${n}`;
|
||||
}
|
||||
Reference in new issue
Block a user