A shader costs a source nothing, because a field was already one draw call (#88)
game-maker / suite (push) Successful in 10m9s
game-maker / suite (push) Successful in 10m9s
A particle source can wear a shader asset the way a sprite does, and it is the
cheapest place in the editor to wear one: a shaded sprite gives up its batch and
costs a draw call of its own, while `ParticleContainerPipe` breaks the batch for
every field regardless. Five hundred shaded particles are the one call five
hundred plain ones were.
The fragment the compiler emits needed no change, only a second vertex stage:
Pixi's particle vertex is GLSL ES 1.00 and ours is ES 3.00, so it is translated
rather than invented. What it sees is a particle's picture, not its life — the
buffer carries five fixed attributes and takes no sixth, so age, index and seed
stay on the graph, which is what the graph is for.
Two things the issue did not ask about:
`Shader.resources` is an accessor over the names its constructor declared, so a
slot that is not declared becomes a loose JS property and is never bound. The
adaptor writes `uTranslationMatrix` into whichever shader the container names —
without an empty block for it the vertex gets no matrix and the particles land
elsewhere.
`schemaOf` in serialize.ts hung on `schema !== schemaFor('sprite')`, an identity
check a source never satisfies, so its shader values would have been dropped on
save.
A source is the one asset drawing rows from two graphs, so a name can arrive
twice. Its own row wins and the uniform keeps its default; nothing is rejected
on the shader's side, because the collision exists only at this one wearer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e0a21dee1
commit
d5e67cd80c
9 files changed
+346
-26
No files matched your search
@@ -31,6 +31,10 @@ export async function particles(t) {
|
||||
await sleep(120);
|
||||
};
|
||||
const drawn = (limit = 8) => json(`__gm.particlesOf(${JSON.stringify(SOURCE)}, 'scene', ${limit})`);
|
||||
const selectSource = async () => {
|
||||
await select({ kind: 'object', holderId: 'scene1', objectIds: [SOURCE], objectId: SOURCE });
|
||||
await sleep(350);
|
||||
};
|
||||
const clickLast = async (text) => {
|
||||
const p = await locate(text, true);
|
||||
await click(p.x, p.y);
|
||||
@@ -609,6 +613,86 @@ export async function particles(t) {
|
||||
JSON.stringify({ linked, unlinked }),
|
||||
);
|
||||
|
||||
// 31r. (#88) A source wears a shader like a sprite does: the picker is a row of its own, and the
|
||||
// shader's Property nodes become rows under it. The graph is planted rather than clicked –
|
||||
// step 28 owns the shader tab; what is new here is a *source* wearing one.
|
||||
const SHADER = 'pt-shader';
|
||||
await mutate(`
|
||||
__gm.state.project.assets.push({
|
||||
id: ${JSON.stringify(SHADER)}, type: 'shader', name: 'Level', props: {},
|
||||
graph: { nodes: [
|
||||
{ id: 'lv-prop', type: 'ShaderProperty', x: 0, y: 0,
|
||||
params: { name: 'level', default: 1, min: 0, max: 1 } },
|
||||
{ id: 'lv-out', type: 'ShaderOutput', x: 200, y: 0, params: {} },
|
||||
], edges: [
|
||||
{ from: { node: 'lv-prop', port: 'value' }, to: { node: 'lv-out', port: 'color' } },
|
||||
] },
|
||||
});
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
Object.assign(o.overrides, { shader: ${JSON.stringify(SHADER)}, level: 1 });`);
|
||||
await sleep(300);
|
||||
await selectSource();
|
||||
// The tab bar has a button reading "Shader" too, and it is drawn first: the row wanted here is the
|
||||
// *last* text of that name, and it has to stand in the inspector's own column to be one.
|
||||
const shaderRow = await json('__gm.inspectorField("Shader", true)');
|
||||
const levelRow = await json('__gm.inspectorField("level")');
|
||||
check(
|
||||
'a source has the shader picker, and the shader’s own rows below it',
|
||||
Boolean(shaderRow) && Boolean(levelRow) && shaderRow.left === levelRow.left && levelRow.y > shaderRow.y,
|
||||
JSON.stringify({ shaderRow, levelRow }),
|
||||
);
|
||||
|
||||
// 31s. (#88) And the particles really wear it: the fragment replaces the container's own, so the
|
||||
// green the source asks for gives way to what the graph computes – white at level 1.
|
||||
await rewind();
|
||||
await at(0.1);
|
||||
const shaded = await json(`__gm.pixelAt(${Math.round(spotOn.x)}, ${Math.round(spotOn.y)})`);
|
||||
check(
|
||||
'the shader reaches every particle, over the colour the source itself sets',
|
||||
colourDistance(shaded, '#ffffff') < 12 && colourDistance(shaded, '#3cc85a') > 60,
|
||||
JSON.stringify({ shaded }),
|
||||
);
|
||||
await shot('31-particles-shaded');
|
||||
|
||||
// 31t. (#88) The row is the uniform: a quarter turns the same particles a quarter grey. Read at
|
||||
// the same frozen second, so nothing but the number has changed.
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
o.overrides.level = 0.25;`);
|
||||
await sleep(300);
|
||||
await at(0.1);
|
||||
const dimmed = await json(`__gm.pixelAt(${Math.round(spotOn.x)}, ${Math.round(spotOn.y)})`);
|
||||
check(
|
||||
'the shader’s row feeds its uniform, particle by particle',
|
||||
colourDistance(dimmed, '#404040') < 16,
|
||||
JSON.stringify({ dimmed }),
|
||||
);
|
||||
|
||||
// 31u. (#88) A source draws rows from two graphs, so one name can arrive twice. The source's own
|
||||
// row wins and the uniform keeps its default – two rows called Rate would be one row with two
|
||||
// meanings, which is #63 in another coat.
|
||||
await mutate(`
|
||||
const shader = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(SHADER)});
|
||||
shader.graph.nodes.find((n) => n.id === 'lv-prop').params.name = 'rate';
|
||||
shader.graph.edges = [];`);
|
||||
await sleep(300);
|
||||
await select(null);
|
||||
await selectSource();
|
||||
const own = await json('__gm.locateAll("Rate")');
|
||||
const uniform = await json('__gm.locateAll("rate")');
|
||||
check(
|
||||
'a uniform named like a row of the source adds no second row of that name',
|
||||
own.length === 1 && uniform.length === 0,
|
||||
JSON.stringify({ own: own.length, uniform: uniform.length }),
|
||||
);
|
||||
await mutate(`
|
||||
const o = __gm.state.activeScene.objects.find((o) => o.id === ${JSON.stringify(SOURCE)});
|
||||
delete o.overrides.shader;
|
||||
delete o.overrides.level;
|
||||
__gm.state.project.assets = __gm.state.project.assets.filter(
|
||||
(a) => a.id !== ${JSON.stringify(SHADER)});`);
|
||||
await sleep(300);
|
||||
|
||||
// 31p. The graph tab draws no Pixi complaint of its own.
|
||||
const logged = await json('__gm.console.lines()');
|
||||
check(
|
||||
|
||||
Reference in new issue
Block a user