Shader graph: UV, Sample texture, Texture size, Time, Sin/Floor/Fract/Step; edge mode on Output (#78)
game-maker / suite (push) Successful in 8m38s
game-maker / suite (push) Successful in 8m38s
Fragment-side coordinate nodes instead of vertex nodes: the quad stays the quad, a UV read plus a sample at a computed coordinate covers flip, scroll, wave, pixelate and scanlines. Time is a uniform only when a Time node reaches the Output; a viewport that drew a Time shader redraws from the ticker so the Scene tab animates without a running world. Edge (Mirror/Repeat/Clamp) is a second texture variant in TextureLibrary because WebGL binds no TextureStyle resource of the shader. The compiler now emits #version 300 es first: Pixi only compiles as ES 3.00 when the fragment says so, and textureSize needs it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e5e5ef925
commit
f85606427b
12 files changed
+586
-38
No files matched your search
+224
-1
@@ -10,6 +10,22 @@ import { colourDistance } from '../lib/harness.mjs';
|
||||
const BASE = '#4080ff';
|
||||
const ADJUST = 'adjust-colors';
|
||||
|
||||
/** A 32 px picture, red on the left and green on the right, pasted in for the sampling checks (28l). */
|
||||
const PASTE_HALVES = `(async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 32;
|
||||
canvas.height = 32;
|
||||
const g = canvas.getContext('2d');
|
||||
g.fillStyle = '#e63c3c'; g.fillRect(0, 0, 16, 32);
|
||||
g.fillStyle = '#3cc85a'; g.fillRect(16, 0, 16, 32);
|
||||
const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
|
||||
const transfer = new DataTransfer();
|
||||
transfer.items.add(new File([blob], 'halves.png', { type: 'image/png' }));
|
||||
window.dispatchEvent(new ClipboardEvent('paste', { clipboardData: transfer, bubbles: true }));
|
||||
})()`;
|
||||
const LEFT = '#e63c3c';
|
||||
const RIGHT = '#3cc85a';
|
||||
|
||||
export async function shader(t) {
|
||||
const {
|
||||
check,
|
||||
@@ -381,6 +397,210 @@ export async function shader(t) {
|
||||
now: await centre(),
|
||||
});
|
||||
|
||||
// 28k. UV into Step: the pixel's place on the picture decides its colour – a hard cut at u = 0.5,
|
||||
// then at v = 0.5, which shows v runs top to bottom. `box` is the sprite's centre and size.
|
||||
const node = (id, type, params = {}, x = 0, y = 0) => ({ id, type, x, y, params });
|
||||
const edge = (from, fromPort, to, toPort) => ({
|
||||
id: `${from}-${to}-${toPort}`,
|
||||
from: { node: from, port: fromPort },
|
||||
to: { node: to, port: toPort },
|
||||
});
|
||||
const graph = (nodes, edges) =>
|
||||
mutate(`
|
||||
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||
a.graph.nodes = ${JSON.stringify(nodes)};
|
||||
a.graph.edges = ${JSON.stringify(edges)};`);
|
||||
const pixel = (fx, fy) =>
|
||||
json(`__gm.pixelAt(${Math.round(box.x + fx * box.w)}, ${Math.round(box.y + fy * box.h)})`);
|
||||
const redOrBlue = (t, edges) =>
|
||||
graph(
|
||||
[
|
||||
...t,
|
||||
node('red', 'ShaderColor', { value: 0xff0000 }, 260, 200),
|
||||
node('blue', 'ShaderColor', { value: 0x0000ff }, 260, 340),
|
||||
node('mix', 'ShaderMix', {}, 520, 200),
|
||||
node('out', 'ShaderOutput', {}, 780, 200),
|
||||
],
|
||||
[
|
||||
...edges,
|
||||
edge('red', 'color', 'mix', 'a'),
|
||||
edge('blue', 'color', 'mix', 'b'),
|
||||
edge('mix', 'result', 'out', 'color'),
|
||||
],
|
||||
);
|
||||
const cutAt = (port) =>
|
||||
redOrBlue(
|
||||
[node('uv', 'ShaderUV'), node('cut', 'ShaderStep', { edge: 0.5 }, 260, 0)],
|
||||
[edge('uv', port, 'cut', 'value'), edge('cut', 'result', 'mix', 't')],
|
||||
);
|
||||
await cutAt('u');
|
||||
await sleep(400);
|
||||
const uLeft = await pixel(-0.3, -0.3);
|
||||
const uRight = await pixel(0.3, -0.3);
|
||||
check(
|
||||
'UV into Step: left of u = 0.5 the sprite is red, right of it blue',
|
||||
colourDistance(uLeft, '#ff0000') < 6 && colourDistance(uRight, '#0000ff') < 6,
|
||||
{ uLeft, uRight },
|
||||
);
|
||||
await cutAt('v');
|
||||
await sleep(400);
|
||||
const vTop = await pixel(-0.3, -0.3);
|
||||
const vBottom = await pixel(-0.3, 0.3);
|
||||
check(
|
||||
'v runs top to bottom: above v = 0.5 red, below it blue',
|
||||
colourDistance(vTop, '#ff0000') < 6 && colourDistance(vBottom, '#0000ff') < 6,
|
||||
{ vTop, vBottom },
|
||||
);
|
||||
|
||||
// 28l. Sample texture reads the picture where the graph says. On a pasted two-colour picture:
|
||||
// flipped u swaps the halves; u + 0.5 past the border shows the Output's Edge – Repeat continues
|
||||
// with the far half, Mirror with the near one.
|
||||
await evaluate(PASTE_HALVES);
|
||||
await sleep(2500);
|
||||
const picture = (
|
||||
await json(
|
||||
"__gm.state.project.assets.filter((a) => a.type === 'texture' && a.width === 32 && a.height === 32).map((a) => ({ id: a.id, file: a.file }))",
|
||||
)
|
||||
)[0];
|
||||
check(
|
||||
'a two-colour picture is pasted in for the sampling checks',
|
||||
Boolean(picture),
|
||||
JSON.stringify(picture),
|
||||
);
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await assetProps({ texture: picture?.id ?? '', color: 0xffffff });
|
||||
await graph(
|
||||
[node('texel', 'ShaderTextureColor'), node('out', 'ShaderOutput')],
|
||||
[edge('texel', 'color', 'out', 'color')],
|
||||
);
|
||||
await sleep(600);
|
||||
const plainLeft = await pixel(-0.3, -0.3);
|
||||
const plainRight = await pixel(0.3, -0.3);
|
||||
check(
|
||||
'through Texture color the picture is on: red left, green right',
|
||||
colourDistance(plainLeft, LEFT) < 12 && colourDistance(plainRight, RIGHT) < 12,
|
||||
{ plainLeft, plainRight },
|
||||
);
|
||||
const sampleAt = (uNodes, uEdges, outParams = {}) =>
|
||||
graph(
|
||||
[
|
||||
node('uv', 'ShaderUV'),
|
||||
...uNodes,
|
||||
node('sample', 'ShaderSample', {}, 520, 0),
|
||||
node('out', 'ShaderOutput', outParams, 780, 0),
|
||||
],
|
||||
[...uEdges, edge('sample', 'color', 'out', 'color')],
|
||||
);
|
||||
await sampleAt(
|
||||
[node('flip', 'ShaderSubtract', { a: 1 }, 260, 0)],
|
||||
[edge('uv', 'u', 'flip', 'b'), edge('flip', 'result', 'sample', 'u')],
|
||||
);
|
||||
await sleep(400);
|
||||
const flipLeft = await pixel(-0.3, -0.3);
|
||||
const flipRight = await pixel(0.3, -0.3);
|
||||
check(
|
||||
'sampling at 1 - u mirrors the picture: green left, red right',
|
||||
colourDistance(flipLeft, RIGHT) < 12 && colourDistance(flipRight, LEFT) < 12,
|
||||
{ flipLeft, flipRight },
|
||||
);
|
||||
const scrolled = async (edgeMode) => {
|
||||
await sampleAt(
|
||||
[node('shift', 'ShaderAdd', { b: 0.5 }, 260, 0)],
|
||||
[edge('uv', 'u', 'shift', 'a'), edge('shift', 'result', 'sample', 'u')],
|
||||
{ edge: edgeMode },
|
||||
);
|
||||
await sleep(400);
|
||||
return { left: await pixel(-0.3, -0.3), right: await pixel(0.3, -0.3) };
|
||||
};
|
||||
const repeat = await scrolled('repeat');
|
||||
check(
|
||||
'u + 0.5 with Edge Repeat: the near half shows the far one, and past the border the picture starts over',
|
||||
colourDistance(repeat.left, RIGHT) < 12 && colourDistance(repeat.right, LEFT) < 12,
|
||||
repeat,
|
||||
);
|
||||
const mirror = await scrolled('mirror');
|
||||
check(
|
||||
'the same with Edge Mirror: past the border the picture turns back on itself',
|
||||
colourDistance(mirror.left, RIGHT) < 12 && colourDistance(mirror.right, RIGHT) < 12,
|
||||
mirror,
|
||||
);
|
||||
await evaluate("__gm.showTab('shader')");
|
||||
await sleep(300);
|
||||
await shot('28-shader-scroll-graph');
|
||||
await evaluate("__gm.showTab('scene')");
|
||||
await sleep(300);
|
||||
const sizeAtLeast = async (px) => {
|
||||
await redOrBlue(
|
||||
[node('size', 'ShaderTextureSize'), node('cut', 'ShaderStep', { edge: px }, 260, 0)],
|
||||
[edge('size', 'width', 'cut', 'value'), edge('cut', 'result', 'mix', 't')],
|
||||
);
|
||||
await sleep(400);
|
||||
return pixel(-0.3, -0.3);
|
||||
};
|
||||
const wide = await sizeAtLeast(31.5);
|
||||
const wider = await sizeAtLeast(32.5);
|
||||
check(
|
||||
'Texture size reads the picture’s pixel width: at least 31.5, not 32.5',
|
||||
colourDistance(wide, '#0000ff') < 6 && colourDistance(wider, '#ff0000') < 6,
|
||||
{ wide, wider },
|
||||
);
|
||||
|
||||
// 28m. Time: with the clock held the pixel is a function of the second; released, a sprite wearing
|
||||
// a Time shader redraws by itself in the scene view, where nothing else ticks, and in play.
|
||||
await redOrBlue(
|
||||
[
|
||||
node('time', 'ShaderTime'),
|
||||
node('saw', 'ShaderFract', {}, 260, 0),
|
||||
node('cut', 'ShaderStep', { edge: 0.5 }, 260, 120),
|
||||
],
|
||||
[
|
||||
edge('time', 'value', 'saw', 'value'),
|
||||
edge('saw', 'result', 'cut', 'value'),
|
||||
edge('cut', 'result', 'mix', 't'),
|
||||
],
|
||||
);
|
||||
await evaluate('__gm.shaderTime(0.2)');
|
||||
await sleep(400);
|
||||
const early = await pixel(-0.3, -0.3);
|
||||
await evaluate('__gm.shaderTime(0.7)');
|
||||
await sleep(400);
|
||||
const late = await pixel(-0.3, -0.3);
|
||||
check(
|
||||
'Time into Fract and Step: red at 0.2 s, blue at 0.7 s',
|
||||
colourDistance(early, '#ff0000') < 6 && colourDistance(late, '#0000ff') < 6,
|
||||
{ early, late },
|
||||
);
|
||||
await redOrBlue(
|
||||
[node('time', 'ShaderTime'), node('saw', 'ShaderFract', {}, 260, 0)],
|
||||
[edge('time', 'value', 'saw', 'value'), edge('saw', 'result', 'mix', 't')],
|
||||
);
|
||||
await evaluate('__gm.shaderTime(null)');
|
||||
await sleep(400);
|
||||
const first = await pixel(-0.3, -0.3);
|
||||
await sleep(400);
|
||||
const second = await pixel(-0.3, -0.3);
|
||||
const animates = await json('__gm.animates()');
|
||||
check(
|
||||
'released, the scene view redraws every tick for the Time shader: the colour moves with nothing running',
|
||||
animates.scene === true && animates.game === false && first !== second,
|
||||
{ animates, first, second },
|
||||
);
|
||||
await play();
|
||||
await sleep(500);
|
||||
const playing = await json('__gm.animates()');
|
||||
await stop();
|
||||
await sleep(300);
|
||||
check('and so does the game view while playing', playing.game === true, JSON.stringify(playing));
|
||||
await assetProps({ shader: '' });
|
||||
await sleep(400);
|
||||
check(
|
||||
'with the shader off the scene view stops redrawing',
|
||||
(await json('__gm.animates()')).scene === false,
|
||||
JSON.stringify(await json('__gm.animates()')),
|
||||
);
|
||||
await assetProps({ texture: '', color: 0x4080ff, shader: made.id });
|
||||
await sleep(400);
|
||||
|
||||
// 28j. Back to None: no filter at all, the plain colour returns bit for bit.
|
||||
await assetProps({ shader: '' });
|
||||
await sleep(300);
|
||||
@@ -393,7 +613,8 @@ export async function shader(t) {
|
||||
|
||||
await mutate(`
|
||||
const project = __gm.state.project;
|
||||
project.assets = project.assets.filter((a) => a.id !== 'shade-sprite' && a.id !== ${JSON.stringify(made.id)});
|
||||
const gone = new Set(['shade-sprite', ${JSON.stringify(made.id)}, ${JSON.stringify(picture?.id ?? '')}]);
|
||||
project.assets = project.assets.filter((a) => !gone.has(a.id));
|
||||
const scene = __gm.state.activeScene;
|
||||
scene.objects = scene.objects.filter((o) => o.id !== 'oshade')`);
|
||||
await evaluate('__gm.state.select(null)');
|
||||
@@ -402,6 +623,8 @@ export async function shader(t) {
|
||||
'the step leaves nothing behind but the fixture’s shader',
|
||||
(await json("__gm.state.project.assets.some((a) => a.id === 'shade-sprite')")) === false &&
|
||||
(await json("__gm.state.activeScene.objects.some((o) => o.id === 'oshade')")) === false &&
|
||||
(await json(`__gm.state.project.assets.some((a) => a.id === ${JSON.stringify(picture?.id ?? '')})`)) ===
|
||||
false &&
|
||||
(await shaders()).map((s) => s.id).join(',') === ADJUST,
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user