Shader: Node Blur (weichzeichnen in einem Pass) #300

Closed
opened 2026-10-03 12:33:38 +02:00 by meleki · 2 comments
Owner

Wunsch des Users (2026-10-03), aus der Frage nach mehreren Passes für einen Blur.

Befund

  • Ein Blur geht schon heute in einem Pass: Sample texture liest an einer beliebigen UV, und UV mit Texture size liefern den Abstand eines Pixels (1 / size). Mehrere Passes (horizontal, dann vertikal) bringen nur Tempo, sie sind keine Voraussetzung.
  • Der Haken ist die Verdrahtung: Der Graph hat keine Schleife. Ein 3x3-Blur sind 9 Sample texture plus Add-Kette plus Teilen, ein 5x5-Blur schon 25 Samples, alles von Hand.
  • Wer die Farbe naiv mittelt, bekommt am Rand dunkle Säume: Sample texture.color ist die gerade Farbe (straight), ein transparenter Nachbar zählt dann mit seiner Farbe voll mit. Richtig ist, die vormultiplizierten Samples zu mitteln und erst danach zu teilen. Das muss man heute wissen.
  • Die Shader laufen auf GLSL ES 3.00 (compile.ts), eine Schleife mit Grenze aus einem Uniform ist dort erlaubt.

