Describe drawing a shader in passes (#301)
- Shaders: the Pass node in the catalogue, the section Drawing in passes with the bloom recipe and Wider than 8, the measurements and In the code. - Particles: a field shader counts a pixel as a unit of its frame, so a Blur reaches as far at every zoom. - Development: 1716 editor checks.
1 parent
e28129747e
commit
d36fc4ad1c
3 files changed
+132
-17
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 # 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
|
||||
|
||||
+8
-3
@@ -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*
|
||||
|
||||
+123
-13
@@ -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<i>` 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
|
||||
|
||||
Reference in new issue
Block a user