Jedes Sprite mit Shader traegt einen eigenen Pixi-Filter (syncShader in src/viewport/symbols.ts), also einen Render-Pass in eine Zwischentextur und zurueck. Gemessen auf der Intel HD 630, 48x48-Sprites in einer 640x360-Kamera, neutrale Werte, drei Laeufe auf 1 fps gleich:
beschattete Sprites
25
50
100
200
400
800
fps
58
56
54
48
34
20
Dieselbe Szene ohne Shader haelt bei jeder dieser Zahlen 60 fps, 800 eingeschlossen.
Der Preis ist der Pass, nicht die Fragment-Arbeit. Vierfache Sprite-Flaeche (96x96) misst dasselbe: 51,6 / 49,5 / 34,2 fps bei 100 / 200 / 400 gegen 53,2 / 48,5 / 34,5 bei 48x48. Es kostet der Wechsel des Render-Ziels, nicht das Rechnen pro Pixel.
Der in #67 angedachte Weg "Sprites teilen sich einen Filter" bringt deshalb nichts. Das GLSL-Programm ist bereits geteilt (GlProgram.from cached nach Quelltext, ein Programm fuer alle Sprites); geteilt wuerden nur die Filter-Objekte. Pixi legt trotzdem fuer jeden Container mit filters einen eigenen Pass an.
Aufgabe
Das kompilierte Fragment dorthin bringen, wo das Sprite ohnehin gezeichnet wird, statt in einen nachgelagerten Filter — dann gibt es gar keinen Extra-Pass.
Zu klaeren, bevor gebaut wird:
Sprites sind heute keine Pixi-Sprites, sondern Graphics-Rechtecke mit Texturfuellung (symbols.ts). Ein eigener Fragment-Shader pro Fuellung ist in dieser Bauweise nicht vorgesehen — der Umbau geht vermutlich ueber Mesh mit eigenem Shader (oder einen eigenen Renderable), und das beruehrt Szene- und Spielansicht, weil beide denselben Zeichenweg teilen (WorldViewport.redraw).
Der Prelude/Epilog des Compilers (src/core/shader/compile.ts) rechnet heute mit premultiplizierten Pixeln, weil ein Filter sie so bekommt. Im Zeichen-Shader liegt die Farbe anders vor; der Compiler braucht dann zwei Rahmen oder einen neutralen.
Batching: Sprites mit verschiedenen Shadern koennen nicht im selben Draw-Call liegen. Der Gewinn haengt daran, dass gleiche Shader zusammen gezeichnet werden.
Beweis beim Schliessen
node tools/seed-e2e.mjs && node tools/perf-shaders.mjs zeigt bei 400 und 800 beschatteten Sprites die Baseline-Rate (rund 60 fps) statt 34 / 20.
e2e-Modul 28 bleibt gruen: dieselben Pixel wie heute (brightness 0.5 -> #204080, contrast 0 -> #808080, neutral = byte-gleich mit "kein Shader"), und Pixel-Art bleibt bei gebrochenem Kamera-Zoom scharf (Modul 12).
Der Check "a shaded sprite carries one filter" in Modul 28 beschreibt dann den alten Zustand und muss mitwandern.
## Befund (gemessen in #67, `tools/perf-shaders.mjs`)
Jedes Sprite mit Shader traegt einen eigenen Pixi-`Filter` (`syncShader` in `src/viewport/symbols.ts`), also einen Render-Pass in eine Zwischentextur und zurueck. Gemessen auf der Intel HD 630, 48x48-Sprites in einer 640x360-Kamera, neutrale Werte, drei Laeufe auf 1 fps gleich:
| beschattete Sprites | 25 | 50 | 100 | 200 | 400 | 800 |
| --- | --- | --- | --- | --- | --- | --- |
| fps | 58 | 56 | 54 | 48 | 34 | 20 |
Dieselbe Szene **ohne** Shader haelt bei jeder dieser Zahlen 60 fps, 800 eingeschlossen.
**Der Preis ist der Pass, nicht die Fragment-Arbeit.** Vierfache Sprite-Flaeche (96x96) misst dasselbe: 51,6 / 49,5 / 34,2 fps bei 100 / 200 / 400 gegen 53,2 / 48,5 / 34,5 bei 48x48. Es kostet der Wechsel des Render-Ziels, nicht das Rechnen pro Pixel.
**Der in #67 angedachte Weg "Sprites teilen sich einen Filter" bringt deshalb nichts.** Das GLSL-Programm ist bereits geteilt (`GlProgram.from` cached nach Quelltext, ein Programm fuer alle Sprites); geteilt wuerden nur die Filter-*Objekte*. Pixi legt trotzdem fuer jeden Container mit `filters` einen eigenen Pass an.
## Aufgabe
Das kompilierte Fragment dorthin bringen, wo das Sprite ohnehin gezeichnet wird, statt in einen nachgelagerten Filter — dann gibt es gar keinen Extra-Pass.
Zu klaeren, bevor gebaut wird:
- Sprites sind heute **keine** Pixi-`Sprite`s, sondern `Graphics`-Rechtecke mit Texturfuellung (`symbols.ts`). Ein eigener Fragment-Shader pro Fuellung ist in dieser Bauweise nicht vorgesehen — der Umbau geht vermutlich ueber `Mesh` mit eigenem Shader (oder einen eigenen Renderable), und das beruehrt Szene- *und* Spielansicht, weil beide denselben Zeichenweg teilen (`WorldViewport.redraw`).
- Der Prelude/Epilog des Compilers (`src/core/shader/compile.ts`) rechnet heute mit **premultiplizierten** Pixeln, weil ein Filter sie so bekommt. Im Zeichen-Shader liegt die Farbe anders vor; der Compiler braucht dann zwei Rahmen oder einen neutralen.
- Batching: Sprites mit verschiedenen Shadern koennen nicht im selben Draw-Call liegen. Der Gewinn haengt daran, dass gleiche Shader zusammen gezeichnet werden.
## Beweis beim Schliessen
- `node tools/seed-e2e.mjs && node tools/perf-shaders.mjs` zeigt bei 400 und 800 beschatteten Sprites die Baseline-Rate (rund 60 fps) statt 34 / 20.
- e2e-Modul 28 bleibt gruen: dieselben Pixel wie heute (`brightness 0.5` -> `#204080`, `contrast 0` -> `#808080`, neutral = byte-gleich mit "kein Shader"), und Pixel-Art bleibt bei gebrochenem Kamera-Zoom scharf (Modul 12).
- Der Check "a shaded sprite carries one filter" in Modul 28 beschreibt dann den alten Zustand und muss mitwandern.
meleki
added the shader label 2026-09-13 14:38:55 +02:00
Ein Sprite ist jetzt ein Mesh (Unit-Quad, Pivot 0,5, Scale = Groesse) als Geschwister seines Graphics-Knotens unter demselben Parent; der Knoten traegt nur noch die HitArea. Ein getragener Shader ist ein GraphShader extends Shader mit eigenem Vertex-Stage, der Pixis Fragment im Zeichenweg des Sprites ersetzt — keine Zwischentextur, kein Filter, kein Extra-Pass. Der Compiler-Rahmen bleibt premultipliziert: texel = texture(...) * vColor (Tint, premultipliziert wie das Texel) vor dem Graphen, entpremultiplizieren, Graph, zurueckmultiplizieren.
Die drei Klaerungen aus dem Issue, gemessen: (1) Graphics mit Texturfuellung ist ersetzt, nicht erweitert — Pixi 8 erlaubt Kinder nur an reinen Containern (allowChildren false, sonst die Deprecation-Warnung), deshalb Geschwister statt Kind; Szene und Spiel teilen den Weg (WorldViewport.syncSprite). (2) Ein Rahmen genuegt, weil vColor premultipliziert eingeht. (3) Batching: ein beschattetes Sprite ist ein eigener Draw-Call, die plain Sprites bleiben im Batch — das ist der ganze verbleibende Preis.
Messung (tools/perf-shaders.mjs, HD 630, 48x48 in 640x360, neutrale Werte, drei Laeufe auf 2 fps gleich)
Bis 400 kostet ein Shader nichts Messbares, bei 800 rund 1,5 us pro Sprite, ab 1600 rund 3 us — dort verpasst schon die shaderlose Szene Takte. shaders == n, filters == 0 an jedem Punkt (neuer Hook __gm.shaderCount()).
e2e Module 10, 12, 25, 27, 28: ALL PASSED – 136 Checks; Actor-Smoke 21. Modul 28 misst dieselben Pixel wie zuvor (brightness 0.5 → #204080, contrast 0 → #808080, neutral byte-gleich mit „kein Shader"), Modul 12c-ii: Nearest-Kante bei Skala 1,43 pixelgleich durch den Shader. Der Check „a shaded sprite carries one filter" ist mitgewandert: jetzt shaders 1 / filters 0 an, 0 / 0 aus.
Gegenproben: Mesh wieder als Kind der Graphics → neue Wache in Modul 12 faellt mit der Kopfzeile PixiJS Deprecation Warning: addChild: …; ohne mesh.unload() beim Wechsel → 7 FAIL (28e on→off→on, 12c-ii), mit → gruen.
Zwei Pixi-Fallen, die den Bau gepraegt haben
Ein Mesh bindet seine Textur nur fuer Pixis eigenen Shader — GraphShader.wear setzt uTexture/uSampler selbst.
MeshPipe cached batched in meshData und prueft es nur in validateRenderable, das Pixi in jedem Frame mit Strukturaenderung ueberspringt — und syncDrawOrder haengt bei jedem Redraw alles neu an. Ein einmal gebatchtes Mesh zeichnete danach fuer immer plain, Shader ignoriert. syncShader ruft beim Wechsel getragen↔nicht getragen mesh.unload().
Nebenbei: Pixis Deprecation-Warnung kam per console.groupCollapsed mit %c-Direktiven — das Konsolen-Panel spiegelt jetzt group/groupCollapsed/groupEnd und fuellt %s %d %i %f %o %O %c %%, vorher fehlte der Kopf der Meldung.
Wiki Shaders: Tabelle und Abschnitt „In the code" auf den Mesh-Weg umgeschrieben. Zaehler 730 (Basis: CI-Lauf 19 mit 728 gemessen). Das Messwerkzeug laeuft weiterhin nicht in CI.
## Gebaut (Commit `0e5e5ef`, Wiki `01d0602`)
**Ein Sprite ist jetzt ein `Mesh`** (Unit-Quad, Pivot 0,5, Scale = Groesse) als Geschwister seines `Graphics`-Knotens unter demselben Parent; der Knoten traegt nur noch die HitArea. Ein getragener Shader ist ein `GraphShader extends Shader` mit eigenem Vertex-Stage, der Pixis Fragment **im Zeichenweg des Sprites** ersetzt — keine Zwischentextur, kein Filter, kein Extra-Pass. Der Compiler-Rahmen bleibt premultipliziert: `texel = texture(...) * vColor` (Tint, premultipliziert wie das Texel) vor dem Graphen, entpremultiplizieren, Graph, zurueckmultiplizieren.
Die drei Klaerungen aus dem Issue, gemessen: (1) `Graphics` mit Texturfuellung ist ersetzt, nicht erweitert — Pixi 8 erlaubt Kinder nur an reinen Containern (`allowChildren` false, sonst die Deprecation-Warnung), deshalb Geschwister statt Kind; Szene und Spiel teilen den Weg (`WorldViewport.syncSprite`). (2) Ein Rahmen genuegt, weil `vColor` premultipliziert eingeht. (3) Batching: ein beschattetes Sprite ist ein eigener Draw-Call, die plain Sprites bleiben im Batch — das ist der ganze verbleibende Preis.
## Messung (`tools/perf-shaders.mjs`, HD 630, 48x48 in 640x360, neutrale Werte, drei Laeufe auf 2 fps gleich)
| Sprites | 50 | 100 | 200 | 400 | 800 | 1600 | 3200 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| alle mit Shader | 60 | 60 | 60 | 60 | 56 | 34 | 17 |
| dieselbe Szene ohne | 60 | 60 | 60 | 60 | 60 | 39 | 21 |
| vorher (Filter-Pass, #67) | 56 | 54 | 48 | 34 | 20 | - | - |
Bis 400 kostet ein Shader nichts Messbares, bei 800 rund 1,5 us pro Sprite, ab 1600 rund 3 us — dort verpasst schon die shaderlose Szene Takte. `shaders == n`, `filters == 0` an jedem Punkt (neuer Hook `__gm.shaderCount()`).
## Beweis
- `./check.sh`: `=== CHECK EXIT 0 ===`, **854 Tests** (850 + 4: Konsolen-Direktiven, Gruppen)
- e2e Module 10, 12, 25, 27, 28: **ALL PASSED – 136 Checks**; Actor-Smoke **21**. Modul 28 misst dieselben Pixel wie zuvor (brightness 0.5 → `#204080`, contrast 0 → `#808080`, neutral byte-gleich mit „kein Shader"), Modul 12c-ii: Nearest-Kante bei Skala 1,43 pixelgleich durch den Shader. Der Check „a shaded sprite carries one filter" ist mitgewandert: jetzt `shaders 1 / filters 0` an, `0 / 0` aus.
- Gegenproben: Mesh wieder als Kind der Graphics → neue Wache in Modul 12 faellt mit der Kopfzeile `PixiJS Deprecation Warning: addChild: …`; ohne `mesh.unload()` beim Wechsel → 7 FAIL (28e on→off→on, 12c-ii), mit → gruen.
## Zwei Pixi-Fallen, die den Bau gepraegt haben
1. **Ein Mesh bindet seine Textur nur fuer Pixis eigenen Shader** — `GraphShader.wear` setzt `uTexture`/`uSampler` selbst.
2. **`MeshPipe` cached `batched` in `meshData` und prueft es nur in `validateRenderable`**, das Pixi in jedem Frame mit Strukturaenderung ueberspringt — und `syncDrawOrder` haengt bei jedem Redraw alles neu an. Ein einmal gebatchtes Mesh zeichnete danach fuer immer plain, Shader ignoriert. `syncShader` ruft beim Wechsel getragen↔nicht getragen `mesh.unload()`.
Nebenbei: Pixis Deprecation-Warnung kam per `console.groupCollapsed` mit `%c`-Direktiven — das Konsolen-Panel spiegelt jetzt `group`/`groupCollapsed`/`groupEnd` und fuellt `%s %d %i %f %o %O %c %%`, vorher fehlte der Kopf der Meldung.
Wiki *Shaders*: Tabelle und Abschnitt „In the code" auf den Mesh-Weg umgeschrieben. Zaehler 730 (Basis: CI-Lauf 19 mit 728 gemessen). Das Messwerkzeug laeuft weiterhin nicht in CI.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Befund (gemessen in #67,
tools/perf-shaders.mjs)Jedes Sprite mit Shader traegt einen eigenen Pixi-
Filter(syncShaderinsrc/viewport/symbols.ts), also einen Render-Pass in eine Zwischentextur und zurueck. Gemessen auf der Intel HD 630, 48x48-Sprites in einer 640x360-Kamera, neutrale Werte, drei Laeufe auf 1 fps gleich:Dieselbe Szene ohne Shader haelt bei jeder dieser Zahlen 60 fps, 800 eingeschlossen.
Der Preis ist der Pass, nicht die Fragment-Arbeit. Vierfache Sprite-Flaeche (96x96) misst dasselbe: 51,6 / 49,5 / 34,2 fps bei 100 / 200 / 400 gegen 53,2 / 48,5 / 34,5 bei 48x48. Es kostet der Wechsel des Render-Ziels, nicht das Rechnen pro Pixel.
Der in #67 angedachte Weg "Sprites teilen sich einen Filter" bringt deshalb nichts. Das GLSL-Programm ist bereits geteilt (
GlProgram.fromcached nach Quelltext, ein Programm fuer alle Sprites); geteilt wuerden nur die Filter-Objekte. Pixi legt trotzdem fuer jeden Container mitfilterseinen eigenen Pass an.Aufgabe
Das kompilierte Fragment dorthin bringen, wo das Sprite ohnehin gezeichnet wird, statt in einen nachgelagerten Filter — dann gibt es gar keinen Extra-Pass.
Zu klaeren, bevor gebaut wird:
Sprites, sondernGraphics-Rechtecke mit Texturfuellung (symbols.ts). Ein eigener Fragment-Shader pro Fuellung ist in dieser Bauweise nicht vorgesehen — der Umbau geht vermutlich ueberMeshmit eigenem Shader (oder einen eigenen Renderable), und das beruehrt Szene- und Spielansicht, weil beide denselben Zeichenweg teilen (WorldViewport.redraw).src/core/shader/compile.ts) rechnet heute mit premultiplizierten Pixeln, weil ein Filter sie so bekommt. Im Zeichen-Shader liegt die Farbe anders vor; der Compiler braucht dann zwei Rahmen oder einen neutralen.Beweis beim Schliessen
node tools/seed-e2e.mjs && node tools/perf-shaders.mjszeigt bei 400 und 800 beschatteten Sprites die Baseline-Rate (rund 60 fps) statt 34 / 20.brightness 0.5->#204080,contrast 0->#808080, neutral = byte-gleich mit "kein Shader"), und Pixel-Art bleibt bei gebrochenem Kamera-Zoom scharf (Modul 12).Gebaut (Commit
0e5e5ef, Wiki01d0602)Ein Sprite ist jetzt ein
Mesh(Unit-Quad, Pivot 0,5, Scale = Groesse) als Geschwister seinesGraphics-Knotens unter demselben Parent; der Knoten traegt nur noch die HitArea. Ein getragener Shader ist einGraphShader extends Shadermit eigenem Vertex-Stage, der Pixis Fragment im Zeichenweg des Sprites ersetzt — keine Zwischentextur, kein Filter, kein Extra-Pass. Der Compiler-Rahmen bleibt premultipliziert:texel = texture(...) * vColor(Tint, premultipliziert wie das Texel) vor dem Graphen, entpremultiplizieren, Graph, zurueckmultiplizieren.Die drei Klaerungen aus dem Issue, gemessen: (1)
Graphicsmit Texturfuellung ist ersetzt, nicht erweitert — Pixi 8 erlaubt Kinder nur an reinen Containern (allowChildrenfalse, sonst die Deprecation-Warnung), deshalb Geschwister statt Kind; Szene und Spiel teilen den Weg (WorldViewport.syncSprite). (2) Ein Rahmen genuegt, weilvColorpremultipliziert eingeht. (3) Batching: ein beschattetes Sprite ist ein eigener Draw-Call, die plain Sprites bleiben im Batch — das ist der ganze verbleibende Preis.Messung (
tools/perf-shaders.mjs, HD 630, 48x48 in 640x360, neutrale Werte, drei Laeufe auf 2 fps gleich)Bis 400 kostet ein Shader nichts Messbares, bei 800 rund 1,5 us pro Sprite, ab 1600 rund 3 us — dort verpasst schon die shaderlose Szene Takte.
shaders == n,filters == 0an jedem Punkt (neuer Hook__gm.shaderCount()).Beweis
./check.sh:=== CHECK EXIT 0 ===, 854 Tests (850 + 4: Konsolen-Direktiven, Gruppen)#204080, contrast 0 →#808080, neutral byte-gleich mit „kein Shader"), Modul 12c-ii: Nearest-Kante bei Skala 1,43 pixelgleich durch den Shader. Der Check „a shaded sprite carries one filter" ist mitgewandert: jetztshaders 1 / filters 0an,0 / 0aus.PixiJS Deprecation Warning: addChild: …; ohnemesh.unload()beim Wechsel → 7 FAIL (28e on→off→on, 12c-ii), mit → gruen.Zwei Pixi-Fallen, die den Bau gepraegt haben
GraphShader.wearsetztuTexture/uSamplerselbst.MeshPipecachedbatchedinmeshDataund prueft es nur invalidateRenderable, das Pixi in jedem Frame mit Strukturaenderung ueberspringt — undsyncDrawOrderhaengt bei jedem Redraw alles neu an. Ein einmal gebatchtes Mesh zeichnete danach fuer immer plain, Shader ignoriert.syncShaderruft beim Wechsel getragen↔nicht getragenmesh.unload().Nebenbei: Pixis Deprecation-Warnung kam per
console.groupCollapsedmit%c-Direktiven — das Konsolen-Panel spiegelt jetztgroup/groupCollapsed/groupEndund fuellt%s %d %i %f %o %O %c %%, vorher fehlte der Kopf der Meldung.Wiki Shaders: Tabelle und Abschnitt „In the code" auf den Mesh-Weg umgeschrieben. Zaehler 730 (Basis: CI-Lauf 19 mit 728 gemessen). Das Messwerkzeug laeuft weiterhin nicht in CI.