Let a shader move the corners of the picture it wears (#79)

A shader could only say what colour a pixel has. Where the picture
is drawn was fixed to the four corners of its rectangle, so nothing
could wave, bend or wobble.

- The new node Vertex output takes x and y, in pixels of the
  picture, and moves every corner of the wearer's grid by them. The
  UVs stay with the corners, so the picture bends along.
- Subdivisions on that node says how fine the grid is: cells per
  axis, 1 to 64, 8 by default. 1 is the plain quad. A shader without
  the node keeps the quad every sprite shares.
- A graph holds one Vertex output at most. UV, Time, Property,
  Number and the math nodes may feed it, and one node may feed both
  outputs.
- Texture color, Texture alpha, Sample texture, Texture size and
  Color cannot feed it. The node says "Not available per corner"
  and the shader does not compile.
- Sprites, texts and the preview in the shader tab wear the grid.
  A nine-slice sprite and particles draw as if the node were not
  there.
- Picking, the selection frame, the size and colliders stay with
  the rectangle. A wearer with a Vertex output is never culled, as
  with Padding, and with Padding the grid covers the room around
  the picture too.
- tools/perf-shaders.mjs takes GM_PERF_SUBDIVISIONS to measure a
  grid against the plain quad.

Measured on stein2 (Intel HD 630), frames per second, one run per
row: at 800 sprites 58 with a shader on the quad, 46 with 8 cells,
41 with 32. Up to 400 sprites a grid hardly shows. Every sprite
holds its own grid.

Tests:
- Gate on melektaus-atx: 220 files, 3416 tests, CHECK EXIT 0.
- 15 new unit tests for the compiler and the grid.
- Four new e2e checks in modules 28 and 30: a fixed offset moves
  the picture, 8 cells bend only the lower half where 1 cell shears
  it all, Texture alpha is refused at the node, a text moves.
- e2e 28,30 on melektaus-atx: 110 checks, all pass.
- The e2e total of 1699 is counted (1695 + 4), the full suite did
  not run.
- No counter-probe, no screenshot. Not looked at: the preview, Time
  in the browser, flipped sprites, Padding together with the grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-03 00:14:00 +02:00
1 parent 963f8a152a
commit 4716726b6b
14 files changed
+586 -75

No files matched your search

+4 -3
View File
@@ -29,7 +29,7 @@ import {
} from '../../core/properties/particles';
import { nextPropertyName, PROPERTY_NAME_PARAM, SHADER_PROPERTY } from '../../core/properties/shaders';
import { nextParticlePropertyName, PARTICLE_NODES, PARTICLE_OUTPUT } from '../../core/particles/nodes';
import { SHADER_NODES, SHADER_OUTPUT } from '../../core/shader/nodes';
import { SHADER_NODES, SHADER_OUTPUT, SHADER_VERTEX_OUTPUT } from '../../core/shader/nodes';
/**
* One row of the palette. `def` is the definition the node would actually have once placed, so the
@@ -95,8 +95,9 @@ export function paletteEntries(project: Project, asset: GraphAsset): PaletteEntr
return [{ id: State.type, hint: State.category, def: State, params }];
}
if (asset.type === 'shader') {
const hasOutput = asset.graph.nodes.some((n) => n.type === SHADER_OUTPUT);
return SHADER_NODES.filter((def) => def.type !== SHADER_OUTPUT || !hasOutput).map((def) => {
const placed = new Set(asset.graph.nodes.map((n) => n.type));
const once = [SHADER_OUTPUT, SHADER_VERTEX_OUTPUT];
return SHADER_NODES.filter((def) => !once.includes(def.type) || !placed.has(def.type)).map((def) => {
const params: PropertyBag =
def.type === SHADER_PROPERTY ? { [PROPERTY_NAME_PARAM]: nextPropertyName(asset.graph) } : {};
return { id: def.type, hint: def.category, def, params };
+3 -2
View File
@@ -114,10 +114,11 @@ export class ShaderPreview extends Widget {
);
this.mesh.position.set(Math.round(this.w / 2), Math.round(this.h / 2));
this.mesh.scale.set(w * zoom, h * zoom);
syncQuadPadding(this.mesh, worn?.padding ?? 0, texture?.width ?? w, texture?.height ?? h);
const counted = { w: texture?.width ?? w, h: texture?.height ?? h };
syncQuadPadding(this.mesh, worn?.padding ?? 0, counted.w, counted.h, worn?.vertex?.subdivisions ?? 1);
this.mesh.texture = texture ?? Texture.WHITE;
this.mesh.tint = picture?.tint ?? 0xffffff;
syncShader(this.mesh, { ...this.values, [SHADER_KEY]: this.shader.id }, source);
syncShader(this.mesh, { ...this.values, [SHADER_KEY]: this.shader.id }, source, counted);
}
override destroy(): void {