A name that a sprite already owns cannot name a property, and a division by zero cannot answer (#63, #64)
game-maker / suite (push) Failing after 11m29s
game-maker / suite (push) Failing after 11m29s
#63: a shader Property node named "width" quietly took over the sprite row of the same name. isSpriteProp lives in PropertySchema now and is derived from FULL_SCHEMAS.sprite instead of being a second list that drifts. isValidPropertyName rejects those ten names, and the compiler reports it at the node as its own problem: "width" is a sprite property, told apart from "not a word" and from a duplicate. #64: Divide emitted (a / b) and handed NaN to the driver. It emits divide(a, b) now, with the guard in the prologue next to straight(), scalar and per channel: zero divided by zero is zero. That was already the house rule in the behavior and particle graphs - only the shader had never been told. Clamp comes along as a shader node for the same reason: the behavior catalog has had it all along.
This commit is contained in:
1 parent
150f2e0906
commit
c990dbcb26
6 files changed
+203
-6
No files matched your search
@@ -306,6 +306,17 @@ export function schemaFor(type: AssetType): PropertySchema {
|
||||
return FULL_SCHEMAS[type];
|
||||
}
|
||||
|
||||
const SPRITE_KEYS = new Set(FULL_SCHEMAS.sprite.map((d) => d.key));
|
||||
|
||||
/**
|
||||
* A row every sprite has already: the switch, the transform and the sprite's own. A shader
|
||||
* parameter under one of these names would be a second row on the same bag key, and its uniform
|
||||
* would read the sprite's value instead of the default the Property node declares (#63).
|
||||
*/
|
||||
export function isSpriteProp(key: string): boolean {
|
||||
return SPRITE_KEYS.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* The type schema, for behaviors followed by their declared variables, for sprites by the
|
||||
* parameters of the shader asset their bag names – which is why the project is needed.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Graph, GraphNode } from '../graph/Graph';
|
||||
import { type PropertyBag, textOf } from '../model/Properties';
|
||||
import { type FlatDef, isValidFieldName } from './PropertySchema';
|
||||
import { type FlatDef, isSpriteProp, isValidFieldName } from './PropertySchema';
|
||||
|
||||
/** The sprite property naming the shader asset it wears; `''` is no shader and no render pass. */
|
||||
export const SHADER_KEY = 'shader';
|
||||
@@ -27,9 +27,13 @@ export function propertyName(node: GraphNode): string {
|
||||
return textOf(node.params[PROPERTY_NAME_PARAM] ?? '');
|
||||
}
|
||||
|
||||
/** A property's name is a bag key and a uniform, so it has to be an identifier. */
|
||||
/**
|
||||
* A property's name is a bag key and a uniform, so it has to be an identifier – and not one of the
|
||||
* rows the sprite brings itself: both rows would share the key and the uniform would read the
|
||||
* sprite's width instead of the declared default (#63). The compiler names which of the two it is.
|
||||
*/
|
||||
export function isValidPropertyName(name: string): boolean {
|
||||
return isValidFieldName(name);
|
||||
return isValidFieldName(name) && !isSpriteProp(name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { edgesInto, findNode, type Graph, type GraphNode } from '../graph/Graph';
|
||||
import { nodeDef, type PortDef } from '../graph/NodeRegistry';
|
||||
import { colorOf } from '../model/Properties';
|
||||
import { isPropertyNode, isValidPropertyName, propertyName, shaderParamDefs } from '../properties/shaders';
|
||||
import { isSpriteProp, isValidFieldName } from '../properties/PropertySchema';
|
||||
import { isPropertyNode, propertyName, shaderParamDefs } from '../properties/shaders';
|
||||
import {
|
||||
asVec3,
|
||||
DIVIDE_FN,
|
||||
type EdgeMode,
|
||||
edgeOf,
|
||||
type Expr,
|
||||
@@ -74,7 +76,8 @@ export function compileShader(graph: Graph): ShaderBuild {
|
||||
for (const node of graph.nodes) {
|
||||
if (!isPropertyNode(node)) continue;
|
||||
const name = propertyName(node);
|
||||
if (!isValidPropertyName(name)) problem(node, 'Name must be one word: letters, digits, _');
|
||||
if (!isValidFieldName(name)) problem(node, 'Name must be one word: letters, digits, _');
|
||||
else if (isSpriteProp(name)) problem(node, `"${name}" is a sprite property`);
|
||||
else if (names.has(name)) problem(node, `Another property is named "${name}"`);
|
||||
names.add(name);
|
||||
}
|
||||
@@ -172,6 +175,7 @@ export function compileShader(graph: Graph): ShaderBuild {
|
||||
...(time ? [`uniform float ${TIME_UNIFORM};`] : []),
|
||||
...uniforms.map((u) => `uniform float ${uniformName(u.name)};`),
|
||||
STRAIGHT_FN,
|
||||
DIVIDE_FN,
|
||||
'void main()',
|
||||
'{',
|
||||
' vec4 texel = texture(uTexture, vTextureCoord) * vColor;',
|
||||
|
||||
@@ -287,7 +287,43 @@ export const ShaderMultiply = arithmetic(
|
||||
'a × b, per channel for colours.',
|
||||
1,
|
||||
);
|
||||
export const ShaderDivide = arithmetic('ShaderDivide', 'Divide', '/', 'a / b, per channel for colours.', 1);
|
||||
/**
|
||||
* Division with an answer at zero. `a / 0.0` is NaN, and the epilog's `clamp(NaN, 0.0, 1.0)` is
|
||||
* undefined in GLSL – the driver decides the pixel, so the same graph looks different on Intel,
|
||||
* Nvidia and a phone (#64). Zero is the answer, the same one the `Divide` of a behavior graph and
|
||||
* of a particle graph give.
|
||||
*/
|
||||
export const DIVIDE_FN = [
|
||||
'float divide(float a, float b) { return b == 0.0 ? 0.0 : a / b; }',
|
||||
'vec3 divide(vec3 a, vec3 b) { return vec3(divide(a.x, b.x), divide(a.y, b.y), divide(a.z, b.z)); }',
|
||||
].join('\n');
|
||||
|
||||
export const ShaderDivide: ShaderNodeDef = {
|
||||
type: 'ShaderDivide',
|
||||
label: 'Divide',
|
||||
category: 'shader',
|
||||
description: 'a / b, per channel for colours. Dividing by 0 is 0, not a pixel the driver picks.',
|
||||
inputs: [anyIn('a', 1), anyIn('b', 1)],
|
||||
outputs: [anyOut('result')],
|
||||
params: [],
|
||||
emit: ({ a, b }) => {
|
||||
const l = lifted(a!, b!);
|
||||
return { result: { code: `divide(${l.a}, ${l.b})`, type: l.type } };
|
||||
},
|
||||
};
|
||||
|
||||
export const ShaderClamp: ShaderNodeDef = {
|
||||
type: 'ShaderClamp',
|
||||
label: 'Clamp',
|
||||
category: 'shader',
|
||||
description: 'Holds the value inside min..max, per channel for colours.',
|
||||
inputs: [anyIn('value', 0), floatIn('min', 0), floatIn('max', 1)],
|
||||
outputs: [anyOut('result')],
|
||||
params: [],
|
||||
emit: ({ value, min, max }) => ({
|
||||
result: { code: `clamp(${value!.code}, ${min!.code}, ${max!.code})`, type: value!.type },
|
||||
}),
|
||||
};
|
||||
|
||||
export const ShaderMix: ShaderNodeDef = {
|
||||
type: 'ShaderMix',
|
||||
@@ -423,6 +459,7 @@ export const SHADER_NODES: readonly ShaderNodeDef[] = [
|
||||
ShaderStep,
|
||||
ShaderPow,
|
||||
ShaderSqrt,
|
||||
ShaderClamp,
|
||||
];
|
||||
|
||||
/** A GLSL float literal: always with a decimal point, so `1` does not become an int. */
|
||||
|
||||
@@ -373,6 +373,71 @@ describe('compileShader', () => {
|
||||
expect(build.problems).toEqual({ [two]: 'Feeds itself' });
|
||||
});
|
||||
|
||||
it('refuses a property named like a row the sprite brings itself (#63)', () => {
|
||||
const f = makeSpriteScene();
|
||||
const s = shaderEditor(f);
|
||||
s.add(SHADER_OUTPUT);
|
||||
// One of the sprite's own rows, one of the transform rows, one legitimate name.
|
||||
const wide = s.add(SHADER_PROPERTY, property('width'));
|
||||
const turned = s.add(SHADER_PROPERTY, property('rotation'));
|
||||
const fine = s.add(SHADER_PROPERTY, property('amount'));
|
||||
const build = compileShader(s.asset().graph);
|
||||
expect(build.compiled).toBeNull();
|
||||
expect(build.problems).toEqual({
|
||||
[wide]: '"width" is a sprite property',
|
||||
[turned]: '"rotation" is a sprite property',
|
||||
});
|
||||
expect(build.problems[fine]).toBeUndefined();
|
||||
// The row is not declared either, so the sprite keeps one width row – the one that is 100.
|
||||
expect(shaderParamDefs(s.asset().graph).map((r) => r.key)).toEqual(['amount']);
|
||||
const sprite = findAssetOf(f.project, f.spriteId, 'sprite')!;
|
||||
f.stack.execute(new SetAssetPropertyCommand(f.spriteId, SHADER_KEY, s.id));
|
||||
const rows = schemaForAsset(f.project, sprite).filter((d) => d.key === 'width');
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({ label: 'Width', default: 100 });
|
||||
});
|
||||
|
||||
it('divides through a guard, so dividing by zero is 0 and not the driver’s choice (#64)', () => {
|
||||
const f = makeProject();
|
||||
const s = shaderEditor(f);
|
||||
const zero = s.add(SHADER_NUMBER, { value: 0 });
|
||||
const half = s.add('ShaderDivide', { a: 1 });
|
||||
const held = s.add('ShaderClamp', { min: 0, max: 0.5 });
|
||||
const output = s.add(SHADER_OUTPUT);
|
||||
s.link(zero, 'value', half, 'b');
|
||||
s.link(half, 'result', held, 'value');
|
||||
s.link(held, 'result', output, 'color');
|
||||
const { fragment } = compileShader(s.asset().graph).compiled!;
|
||||
expect(fragment).toContain('float divide(float a, float b) { return b == 0.0 ? 0.0 : a / b; }');
|
||||
expect(fragment).toMatch(/float n\d+_result = divide\(1\.0, n\d+_value\);/);
|
||||
expect(fragment).toMatch(/float n\d+_result = clamp\(n\d+_result, 0\.0, 0\.5\);/);
|
||||
// Nothing writes the bare operator any more – that was the NaN (#64).
|
||||
expect(fragment).not.toMatch(/n\d+_\w+ \/ n\d+_\w+/);
|
||||
|
||||
// A colour divides per channel, through the vec3 overload of the same guard.
|
||||
const c = shaderEditor(f, 'Faded');
|
||||
const texel = c.add(SHADER_TEXTURE_COLOR);
|
||||
const by = c.add('ShaderDivide', { b: 2 });
|
||||
const out = c.add(SHADER_OUTPUT);
|
||||
c.link(texel, 'color', by, 'a');
|
||||
c.link(by, 'result', out, 'color');
|
||||
const coloured = compileShader(c.asset().graph).compiled!.fragment;
|
||||
expect(coloured).toContain(
|
||||
'vec3 divide(vec3 a, vec3 b) { return vec3(divide(a.x, b.x), divide(a.y, b.y), divide(a.z, b.z)); }',
|
||||
);
|
||||
expect(coloured).toMatch(/vec3 n\d+_result = divide\(n\d+_color, vec3\(2\.0\)\);/);
|
||||
// A clamped colour keeps its type: clamp(vec3, float, float) is one of the GLSL overloads.
|
||||
const k = shaderEditor(f, 'Held');
|
||||
const rgb = k.add(SHADER_TEXTURE_COLOR);
|
||||
const clamped = k.add('ShaderClamp', { min: 0.25, max: 0.75 });
|
||||
const out2 = k.add(SHADER_OUTPUT);
|
||||
k.link(rgb, 'color', clamped, 'value');
|
||||
k.link(clamped, 'result', out2, 'color');
|
||||
expect(compileShader(k.asset().graph).compiled!.fragment).toMatch(
|
||||
/vec3 n\d+_result = clamp\(n\d+_color, 0\.25, 0\.75\);/,
|
||||
);
|
||||
});
|
||||
|
||||
it('gives the graph the pixel’s place, the picture at another spot, its size and the clock', () => {
|
||||
const f = makeProject();
|
||||
const s = shaderEditor(f, 'Flip');
|
||||
|
||||
@@ -546,6 +546,82 @@ export async function shader(t) {
|
||||
{ foldLeft, foldMiddle, foldRight },
|
||||
);
|
||||
|
||||
// 28k-iv. A property named like a row the sprite has anyway is a problem on the node (#63): both
|
||||
// rows would sit on the same bag key and the uniform would read the sprite's width, 160 here,
|
||||
// instead of the default the node declares. Renaming it compiles the same graph.
|
||||
const namedProperty = (name) =>
|
||||
graph(
|
||||
[
|
||||
node('named', 'ShaderProperty', { name, default: 0.25, min: 0, max: 1 }),
|
||||
node('red', 'ShaderColor', { value: 0xff0000 }, 260, 200),
|
||||
node('dim', 'ShaderMultiply', {}, 520, 0),
|
||||
node('out', 'ShaderOutput', {}, 780, 0),
|
||||
],
|
||||
[
|
||||
edge('named', 'value', 'dim', 'b'),
|
||||
edge('red', 'color', 'dim', 'a'),
|
||||
edge('dim', 'result', 'out', 'color'),
|
||||
],
|
||||
);
|
||||
await namedProperty('width');
|
||||
await sleep(400);
|
||||
const spriteProp = await info();
|
||||
check(
|
||||
'a property named like a sprite row is a problem on the node, not a second width row (#63)',
|
||||
(await json("__gm.shader.nodeProblem('named')")) === '"width" is a sprite property' &&
|
||||
spriteProp === '"width" is a sprite property',
|
||||
JSON.stringify({ spriteProp, problem: await json("__gm.shader.nodeProblem('named')") }),
|
||||
);
|
||||
await namedProperty('amount');
|
||||
await sleep(400);
|
||||
check(
|
||||
'the same graph under a free name compiles: a quarter of the red',
|
||||
(await info()) === '1 property' && colourDistance(await pixel(-0.3, -0.3), '#400000') < 6,
|
||||
JSON.stringify({ info: await info(), pixel: await pixel(-0.3, -0.3) }),
|
||||
);
|
||||
|
||||
// 28k-v. Dividing by zero has an answer (#64). `a / 0.0` is NaN, and the epilog's clamp(NaN) is
|
||||
// undefined in GLSL – the pixel would be the driver's choice. The guard makes it 0, so the sprite
|
||||
// goes black instead of "whatever this machine does".
|
||||
const dividedBy = (value) =>
|
||||
graph(
|
||||
[
|
||||
node('red', 'ShaderColor', { value: 0xff0000 }),
|
||||
node('by', 'ShaderNumber', { value }, 260, 200),
|
||||
node('div', 'ShaderDivide', {}, 520, 0),
|
||||
node('out', 'ShaderOutput', {}, 780, 0),
|
||||
],
|
||||
[
|
||||
edge('red', 'color', 'div', 'a'),
|
||||
edge('by', 'value', 'div', 'b'),
|
||||
edge('div', 'result', 'out', 'color'),
|
||||
],
|
||||
);
|
||||
await dividedBy(2);
|
||||
await sleep(400);
|
||||
const halved = await pixel(-0.3, -0.3);
|
||||
await dividedBy(0);
|
||||
await sleep(400);
|
||||
const byZero = await pixel(-0.3, -0.3);
|
||||
check(
|
||||
'Divide by 2 halves the red, and dividing by 0 is 0 – a black pixel, not the driver’s choice (#64)',
|
||||
colourDistance(halved, '#800000') < 6 && colourDistance(byZero, '#000000') < 6,
|
||||
JSON.stringify({ halved, byZero }),
|
||||
);
|
||||
|
||||
// 28k-vi. Clamp holds a value inside a range, per channel for a colour.
|
||||
await graph(
|
||||
[
|
||||
node('red', 'ShaderColor', { value: 0xff0000 }),
|
||||
node('hold', 'ShaderClamp', { min: 0, max: 0.25 }, 260, 0),
|
||||
node('out', 'ShaderOutput', {}, 520, 0),
|
||||
],
|
||||
[edge('red', 'color', 'hold', 'value'), edge('hold', 'result', 'out', 'color')],
|
||||
);
|
||||
await sleep(400);
|
||||
const held = await pixel(-0.3, -0.3);
|
||||
check('Clamp at 0..0.25 holds full red down to a quarter', colourDistance(held, '#400000') < 6, held);
|
||||
|
||||
// 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.
|
||||
|
||||
Reference in new issue
Block a user