Shaders: the shader asset, its tab, the node set and the Adjust colors recipe (#62)

admin committed 2026-09-12 22:23:32 +02:00
1 parent e3e65b951a
commit a0fad88eb2
5 files changed
+93 -4

No files matched your search

+3 -2
@@ -1,8 +1,9 @@
| Term | Meaning |
|---|---|
| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`) and typed properties. Assets have **no position**. |
| **Texture** | An uploaded image. Scenes, textures, animations and state machines are the four types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). |
| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`, `shader`) and typed properties. Assets have **no position**. |
| **Texture** | An uploaded image. Scenes, textures, animations, state machines and shaders are the five types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). |
| **State machine** | A named set of **states**, edited as nodes in its own tab. A behavior puts it into a state with `Set state` and branches on it with `State branch`; each behavior instance keeps its own current state. See [State machines](State-machines). |
| **Shader** | A node graph that becomes the GLSL a sprite is drawn with, edited in its own tab. A sprite wears it through `shader`; the numbers the graph declares are rows below that dropdown. See [Shaders](Shaders). |
| **Text** | An asset that draws a line of text (`text`, `fontSize`, `color`, `align`) — titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. |
| **Enabled** | Every placeable asset has the switch `enabled` (default on). A disabled object is not drawn, does not collide and its behavior does not run — **and neither does anything parented below it**. Switching one instance off is a normal override, which makes a container a *screen*: switch it on to show it, off to hide it. From a graph, `Properties` with `enabled` switched to Set does it, and `Is enabled` asks (including the parents); a behavior hears the switch as `On enable` / `On disable`. |
| **Scene object** | An asset placed into a scene: a link to the asset + a **position** (`x`, `y`, `z`) + sparse **overrides** of the asset's properties + an optional **parent** (another object of the scene). Scenes cannot be placed. |
+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 # 671 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 700 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
@@ -150,4 +150,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 671 checks as here.
WebGL 2.0, and the same 700 checks as here.
+1
@@ -16,6 +16,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
| [Textures](Textures) | Getting images in, where they go, tinting, the filter, frames, upload limits |
| [Animations](Animations) | Animation assets, the Animation tab, how they run |
| [State machines](State-machines) | The state machine asset, `State branch` and `Set state` |
| [Shaders](Shaders) | The shader asset, the Shader tab, the node set, the Adjust colors recipe, wearing a shader |
| [Containers](Containers) | Editing a template, instances and their edits, screens without scene switching |
| [Keyboard](Keyboard) | Every shortcut |
+86
@@ -0,0 +1,86 @@
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.
## The Shader tab
`+` in the **Assets** panel makes a shader; selecting one opens the **Shader** tab on it, with the
same canvas the Behavior tab has: right-click places a node, drag moves, drag from a port links,
`Snap` snaps to the 24 px grid, `Fit` frames the graph, undo takes every step back. The readout on
the right says `N properties` while the graph compiles, otherwise why it does not (`No Output node`,
a bad property name, ...). A node that keeps the graph from compiling shows the reason on its body
and a red frame.
A shader graph is compiled on every change. A sprite wearing a graph that does not compile is drawn
without a filter, as if it wore none.
## The nodes
Every value is either a **number** (a GLSL `float`) or a **colour** (a `vec3`, RGB in 0-1). Ports
typed `any` take both: with a colour on either side the result is a colour, per channel; a number
fed to a colour port is broadcast to all three channels. A colour cannot be fed to a number port.
| 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). |
| **Output** | in `color` | The colour the sprite is drawn with, clamped to 0-1. Its alpha stays the texture's. Left unconnected, the pixel passes through. One per graph. |
| **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. |
| **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. |
A colour input with no field (Output, Luminance) that is left unconnected reads the texel's colour.
## Adjust colors
Brightness, contrast and saturation with **1 as neutral** on every one, on the straight colour so a
transparent pixel never gains colour:
```
rgb = rgb * brightness
rgb = (rgb - 0.5) * contrast + 0.5
rgb = mix(luminance(rgb), rgb, saturation)
```
As nodes: `Texture color` → `Multiply` (b: Property *brightness*) → `Subtract` (b: 0.5) →
`Multiply` (b: Property *contrast*) → `Add` (b: 0.5) → both into `Luminance` and into `Mix` as
`b`, `Luminance` into `Mix` as `a`, Property *saturation* into `t`, `Mix` into `Output`. Three
`Property` nodes with default 1, min 0, max 4. The test fixture and the three example games ship
this graph as the asset "Adjust colors" (`tools/lib/shaders.mjs`).
Measured on a flat `#4080ff` sprite: brightness 0.5 gives `#204080`, contrast 0 gives `#808080`,
saturation 0 gives `#7c7c7c` — all byte-exact against the formula.
## Wearing a shader
The sprite's `Shader` row is a picker over the project's shader assets (`None` is no shader and no
extra render pass). The rows below it are the shader's `Property` nodes, in node order; they are
ordinary sprite properties: overridable per placed object, reset (↺) and apply (↑) like any other,
readable and writable by a `Properties` node at runtime — a behavior can fade a sprite by writing
its `brightness` every frame. A value with no row (the shader was switched, the property renamed)
stays stored but marks nothing; it comes back with the shader.
A placed object may override the shader itself; it then shows *that* shader's rows. Switching the
shader keeps the values of the old one's rows in the bag — they are not visible and do not count as
deviations while their shader is not worn.
## What it costs, and where it stops
A shader is a Pixi filter: the sprite is rendered into an intermediate texture and drawn back with
the fragment, one pass per sprite wearing one. Fine for dozens, not for thousands. The pass runs at
screen resolution, so pixel art stays sharp. 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.
## In the code
`src/core/shader/nodes.ts` is the catalogue, `src/core/shader/compile.ts` turns a graph into the
fragment (every node reachable from the Output becomes one local per output, in dependency order;
every Property a `uniform float u_<name>`), `src/viewport/shaders.ts` wraps the result in a filter
and `src/viewport/ShaderLibrary.ts` caches it per asset. 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.
+1
@@ -7,6 +7,7 @@
- [Textures](Textures)
- [Animations](Animations)
- [State machines](State-machines)
- [Shaders](Shaders)
- [Containers](Containers)
- [Keyboard](Keyboard)