A Blur stops at radius 8 in one pass, and bloom needs a softened copy
of the picture beside the original. A Pass node cuts the graph: what
feeds it is drawn into a picture of its own first, and the nodes that
read a picture read that one when it feeds them.
- Pass has color and alpha like the Output, a Resolution setting
(Full, Half, Quarter) and a picture output. Texture color, Texture
alpha, Sample texture, Blur and Texture size get a picture input.
Unconnected it is the wearer's own picture, so the original stays
readable beside any pass.
- picture is a port type of its own. It plugs only into a picture
input, not even into an any port.
- The walk shared by the compilers can stop at a node (cut). The
shader compiler draws each Pass it meets as a fragment of its own,
earlier passes first. A ring through a picture says Feeds itself,
and more than 8 passes say so at the node.
- A pass draws untinted. The tint comes on when the last part reads a
picture, so a particle source draws its passes once for all of its
particles (user decision).
- A pass's picture is the wearer's picture and its Padding, at the
pixels of the picture times Resolution, at most 2048 per side. It is
read with the picture's own filter and continues past its frame as
the Output's Edge says.
- PassPictures keeps one render texture per pass and wearer. It draws
again only when picture, size, Padding or a uniform changed. A
field's picture is a render texture that changes under the same id,
so a field draws its passes every time.
- Sprite, sliced sprite, text, particle source, field shader and the
Shader tab's preview all draw passes. syncShader takes a fixed
picture for a 9-slice, which holds its corners but keeps the margin.
- texelAt and blur take the sampler they read. Existing shaders
compile to the same program apart from that argument.
- A field shader's Blur and Texture size count a pixel as a unit of
the frame (uPixelScale), not of the screen. Before, a field's blur
grew when zooming out and shrank when zooming in. This came with
#170 and #300 and is fixed here because passes inherit it.
- tools/perf-shaders.mjs and tools/perf-field-shaders.mjs measure two
passes with GM_PERF_PASSES=1 and GM_PERF_PASS_RESOLUTION. Below Full
an open Pass shrinks the picture first, as the wiki's recipe does.
Found on the way:
- Destroying the pictures before the shader made Pixi warn that a
texture was still bound. The shader goes first now, and a picture of
a new size is resized rather than replaced.
- Two passes in a row could draw plain white the first time. Pixi
builds the code that binds a program's resources once and finds them
by position. A pass of one graph is often the same program as a pass
of another with fewer slots, so it bound a placeholder. Every shader
now declares all 8 pass slots and its uniform group.
Tests:
- Gate on melektaus-atx: 225 files, 3480 tests (3467 + 13), exit 0.
- e2e 28 and 31 on melektaus-atx: 97 checks (94 + 3) and 44 checks
(43 + 1), all passed, no Pixi warning in the console.
- 28l-vii: a Blur across, a Pass and a Blur down draw the 5x5 spot of
a Blur of Both (difference 0.000). On a grey sprite a pass's picture
added to the original is 231/60/60, each tinted once. Two Passes in
a row after a graph with one show red and green, not white.
- 31v-iii: a Blur of 8 fades a field's frame edge over 8.0 units at
50 % and at 200 % zoom.
- Counter-checks: a tinted pass fails 1 unit test. Pictures never
bound fail both e2e checks (spot 0.20 instead of 0.04, 243/158/158).
Pass slots by the number of passes draw #ffffff, and a field without
its pixel scale ramps over 16 and 4 units: exactly the two new
checks fail.
- e2e sum 1716 is counted, not yet measured (1712 + 4).
- Measured on stein2 (HD 630, one run per row): 50 field shaders at
radius 8 go from 11.4 to 41.3 fps in two passes, 44.4 at radius 16
(three passes at Half). 800 sprites at radius 8 go from 13.9 to
24.5 fps. At radius 4 one pass stays faster from 400 sprites on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shader could only ask its sprites for numbers. A tint, an outline
colour or a glow colour had to be built from three Property nodes.
- New node Color property next to Property: Name and Default (a
colour, white unless picked), output `color`. It becomes a colour
row below the Shader dropdown and a vec3 uniform in the program.
- Number and colour properties share one set of names. A clash is
the existing "Another property is named" problem.
- Like Color it cannot feed the Vertex output.
- Placed from the palette it is called tint, then tint2.
- The preview in the Shader tab shows a colour field for it. Like
the sliders it starts at the sprite's value and never writes into
the project.
- `declaredRows` takes the row builder, so the particle graph keeps
its number rows unchanged.
Tests:
- Gate: 222 files, 3452 tests (3445 + 7)
- e2e 28: 89 checks (28l-v: default colour drawn, then the colour
the sprite sets)
- e2e total 1704, counted (1703 + 1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shader ran only inside the rectangle of what wears it: the quad
has the size of the sprite, and outside of it no fragment runs. An
outline or a glow built with Sample texture was cut off at the
border. The issue still spoke of Filter.padding, but since #77 the
shader is no filter any more, the limit is the geometry.
- The Output node has a number Padding next to Edge: so many pixels
of the picture per side the wearer draws beyond it. 0 is the
default and draws as before, 256 is the most.
- The quad grows and its UVs grow with it, so the picture keeps its
place and size. In the margin the UVs run on below 0 and above 1.
- Transparent is a fourth Edge mode: outside of the picture there
is nothing. It is cut in the fragment (texelAt), so it needs no
copy of the picture with an address mode of its own.
- Edge holds everywhere alike, also for Texture color, Texture
alpha and unconnected inputs.
- Padding above 0 with Edge on Clamp puts the hint "Padding with
Clamp smears the border" on the Output. The shader still
compiles. A chosen Clamp and the default cannot be told apart.
- All four wearers draw the margin. A sprite counts in pixels of
its frame, a text in its units, a particle source grows every
particle about its centre.
- A nine-slice sprite gets a ring around its nine fields, as wide
as so many border pixels. The fields themselves stay.
- Without a picture a pixel is one unit of the wearer's own size,
or of the particle size.
- Particles now take the Edge mode of their shader. It had no
effect on them before.
- An object wearing a padded shader is never culled, or its margin
would vanish at the edge of the screen.
- The preview in the shader tab shows the margin and leaves room
for it.
- Picking, the selection frame, the size and the collider stay with
the rectangle without the margin.
Tests:
- Gate on melektaus-atx right before this commit: 216 files,
3336 tests (3319 + 17), exit 0.
- e2e 28,30,31,37,41 on melektaus-atx: 178 checks, all pass.
e2e 1-37,47,48,58 ran before the last change (no culling with
padding): 1281 checks, all pass.
- 17 new unit tests: the compiler (padding, Transparent, the hint)
and the geometry (quad, nine-slice ring).
- Seven new e2e checks: four at the sprite in module 28, one each
for text, particles and nine-slice.
- 17 unit counter-probes all fail their tests. Three browser
counter-probes fail their checks (5, 3 and 1 of them).
- Three screenshots looked at: the sprite with Clamp and margin in
the scene, the preview in the shader tab, the text.
- The e2e sum of 1687 is counted (1680 + 7), the full suite and
the smokes did not run.
- Not seen: the hint at the node in a picture, particles and
nine-slice in a picture, the game view while playing, sprites
without a picture, vector and pixel art pictures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The result of Add, Multiply, Mix and the other arithmetic nodes of
a shader graph was drawn as any, whatever went in. The compiler
already lifts a colour on any side to a colour result, so the pin
said less than the code knew. A colour result could also be wired
into a number input, and the shader then failed to compile.
- liftedType in core/shader/nodes.ts is the one rule. The compiler
reads it when it lifts two operands, and the editor reads it
through liftedPortType for the ports it draws.
- A SharedPortGroup may bring its own rule as agree. Without one
it is agreedType, as before. The thirteen arithmetic nodes share
their type by liftedPortType.
- Only the any inputs decide. An input nothing feeds counts as the
number typed into it. t, edge, exponent, min and max have no say.
- The type travels through a chain of such nodes without code of
its own, because #210 hands a shared type on.
- In a ring the result stays any, unless a colour has settled it.
- A number may feed a colour input. Without that a number result
no longer fitted Output.color, although the compiler spreads it
over three channels. The rule sits in portsCompatible, but only
the shader graph has colour ports.
- A colour result no longer fits a number input. An edge laid
before the colour came in stays and is drawn in the danger
colour, and the node at its end names the problem.
- The nine shaders in the stored projects (108 edges) all stay
valid under the new rule.
Tests:
- Gate on melektaus-atx: 212 files, 3260 tests (3240 + 20), exit 0.
The gate right before this commit ran on the tree of this and
the next commit together, with the same numbers.
- e2e 28 on melektaus-atx: 81 checks, all pass.
- e2e 1-31: 1059 pass, 1 fail in module 18 (the editor clock read
1.97 s after a stop). A rerun of 1-18 passed with 602 checks and
0.18 s. 1-31 was not run in full again.
- No full run and no smokes. The sum of 1676 (1673 + 3) is reckoned.
- Twenty unit tests in shader-port-types.test.ts, one of them runs
all thirteen nodes through every mix of number and colour and
compares what is drawn with the code made.
- Two tests in shaders.test.ts now lay the edge first and bring the
colour in after, the only order that still wires such a graph.
- Three e2e checks in module 28 with the real mouse: a colour into
Multiply, the red edge with its message, and the colour result
refused at alpha and taken at color.
- Fifteen unit counter-probes and two browser counter-probes all
fail their tests.
- Not checked in the browser: the one-input nodes, Mix and the
ring, and a number wired straight into a colour input. Nothing
was looked at after the build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Load scene destroyed every object of the old scene. A player, a HUD
or a music object had to be built again in each scene, with its state
pushed through shared variables. A top-level object with the new switch
Keep on scene load now travels into the next scene as the same entity,
with everything below it.
- The switch is a common property right under Enabled, off by default,
on the asset and on a placed object. The inspector shows the row on
top-level objects of a scene only, as answered in the issue.
- runtime/keptObjects.ts holds the rules as pure functions.
keptEntities picks what travels, objectsToBring copies the stored
objects as the run parents them, and bringObjects puts them into the
next scene.
- World.instantiate takes a handed entity as it is instead of making
one. The next syncToModel then writes the run's values into the
scene.
- BehaviorSystem.release takes the running instances out before the
old world is destroyed, and the next system adopts them. They hear
no On destroy and no second On start, and their sounds play on.
- PhysicsSystem.resume gives a dynamic body the velocity and spin it
had.
- Duplicates: the object brought along wins. The next scene does not
build its own object with the same id, which is the way back into
the scene the object came from.
- Decided here: the next scene does not build a top-level object of
the same asset that is kept itself either. The issue only names the
id. With the id alone a level that places its own player would hold
two after a swap.
- Decided here: kept objects land behind the scene's own, the switch
is no animation row, and collisions are found anew.
A finding, fixed along with it: Load scene did not build a scene as it
is stored, but as the run had left it. A moved object stayed moved and
a destroyed one stayed gone. The way back into a scene needs a clean
build.
- GameRun copies a scene's objects and groups before it first builds
it and puts them back before it builds it again.
- GameRun.end puts every such scene back. The player calls it on stop,
so spawned objects no longer stay for "Click to play again".
- The consequence: an edit made in a scene during Play is gone the
next time that scene is loaded.
Tests:
- Gate: 199 files, 2944 tests (2929 + 15), exit 0
- e2e, full run: 1625 checks passed (1619 + 6), exit 0
- Smokes: 24 / 19 / 21, exit 0
- 15 unit tests in tests/core/keep-on-scene-load.test.ts
- Six e2e checks in module 35: the switch is set with a real mouse,
the child has no row, the object is in the new scene, its counter
still reads 1, its sound plays on, and the stored scenes are
unchanged after Stop
- Eleven tests with fixed row lists only count the new schema row
- 16 unit counter-probes all fail tests, run on stein2. One did not
fail at first (same id not left out), it has its own test now.
- The browser counter-probe fails 4 of the 6 new checks
- Not looked at in the browser: the switch as a port of the Properties
node, and a released game with a kept object
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recurring cleanup round over the code since round 7 (bc688f3). The
issue now also asks to fix bugs and small inconsistencies found on the
way, so unlike the rounds before, this one changes behaviour in a few
places. Duplicated code in api went from 23 clones with 228 lines to
none (jscpd, at least 50 tokens). In src it stays at 22 clones with
271 lines, the same before and after.
Bugs:
- nginx served project and snapshot files as they were, so anyone who
knew a project id could read it without signing in. Every file the
API writes now goes through AtomicFile (temp file, mode, rename).
Projects, snapshots, releases, accounts and sessions are 0640, and
nginx answers 403 for them.
- Any signed-in account could delete a shared image, font or sound,
which projects of other accounts may wear. Deleting one is now for
administrators only. The editor never deleted a file, so the
client's delete is gone.
- Two saves of one project could run at once. The lock file was
removed after each save, so a waiting save and the next one locked
different files. The lock file now stays, and deleting a project
takes the lock as well.
- Bytes that are no picture got a 500 instead of a 422, because the
error handler also threw for warnings silenced with @.
- A session file was rewritten in place. A request reading it at that
moment found it empty and was signed out. It is replaced atomically.
- Delete did nothing for a shape picked in the vector art list. Delete
now goes through removeSelection, one case per kind of selection,
and a kind without a case fails the type check.
Inconsistencies:
- Escape on a question does what its Cancel does. Cancel on "Delete
project?" and "Restore snapshot?" leads back to the picker or the
list like the other questions did, and so does deleting a project.
- Editor texts use American spelling throughout, like the port names:
48 British spellings in 45 lines (colour, centre, initialised).
- objectName and groupName replace eleven copies of "own name, else
the asset's" and three of "Group". One copy fell back to the object
id instead of "?".
Duplicates, dead code, comments:
- routeFiles serves images, fonts and sounds instead of three copies.
HashedFileStore holds what the three stores shared: keeping a file
under its hash with its name, listing and deleting. The stores keep
only which bytes they take and what a record says.
- AtomicFile replaces nine copies of temp file plus rename.
- removeShapes is the one way the canvas, the inspector button and
Delete take shapes out.
- GraphInterpreter.fire was called by tests only. The 54 calls use a
helper in tests/helpers.ts now.
- FileStore.delete, HttpFileStore.delete and the PluginApi type were
unused. A doc comment in actions.ts sat above the wrong function.
Checked and left as is:
- solidCells is used by tests only. It is the per-cell view before the
merge into rectangles, the one place the largest-scale rule shows.
- An undo checker ran all 124 commands of the unit tests and compared
the project before and after undo. The only differences are empty
ports, groups and notes lists where there were none, which the file
format drops anyway.
Docs:
- README: the check sum.
- Wiki, own commit: API (admin-only deletes, 422, file modes, lock
file), Home (the "Not yet" list named three things that exist),
Concepts, Keyboard, Vector art, Architecture and Development, and
the 38 characters Gitea flagged as ambiguous on eight pages.
- .gitignore: the lock files, and pasted-*.png in any folder, which
keeps the screenshot archive out.
Tests:
- ./check.sh: 195 files, 2817 tests, exit 0 (+2 for the name helpers).
- PHP: accounts 52 in the web container and 53 in the CI image with
the race test (+2), snapshots 15 (+1), releases 20.
- Full e2e run: ALL PASSED, 1586 checks (+6 in 12, 47, 49 and 50).
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks. The example
project files were restored byte for byte afterwards.
- The file stores of HEAD and of this commit give byte-identical
output for the live folders and for store, list and delete in a
throwaway folder.
- Counter-checks, all fail as expected: a 500 for bad bytes fails 12,
no admin check fails 49, removing the lock file again loses saves
in 3 of 3 runs (151 to 170 of 321), Delete ignoring shapes fails 47,
Escape that only closes fails 50, and dropping the shape case fails
the type check (TS2366).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An animated or set Z of -1 on a weapon put it behind everything on Z 0,
not just behind the player holding it. A child can now opt in to being
drawn at its parent's place, so its own Z only orders it against the
parent and its siblings.
- New property "Sort with parent" on sprites, texts and particle
emitters, shown in the inspector right under Z. It is a normal
property, set on the asset and overridable per object.
- With it on, the rest of the scene sees only the parent's Z and, where
the layer sorts by Y, the parent's feet. Below 0 the child is drawn
behind the parent, from 0 on in front.
- It works across levels: a grandchild with the switch hangs on the top
object of the chain.
- It has no effect under a parent that is not drawn itself (container,
behavior, camera, collider) or under a tilemap.
- New assets start with it on, saved ones read as off, so old projects
draw unchanged. For that a schema row now has "initial" next to
"default". The gold "changed" mark and reset follow "initial", so a
fresh sprite is not marked as changed. No load migration is needed.
- parentGroups() in drawOrder.ts gives a slot only to the top of a
chain and inserts the children there. Editor, play mode and picking
all use it, so they agree.
Tests:
- 10 new unit tests in tests/viewport/sort-with-parent.test.ts.
- 8 existing tests list the sprite rows or defaults and got the row.
- New e2e check 5b-ii clicks the switch and checks order and undo.
- Counter-checks, all originals restored by cmp: switch ignored fails
7, children not sorted by Z fails 6, no chain fails 1, "initial"
ignored fails 9, row missing in the inspector fails e2e 5.
- ./check.sh: 166 files, 2426 tests, CHECK EXIT 0.
- e2e 5: ALL PASSED, 13 checks. Check total 1398, computed only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#63: a shader Property node named "width" quietly took over the sprite
row of the same name. isSpriteProp lives in PropertySchema now and is
derived from FULL_SCHEMAS.sprite instead of being a second list that
drifts. isValidPropertyName rejects those ten names, and the compiler
reports it at the node as its own problem: "width" is a sprite
property, told apart from "not a word" and from a duplicate.
#64: Divide emitted (a / b) and handed NaN to the driver. It emits
divide(a, b) now, with the guard in the prologue next to straight(),
scalar and per channel: zero divided by zero is zero. That was already
the house rule in the behavior and particle graphs - only the shader
had never been told. Clamp comes along as a shader node for the same
reason: the behavior catalog has had it all along.
`Sqrt` already exists in the behavior graph (`data/Sqrt.ts`) and in particles
(`ParticleSqrt`); the shader was the only graph without it. `Pow` with exponent
0.5 does the same job, so this is convenience, not a missing capability -- a
radial falloff no longer needs a node whose purpose has to be read off its
parameter.
Like `Pow` it cannot go through `unary()`: GLSL leaves `sqrt` undefined below 0,
so the value is held there with `max(value, 0.0)`. That form takes a float
second argument for a `vec3` too, so unlike `pow` the exponent broadcast has no
counterpart here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The default of the Edge param and the address mode the shared sheet physically
carries were the same constant; changing the value would have turned every
sentinel with it and given each shader sprite a full copy of its texture.
SHEET_EDGE ('mirror', what TextureLibrary uploads) is now separate from
DEFAULT_EDGE ('clamp'). A graph whose Sample reads nothing but its own pixel
draws with the plain sheet whatever the Edge says, so the common shader costs
no second copy.
Without a shader the quad's coordinates never leave 0..1, and mirror-repeat
reads like clamp within a texel - so clamp is what a sprite shows anyway.
shaderFingerprint(graph) covers node id/type/params/ports and edge endpoints only;
x/y, collapsed, folded and edge ids stay out. ShaderLibrary keys on it and counts
compiles (__gm.shaderCompiles()). Measured before: one compile per preview step of a
node drag (12 for a harness drag); after: 0, a param change exactly +1.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four new rows on sprite, text, collider and container (block TRANSFORM):
scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they
get override/reset/apply, loading of old projects, write-back after play and
the Properties node for free; camera and behavior have none.
The hierarchy composes T*R*S per level (composeAlongChain replaces
sumAlongChain), so children rotate and scale with their parent. Reparenting
and grouping keep the look (carriedTransform). Drawing goes through
placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags
are computed in the parent's system.
Colliders are oriented boxes: SAT for box/box, box-circle in the box frame;
exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along
the object's own axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fragment-side coordinate nodes instead of vertex nodes: the quad stays the
quad, a UV read plus a sample at a computed coordinate covers flip, scroll,
wave, pixelate and scanlines. Time is a uniform only when a Time node reaches
the Output; a viewport that drew a Time shader redraws from the ticker so the
Scene tab animates without a running world. Edge (Mirror/Repeat/Clamp) is a
second texture variant in TextureLibrary because WebGL binds no TextureStyle
resource of the shader. The compiler now emits #version 300 es first: Pixi
only compiles as ES 3.00 when the fragment says so, and textureSize needs it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A sprite's picture is now a Mesh (unit quad, sibling of its Graphics node, which keeps
only the hit area); a worn shader replaces Pixi's fragment in that draw, so there is no
intermediate texture and no extra render pass. Measured on the HD 630: 400 shaded
sprites hold 60 fps (was 34), 800 hold 56 (was 20).
Two Pixi facts the code leans on: a mesh's texture is bound only for Pixi's own shader,
so GraphShader.wear hands it over; and MeshPipe caches whether a mesh batches and
re-checks it only in a frame without structure changes, so switching a shader on or
off calls mesh.unload().
The console panel mirrors console.group/groupCollapsed/groupEnd and fills the %s %d
%o %c directives, so Pixi's deprecation warning shows its head, not only its stack.
E2E 12 guards against the addChild-on-Graphics warning, 28 counts shaders instead of
filters.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The + Property menu of the Properties node offered every shader's params
for every sprite. Each shader row now carries its shader name, and a name
shared by two shaders lists both: "brightness (Adjust colors, Neon glow)".
Sprite-owned rows stay unlabelled.
The note hangs off a map keyed by the def object, not by the name -- a
sprite property and a shader param may share a name (#63), and keying by
name would label the wrong row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No input, one Value param (step 0.05, drawn on the node), output `value`
as a float literal via floatLiteral (2 -> 2.0, NaN -> 0.0). No uniform,
no sprite row - that is the difference to Property.
The e2e negative check for "no behavior node in the palette" now uses
Boolean as its marker, and step 28 types the palette search first
because the palette scrolls from twelve entries on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Output gains a number input `alpha`; unconnected it reads the texel's
alpha, like `color` reads the texel. The epilogue premultiplies with the
resolved alpha. New `Texture alpha` node exposes texel.a for maths.
PortDef.inline=false keeps the alpha port without an inline field: every
number port is inline by default, and a typed default of 0 would have made
every shaded sprite invisible. The compiler's texel fallback is typed:
colour ports get rgb, number ports get texel.a.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shipped shader graph gains a `brightness - 1` offset node, so 1 stays
neutral and brightness lifts every channel by the same amount; contrast keeps
multiplying, saturation keeps mixing with the luma. Fixture, seeds, unit and
e2e expectations follow (twelve nodes, 1.25 -> #80c0ff).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>