Shaders: UV, Sample texture, Texture size, Time, math nodes, edge mode; five coordinate recipes (#78)
Development: 742 checks
1 parent
01d0602654
commit
288e156e68
2 files changed
+54
-9
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 # 730 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 742 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 728 checks as here.
|
||||
WebGL 2.0, and the same 742 checks as here.
|
||||
+52
-7
@@ -1,7 +1,8 @@
|
||||
A **shader** is an asset: a node graph that becomes the GLSL fragment a sprite is drawn with. Like a
|
||||
texture or a state machine it cannot be placed — a sprite *wears* it through its `Shader` property,
|
||||
and the numbers the shader declares appear as rows below that dropdown. There are no built-in
|
||||
shaders; the one effect the node set is sized for is the "Adjust colors" recipe below.
|
||||
shaders; the node set is sized for the recipes below: "Adjust colors", and the coordinate effects
|
||||
(flip, scroll, wave, pixelate, scanlines).
|
||||
|
||||
## The Shader tab
|
||||
|
||||
@@ -26,13 +27,19 @@ fed to a colour port is broadcast to all three channels. A colour cannot be fed
|
||||
|---|---|---|
|
||||
| **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. |
|
||||
| **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. |
|
||||
| **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. |
|
||||
| **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. |
|
||||
| **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). |
|
||||
| **Add**, **Subtract**, **Multiply**, **Divide** | in `a`, `b` (any) · out `result` | Arithmetic, per channel for colours. An unconnected input is the number typed on the node (0, or 1 for the second input of Multiply and Divide). |
|
||||
| **Mix** | in `a`, `b` (any), `t` (number) · out `result` | Blends from `a` to `b`: `t` = 0 is `a`, 1 is `b`, beyond that it extrapolates. |
|
||||
| **Luminance** | in `color` · out `value` | How bright a colour is (Rec. 709 weights), as one number. |
|
||||
| **Sin**, **Floor**, **Fract** | in `value` (any) · out `result` | The sine of a number in radians (one wave every 6.283); rounded down to a whole number; the part after the decimal point (2.75 becomes 0.75, a rising value becomes a sawtooth). Per channel for colours. |
|
||||
| **Step** | in `edge` (number), `value` (any) · out `result` | 0 while `value` is below `edge`, 1 from `edge` on: a hard cut. Per channel for colours. |
|
||||
|
||||
An input with nothing to type on it (the Output's two, Luminance's colour) that is left unconnected
|
||||
reads the texel: its colour for a colour port, its alpha for the Output's `alpha`. That is why the
|
||||
@@ -64,6 +71,35 @@ this graph as the asset "Adjust colors" (`tools/lib/shaders.mjs`).
|
||||
Measured on a flat `#4080ff` sprite: brightness 1.25 gives `#80c0ff`, contrast 0 gives `#808080`,
|
||||
saturation 0 gives `#7c7c7c` — all byte-exact against the formula.
|
||||
|
||||
## Coordinates and time
|
||||
|
||||
`UV` says where the pixel is, `Sample texture` reads the picture somewhere else, and the arithmetic
|
||||
in between is the effect. A `Property` in place of a constant makes the amount a sprite row a
|
||||
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.
|
||||
- **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
|
||||
cell size, `Divide` by the width, into `Sample texture.u`; the same for `v` and the height.
|
||||
- **Scanlines:** `UV.v` times `Texture size.height`, `Fract`, `Step` (edge 0.5), `Multiply` with
|
||||
`Texture color` into the Output's colour (or the step alone into its `alpha`).
|
||||
|
||||
A sprite wearing a graph the `Time` node reaches redraws on every tick: in play, and in the scene
|
||||
view as well, where nothing else moves. Time is the editor's clock, not the game's: it does not
|
||||
stop for a pause and does not start over with play. A graph the Time node does not reach draws
|
||||
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
|
||||
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.
|
||||
|
||||
## Wearing a shader
|
||||
|
||||
The sprite's `Shader` row is a picker over the project's shader assets (`None` is no shader: the
|
||||
@@ -101,10 +137,12 @@ 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 deliberately small — no channel split, no texture sampling at other coordinates, no
|
||||
time input; each is one node in `src/core/shader/nodes.ts` (a `ShaderNodeDef` with an `emit`) and
|
||||
one row in the table above.
|
||||
and it does not resample. Only sprites wear shaders; text objects do not. 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. 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
|
||||
|
||||
@@ -118,7 +156,14 @@ same parent: Pixi 8 lets only plain containers have children, and the node keeps
|
||||
Two Pixi facts the code leans on: a mesh's texture is bound only for Pixi's own shader, so
|
||||
`GraphShader.wear` hands it over itself; and Pixi caches whether a mesh batches and re-checks that
|
||||
only in a frame without structure changes, so switching a shader on or off calls `mesh.unload()`
|
||||
to drop that cache — otherwise a mesh that ever drew batched keeps drawing plain, shader ignored. The rows come from
|
||||
to drop that cache — otherwise a mesh that ever drew batched keeps drawing plain, shader ignored.
|
||||
The fragment starts with `#version 300 es`: that line is what makes Pixi compile the program as
|
||||
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
|
||||
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
|
||||
`schemaForObject` (`PropertySchema.ts`) append them to a sprite's schema, which is why both take
|
||||
the project.
|
||||
Reference in new issue
Block a user