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
@@ -4,7 +4,7 @@
|
||||
* pin: the same second always looks the same, and the whole field is a function of the clock.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||||
import { AddNodeCommand, ConnectCommand, MoveNodeCommand } from '../../src/core/commands/graphCommands';
|
||||
import { isPlaceableType } from '../../src/core/model/AssetTypes';
|
||||
import { findAssetOf } from '../../src/core/model/Project';
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
TIME_SCALE_KEY,
|
||||
} from '../../src/core/properties/particles';
|
||||
import { schemaFor, schemaForAsset } from '../../src/core/properties/PropertySchema';
|
||||
import { SHADER_KEY, SHADER_PROPERTY } from '../../src/core/properties/shaders';
|
||||
import { paletteEntries } from '../../src/editor/graph/paletteEntries';
|
||||
import { makeProject, type ProjectFixture, roundTrip } from '../helpers';
|
||||
|
||||
@@ -379,3 +380,65 @@ describe('the source rows', () => {
|
||||
expect([...PARTICLE_OUTPUTS]).toEqual(['x', 'y', 'scaleX', 'scaleY', 'rotation', 'color', 'alpha']);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A source wears a shader the way a sprite does (#88) – but it is the only asset drawing its rows
|
||||
* from two graphs at once, so what a name means when both declare it is pinned here.
|
||||
*/
|
||||
describe('a source wearing a shader', () => {
|
||||
function wearing(uniform = 'glow') {
|
||||
const f = makeProject();
|
||||
const p = particlesEditor(f);
|
||||
const create = new CreateAssetCommand('shader', 'Glow');
|
||||
f.stack.execute(create);
|
||||
const shaderId = create.asset.id;
|
||||
const declared = { name: uniform, default: 3, min: 0, max: 10 };
|
||||
f.stack.execute(new AddNodeCommand(shaderId, SHADER_PROPERTY, 0, 0, declared));
|
||||
const keys = () => schemaForAsset(f.project, p.asset()).map((d) => d.key);
|
||||
const wear = () => f.stack.execute(new SetAssetPropertyCommand(p.id, SHADER_KEY, shaderId));
|
||||
return { ...f, p, shaderId, keys, wear };
|
||||
}
|
||||
|
||||
it('has the picker, and the shader’s rows once one is picked', () => {
|
||||
const f = wearing();
|
||||
expect(schemaFor('particles').find((d) => d.key === SHADER_KEY)).toMatchObject({
|
||||
kind: 'asset',
|
||||
assetTypes: ['shader'],
|
||||
default: '',
|
||||
});
|
||||
expect(f.keys()).not.toContain('glow');
|
||||
f.wear();
|
||||
expect(f.keys().at(-1)).toBe('glow');
|
||||
expect(schemaForAsset(f.project, f.p.asset()).at(-1)).toMatchObject({ kind: 'number', default: 3 });
|
||||
});
|
||||
|
||||
it('keeps a name it already has: the uniform gets no second row', () => {
|
||||
const f = wearing(RATE_KEY);
|
||||
f.wear();
|
||||
// One row named `rate`, and it is the source's own – the uniform stays on its declared default.
|
||||
expect(f.keys().filter((k) => k === RATE_KEY)).toEqual([RATE_KEY]);
|
||||
expect(schemaForAsset(f.project, f.p.asset()).find((d) => d.key === RATE_KEY)).toMatchObject({
|
||||
label: 'Rate',
|
||||
default: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it('takes a name the particle graph declares, too', () => {
|
||||
const f = wearing('spin');
|
||||
f.p.add(PARTICLE_PROPERTY, { name: 'spin', default: 7 });
|
||||
f.wear();
|
||||
expect(f.keys().filter((k) => k === 'spin')).toEqual(['spin']);
|
||||
expect(schemaForAsset(f.project, f.p.asset()).find((d) => d.key === 'spin')).toMatchObject({
|
||||
default: 7,
|
||||
});
|
||||
});
|
||||
|
||||
it('carries the shader and its values through a save', () => {
|
||||
const f = wearing();
|
||||
f.wear();
|
||||
f.stack.execute(new SetAssetPropertyCommand(f.p.id, 'glow', 5));
|
||||
const loaded = findAssetOf(roundTrip(f.project), f.p.id, 'particles')!;
|
||||
expect(loaded.props[SHADER_KEY]).toBe(f.shaderId);
|
||||
expect(loaded.props.glow).toBe(5);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user