Shaders: Clamp is the default edge, and what the sheet itself carries (#81)

admin committed 2026-09-13 21:33:47 +02:00
1 parent 8916a38cd6
commit 7ed7bc85b5
2 files changed
+15 -7

No files matched your search

+2 -2
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
node tools/seed-e2e.mjs && node tools/e2e.mjs # 784 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 785 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
@@ -151,4 +151,4 @@ widths to find click points. The **render device is not in this repo** — `/dev
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
WebGL 2.0, and the same 784 checks as here.
WebGL 2.0, and the same 785 checks as here.
+13 -5
@@ -31,7 +31,7 @@ fed to a colour port is broadcast to all three channels. A colour cannot be fed
| **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: *Mirror* (the default, and what every picture has without a shader), *Repeat* for tiles, *Clamp* to stretch the border pixel. |
| **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. |
| **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. |
| **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). |
@@ -81,7 +81,7 @@ behavior can write.
- **Flip:** `Subtract` with 1 typed in as `a` and `UV.u` as `b`, into `Sample texture.u`.
- **Scroll:** `Add` `UV.u` and a Property *offset* (or `Time` times a speed), into
`Sample texture.u`. Set the Output's Edge to *Repeat*, so the picture comes round again; with
*Mirror* it turns back on itself, with *Clamp* the border pixel stretches.
*Clamp* (the default) the border pixel stretches, with *Mirror* the picture turns back on itself.
- **Wave:** `UV.u` times 20, `Add` `Time` times 4, `Sin`, times 0.02, `Add` `UV.v`, into
`Sample texture.v`.
- **Pixelate:** `UV.u` times `Texture size.width`, `Divide` by the cell size, `Floor`, times the
@@ -97,7 +97,9 @@ once, like any other.
Measured in the checks (step 28): a `Step` at 0.5 on `u` is red left of the middle and blue right
of it, on `v` red above and blue below; on a picture red on the left and green on the right,
sampling at `1 - u` swaps the halves; `u + 0.5` with Edge *Repeat* shows green on the left and,
past the border, red again on the right, with *Mirror* green on both sides; with the clock held at
past the border, red again on the right, with *Mirror* green on both sides; `u + 0.8` reaches far
enough to separate the two remaining modes — with nothing set on the Output the right side is green
(*Clamp*), with *Mirror* it is red; with the clock held at
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.
@@ -142,7 +144,9 @@ and it does not resample. Only sprites wear shaders; text objects do not. The no
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. What no shader can do is draw outside the sprite's rectangle: the
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).
## In the code
@@ -165,7 +169,11 @@ The fragment starts with `#version 300 es`: that line is what makes Pixi compile
GLSL ES 3.00 (`GlProgram` looks for it in the fragment); without it `texture()` is only a
`#define` and `textureSize` does not exist. The Output's Edge is a second cut of the picture with
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. Time is `ShaderLibrary.now()`, held still
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
`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
editor tick (`WorldViewport.animate`). The rows come from
`shaderParamDefs(graph)` in `src/core/properties/shaders.ts`; `schemaForAsset` and