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

#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:
admin committed 2026-09-15 18:43:40 +02:00
1 parent 150f2e0906
commit c990dbcb26
6 files changed
+203 -6

No files matched your search

+11
View File
@@ -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.
+7 -3
View File
@@ -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);
}
/**
+6 -2
View File
@@ -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;',
+38 -1
View File
@@ -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. */