Shaders: Padding and the Edge mode Transparent give room around the picture (#69)

admin committed 2026-10-02 20:33:30 +02:00
1 parent d554dfddbf
commit 6e7ecb0260
2 files changed
+53 -6

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 # 1680 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1687 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
+52 -5
@@ -79,7 +79,7 @@ plugged into a colour port, straight from a `Number` or a `Luminance` as well.
| **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. |
| **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. |
| **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*). |
| **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** | out `color` | A constant colour, picked in the inspector. |
| **Number** | out `value` | A constant number, typed on the node. Handy where the same value feeds more than one input, or where an input has nothing to type on (Luminance, the Output's alpha). |
@@ -153,6 +153,45 @@ enough to separate the two remaining modes — with nothing set on the Output th
0.2 s and 0.7 s a `Fract` into a `Step` gives red and then blue; released, the pixel of a
`Fract(Time)` mix changes between two reads with nothing running.
## Room around the picture
A shader draws where its wearer's rectangle is and nowhere else, so an outline, a glow or a shadow
is cut off at the border. *Padding* on the Output node gives it room: that many pixels of the
picture on every side. The picture stays where it is and as big as it is; `UV` simply runs on past
0 and 1 in the margin, 8 pixels of padding on a 32-pixel picture from -0.25 to 1.25.
What the margin reads is the Output's *Edge*, for `Texture color`, `Texture alpha` and unconnected
inputs just as for a `Sample texture`. For anything that should stand free around the picture that
is *Transparent*: outside the picture there is nothing, and only what the graph puts there shows.
With *Clamp* the border pixel goes on through the margin, which is rarely meant, so the Output says
"Padding with Clamp smears the border" while it is set that way. The shader still compiles.
The simplest case: a `Sample texture` reading at `UV.u` minus 0.25, its `color` and its `alpha`
both into the Output, shows the picture a quarter further right. Without padding it ends at the
sprite's right border; with Padding 8 on a 32-pixel picture and Edge *Transparent* all of it is
there, and left of it the sprite is empty. Take the `alpha` from the sample as well: the pixel's
own is 0 everywhere in the margin.
The pixel it counts in is the wearer's own:
- **Sprite with a picture:** a pixel of the frame, however big the sprite draws it.
- **Sprite without one:** a unit of its size.
- **Nine-sliced sprite:** a border pixel, drawn at the texture's nine-slice scale. The margin is a
ring around the nine parts; the parts themselves do not move.
- **Text:** a unit of the text, whatever resolution the zoom had it rastered at.
- **Particles:** a pixel of the particle picture, around every particle; without a picture a
particle's size counts as that many pixels.
The margin is drawn only. Clicking, the selection frame, the size rows and colliders keep to the
rectangle without it. The preview in the Shader tab leaves room for the margin and shows it.
Measured in the checks (steps 28, 30, 31 and 37): with Padding 8 and *Transparent* on a 32-pixel
picture the picture keeps its place and the margin stays the background; the picture sampled a
quarter to the right ends at the rectangle without padding and is drawn past it with; with *Clamp*
the border colour fills the margin and the Output carries the hint; a text's filled quad reaches 6
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.
## Wearing a shader
The sprite's `Shader` row is a picker over the project's shader assets (`None` is no shader: the
@@ -205,14 +244,15 @@ filter pass per sprite (up to #77) the same counts measured 56 / 54 / 48 / 34 /
texture itself, so pixel art stays sharp: measured in play at a fractional camera scale (1.43
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. Only sprites wear shaders; text objects do not. 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
`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
coordinates run 0 to 1 and never leave the picture — which is why the default is the mode that
matches what you see there: *Clamp*. What no shader can do is draw outside the sprite's rectangle: the
quad has four corners and stays a quad (#79 is the vertex-side step that would change that).
matches what you see there: *Clamp*. Outside the sprite's rectangle a shader draws only as far as
its Padding reaches, and what it draws with is still a rectangle: the quad has four corners and
stays a quad (#79 is the vertex-side step that would change that).
## In the code
@@ -241,7 +281,14 @@ GLSL ES 3.00 (`GlProgram` looks for it in the fragment); without it `texture()`
that address mode (`TextureLibrary.get(ref, edge)`), because WebGL takes the sampler state from the
texture source, not from a style the shader hands over. That cut is only made where it can show:
a graph whose `Sample texture` reads nothing but its own pixel draws with the plain sheet, whatever
the Edge says, so the common shader costs no second copy of the picture. The sheet itself carries
the Edge says, so the common shader costs no second copy of the picture. *Transparent* needs none
either: no sampler state makes the outside empty, so the fragment cuts it itself (every read of the
picture goes through `texelAt`, which is the plain `texture()` in the other modes). Padding is
geometry (`src/viewport/quadPadding.ts`): the quad's corners and its UVs grow by the same fraction,
a sliced sprite gets a ring of spans around its parts (`paddedSpans` in `nineSlice.ts`), and for
particles, whose quads Pixi builds, the vertex stage does the same with a `uPad` uniform. A graph
with padding takes the Edge's cut of the picture even without a shifted sample, because every
pixel of the margin reads past the border. The sheet itself carries
`mirror-repeat` (`SHEET_EDGE`) and cannot carry `clamp-to-edge`: Pixi rewrites that to `repeat` on
every texture a Graphics fill draws with. Time is `ShaderLibrary.now()`, held still
by `__gm.shaderTime(seconds)` for the checks; a view that drew a Time shader redraws on every