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) # 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/snapshots.test.php # the snapshot store (#231), the same way
php tests/php/releases.test.php # the release store (#266), 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 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 start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result ./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. | | **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. | | **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. | | **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. | | **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. | | **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*). | | **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 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. 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 ## Moving the corners
Everything above decides what colour a pixel gets. A **Vertex output** node decides where the 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`, 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`, `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 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. 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 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, 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 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 `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 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 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 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. 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 ## In the code
`src/core/shader/nodes.ts` is the catalogue, `src/core/shader/compile.ts` turns a graph into the `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 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 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`. 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 `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 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 moving, collapsing or folding compiles nothing again; a changed param or link does, once