Describe the camera shader (#299)

Shaders gets the camera as a wearer, its measurement and In the code. Scene editing gets the Shader switch, Concepts a pointer, Development the e2e sum 1725.
admin committed 2026-10-03 16:03:25 +02:00
1 parent e3775c2ae9
commit 986aad5bd7
4 files changed
+62 -5

No files matched your search

+3 -1
@@ -55,7 +55,9 @@ that is made the main one, with the game camera as an overlay. The main camera d
of the picture and the zoom of the tab; an overlay camera is fitted into that picture without of the picture and the zoom of the tab; an overlay camera is fitted into that picture without
distortion, so one of the same size and zoom covers it exactly. The mouse is reported through the distortion, so one of the same size and zoom covers it exactly. The mouse is reported through the
main camera (#304 is about asking another). Two cameras switched on without *Overlay* draw as main camera (#304 is about asking another). Two cameras switched on without *Overlay* draw as
before: through the first one only. The Scene tab shows every layer. before: through the first one only. The Scene tab shows every layer. A camera can also wear a
shader over its picture (#299, see *Wearing a shader* in [Shaders](Shaders)); it works on that
camera's picture alone, before the pictures are laid on top of each other.
Measured in the checks (step 63): a green sprite of an overlay camera at `z` -10 covers a red one Measured in the checks (step 63): a green sprite of an overlay camera at `z` -10 covers a red one
of the main camera at `z` 1000, a `UI` sprite in front of the main camera and a `Default` one in of the main camera at `z` 1000, a `UI` sprite in front of the main camera and a `Default` one in
front of the overlay stay away, and the same holds in the running game. front of the overlay stay away, and the same holds in the running game.
+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) # 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/snapshots.test.php # the snapshot store (#231), the same way
php tests/php/releases.test.php # the release store (#266), 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 # 1720 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 1725 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 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 start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result ./e2e.sh status # live progress, or the last run's result
+4 -2
@@ -78,9 +78,11 @@ side. Names off screen take their row as well, so none jumps while the view pans
**The eye picks what the Scene tab shows.** Beside *Fit* in the tab's header, the eye opens a list of **The eye picks what the Scene tab shows.** Beside *Fit* in the tab's header, the eye opens a list of
every type that can be placed, Behavior to Tilemap, and per type up to three switches: **Symbol** every type that can be placed, Behavior to Tilemap, and per type up to three switches: **Symbol**
(its icon), **Name** (its name plate) and **Shape**: on a collider its outline, on a tilemap the (its icon), **Name** (its name plate) and **Shape**: on a collider its outline, on a tilemap the
outline of its solid cells, while the map's tiles stay drawn. Sprite, outline of its solid cells, while the map's tiles stay drawn. The Camera row has a **Shader**
switch instead, off by default: it lays the main camera's shader over its frame (#299, see
[Shaders](Shaders)). Sprite,
Text and Tilemap have no icon, so their Symbol switch is greyed out; their Name switch puts a plate Text and Tilemap have no icon, so their Symbol switch is greyed out; their Name switch puts a plate
centred on the object, and it starts off. Every other switch starts on, which is what the Scene centred on the object, and it starts off. Every other switch but the Shader starts on, which is what the Scene
tab always showed. A hidden icon takes no room on a ring and draws no owner link, and its name then tab always showed. A hidden icon takes no room on a ring and draws no owner link, and its name then
stands centred on the object. A hidden icon or outline cannot be clicked either: the object is stands centred on the object. A hidden icon or outline cannot be clicked either: the object is
still selected in the hierarchy. The list stays open while you switch. The switches are saved with still selected in the hierarchy. The list stays open while you switch. The switches are saved with
+54 -1
@@ -374,6 +374,35 @@ a text keeps a name it already has (`text`, `fontSize`, `align` …): a shader p
gets no row. In a `Properties` node the parameters are offered under *Type: Text* as they are under gets no row. In a `Properties` node the parameters are offered under *Type: Text* as they are under
*Type: Sprite*. A shaded text is one draw call, like a shaded sprite. *Type: Sprite*. A shaded text is one draw call, like a shaded sprite.
**A camera wears one over its whole picture** (#299): post-processing such as a vignette, a colour
grade, grey, scanlines, a red flash on a hit or a fade. The camera's *Shader* row sits under
*Overlay*, its parameters below it, overridable, animatable and in a `Properties` node under
*Type: Camera* like a sprite's. A camera keeps a name it already has (`zoom`, `sees` …). The world
the camera sees is drawn into a picture of its frame first, cleared to the scene's background,
and the shader draws that picture where the frame stands, so `u`/`v` run from 0 to 1 across the
camera's picture. Each camera wears its own, so a change of camera changes the effect, and an
overlay camera (#302) wears one over its own see-through picture before it is laid on the main
one: a red flash on the game camera leaves the HUD of an overlay camera alone. The row
**Shader resolution**, shown while a shader is picked, says how many pixels that picture has:
- **Screen pixels** (default): as many as the camera's picture covers on screen, so it stays
sharp at any game scale.
- **Camera pixels**: *View width* x *View height*, scaled up to the game scale without smoothing,
for pixel art: a scanline or a blur step per game pixel.
**Either way a pixel counts as a camera pixel.** `Texture size` is *View width* x *View height*
in both, and a `Blur` of 4 reaches 4 camera pixels at any game scale and any size of the window,
the same rule a field shader follows with its frame. *Screen pixels* only looks sharper. The frame
is the edge, so `Padding` and `Vertex output` do nothing there, as on a field. The Game tab, the
running game and the game link (`play.html`) all draw it. In the Scene tab the eye in the header
has a **Shader** switch on the Camera row, off by default: switched on, the main camera's shader
lies over its frame there, under the symbols, names and gizmos. That picture holds every render
layer, since the Scene tab shows all of them. Measured in the checks (step 63): a camera shader
that hands the picture through draws the game picture byte for byte at 100%, the right way up; a
flat red fills it to the corners while the overlay camera's square keeps its green; `Texture size`
reads 640 x 360 in both resolutions with the Game tab fitted to 670 x 377 pixels; and the Scene
tab shows the red only with the switch on.
## What it costs, and where it stops ## What it costs, and where it stops
A shader runs in the sprite's own draw: the mesh is drawn once, with the graph's fragment in place A shader runs in the sprite's own draw: the mesh is drawn once, with the graph's fragment in place
@@ -483,6 +512,23 @@ more than the samples save: the radius hardly matters there, at radius 4 one pas
400 sprites on, and radius 16, with its third draw, is as slow at 800 sprites as radius 8 in one 400 sprites on, and radius 16, with its third draw, is as slow at 800 sprites as radius 8 in one
pass. Passes are for wide blurs and large pictures, not for every soft edge. pass. Passes are for wide blurs and large pictures, not for every soft edge.
A camera shader (#299) is one pass for the whole view. Measured with `tools/perf-camera-shader.mjs`
on the same HD 630 in a 1920x1080 window, the Game tab at Fit (1310x679, the picture 1207x679 at
*Screen pixels*), plain sprites of 48x48 in the 640x360 camera, in frames per second. At that size
the editor holds well under 60 without any shader, and the scene alone moved by up to 5 frames a
second from one run to the next, so the hand-through rows are three runs each:
| sprites | 100 | 400 |
| --- | --- | --- |
| no camera shader | 35.5 - 41.0 | 29.2 - 33.3 |
| hands through, Screen pixels | 35.0 - 41.5 | 31.1 - 34.1 |
| hands through, Camera pixels | 33.9 - 35.9 | 30.0 - 31.7 |
| Blur 4, Screen pixels (one run, its own scene 41.7 / 37.9) | 33.5 | 33.3 |
| Blur 4, Camera pixels (same run) | 33.9 | 33.6 |
A camera shader that hands the picture through costs less than the scene varies by itself. A Blur
of 4 over the whole view cost about 4 to 8 frames a second in its one run, at either resolution.
## In the code ## In the code
`src/core/shader/nodes.ts` is the catalogue, `src/core/shader/compile.ts` turns a graph into the `src/core/shader/nodes.ts` is the catalogue, `src/core/shader/compile.ts` turns a graph into the
@@ -517,7 +563,14 @@ since two graphs often share a pass's program, every shader declares all 8 pass
uniform group, even an empty one. Otherwise the second graph reads a placeholder and draws white. uniform group, even an empty one. Otherwise the second graph reads a placeholder and draws white.
`Blur` and `Texture size` count the picture's size through the uniform `uPixelScale`: 1 on every `Blur` and `Texture size` count the picture's size through the uniform `uPixelScale`: 1 on every
wearer but a field, where it is the frame's units per pixel of its picture (`unitsPerPixel` in wearer but a field, where it is the frame's units per pixel of its picture (`unitsPerPixel` in
`src/viewport/fieldFrame.ts`). `src/viewport/fieldFrame.ts`), and a camera, where it is camera pixels per pixel of its picture
(`pixelScale` in `src/viewport/cameraPicture.ts`).
A camera's shader is `CameraShade` (`src/viewport/cameraShade.ts`): every view keeps its masks and
objects in one container, which is drawn into the camera's picture with Pixi's `render` and a
transform from world units to that picture. The Game tab and the game link then take that container
off the stage and let the picture stand in for it; the Scene tab leaves it drawn, since it shows
more than the frame, and lays the picture over it below the symbols (`shadingCamera` and
`keepsWorldAroundShader` in `WorldViewport`).
`src/viewport/ShaderLibrary.ts` caches the compiled graph per asset, keyed by a fingerprint of what `src/viewport/ShaderLibrary.ts` caches the compiled graph per asset, keyed by a fingerprint of what
the fragment depends on — node ids, types, params, declared ports, and which port feeds which — so the fragment depends on — node ids, types, params, declared ports, and which port feeds which — so
moving, collapsing or folding compiles nothing again; a changed param or link does, once moving, collapsing or folding compiles nothing again; a changed param or link does, once