Shaders: the shader draws in the sprite's own pass, measured again (#77)
1 parent
bd26824b01
commit
01d0602654
2 files changed
+30
-21
No files matched your search
+1
-1
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
|
||||
# code is lost (tail's is reported), so a red run reads as green
|
||||
npm run build # writes dist/ and regenerates index.html
|
||||
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 728 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 730 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
|
||||
|
||||
+29
-20
@@ -14,7 +14,7 @@ a bad property name, ...). A node that keeps the graph from compiling shows the
|
||||
and a red frame.
|
||||
|
||||
A shader graph is compiled on every change. A sprite wearing a graph that does not compile is drawn
|
||||
without a filter, as if it wore none.
|
||||
plain, as if it wore none.
|
||||
|
||||
## The nodes
|
||||
|
||||
@@ -66,8 +66,8 @@ saturation 0 gives `#7c7c7c` — all byte-exact against the formula.
|
||||
|
||||
## Wearing a shader
|
||||
|
||||
The sprite's `Shader` row is a picker over the project's shader assets (`None` is no shader and no
|
||||
extra render pass). The rows below it are the shader's `Property` nodes, in node order; they are
|
||||
The sprite's `Shader` row is a picker over the project's shader assets (`None` is no shader: the
|
||||
sprite draws in Pixi's batch like any other). The rows below it are the shader's `Property` nodes, in node order; they are
|
||||
ordinary sprite properties: overridable per placed object, reset (↺) and apply (↑) like any other,
|
||||
readable and writable by a `Properties` node at runtime — a behavior can fade a sprite by writing
|
||||
its `brightness` every frame. That node's `+ Property` list (under *Type: Sprite*) offers the
|
||||
@@ -82,24 +82,26 @@ deviations while their shader is not worn.
|
||||
|
||||
## What it costs, and where it stops
|
||||
|
||||
A shader is a Pixi filter: the sprite is rendered into an intermediate texture and drawn back with
|
||||
the fragment, one pass per sprite wearing one. Measured with `tools/perf-shaders.mjs` on an Intel
|
||||
HD 630 (the GPU CI uses as well), sprites of 48x48 inside a 640x360 camera, neutral values, three
|
||||
runs agreeing to within 1 fps:
|
||||
A shader runs in the sprite's own draw: the mesh is drawn once, with the graph's fragment in place
|
||||
of Pixi's, so there is no intermediate texture and no extra pass. What it costs is the batch — a
|
||||
sprite drawing with a shader of its own cannot share a draw call with the plain ones, so every
|
||||
shaded sprite is one draw call. Measured with `tools/perf-shaders.mjs` on an Intel HD 630 (the GPU
|
||||
CI uses as well), sprites of 48x48 inside a 640x360 camera, neutral values, three runs agreeing to
|
||||
within 2 fps:
|
||||
|
||||
| shaded sprites | 25 | 50 | 100 | 200 | 400 | 800 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| frames per second | 58 | 56 | 54 | 48 | 34 | 20 |
|
||||
| sprites | 50 | 100 | 200 | 400 | 800 | 1600 | 3200 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| all wearing the shader | 60 | 60 | 60 | 60 | 56 | 34 | 17 |
|
||||
| the same scene, no shader | 60 | 60 | 60 | 60 | 60 | 39 | 21 |
|
||||
|
||||
The very same scene without shaders holds 60 fps at every one of those counts, 800 included, so the
|
||||
whole drop is the passes: about 20 microseconds each up to 200 sprites, 40 at 800. Sprite size
|
||||
hardly matters — four times the area (96x96) measures the same, so what costs is the pass, not the
|
||||
pixels in it. A few dozen shaded sprites are free in practice; a few hundred cost half the frame
|
||||
rate. The pass runs at
|
||||
screen resolution, so pixel art stays sharp: measured in play at a fractional camera scale (1.43
|
||||
Up to 400 shaded sprites nothing shows; at 800 the shader costs about 1.5 microseconds per sprite,
|
||||
from 1600 on about 3 — and there the plain scene misses ticks by itself. For comparison, as a
|
||||
filter pass per sprite (up to #77) the same counts measured 56 / 54 / 48 / 34 / 20 fps from 50 to
|
||||
800: what cost then was the change of render target, not the pixels. The shader samples the
|
||||
texture itself, so pixel art stays sharp: measured in play at a fractional camera scale (1.43
|
||||
screen pixels per world unit), a *Nearest* texture keeps its hard border pixel for pixel with a
|
||||
neutral shader on, and brightness 0.5 darkens every one of those pixels — the pass runs there, and
|
||||
it does not resample. Only sprites wear shaders; text objects do not. The
|
||||
neutral shader on, and brightness 0.5 darkens every one of those pixels — the shader draws there,
|
||||
and it does not resample. Only sprites wear shaders; text objects do not. The
|
||||
node set is deliberately small — no channel split, no texture sampling at other coordinates, no
|
||||
time input; each is one node in `src/core/shader/nodes.ts` (a `ShaderNodeDef` with an `emit`) and
|
||||
one row in the table above.
|
||||
@@ -108,8 +110,15 @@ one row in the table above.
|
||||
|
||||
`src/core/shader/nodes.ts` is the catalogue, `src/core/shader/compile.ts` turns a graph into the
|
||||
fragment (every node reachable from the Output becomes one local per output, in dependency order;
|
||||
every Property a `uniform float u_<name>`), `src/viewport/shaders.ts` wraps the result in a filter
|
||||
and `src/viewport/ShaderLibrary.ts` caches it per asset. The rows come from
|
||||
every Property a `uniform float u_<name>`), `src/viewport/shaders.ts` wraps the result in a Pixi
|
||||
`Shader` for the sprite's mesh (`GraphShader`, with a vertex stage of its own) and
|
||||
`src/viewport/ShaderLibrary.ts` caches the compiled graph per asset. A sprite's picture is a `Mesh`
|
||||
(a unit quad sized by its scale, `symbols.ts`), a sibling of the sprite's `Graphics` node under the
|
||||
same parent: Pixi 8 lets only plain containers have children, and the node keeps just the hit area.
|
||||
Two Pixi facts the code leans on: a mesh's texture is bound only for Pixi's own shader, so
|
||||
`GraphShader.wear` hands it over itself; and Pixi caches whether a mesh batches and re-checks that
|
||||
only in a frame without structure changes, so switching a shader on or off calls `mesh.unload()`
|
||||
to drop that cache — otherwise a mesh that ever drew batched keeps drawing plain, shader ignored. The rows come from
|
||||
`shaderParamDefs(graph)` in `src/core/properties/shaders.ts`; `schemaForAsset` and
|
||||
`schemaForObject` (`PropertySchema.ts`) append them to a sprite's schema, which is why both take
|
||||
the project.
|
||||
Reference in new issue
Block a user