diff --git a/Development.md b/Development.md index ff9fbe3..7e94ab0 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 # 1712 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1716 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/Particles.md b/Particles.md index 885ce4e..237e92e 100644 --- a/Particles.md +++ b/Particles.md @@ -118,7 +118,8 @@ source draws at once: the particles are drawn into a picture of a fixed frame, * shader per particle cannot: a glow or blur where particles overlap, an outline round the whole cloud, a ripple through all of it. It is an ordinary shader asset; its `u`/`v` run from 0 to 1 across the frame and never jump, however the cloud changes, and `Texture color`, `Texture alpha` -and `Sample texture` read the picture of the particles (`Texture size` is that picture in pixels). +and `Sample texture` read the picture of the particles (`Texture size` is *Field width* by *Field +height*: a pixel of a field shader is a unit of the frame). The frame is the edge: whatever flies past it is cut off, so make it as large as the effect reaches. The scene view draws it as a dashed rectangle round the selected source. It turns and grows with the source. @@ -129,13 +130,17 @@ over it. A name both of them declare is one row, and both read it. The field sha stay put, as with a nine-sliced sprite. The picture has as many pixels as the frame covers on screen, so it stays sharp at any zoom, but at -most 2048 per side. A source whose frame is off screen draws no picture at all. What it costs is one +most 2048 per side. Still the shader counts in units of the frame, not in those pixels: a `Blur` of +4 reaches 4 units at 50 % and at 400 %, and a step of `1 / Texture size` is one unit. Zoomed far in, +the samples of a Blur therefore lie several pixels apart. A source whose frame is off screen draws +no picture at all. What it costs is one extra pass per source, sized by the frame, not by the number of particles; see *What it costs* in [Shaders](Shaders) for the measurement. Measured in the checks (step 31): a field shader that changes nothing draws the field where it was and the right way up, with a picture of 256 pixels at zoom 1; a flat red fills the frame 100 units -off the source where no particle is, and stops before 140; taken off again, the particles draw as +off the source where no particle is, and stops before 140; a Blur of 8 across a flat picture fades +the frame's edge over 8 units at 50 % and at 200 % alike; taken off again, the particles draw as before. A **behavior** can start a source over with the `Emit` node (its `source` input names which one): in *Burst* diff --git a/Shaders.md b/Shaders.md index 8e74122..8245992 100644 --- a/Shaders.md +++ b/Shaders.md @@ -74,14 +74,15 @@ plugged into a colour port, straight from a `Number` or a `Luminance` as well. | Node | Ports | What it does | |---|---|---| -| **Texture color** | out `color` | The sprite's own pixel before the shader: its texture tinted by its colour, straight (unpremultiplied). | -| **Texture alpha** | out `value` | The opacity of the sprite's own pixel: 0 transparent, 1 opaque. | +| **Texture color** | in `picture` · out `color` | The sprite's own pixel before the shader: its texture tinted by its colour, straight (unpremultiplied). Fed a Pass's `picture`, that picture's pixel at the same spot (see *Drawing in passes*). | +| **Texture alpha** | in `picture` · out `value` | The opacity of the sprite's own pixel: 0 transparent, 1 opaque. Fed a Pass's `picture`, that picture's. | | **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. | +| **Sample texture** | in `u`, `v` (number), `picture` · out `color`, `alpha` | The picture at another spot, tinted like Texture color. Unconnected, `u` and `v` are this pixel's own and `picture` is the sprite's. Past the border the picture continues as the Output's *Edge* says. | +| **Blur** | in `radius` (number, 2), `u`, `v` (number), `picture` · 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 and `picture` is the sprite's. See *Softening the picture*. | +| **Texture size** | in `picture` · out `width`, `height` | The picture in pixels: of the frame, when the sprite wears one. Fed a Pass's `picture`, that one's, Padding included. | | **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*). | +| **Pass** | in `color`, `alpha` (number) · out `picture` | Draws what feeds it into a picture of its own first; the nodes with a `picture` input read that picture when it feeds them (see *Drawing in passes*). Unconnected, `color` and `alpha` are the texel's. Its *Resolution* is *Full* (the default), *Half* or *Quarter* of the sprite's picture per side. Up to 8 per graph. | | **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 property** | out `color` | Declares a colour the sprite sets: the same as **Property**, with a colour row (swatch and hex) instead of a number row and a `vec3` uniform. `Default` is a colour, white unless picked; there is no `Min` and `Max`. Placed from the palette it is called `tint`, then `tint2`. Number and colour properties share one set of names: a second node of the same name says *Another property is named "tint"*. Like `Color` it cannot feed the Vertex output. | @@ -209,13 +210,14 @@ go where a `Sample texture`'s would; for the plain blurred sprite, both into the 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). + pixel. Wider blurs and bloom are drawn in passes (see *Drawing in passes*). - **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. +- **The pixel** is the wearer's picture's: a pixel of the frame for a sprite, and of the rastered + text for a text (which depends on the zoom with *Linear*). In a field shader a pixel is a unit of + the frame: the field's picture has as many pixels as the frame covers on screen, but a radius of + 4 reaches 4 units of the frame at every zoom. - **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. @@ -230,6 +232,68 @@ horizontally through a Property, goes to 3/4, 2/3 and 5/9 red at 0.5, 1 and 4 wi 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. +## Drawing in passes + +A **Pass** node (#301) cuts the graph in two. What feeds its `color` and `alpha` is drawn first, +into a picture of its own; its `picture` output then feeds the nodes that read a picture +(`Texture color`, `Texture alpha`, `Sample texture`, `Blur`, `Texture size`), and they read that +picture instead of the sprite's. A `picture` input left unconnected is the sprite's own picture, so +the original is always there beside any pass. A picture plugs only into a `picture` input. + +**A wide blur in two passes:** `Blur` *Horizontal* into a Pass (`color` and `alpha` both), its +`picture` into a second `Blur` *Vertical*, that one into the Output. That is the same picture as +one `Blur` *Both* of the same radius, for 2*(2r+1) samples per pixel instead of (2r+1)*(2r+1). + +**Wider than 8:** a `Blur` counts its radius in pixels of the picture it reads. Put an open Pass at +*Half* first (nothing into its `color` and `alpha`: it is the picture at half size), feed its +`picture` to the `Blur` *Horizontal*, and set the second Pass to *Half* as well. Both Blurs then +read pixels twice the size, and a radius of 8 reaches 16 pixels of the sprite's picture; *Quarter* +reaches 32. Give the Output a Padding as wide as that. With *Nearest* the shrinking Pass skips +every other pixel and thin details flicker; a `Blur` of 1 into it, instead of leaving it open, +smooths that out. + +**Bloom**, the bright parts glowing over the picture: + +1. `Texture color` into `Luminance`, that into a `Step` (its `edge` is the threshold, say 0.7). + The Step times `Texture color` is the bright colour, the Step times `Texture alpha` its opacity. + Both into a Pass at *Half*. +2. That picture into a `Blur` *Horizontal* (radius 8), both outputs into a second Pass at *Half*. +3. That picture into a `Blur` *Vertical* (radius 8): the glow, with its `color` and `alpha`. +4. Glow `color` times glow `alpha` (times a strength), `Add`ed to `Texture color`, into the + Output's `color`. For a glow beyond the sprite, the Output's `alpha` as in the glow above (`Mix` + with `a` the glow's `alpha`, `b` 1, `t` `Texture alpha`), Edge *Transparent* and Padding. + +What a pass's picture is: + +- **The frame** is the sprite's picture and its Padding, the same rectangle the sprite draws + into. Padding is set once, at the Output, and holds for every pass; `Texture size` fed a pass + counts the margin in. +- **Resolution** *Full* has as many pixels as the sprite's picture, *Half* and *Quarter* a half + and a quarter of them per side. A smaller picture is cheaper and blurs further at the same radius; + it is read with the picture's own filter, so *Linear* smooths it and *Nearest* shows its pixels. +- **Untinted.** A pass sees the picture without the sprite's colour; the colour comes on when the + last part (the one into the Output) reads a picture. A pass's picture added to the original is + therefore tinted once, not twice. For a blur that makes no difference; for a threshold like the + Step in Bloom it means the threshold looks at the untinted picture. +- **Past its frame** it continues as the Output's *Edge* says. +- **Drawn when needed.** A pass is drawn again when the picture, its size, the Padding or a + property changes; a still sprite draws its passes once. With `Time` in the graph it is drawn + every frame. Every wearer has pictures of its own. +- **Limits:** up to 8 passes per graph (*At most 8 passes* at the node otherwise) and 2048 pixels + per side. A pass that reads its own picture says *Feeds itself*. A corner has no picture, so a + Pass cannot feed the Vertex output. + +Who draws passes: a sprite (a sliced one over its unsliced picture), a text, a particle source and +a field shader (over the field's picture), and the preview in the Shader tab. A particle source +draws its passes once for all its particles, from the particles' picture, and the particle's own +colour comes on at the end. + +Measured in the checks (step 28): one white pixel on a 16-pixel picture, blurred by 2 across, a +Pass and blurred down, is the same 5x5 spot of 1/25 each that a `Blur` *Both* draws (difference +0.000); on a sprite tinted grey, the red half's pass picture added to the original comes out at +231/60/60, red once halved in each; two Passes in a row, the first the very program of an earlier +graph's only Pass, still show the picture and not white. + ## Moving the corners Everything above decides what colour a pixel gets. A **Vertex output** node decides where the @@ -252,7 +316,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`, `Blur`, `Texture size`, `Color` and `Color property` cannot feed it; the Vertex output +`Texture alpha`, `Sample texture`, `Blur`, `Texture size`, `Pass`, `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. @@ -332,7 +396,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 second picture; each is one node in +small — no channel split; 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 @@ -387,8 +451,37 @@ Output, in frames per second. Without a shader the same scenes held 59.0 to 60.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. +289 samples for each of its 65536 pixels and already costs 8 frames a second. + +The same blur in two passes (#301: across, a Pass, down) is 2*(2r+1) samples per pixel, plus a +change of render target per pass. One run per row with `GM_PERF_PASSES=1` in both tools, the same +HD 630 and scenes; the one-pass rows were measured again in the same sitting. Without a shader +the scenes held 58.3 to 60.2. Radius 16 is radius 8 on two passes at *Half* behind an open Pass at +*Half* (see *Wider than 8*), so three draws instead of two. On sprites the radius is tied to +Time, so their Pass is drawn every frame, as a field's always is; a still sprite draws it once and +then only pays the last part. + +| sprites of 48x48 | 100 | 400 | 800 | +| --- | --- | --- | --- | +| radius 4, one pass | 53.1 | 50.7 | 35.5 | +| radius 8, one pass | 49.4 | 24.2 | 13.9 | +| radius 4, two passes | 59.1 | 45.6 | 24.1 | +| radius 8, two passes | 56.2 | 46.0 | 24.5 | +| radius 16, three passes at Half | 51.5 | 27.1 | 14.2 | + +| field shaders, 256x256 pixels each | 1 | 10 | 50 | +| --- | --- | --- | --- | +| radius 4, one pass | 59.0 | 49.7 | 27.2 | +| radius 8, one pass | 54.5 | 35.0 | 11.4 | +| radius 4, two passes | 60.0 | 55.1 | 49.2 | +| radius 8, two passes | 59.9 | 51.3 | 41.3 | +| radius 16, three passes at Half | 59.9 | 55.6 | 44.4 | + +On a large picture two passes pay off: 50 fields at radius 8 go from 11 to 41 frames a second, and +radius 16 at *Half* costs only a little more than that. On small sprites the change of target costs +more than the samples save: the radius hardly matters there, at radius 4 one pass is faster from +400 sprites on, and radius 16, with its third draw, is as slow at 800 sprites as radius 8 in one +pass. Passes are for wide blurs and large pictures, not for every soft edge. ## In the code @@ -408,6 +501,23 @@ the grid is `gridQuad` in `src/viewport/quadPadding.ts`. 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. +A Pass is where a walk stops (`cut` in `emitGraph.ts`): the compiler's `passDrawer` walks what feeds +it as a fragment of its own, drawing any Pass it meets in turn first, so `CompiledShader.passes` is +in drawing order and `uPass` is the i-th picture. A pass's fragment has `vColor` as a constant 1, +which is what leaves it untinted. Every program reads a picture through `texelAt(sampler, uv)`; a +pass's picture covers the Padding too, so a place on the sprite's picture becomes one on the pass's +through `passUv` and the uniform `uPassPad`. `src/viewport/shaderPasses.ts` (`PassPictures`) holds +one render texture per pass for each wearer, sized by `passPixels` in `quadPadding.ts`, and draws +them only when the key of picture, size, Padding and uniform values changed (a field's picture is a +render texture that changes under the same id, so it is drawn every time). Destroyed in that order: +the shader first, which lets go of the pictures, then the pictures; the other way round Pixi warns +that a texture was destroyed while still bound. Pixi builds the code that binds a program's +resources once, from the first shader that draws with it, and finds them by position after that; +since two graphs often share a pass's program, every shader declares all 8 pass slots and its +uniform group, even an empty one. Otherwise the second graph reads a placeholder and draws white. +`Blur` and `Texture size` count the picture's size through the uniform `uPixelScale`: 1 on every +wearer but a field, where it is the frame's units per pixel of its picture (`unitsPerPixel` in +`src/viewport/fieldFrame.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