From 315ee9215c7080da7f768d5c653296f47d7fe364 Mon Sep 17 00:00:00 2001 From: meleki Date: Sat, 3 Oct 2026 12:25:17 +0200 Subject: [PATCH] Describe the field shader of a particle source (#170) - Particles: rows Field shader, Field width and Field height, and a section on how the frame, its UV, its resolution and the checks work. - Shaders: a pointer to it, and the measurement with 1, 10 and 50 sources under What it costs. - Development: e2e total 1707. Co-Authored-By: Claude Opus 5.5 --- Development.md | 2 +- Particles.md | 28 ++++++++++++++++++++++++++++ Shaders.md | 21 +++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Development.md b/Development.md index ab6a0b9..1463988 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 # 1704 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1707 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/Particles.md b/Particles.md index 43606f2..885ce4e 100644 --- a/Particles.md +++ b/Particles.md @@ -87,6 +87,8 @@ mirror), so hanging one on a moving ship turns the whole spray with it. Its own | **Color** | white | The tint, and what an unconnected `color` output reads. | | **Move with source** | off | Off: a particle stays where it was born, so a moving source leaves a trail (rocket smoke). On: it hangs on the source and travels with it (sparks on a carried torch). The difference shows only while the source moves. | | **Shader** | none | A shader asset worn by **every particle** of this source, the way a sprite wears one. Its `Property` rows appear below, like a sprite's. | +| **Field shader** | none | A shader asset laid over the **whole field** at once (see *A shader over the whole field*). Its rows appear below as well. | +| **Field width**, **Field height** | 256 | The frame the field shader covers, in units, centred on the source. Shown only while a field shader is picked. | Below them come the rows the asset's graph declares, one per `Property` node — so two fountains of the same asset can spray at different strengths, exactly the way two sprites wearing the same shader @@ -110,6 +112,32 @@ own row wins and the uniform of that name keeps its declared default: two rows c one row with two meanings. Nothing is rejected on the shader's side — the same shader on a sprite is perfectly fine, it is only this source that already has the name. +**A shader over the whole field** (#170). The *Field shader* row lays a shader over everything the +source draws at once: the particles are drawn into a picture of a fixed frame, *Field width* by +*Field height* units centred on the source, and the shader draws that picture. So it can do what a +shader per particle cannot: a glow or blur where particles overlap, an outline round the whole +cloud, a ripple through all of it. It is an ordinary shader asset; its `u`/`v` run from 0 to 1 +across the frame and never jump, however the cloud changes, and `Texture color`, `Texture alpha` +and `Sample texture` read the picture of the particles (`Texture size` is that picture in pixels). +The frame is the edge: whatever flies past it is cut off, so make it as large as the effect reaches. +The scene view draws it as a dashed rectangle round the selected source. It turns and grows with the +source. + +Both shaders may be worn together: the one per particle draws into the picture, the field shader +over it. A name both of them declare is one row, and both read it. The field shader's *Padding* and +*Vertex output* do nothing here: the frame already is the room around the field, and its corners +stay put, as with a nine-sliced sprite. + +The picture has as many pixels as the frame covers on screen, so it stays sharp at any zoom, but at +most 2048 per side. A source whose frame is off screen draws no picture at all. What it costs is one +extra pass per source, sized by the frame, not by the number of particles; see *What it costs* in +[Shaders](Shaders) for the measurement. + +Measured in the checks (step 31): a field shader that changes nothing draws the field where it was +and the right way up, with a picture of 256 pixels at zoom 1; a flat red fills the frame 100 units +off the source where no particle is, and stops before 140; taken off again, the particles draw as +before. + A **behavior** can start a source over with the `Emit` node (its `source` input names which one): in *Burst* mode that is another burst, in *Continuous* mode the stream begins again at particle 0. diff --git a/Shaders.md b/Shaders.md index f5b0b74..f5689d9 100644 --- a/Shaders.md +++ b/Shaders.md @@ -260,6 +260,12 @@ never there to lose. The difference is what the fragment can see — a particle' age or index — and that a source, having rows of its own graph as well, keeps a name the shader also declares: the uniform then stays on its default rather than taking the row. +**A source can also wear one over its whole field** (#170, the *Field shader* row, see +*A shader over the whole field* in [Particles](Particles)). There the shader draws a picture of +all the particles inside a fixed frame round the source, so its `u`/`v` span the frame and +`Sample texture` reaches across neighbouring particles: glow, blur and outlines of the whole cloud. +Unlike a shader per particle this is an extra pass, one per source. + **A text object wears one the same way** (#169), through a *Shader* row below Align. Its glyphs are rasterised into a picture of their own and drawn on the sprite quad, so the graph sees exactly what a sprite's graph sees: `Texture color` is the glyph pixel in the text's colour, `Texture alpha` the @@ -314,6 +320,21 @@ Up to 400 wearers a grid hardly shows; at 800 it costs a quarter of the frame ra with 32 subdivisions is below the 57 at 400, so single values here are good to a few frames, not to one. +A field shader on a particle source (#170) is the one place a shader costs a pass: the source's +particles are drawn into a picture of its frame first. One run per row with +`tools/perf-field-shaders.mjs` on the same HD 630, sources spread over the 640x360 camera, a frame +of 256 units (256x256 pixels at that zoom), 50 particles a second each, a field shader that hands +the picture through unchanged, in frames per second: + +| sources | 1 | 10 | 50 | +| --- | --- | --- | --- | +| no field shader | 60.1 | 60.0 | 59.2 | +| field shader | 60.0 | 60.0 | 57.6 | + +Up to 10 sources nothing shows; at 50 the field shaders cost about 1.6 frames a second. The 60 Hz +cap hides what a pass costs inside a frame that is not full, and a graph that samples the picture +many times (a blur) adds its own price per pixel on top of the pass. + ## In the code `src/core/shader/nodes.ts` is the catalogue, `src/core/shader/compile.ts` turns a graph into the