Shader graph: Number node, a typed constant like Color (#72)
game-maker / suite (push) Canceled after 6m55s
game-maker / suite (push) Canceled after 6m55s
No input, one Value param (step 0.05, drawn on the node), output `value` as a float literal via floatLiteral (2 -> 2.0, NaN -> 0.0). No uniform, no sprite row - that is the difference to Property. The e2e negative check for "no behavior node in the palette" now uses Boolean as its marker, and step 28 types the palette search first because the palette scrolls from twelve entries on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d9c4982eb
commit
ccc6dc5b6e
5 files changed
+89
-4
No files matched your search
@@ -37,7 +37,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 717 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 720 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
||||
|
||||
@@ -36,6 +36,7 @@ export const SHADER_TEXTURE_COLOR = 'ShaderTextureColor';
|
||||
export const SHADER_TEXTURE_ALPHA = 'ShaderTextureAlpha';
|
||||
export const SHADER_OUTPUT = 'ShaderOutput';
|
||||
export const SHADER_COLOR = 'ShaderColor';
|
||||
export const SHADER_NUMBER = 'ShaderNumber';
|
||||
|
||||
/** What the fragment prelude binds: the texel's straight (unpremultiplied) colour. */
|
||||
export const TEXEL_COLOR: Expr = { code: 'rgb', type: 'vec3' };
|
||||
@@ -160,6 +161,17 @@ export const ShaderColor: ShaderNodeDef = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const ShaderNumber: ShaderNodeDef = {
|
||||
type: SHADER_NUMBER,
|
||||
label: 'Number',
|
||||
category: 'shader',
|
||||
description: 'A constant number.',
|
||||
inputs: [],
|
||||
outputs: [{ key: 'value', label: 'value', type: 'number' }],
|
||||
params: [{ key: 'value', label: 'Value', kind: 'number', default: 0, step: NUMBER_STEP }],
|
||||
emit: (_inputs, params) => ({ value: { code: floatLiteral(Number(params.value ?? 0)), type: 'float' } }),
|
||||
};
|
||||
|
||||
export const ShaderAdd = arithmetic('ShaderAdd', 'Add', '+', 'a + b, per channel for colours.', 0);
|
||||
export const ShaderSubtract = arithmetic(
|
||||
'ShaderSubtract',
|
||||
@@ -211,6 +223,7 @@ export const SHADER_NODES: readonly ShaderNodeDef[] = [
|
||||
ShaderOutput,
|
||||
ShaderProperty,
|
||||
ShaderColor,
|
||||
ShaderNumber,
|
||||
ShaderAdd,
|
||||
ShaderSubtract,
|
||||
ShaderMultiply,
|
||||
|
||||
@@ -278,6 +278,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
nodeLabel: (nodeId: string) => editor.shaderEditor.canvas.nodeLabel(nodeId),
|
||||
nodeRect: (nodeId: string) => editor.shaderEditor.canvas.nodeRect(nodeId),
|
||||
nodeProblem: (nodeId: string) => editor.shaderEditor.canvas.nodeProblem(nodeId),
|
||||
nodeSettings: (nodeId: string) => editor.shaderEditor.canvas.nodeSettings(nodeId),
|
||||
canvasRect: () => editor.shaderEditor.canvas.canvasRect(),
|
||||
emptySpot: () => editor.shaderEditor.canvas.emptySpotGlobal(),
|
||||
center: () => centreOf(editor.shaderEditor.canvas),
|
||||
|
||||
@@ -25,6 +25,7 @@ import { inlineInputs } from '../../src/core/graph/NodeRegistry';
|
||||
import { compileShader } from '../../src/core/shader/compile';
|
||||
import {
|
||||
SHADER_NODES,
|
||||
SHADER_NUMBER,
|
||||
SHADER_OUTPUT,
|
||||
SHADER_TEXTURE_ALPHA,
|
||||
SHADER_TEXTURE_COLOR,
|
||||
@@ -227,6 +228,35 @@ describe('compileShader', () => {
|
||||
expect(tinted).toMatch(/vec3 n1_result = \(vec3\(1\.0\) \* n0_color\);/);
|
||||
});
|
||||
|
||||
it('turns a Number node into one float literal: decimal point kept, sign kept, NaN made 0', () => {
|
||||
const f = makeProject();
|
||||
const s = shaderEditor(f);
|
||||
const half = s.add(SHADER_NUMBER, { value: 0.5 });
|
||||
const two = s.add(SHADER_NUMBER, { value: 2 });
|
||||
const minus = s.add(SHADER_NUMBER, { value: -0.25 });
|
||||
const broken = s.add(SHADER_NUMBER, { value: Number.NaN });
|
||||
const sum = s.add('ShaderAdd');
|
||||
const times = s.add('ShaderMultiply');
|
||||
const diff = s.add('ShaderSubtract');
|
||||
const output = s.add(SHADER_OUTPUT);
|
||||
s.link(half, 'value', sum, 'a');
|
||||
s.link(two, 'value', sum, 'b');
|
||||
s.link(sum, 'result', times, 'a');
|
||||
s.link(minus, 'value', times, 'b');
|
||||
s.link(times, 'result', diff, 'a');
|
||||
s.link(broken, 'value', diff, 'b');
|
||||
s.link(diff, 'result', output, 'color');
|
||||
const { fragment } = compileShader(s.asset().graph).compiled!;
|
||||
// Locals are numbered in visiting order; the literal's form is what matters here.
|
||||
expect(fragment).toMatch(/float n\d+_value = 0\.5;/);
|
||||
expect(fragment).toMatch(/float n\d+_value = 2\.0;/);
|
||||
expect(fragment).toMatch(/float n\d+_value = -0\.25;/);
|
||||
expect(fragment).toMatch(/float n\d+_value = 0\.0;/);
|
||||
expect(fragment).toMatch(/float n\d+_result = \(n\d+_value \+ n\d+_value\);/);
|
||||
expect(fragment).toMatch(/clamp\(vec3\(n\d+_result\), 0\.0, 1\.0\)/);
|
||||
expect(fragment).not.toContain('uniform float u_');
|
||||
});
|
||||
|
||||
it('keeps the texture’s alpha until the Output’s alpha is fed, then clamps what feeds it', () => {
|
||||
const f = makeProject();
|
||||
const s = shaderEditor(f);
|
||||
|
||||
+44
-3
@@ -86,10 +86,14 @@ export async function shader(t) {
|
||||
const offered = await json('__gm.overlayTexts()');
|
||||
check(
|
||||
'right-click offers shader nodes and no behavior node',
|
||||
['Texture color', 'Output', 'Property', 'Mix', 'Luminance'].every((text) => offered.includes(text)) &&
|
||||
!offered.some((text) => /On start|Number|Branch|Set state/.test(text)),
|
||||
['Texture color', 'Output', 'Property', 'Number', 'Mix', 'Luminance'].every((text) =>
|
||||
offered.includes(text),
|
||||
) && !offered.some((text) => /On start|Boolean|Branch|Set state/.test(text)),
|
||||
JSON.stringify(offered),
|
||||
);
|
||||
// A dozen entries scroll; searched, the row is the only one and in view.
|
||||
await typeText('Texture color');
|
||||
await sleep(150);
|
||||
await clickLast('Texture color');
|
||||
await sleep(300);
|
||||
await click(canvas.x + 420, canvas.y + 60, 'right');
|
||||
@@ -323,7 +327,44 @@ export async function shader(t) {
|
||||
await sleep(300);
|
||||
check('opacity back at 1 draws the pixel from before, byte for byte', (await centre()) === red);
|
||||
|
||||
// 28i. Back to None: no filter at all, the plain colour returns bit for bit.
|
||||
// 28i. A Number node is a constant in the fragment: red times 0.5 is half red; typed back to 1,
|
||||
// the pixel is the red from before. Its value is a setting row on the node.
|
||||
await mutate(`
|
||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||
const out = a.graph.nodes.find((n) => n.type === 'ShaderOutput');
|
||||
a.graph.nodes.push(
|
||||
{ id: 'sh-half', type: 'ShaderNumber', x: 200, y: 200, params: { value: 0.5 } },
|
||||
{ id: 'sh-dim', type: 'ShaderMultiply', x: 400, y: 200, params: {} });
|
||||
a.graph.edges = a.graph.edges.filter((e) => e.id !== 'sh-red-edge');
|
||||
a.graph.edges.push(
|
||||
{ id: 'sh-dim-a', from: { node: 'sh-red', port: 'color' }, to: { node: 'sh-dim', port: 'a' } },
|
||||
{ id: 'sh-dim-b', from: { node: 'sh-half', port: 'value' }, to: { node: 'sh-dim', port: 'b' } },
|
||||
{ id: 'sh-dim-out', from: { node: 'sh-dim', port: 'result' }, to: { node: out.id, port: 'color' } });`);
|
||||
await sleep(400);
|
||||
const dimmed = await centre();
|
||||
check('a Number of 0.5 into a Multiply halves the red', colourDistance(dimmed, '#800000') < 6, {
|
||||
red,
|
||||
dimmed,
|
||||
});
|
||||
await evaluate("__gm.showTab('shader')");
|
||||
await sleep(300);
|
||||
check(
|
||||
'the Number node draws its value as a setting row',
|
||||
(await json("__gm.shader.nodeSettings('sh-half')"))?.join() === 'Value: 0.500',
|
||||
JSON.stringify(await json("__gm.shader.nodeSettings('sh-half')")),
|
||||
);
|
||||
await shot('28-number-node');
|
||||
await evaluate("__gm.showTab('scene')");
|
||||
await mutate(`
|
||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||
a.graph.nodes.find((n) => n.id === 'sh-half').params.value = 1`);
|
||||
await sleep(400);
|
||||
check('the Number typed to 1 gives the red from before, byte for byte', (await centre()) === red, {
|
||||
red,
|
||||
now: await centre(),
|
||||
});
|
||||
|
||||
// 28j. Back to None: no filter at all, the plain colour returns bit for bit.
|
||||
await assetProps({ shader: '' });
|
||||
await sleep(300);
|
||||
const back = await centre();
|
||||
|
||||
Reference in new issue
Block a user