diff --git a/Development.md b/Development.md index 1463988..ff9fbe3 100644 --- a/Development.md +++ b/Development.md @@ -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 diff --git a/Shaders.md b/Shaders.md index f5689d9..8e74122 100644 --- a/Shaders.md +++ b/Shaders.md @@ -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