A shader costs a source nothing, because a field was already one draw call (#88)
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:
rootandClaude Opus 5 committed 2026-09-14 11:06:45 +02:00
1 parent 0e0a21dee1
commit d5e67cd80c
9 files changed
+346 -26

No files matched your search

+11 -3
View File
@@ -45,6 +45,7 @@ import {
isValidVariableName,
type PropertyDef,
type PropertySchema,
rowsNotClaimedBy,
schemaFor,
type StructFieldDef,
variableDef,
@@ -67,9 +68,11 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
const shaderGraphs = new Map<string, Graph>();
const shaderRows = new Map<string, PropertyDef[]>();
const particleGraphs = new Map<string, Graph>();
const wearers = new Map<string, 'sprite' | 'particles'>();
for (const a of rawAssets) {
if (typeof a.id !== 'string' || !a.id || !isAssetType(a.type)) continue;
rawProps.set(a.id, asRecord(a.props));
if (a.type === 'sprite' || a.type === 'particles') wearers.set(a.id, a.type);
if (a.type === 'behavior') {
const vars = normalizeVariables(a.variables);
variables.set(a.id, vars);
@@ -89,13 +92,18 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
schemas.set(a.id, schemaFor(a.type));
}
}
/** A sprite's rows end with the parameters of the shader its bag names – an override's, else the asset's. */
/**
* A sprite's or source's rows end with the parameters of the shader its bag names – an override's,
* else the asset's. A source already carries its own graph's rows, so a name it has stays its own.
*/
const schemaOf = (assetId: string, bag: Record<string, unknown>): PropertySchema | undefined => {
const schema = schemas.get(assetId);
if (!schema || schema !== schemaFor('sprite')) return schema;
const wearer = wearers.get(assetId);
if (!schema || !wearer) return schema;
const own = bag[SHADER_KEY];
const shader = typeof own === 'string' ? own : rawProps.get(assetId)?.[SHADER_KEY];
return [...schema, ...(shaderRows.get(typeof shader === 'string' ? shader : '') ?? [])];
const rows = shaderRows.get(typeof shader === 'string' ? shader : '') ?? [];
return [...schema, ...(wearer === 'particles' ? rowsNotClaimedBy(rows, schema) : rows)];
};
// Pass 2: build assets. Frame ids share the reference space with asset ids, so `normalizeTexture`