Describe the Blur node and what it costs (#300)

- Shaders: catalogue row, section Softening the picture with the
  glow recipe, Blur in the list of nodes a corner cannot read, the
  measured tables under What it costs, and a note in In the code.
- Shaders: drop "no colour-typed property", which #298 made untrue.
- Development: e2e total 1712.
admin committed 2026-10-03 13:11:47 +02:00
1 parent 315ee9215c
commit e28129747e
2 files changed
+62 -3

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 # 1707 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1712 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
+61 -2
@@ -78,6 +78,7 @@ plugged into a colour port, straight from a `Number` or a `Luminance` as well.
| **Texture alpha** | out `value` | The opacity of the sprite's own pixel: 0 transparent, 1 opaque. |
| **UV** | out `u`, `v` | Where this pixel is on the sprite's picture, 0 to 1: `u` left to right, `v` top to bottom. |
| **Sample texture** | in `u`, `v` (number) · out `color`, `alpha` | The picture at another spot, tinted like Texture color. Unconnected, `u` and `v` are this pixel's own. Past the border the picture continues as the Output's *Edge* says. |
| **Blur** | in `radius` (number, 2), `u`, `v` (number) · out `color`, `alpha` · *Kind*, *Direction* | The picture softened around a spot, tinted like Texture color. `radius` is in pixels of the picture, 0 to 8, fractions allowed. *Kind* Box (default) weighs every pixel alike, Gaussian the middle more; *Direction* Both, Horizontal or Vertical. Unconnected, `u` and `v` are this pixel's own. See *Softening the picture*. |
| **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*). |
@@ -195,6 +196,40 @@ 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.
## Softening the picture
`Blur` (#300) averages the pixels around a spot in one pass: with *Direction* Both a square of
(2r+1)*(2r+1) pixels, with Horizontal or Vertical one row or column of 2r+1. Its `color` and `alpha`
go where a `Sample texture`'s would; for the plain blurred sprite, both into the Output.
- **No dark seam.** The average is taken over the premultiplied pixels and only then turned back
into a colour, so a transparent neighbour lowers the opacity and adds no black. Averaging the
`color` of several `Sample texture` nodes by hand does add it.
- **Fractions.** The outermost ring counts by the part of the radius that reaches it: 0.5 weighs
the neighbours half, 1 fully. A radius from a `Property` can therefore be animated smoothly, and
changing it compiles nothing again.
- **Radius 0** is the picture unchanged. Above 8 the radius stays at 8: that is 289 samples per
pixel. Wider blurs and bloom need several passes (#301).
- **Past the border** the picture continues as the Output's *Edge* says. For a soft edge that
stands free around the sprite, set *Transparent* and give it *Padding* at least as wide as the
radius.
- **The pixel** is the wearer's picture's: a pixel of the frame for a sprite, of the rastered text
for a text (which depends on the zoom with *Linear*), and of the field picture, that is a screen
pixel, for a field shader.
- **Kind:** *Box* weighs every pixel alike; *Gaussian* weighs the middle more (sigma = radius / 2)
and looks softer at the same radius and the same cost.
A glow lays the blurred shape behind the picture. Colour: `Mix` with `a` the glow colour, `b`
`Texture color` and `t` `Texture alpha`. Opacity: `Mix` with `a` the Blur's `alpha`, `b` 1 and `t`
`Texture alpha`. Both into the Output, with Edge *Transparent* and Padding for the glow to spread
into.
Measured in the checks (step 28), on a 32-pixel picture red on the left and green on the right,
both *Nearest*: radius 0 draws the picture byte for byte; the last red pixel, softened
horizontally through a Property, goes to 3/4, 2/3 and 5/9 red at 0.5, 1 and 4 with no compile in
between; Vertical keeps it red, Gaussian keeps more red than Box; with Edge *Transparent* the first
pixel stays red at 5/9 opacity; one white pixel blurred by 1 is a 3x3 spot of 1/9 each.
## Moving the corners
Everything above decides what colour a pixel gets. A **Vertex output** node decides where the
@@ -217,7 +252,7 @@ there are no corners in between, and the whole picture leans instead. For a wave
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`, `Color` and `Color property` cannot feed it; the Vertex output
`Texture alpha`, `Sample texture`, `Blur`, `Texture size`, `Color` and `Color property` 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.
@@ -297,7 +332,7 @@ texture itself, so pixel art stays sharp: measured in play at a fractional camer
screen pixels per world unit), a *Nearest* texture keeps its hard border pixel for pixel with a
neutral shader on, and brightness 0.5 darkens every one of those pixels — the shader draws there,
and it does not resample. The node set is still
small — no channel split, no colour-typed property, no second picture; each is one node in
small — no channel split, no second picture; each is one node in
`src/core/shader/nodes.ts` (a `ShaderNodeDef` with an `emit`) and one row in the table above. A
picture sampled past its border shows what Edge says, never the sprite's neighbour on the sheet: a
frame is a texture of its own. Without a shader the question never comes up — the quad's own
@@ -335,6 +370,26 @@ Up to 10 sources nothing shows; at 50 the field shaders cost about 1.6 frames a
cap hides what a pass costs inside a frame that is not full, and a graph that samples the picture
many times (a blur) adds its own price per pixel on top of the pass.
A `Blur` (#300) pays per pixel: (2r+1)*(2r+1) samples with *Direction* Both. One run per row with
`GM_PERF_BLUR` in both tools on the same HD 630, a shader of nothing but a Box Blur into the
Output, in frames per second. Without a shader the same scenes held 59.0 to 60.1.
| sprites of 48x48 | 100 | 400 | 800 |
| --- | --- | --- | --- |
| radius 2 | 59.8 | 58.4 | 55.6 |
| radius 4 | 53.1 | 51.4 | 36.3 |
| radius 8 | 46.6 | 24.4 | 14.4 |
| field shaders, 256x256 pixels each | 1 | 10 | 50 |
| --- | --- | --- | --- |
| radius 2 | 60.2 | 56.0 | 49.0 |
| radius 4 | 60.1 | 49.0 | 27.1 |
| radius 8 | 52.4 | 34.9 | 11.4 |
Radius 2 is cheap. From 4 on the samples set the price: a single field of 256x256 at radius 8 is
289 samples for each of its 65536 pixels and already costs 8 frames a second. Two passes (#301)
would bring that to 34 samples per pixel.
## In the code
`src/core/shader/nodes.ts` is the catalogue, `src/core/shader/compile.ts` turns a graph into the
@@ -349,6 +404,10 @@ the two. `src/viewport/shaders.ts` wraps the result in a Pixi
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`.
`Blur` writes no loop of its own: the loop is `blur()` in the prelude (`BLUR_FN`, only there when
a Blur is reached), and Kind and Direction are its arguments. A node with expensive work declares
it as `prepare` (a vec4 the compiler puts into one local), so `color` and `alpha` read one result
instead of blurring twice.
`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