From 6e7ecb02604ba2300fd20d956e61d4fdfb765997 Mon Sep 17 00:00:00 2001 From: meleki Date: Fri, 2 Oct 2026 20:33:30 +0200 Subject: [PATCH] Shaders: Padding and the Edge mode Transparent give room around the picture (#69) --- Development.md | 2 +- Shaders.md | 57 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/Development.md b/Development.md index b5ad7a3..372072b 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 # 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 diff --git a/Shaders.md b/Shaders.md index 0880e5c..d6be40f 100644 --- a/Shaders.md +++ b/Shaders.md @@ -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