Shaders: moving the corners with the Vertex output node (#79)

A new section on the Vertex output node, its Subdivisions and what may feed it, with the measured frame rates of a grid against the plain quad. Development carries the new e2e total of 1699.
admin committed 2026-10-03 00:14:09 +02:00
1 parent 7095ad8157
commit 8a9c36f21e
2 files changed
+64 -4

No files matched your search

+1 -1
@@ -18,7 +18,7 @@ php tests/php/accounts.test.php # the API's accoun
# has and the web server's container has not (SKIP there)
php tests/php/snapshots.test.php # the snapshot store (#231), the same way
php tests/php/releases.test.php # the release store (#266), the same way
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1695 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1699 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
+63 -3
@@ -80,6 +80,7 @@ plugged into a colour port, straight from a `Number` or a `Luminance` as well.
| **Texture size** | out `width`, `height` | The picture in pixels: of the frame, when the sprite wears one. |
| **Time** | out `value` | Seconds since the editor started; see *Coordinates and time* below. |
| **Output** | in `color`, `alpha` (number) | What the sprite is drawn with: the colour, clamped to 0-1, and the opacity, clamped too. Each input left unconnected stays the texture's. One per graph. Its *Edge* setting says how the picture continues past its border when sampled outside it: *Clamp* (the default) holds the border pixel, *Mirror* turns the picture back on itself, *Repeat* starts it over for tiles, *Transparent* leaves everything outside empty. Its *Padding* is room around the picture to draw into, in pixels of the picture per side (see *Room around the picture*). |
| **Vertex output** | in `x`, `y` (number) | Moves the corners of the grid the picture is drawn on: `x` to the right and `y` down, in pixels of the picture; the picture bends with them. One per graph, and optional: without it the wearer draws on a plain rectangle. Its *Subdivisions* setting is how many cells the grid has each way, 1 to 64, 8 by default; 1 is the plain rectangle (see *Moving the corners*). |
| **Property** | out `value` | Declares a number the sprite sets: `Name` is the row below the sprite's Shader dropdown and the uniform in the fragment; `Default`, `Min`, `Max` are the row's range. A name is one word (letters, digits, `_`), unique in the graph, and not one of the rows a sprite has anyway — `enabled`, `scale`, `rotation`, `mirrorX`, `mirrorY`, `width`, `height`, `color`, `texture`, `shader`. Both rows would sit on the same key and the uniform would read the sprite's width instead of the declared default, so the node says *"width" is a sprite property* and nothing compiles. |
| **Color** | out `color` | A constant colour, picked in the inspector. |
| **Number** | out `value` | A constant number, typed on the node. Handy where the same value feeds more than one input, or where an input has nothing to type on (Luminance, the Output's alpha). |
@@ -192,6 +193,48 @@ the border colour fills the margin and the Output carries the hint; a text's fil
units past its rectangle and no further; particles grow about their centres; a sliced sprite gets
a ring of 4 border pixels and keeps its 8-pixel border.
## Moving the corners
Everything above decides what colour a pixel gets. A **Vertex output** node decides where the
picture is drawn: what feeds its `x` and `y` runs once for every corner of a grid laid over the
wearer and moves that corner, in pixels of the picture, `x` to the right and `y` down. The picture
is pinned to the corners, so it bends with them: a wave, a flag, a wobble. Between two corners the
move is a straight blend of theirs.
Whether the wearer is a plain rectangle or a grid is said in one place, the node's *Subdivisions*:
the number of cells each way. 8 is an 8 by 8 grid with 81 corners, and 1 is the rectangle with its
four. There is no switch beside it. A shader without a Vertex output node draws on the one
rectangle all sprites share and costs nothing extra. The setting is the shader's, not the
sprite's: how fine the grid has to be depends on the effect, so every wearer of the shader gets
the same grid, whatever its size.
A number feeding `x` moves the whole picture. `UV.v` through a `Step` at 0.5, times 16, into `x`
moves only the lower half of a 32-pixel picture half its width to the right; with *Subdivisions* 1
there are no corners in between, and the whole picture leans instead. For a wave, feed `y` with
`Sin` of `UV.u` times a frequency plus `Time`, times the height in pixels.
What may feed it is what a corner can know: `UV` (the corner's own place on the picture), `Time`,
`Property`, `Number` and the calculating nodes. A corner has no pixel, so `Texture color`,
`Texture alpha`, `Sample texture`, `Texture size` and `Color` cannot feed it; the Vertex output
then says "Not available per corner: …" and the shader does not compile. The same node may feed
both outputs: one `Property` can drive the bend and the colour.
Who it applies to:
- **Sprite and text:** the grid, with the pixel counted as for Padding (a pixel of the frame, a
unit of the text).
- **Nine-sliced sprite and particles:** not at all. A sliced sprite keeps its nine parts, a
particle its four corners, and both draw as if the node were not there.
With Padding the grid covers the margin as well. Like the margin, the bend is drawn only: clicking,
the selection frame, the size rows and colliders keep to the rectangle, and a wearer is drawn even
when its rectangle is off screen, because its corners may reach in.
Measured in the checks (steps 28 and 30): a Vertex output fed 16 draws a 32-pixel picture half a
sprite to the right; the cut on `v` moves only the lower half with 8 subdivisions and leans the
upper half along with 1; `Texture alpha` into `x` is refused at the node; a text's filled quad
stands 6 units to the right.
## Wearing a shader
The sprite's `Shader` row is a picker over the project's shader assets (`None` is no shader: the
@@ -251,8 +294,22 @@ picture sampled past its border shows what Edge says, never the sprite's neighbo
frame is a texture of its own. Without a shader the question never comes up — the quad's own
coordinates run 0 to 1 and never leave the picture — which is why the default is the mode that
matches what you see there: *Clamp*. Outside the sprite's rectangle a shader draws only as far as
its Padding reaches, and what it draws with is still a rectangle: the quad has four corners and
stays a quad (#79 is the vertex-side step that would change that).
its Padding reaches and its Vertex output moves the corners.
A grid has a price of its own, because every corner is computed and every wearer carries its own
copy of the grid. One run per column with `tools/perf-shaders.mjs` (`GM_PERF_SUBDIVISIONS`) on the
same HD 630, the same scene, a Vertex output with nothing feeding it, in frames per second:
| sprites | 200 | 400 | 800 | 1600 |
| --- | --- | --- | --- | --- |
| no shader | 60 | 60 | 60 | 41 |
| shader, no Vertex output | 60 | 60 | 58 | 36 |
| Subdivisions 8 | 59 | 58 | 46 | 27 |
| Subdivisions 32 | 54 | 57 | 41 | 23 |
Up to 400 wearers a grid hardly shows; at 800 it costs a quarter of the frame rate. The 54 at 200
with 32 subdivisions is below the 57 at 400, so single values here are good to a few frames, not
to one.
## In the code
@@ -264,7 +321,10 @@ particle compiler). The type of a `result` has one source: `liftedType` in the c
one and the same value* in [Behaviors](Behaviors)) is the same rule for the port the editor draws.
A test runs every node with an open result through every mix of numbers and colours and compares
the two. `src/viewport/shaders.ts` wraps the result in a Pixi
`Shader` for the sprite's mesh (`GraphShader`, with a vertex stage of its own) and
`Shader` for the sprite's mesh (`GraphShader`, with a vertex stage of its own).
A Vertex output is a second walk of the same graph (`ShaderVertex` in `compile.ts`): its lines go
into the sprite's vertex stage (`cornerVertex` in `shaders.ts`), which reads the same uniforms, and
the grid is `gridQuad` in `src/viewport/quadPadding.ts`.
`src/viewport/ShaderLibrary.ts` caches the compiled graph per asset, keyed by a fingerprint of what
the fragment depends on — node ids, types, params, declared ports, and which port feeds which — so
moving, collapsing or folding compiles nothing again; a changed param or link does, once