Let a shader declare a color the sprite sets (#298)
A shader could only ask its sprites for numbers. A tint, an outline colour or a glow colour had to be built from three Property nodes. - New node Color property next to Property: Name and Default (a colour, white unless picked), output `color`. It becomes a colour row below the Shader dropdown and a vec3 uniform in the program. - Number and colour properties share one set of names. A clash is the existing "Another property is named" problem. - Like Color it cannot feed the Vertex output. - Placed from the palette it is called tint, then tint2. - The preview in the Shader tab shows a colour field for it. Like the sliders it starts at the sprite's value and never writes into the project. - `declaredRows` takes the row builder, so the particle graph keeps its number rows unchanged. Tests: - Gate: 222 files, 3452 tests (3445 + 7) - e2e 28: 89 checks (28l-v: default colour drawn, then the colour the sprite sets) - e2e total 1704, counted (1703 + 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4f4c149211
commit
bf27e42e8a
13 files changed
+303
-57
No files matched your search
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* A shader's `Color property` (#298): a colour row on every sprite wearing it, and a vec3 uniform
|
||||
* in the program – the colour counterpart of `Property`.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||||
import { AddNodeCommand, ConnectCommand } from '../../src/core/commands/graphCommands';
|
||||
import { findAssetOf } from '../../src/core/model/Project';
|
||||
import { schemaForAsset } from '../../src/core/properties/PropertySchema';
|
||||
import {
|
||||
SHADER_COLOR_PROPERTY,
|
||||
SHADER_KEY,
|
||||
SHADER_PROPERTY,
|
||||
shaderParamDefs,
|
||||
} from '../../src/core/properties/shaders';
|
||||
import { compileShader, perCornerProblem } from '../../src/core/shader/compile';
|
||||
import { SHADER_OUTPUT, SHADER_VERTEX_OUTPUT } from '../../src/core/shader/nodes';
|
||||
import { paletteEntries } from '../../src/editor/graph/paletteEntries';
|
||||
import { startValues } from '../../src/editor/shader/previewPicture';
|
||||
import { makeSpriteScene, roundTrip } from '../helpers';
|
||||
|
||||
function shader() {
|
||||
const f = makeSpriteScene();
|
||||
const create = new CreateAssetCommand('shader', 'Recolor');
|
||||
f.stack.execute(create);
|
||||
const id = create.asset.id;
|
||||
const asset = () => findAssetOf(f.project, id, 'shader')!;
|
||||
const add = (type: string, params = {}) => {
|
||||
const command = new AddNodeCommand(id, type, 0, 0, params);
|
||||
f.stack.execute(command);
|
||||
return command.node.id;
|
||||
};
|
||||
const link = (from: string, fromPort: string, to: string, toPort: string) =>
|
||||
f.stack.execute(new ConnectCommand(id, { node: from, port: fromPort }, { node: to, port: toPort }));
|
||||
const out = add(SHADER_OUTPUT);
|
||||
return { f, id, asset, add, link, out, build: () => compileShader(asset().graph) };
|
||||
}
|
||||
|
||||
describe('Color property (#298)', () => {
|
||||
it('declares a colour row, in node order with the number rows', () => {
|
||||
const s = shader();
|
||||
s.add(SHADER_PROPERTY, { name: 'amount', default: 0.5, min: 0, max: 1 });
|
||||
s.add(SHADER_COLOR_PROPERTY, { name: 'tint', default: 0x3cc85a });
|
||||
s.add(SHADER_COLOR_PROPERTY, { name: 'shade', default: 'not a colour' });
|
||||
const rows = shaderParamDefs(s.asset().graph);
|
||||
expect(rows.map((r) => [r.key, r.kind])).toEqual([
|
||||
['amount', 'number'],
|
||||
['tint', 'color'],
|
||||
['shade', 'color'],
|
||||
]);
|
||||
expect(rows[1]).toEqual({ key: 'tint', label: 'tint', kind: 'color', default: 0x3cc85a });
|
||||
expect(rows[2]!.default).toBe(0xffffff);
|
||||
});
|
||||
|
||||
it('compiles to a vec3 uniform the Output can take as its colour', () => {
|
||||
const s = shader();
|
||||
const tint = s.add(SHADER_COLOR_PROPERTY, { name: 'tint', default: 0xff0000 });
|
||||
const mixed = s.add('ShaderMultiply');
|
||||
s.link(tint, 'color', mixed, 'a');
|
||||
s.link(mixed, 'result', s.out, 'color');
|
||||
const build = s.build();
|
||||
expect(build.problems).toEqual({});
|
||||
expect(build.compiled!.uniforms).toEqual([{ name: 'tint', kind: 'color', default: 0xff0000 }]);
|
||||
expect(build.compiled!.fragment).toContain('uniform vec3 u_tint;');
|
||||
expect(build.compiled!.fragment).toContain('vec3 n0_color = u_tint;');
|
||||
expect(build.compiled!.fragment).toContain('vec3 n1_result = (n0_color * vec3(1.0));');
|
||||
});
|
||||
|
||||
it('shares its names with the number properties', () => {
|
||||
const s = shader();
|
||||
s.add(SHADER_PROPERTY, { name: 'tint', default: 1 });
|
||||
const second = s.add(SHADER_COLOR_PROPERTY, { name: 'tint' });
|
||||
expect(s.build().problems[second]).toBe('Another property is named "tint"');
|
||||
const rename = s.add(SHADER_COLOR_PROPERTY, { name: 'color' });
|
||||
expect(s.build().problems[rename]).toBe('"color" is a sprite property');
|
||||
});
|
||||
|
||||
it('is a colour, so it cannot feed the Vertex output', () => {
|
||||
const s = shader();
|
||||
const corner = s.add(SHADER_VERTEX_OUTPUT);
|
||||
const tint = s.add(SHADER_COLOR_PROPERTY, { name: 'tint' });
|
||||
const bright = s.add('ShaderLuminance');
|
||||
s.link(tint, 'color', bright, 'color');
|
||||
s.link(bright, 'value', corner, 'x');
|
||||
const build = s.build();
|
||||
expect(build.compiled).toBeNull();
|
||||
expect(build.problems[corner]).toBe(perCornerProblem('Color property'));
|
||||
});
|
||||
|
||||
it('is placed from the palette as tint, then tint2', () => {
|
||||
const s = shader();
|
||||
const entry = () =>
|
||||
paletteEntries(s.f.project, s.asset()).find((e) => e.def.type === SHADER_COLOR_PROPERTY)!;
|
||||
expect(entry().params).toEqual({ name: 'tint' });
|
||||
s.add(SHADER_COLOR_PROPERTY, entry().params);
|
||||
expect(entry().params).toEqual({ name: 'tint2' });
|
||||
});
|
||||
|
||||
it('gives a sprite wearing it a colour row that survives a save', () => {
|
||||
const s = shader();
|
||||
s.add(SHADER_COLOR_PROPERTY, { name: 'tint', default: 0x112233 });
|
||||
s.f.stack.execute(new SetAssetPropertyCommand(s.f.spriteId, SHADER_KEY, s.id));
|
||||
const sprite = () => findAssetOf(s.f.project, s.f.spriteId, 'sprite')!;
|
||||
expect(schemaForAsset(s.f.project, sprite()).find((d) => d.key === 'tint')).toMatchObject({
|
||||
kind: 'color',
|
||||
default: 0x112233,
|
||||
});
|
||||
s.f.stack.execute(new SetAssetPropertyCommand(s.f.spriteId, 'tint', 0xe63c3c));
|
||||
const loaded = findAssetOf(roundTrip(s.f.project), s.f.spriteId, 'sprite')!;
|
||||
expect(loaded.props.tint).toBe(0xe63c3c);
|
||||
});
|
||||
|
||||
it('starts the preview at the sprite’s colour when it wears the shader, else at the default', () => {
|
||||
const s = shader();
|
||||
s.add(SHADER_COLOR_PROPERTY, { name: 'tint', default: 0x112233 });
|
||||
const defs = shaderParamDefs(s.asset().graph);
|
||||
expect(startValues(defs, { [SHADER_KEY]: s.id, tint: '#e63c3c' }, s.id)).toEqual({ tint: 0xe63c3c });
|
||||
expect(startValues(defs, { [SHADER_KEY]: 'other', tint: 0xe63c3c }, s.id)).toEqual({ tint: 0x112233 });
|
||||
expect(startValues(defs, { [SHADER_KEY]: s.id }, s.id)).toEqual({ tint: 0x112233 });
|
||||
});
|
||||
});
|
||||
@@ -83,7 +83,7 @@ describe('Vertex output (#79)', () => {
|
||||
s.link(amount, 'value', s.corner, 'y');
|
||||
s.link(amount, 'value', s.out, 'alpha');
|
||||
const { compiled } = s.build();
|
||||
expect(compiled!.uniforms).toEqual([{ name: 'amount', default: 1 }]);
|
||||
expect(compiled!.uniforms).toEqual([{ name: 'amount', kind: 'number', default: 1 }]);
|
||||
expect(compiled!.fragment).toContain('float n0_value = u_amount;');
|
||||
expect(compiled!.vertex!.body).toContain('float c0_value = u_amount;');
|
||||
// One precision for a uniform both stages declare: the fragment's default.
|
||||
|
||||
@@ -215,9 +215,9 @@ describe('compileShader', () => {
|
||||
expect(build.reason).toBe('');
|
||||
const compiled = build.compiled!;
|
||||
expect(compiled.uniforms).toEqual([
|
||||
{ name: 'brightness', default: 1 },
|
||||
{ name: 'contrast', default: 1 },
|
||||
{ name: 'saturation', default: 1 },
|
||||
{ name: 'brightness', kind: 'number', default: 1 },
|
||||
{ name: 'contrast', kind: 'number', default: 1 },
|
||||
{ name: 'saturation', kind: 'number', default: 1 },
|
||||
]);
|
||||
const { fragment } = compiled;
|
||||
for (const name of ['brightness', 'contrast', 'saturation'])
|
||||
|
||||
Reference in new issue
Block a user