Zu bauen (Vorschlag)

  • Node Blur in der Shader-Kategorie, neben Sample texture.
    • Eingänge: radius (Pixel des Bildes, inline mit Default 2), dazu u/v wie bei Sample texture (unverbunden der eigene Pixel).
    • Ausgänge: color und alpha, wie bei Sample texture.
  • Der Compiler schreibt die Schleife selbst ins GLSL (Hilfsfunktion im Prelude wie texelAt), es bleibt ein Pass.
  • Gemittelt wird vormultipliziert, color kommt danach gerade heraus. Kein dunkler Saum am Rand.
  • Der Rand folgt wie bei Sample texture der Einstellung Edge am Output. Mit Transparent und Padding läuft der Blur über das Sprite hinaus.
  • Gilt überall, wo ein Shader läuft: Sprite, Text, Partikel, Feld-Shader (#170) und später die Kamera (#299).
  • Messung wie in #67 und #170: Bilder je Sekunde mit einem Sprite bzw. einem Feld mit Blur bei Radius 2, 4 und 8.

Beweis beim Schließen

  • Radius 0 zeichnet das Bild unverändert.
  • Ein einzelnes weißes Pixel auf Transparent wird zu einem Fleck der erwarteten Größe, die Summe der Deckkraft bleibt gleich.
  • Eine harte Kante (Rot an Transparent) bekommt einen weichen Übergang ohne dunklen Saum.
  • radius aus einer Property lässt sich am Sprite ändern, ohne den Shader neu zu bauen.

Offen (mit Vorschlag)

  1. Form: Box (alle Samples gleich gewichtet) oder Gauß (die Mitte zählt mehr)? Vorschlag: Gauß, das sieht bei gleichem Radius weicher aus und kostet gleich viel. Box wäre als Setting Kind am Node billig mitzunehmen.
  2. Obergrenze für radius: Die Kosten wachsen mit (2r+1)². Vorschlag: höchstens 8 (289 Samples). Darüber wird es auch auf schneller Hardware teuer, und ab dort lohnen sich mehrere Passes wirklich (eigenes Issue, wenn gebraucht).
  3. Bruchteile beim Radius: Vorschlag: erlaubt, der Rand wird anteilig gewichtet. Dann lässt sich ein Blur per Animation weich ein- und ausblenden, statt in ganzen Pixeln zu springen.
  4. Richtung: Ein Setting Direction (Both / Horizontal / Vertical) wäre fast gratis und deckt Bewegungsunschärfe in einer Achse ab. Vorschlag: mitnehmen, Standard Both.
Wunsch des Users (2026-10-03), aus der Frage nach mehreren Passes für einen Blur. ## Befund - Ein Blur geht schon heute in **einem** Pass: `Sample texture` liest an einer beliebigen UV, und `UV` mit `Texture size` liefern den Abstand eines Pixels (`1 / size`). Mehrere Passes (horizontal, dann vertikal) bringen nur Tempo, sie sind keine Voraussetzung. - Der Haken ist die Verdrahtung: Der Graph hat keine Schleife. Ein 3x3-Blur sind 9 `Sample texture` plus `Add`-Kette plus Teilen, ein 5x5-Blur schon 25 Samples, alles von Hand. - Wer die Farbe naiv mittelt, bekommt am Rand dunkle Säume: `Sample texture.color` ist die gerade Farbe (`straight`), ein transparenter Nachbar zählt dann mit seiner Farbe voll mit. Richtig ist, die vormultiplizierten Samples zu mitteln und erst danach zu teilen. Das muss man heute wissen. - Die Shader laufen auf GLSL ES 3.00 (`compile.ts`), eine Schleife mit Grenze aus einem Uniform ist dort erlaubt. ## Zu bauen (Vorschlag) - Node **`Blur`** in der Shader-Kategorie, neben `Sample texture`. - Eingänge: `radius` (Pixel des Bildes, inline mit Default 2), dazu `u`/`v` wie bei `Sample texture` (unverbunden der eigene Pixel). - Ausgänge: `color` und `alpha`, wie bei `Sample texture`. - Der Compiler schreibt die Schleife selbst ins GLSL (Hilfsfunktion im Prelude wie `texelAt`), es bleibt ein Pass. - Gemittelt wird vormultipliziert, `color` kommt danach gerade heraus. Kein dunkler Saum am Rand. - Der Rand folgt wie bei `Sample texture` der Einstellung *Edge* am `Output`. Mit *Transparent* und `Padding` läuft der Blur über das Sprite hinaus. - Gilt überall, wo ein Shader läuft: Sprite, Text, Partikel, Feld-Shader (#170) und später die Kamera (#299). - Messung wie in #67 und #170: Bilder je Sekunde mit einem Sprite bzw. einem Feld mit Blur bei Radius 2, 4 und 8. ## Beweis beim Schließen - Radius 0 zeichnet das Bild unverändert. - Ein einzelnes weißes Pixel auf Transparent wird zu einem Fleck der erwarteten Größe, die Summe der Deckkraft bleibt gleich. - Eine harte Kante (Rot an Transparent) bekommt einen weichen Übergang ohne dunklen Saum. - `radius` aus einer `Property` lässt sich am Sprite ändern, ohne den Shader neu zu bauen. ## Offen (mit Vorschlag) 1. **Form:** Box (alle Samples gleich gewichtet) oder Gauß (die Mitte zählt mehr)? Vorschlag: Gauß, das sieht bei gleichem Radius weicher aus und kostet gleich viel. Box wäre als Setting `Kind` am Node billig mitzunehmen. 2. **Obergrenze für `radius`:** Die Kosten wachsen mit (2r+1)². Vorschlag: höchstens 8 (289 Samples). Darüber wird es auch auf schneller Hardware teuer, und ab dort lohnen sich mehrere Passes wirklich (eigenes Issue, wenn gebraucht). 3. **Bruchteile beim Radius:** Vorschlag: erlaubt, der Rand wird anteilig gewichtet. Dann lässt sich ein Blur per Animation weich ein- und ausblenden, statt in ganzen Pixeln zu springen. 4. **Richtung:** Ein Setting `Direction` (Both / Horizontal / Vertical) wäre fast gratis und deckt Bewegungsunschärfe in einer Achse ab. Vorschlag: mitnehmen, Standard Both.
meleki added the shader label 2026-10-03 12:33:38 +02:00
admin added this to the Development project 2026-10-03 12:34:49 +02:00
  1. kind setting einführen, standard Box
  2. höchstens 8, folge-issue für mehrere passes anlegen
  3. vorschlag passt
  4. vorschlag passt, kannst auch gleich mitnehmen
1. kind setting einführen, standard Box 2. höchstens 8, folge-issue für mehrere passes anlegen 3. vorschlag passt 4. vorschlag passt, kannst auch gleich mitnehmen
admin moved this to Next in Development on 2026-10-03 12:36:35 +02:00
admin moved this to Done in Development on 2026-10-03 13:11:28 +02:00
Author
Owner

Erledigt mit df61b08, Wiki e281297 (Shaders, Development).

Deine Antworten (Kommentar 3003)

  1. Setting Kind, Standard Box, dazu Gaussian.
  2. Radius höchstens 8. Das Folge-Issue für mehrere Passes ist #301.
  3. Bruchteile sind erlaubt, der äußerste Ring zählt anteilig.
  4. Direction (Both / Horizontal / Vertical, Standard Both) ist mit drin.

Was neu ist

  • Node Blur in der Shader-Palette:
    • Eingänge radius (Pixel des Bildes, Standard 2, 0 bis 8) und u/v wie bei Sample texture.
    • Ausgänge color und alpha.
    • Settings Kind und Direction.
  • Die Schleife steht als Hilfsfunktion blur() im Prelude und kommt nur in Shader, die einen Blur erreichen. Es bleibt ein Pass.
  • Kein dunkler Saum: Gemittelt wird vormultipliziert, erst danach wird daraus die gerade Farbe. Ein transparenter Nachbar senkt nur die Deckkraft.
  • Radius aus einer Property ist ein Uniform: Ändern kompiliert nichts neu.
  • Der Rand folgt Edge am Output. Für einen freistehenden weichen Rand Transparent und Padding setzen.
  • Gilt überall, wo ein Shader läuft: Sprite, Text, Partikel, Feld-Shader.
  • GM_PERF_BLUR=<radius> in tools/perf-shaders.mjs und tools/perf-field-shaders.mjs.

Was ich selbst festgelegt habe

  • Gaussian: sigma = radius / 2.
  • Ein Ergebnis für beide Ausgänge: Ein Node darf jetzt eine Vorarbeit (prepare) melden, die der Compiler einmal in eine lokale Variable legt. Sonst hätten color und alpha je einen eigenen Blur gerechnet, also doppelte Kosten.
  • Ein Blur zählt als verschobenes Sample. Er bekommt deshalb die Texturvariante für Edge, wie ein Sample texture mit gespeister UV.
  • Pixel des Bildes: Beim Text ist das ein Pixel der gerasterten Schrift. Bei Linear hängt dessen Größe vom Zoom ab, der Blur wirkt dann je nach Zoom verschieden breit. Beim Feld-Shader ist es ein Bildschirmpixel.
  • Der Wert im Feld radius hat Schrittweite 0,5.

Messung (stein2, Intel HD 630, Bilder je Sekunde, ein Lauf je Wert, Box)

Ohne Shader hielten dieselben Szenen 59,0 bis 60,1.

Sprites 48x48 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
Feld-Shader 256x256 Pixel 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

Radius 2 ist billig. Ab 4 bestimmen die Samples den Preis. Ein einziges Feld mit Radius 8 kostet schon 8 Bilder je Sekunde (289 Samples je Pixel). Zwei Passes (#301) kämen dort mit 34 aus.

Tests

Gate   → 224 Files, 3467 Tests, CHECK EXIT 0   (3460 + 7)
e2e 28 → ALL PASSED – 94 checks                (89 + 5)
  • Der Beweis aus dem Issue, e2e 28l-vi (32-px-Bild links rot, rechts grün, Filter Nearest):
    • Radius 0 zeichnet das Bild bytegleich.
    • Das letzte rote Pixel, waagrecht über eine Property weichgezeichnet: 187 / 173 / 154 im Rotkanal bei Radius 0,5 / 1 / 4. Gerechnet sind 188 / 173 / 154. Dazwischen wird nichts kompiliert.
    • Vertical bleibt rot, Gaussian behält mehr Rot als Box (162 gegen 154).
    • Mit Edge Transparent bleibt das erste Pixel rot (#e63c3c) und hat 5/9 Deckkraft (gemessen 0,556), also kein Saum.
    • Ein einzelnes weißes Pixel wird bei Radius 1 zu einem Fleck aus 3x3 Pixeln mit je 1/9 Deckkraft (Summe 0,977 bei 8-Bit-Rundung).
  • 7 Unit-Tests in tests/core/shader-blur.test.ts.
  • Gegenproben:
    • Zählt der Blur nicht als verschobenes Sample, fällt 1 Unit-Test.
    • Gibt der Blur die vormultiplizierte Farbe aus, fallen im Browser der Saum- und der Fleck-Check (Rand #802121).
  • Die e2e-Summe 1712 ist gerechnet (1707 + 5).
  • Nicht im Browser geprüft: Blur an Text, Partikeln und im Feld-Shader (nur die Messung lief dort), Gaussian mit Both, das Glow-Rezept aus dem Wiki und die Vorschau im Shader-Tab.
Erledigt mit df61b08, Wiki e281297 (*Shaders*, *Development*). ## Deine Antworten (Kommentar 3003) 1. Setting `Kind`, Standard **Box**, dazu **Gaussian**. 2. Radius höchstens 8. Das Folge-Issue für mehrere Passes ist **#301**. 3. Bruchteile sind erlaubt, der äußerste Ring zählt anteilig. 4. `Direction` (Both / Horizontal / Vertical, Standard Both) ist mit drin. ## Was neu ist - Node **`Blur`** in der Shader-Palette: - Eingänge `radius` (Pixel des Bildes, Standard 2, 0 bis 8) und `u`/`v` wie bei `Sample texture`. - Ausgänge `color` und `alpha`. - Settings *Kind* und *Direction*. - Die Schleife steht als Hilfsfunktion `blur()` im Prelude und kommt nur in Shader, die einen Blur erreichen. Es bleibt **ein** Pass. - **Kein dunkler Saum:** Gemittelt wird vormultipliziert, erst danach wird daraus die gerade Farbe. Ein transparenter Nachbar senkt nur die Deckkraft. - **Radius aus einer `Property`** ist ein Uniform: Ändern kompiliert nichts neu. - Der Rand folgt *Edge* am `Output`. Für einen freistehenden weichen Rand *Transparent* und *Padding* setzen. - Gilt überall, wo ein Shader läuft: Sprite, Text, Partikel, Feld-Shader. - `GM_PERF_BLUR=<radius>` in `tools/perf-shaders.mjs` und `tools/perf-field-shaders.mjs`. ## Was ich selbst festgelegt habe - **Gaussian:** sigma = radius / 2. - **Ein Ergebnis für beide Ausgänge:** Ein Node darf jetzt eine Vorarbeit (`prepare`) melden, die der Compiler einmal in eine lokale Variable legt. Sonst hätten `color` und `alpha` je einen eigenen Blur gerechnet, also doppelte Kosten. - **Ein Blur zählt als verschobenes Sample.** Er bekommt deshalb die Texturvariante für *Edge*, wie ein `Sample texture` mit gespeister UV. - **Pixel des Bildes:** Beim Text ist das ein Pixel der gerasterten Schrift. Bei *Linear* hängt dessen Größe vom Zoom ab, der Blur wirkt dann je nach Zoom verschieden breit. Beim Feld-Shader ist es ein Bildschirmpixel. - Der Wert im Feld `radius` hat Schrittweite 0,5. ## Messung (stein2, Intel HD 630, Bilder je Sekunde, ein Lauf je Wert, Box) Ohne Shader hielten dieselben Szenen 59,0 bis 60,1. | Sprites 48x48 | 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 | | Feld-Shader 256x256 Pixel | 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 | Radius 2 ist billig. Ab 4 bestimmen die Samples den Preis. Ein einziges Feld mit Radius 8 kostet schon 8 Bilder je Sekunde (289 Samples je Pixel). Zwei Passes (#301) kämen dort mit 34 aus. ## Tests ``` Gate → 224 Files, 3467 Tests, CHECK EXIT 0 (3460 + 7) e2e 28 → ALL PASSED – 94 checks (89 + 5) ``` - **Der Beweis aus dem Issue, e2e 28l-vi** (32-px-Bild links rot, rechts grün, Filter *Nearest*): - Radius 0 zeichnet das Bild bytegleich. - Das letzte rote Pixel, waagrecht über eine `Property` weichgezeichnet: 187 / 173 / 154 im Rotkanal bei Radius 0,5 / 1 / 4. Gerechnet sind 188 / 173 / 154. Dazwischen wird nichts kompiliert. - *Vertical* bleibt rot, *Gaussian* behält mehr Rot als *Box* (162 gegen 154). - Mit Edge *Transparent* bleibt das erste Pixel rot (`#e63c3c`) und hat 5/9 Deckkraft (gemessen 0,556), also kein Saum. - Ein einzelnes weißes Pixel wird bei Radius 1 zu einem Fleck aus 3x3 Pixeln mit je 1/9 Deckkraft (Summe 0,977 bei 8-Bit-Rundung). - **7 Unit-Tests** in `tests/core/shader-blur.test.ts`. - **Gegenproben:** - Zählt der Blur nicht als verschobenes Sample, fällt 1 Unit-Test. - Gibt der Blur die vormultiplizierte Farbe aus, fallen im Browser der Saum- und der Fleck-Check (Rand `#802121`). - Die e2e-Summe **1712** ist gerechnet (1707 + 5). - **Nicht im Browser geprüft:** Blur an Text, Partikeln und im Feld-Shader (nur die Messung lief dort), *Gaussian* mit *Both*, das Glow-Rezept aus dem Wiki und die Vorschau im Shader-Tab.
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#300