Soften a shader's picture in one pass with a Blur node (#300)
game-maker / gate (push) Successful in 3m3s
game-maker / suite (push) Successful in 14m26s

A blur already worked with Sample texture, but only by wiring every
sample by hand: 25 nodes for a 5x5. Averaging their colours also gave
a dark seam wherever a neighbour was transparent. The Blur node writes
the loop itself and averages the right way.

- Blur takes radius (pixels of the picture, 0 to 8, fractions allowed)
  and u/v like Sample texture, and gives color and alpha.
- Kind is Box (default) or Gaussian, Direction is Both, Horizontal or
  Vertical. Both are arguments of one prelude helper, blur().
- The samples are averaged premultiplied and only then made straight,
  so a transparent neighbour lowers the opacity and adds no black.
- The outermost ring counts by the part of the radius that reaches
  it, so a radius from a Property changes smoothly and compiles
  nothing again.
- A node may now declare prepare, a vec4 the compiler works out once
  into a local. Color and alpha read that one sum instead of blurring
  twice.
- A reached Blur counts as a shifted sample, so the picture comes
  with the Output's Edge. It cannot feed the Vertex output.
- Both perf tools take GM_PERF_BLUR=<radius>.

Wider blurs and bloom need several passes. That is #301.

Measured on stein2 (HD 630, one run per value, fps), Box blur:
- 48x48 sprites at 100/400/800: radius 2 59.8/58.4/55.6,
  radius 4 53.1/51.4/36.3, radius 8 46.6/24.4/14.4.
- 256x256 field shaders at 1/10/50: radius 2 60.2/56.0/49.0,
  radius 4 60.1/49.0/27.1, radius 8 52.4/34.9/11.4.

Tests:
- Gate: 224 files, 3467 tests, CHECK EXIT 0 (+7).
- e2e 28: ALL PASSED, 94 checks (+5 in 28l-vi). Pixels match the
  arithmetic to 1: the last red texel at radius 0.5/1/4 reads
  187/173/154 (expected 188/173/154).
- Counter-checks: Blur not counted as shifted fails 1 unit test.
  Blur output left premultiplied fails the seam and spot checks.
- e2e total 1712 is computed (1707 + 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-03 13:11:31 +02:00
1 parent d6d38a6422
commit df61b08bd1
8 files changed
+398 -16

No files matched your search

+19 -2
View File
@@ -11,6 +11,7 @@
*
* Usage: node tools/seed-e2e.mjs && node tools/perf-field-shaders.mjs
* GM_PERF_COUNTS=... source counts (default 1,10,50), GM_PERF_FRAME=... frame edge in units
* GM_PERF_BLUR=... puts a Blur (#300) with that radius between picture and Output
*
* It is a measuring tool, not a test: frame times depend on the machine, so CI does not run it.
* Read the rate, not the median – see `perf-shaders.mjs` for why, and for the 60 Hz cap.
@@ -23,6 +24,20 @@ const { projectId, baseUrl } = cliArgs('perf-field-shaders.mjs', PROJECTS.fixtur
const COUNTS = (process.env.GM_PERF_COUNTS ?? '1,10,50').split(',').map(Number);
const FRAME = Number(process.env.GM_PERF_FRAME ?? 256);
const BLUR = Number(process.env.GM_PERF_BLUR ?? 0);
const FIELD_GRAPH =
BLUR > 0
? {
nodes: [
{ id: 'pf-blur', type: 'ShaderBlur', x: 0, y: 0, params: { radius: BLUR } },
{ id: 'pf-out', type: 'ShaderOutput', x: 300, y: 0, params: {} },
],
edges: [
{ id: 'pf-c', from: { node: 'pf-blur', port: 'color' }, to: { node: 'pf-out', port: 'color' } },
{ id: 'pf-a', from: { node: 'pf-blur', port: 'alpha' }, to: { node: 'pf-out', port: 'alpha' } },
],
}
: { nodes: [{ id: 'pf-out', type: 'ShaderOutput', x: 0, y: 0, params: {} }], edges: [] };
const WARMUP_MS = 1500;
const SAMPLE_MS = 4000;
@@ -40,7 +55,7 @@ await runChecks({ projectId, baseUrl, port: 9348, shotPrefix: 'gm-perf-field' },
const p = __gm.state.project;
p.assets = p.assets.filter((a) => a.id !== 'perf-source' && a.id !== 'perf-field');
p.assets.push({ id: 'perf-field', type: 'shader', name: 'Perf field', props: {},
graph: { nodes: [{ id: 'pf-out', type: 'ShaderOutput', x: 0, y: 0, params: {} }], edges: [] } });
graph: ${JSON.stringify(FIELD_GRAPH)} });
p.assets.push({ id: 'perf-source', type: 'particles', name: 'Perf source',
props: { rate: 50, lifetime: 1, size: 24, color: 0x4080ff, fieldShader: '',
fieldWidth: ${FRAME}, fieldHeight: ${FRAME} },
@@ -131,7 +146,9 @@ await runChecks({ projectId, baseUrl, port: 9348, shotPrefix: 'gm-perf-field' },
await shot('heaviest');
await stop();
console.log(`\nframe ${FRAME}x${FRAME} units, pass-through field shader, ${SAMPLE_MS} ms per point`);
console.log(
`\nframe ${FRAME}x${FRAME} units, ${BLUR > 0 ? `Blur radius ${BLUR}` : 'pass-through'} field shader, ${SAMPLE_MS} ms per point`,
);
console.log('| sources | no field shader | field shader | picture |');
console.log('| --- | --- | --- | --- |');
for (const r of rows)