Let a shader declare a color the sprite sets (#298)
game-maker / gate (push) Successful in 3m1s
game-maker / suite (push) Successful in 14m11s

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:
adminandClaude Opus 5.5 committed 2026-10-03 11:43:54 +02:00
1 parent 4f4c149211
commit bf27e42e8a
13 files changed
+303 -57

No files matched your search

+14 -7
View File
@@ -4,7 +4,7 @@ import { flatDef, type FlatDef, type PropertyDef } from './PropertySchema';
/** The params of a `Property` node in a shader or particle graph: its name and the row it declares. */
export const PROPERTY_NAME_PARAM = 'name';
const PROPERTY_DEFAULT_PARAM = 'default';
export const PROPERTY_DEFAULT_PARAM = 'default';
const PROPERTY_MIN_PARAM = 'min';
const PROPERTY_MAX_PARAM = 'max';
@@ -25,23 +25,30 @@ export interface Declaration {
}
/**
* The number rows a graph declares, in node order: one per declaring node with a valid name. A
* second node of the same name declares nothing new – both read the one value.
* The rows a graph declares, in node order: one per declaring node with a valid name. A second node
* of the same name declares nothing new – both read the one value.
*/
export function declaredNumberRows(graph: Graph, of: Declaration): FlatDef<'number'>[] {
const rows: FlatDef<'number'>[] = [];
export function declaredRows<R extends FlatDef>(
graph: Graph,
of: Declaration & { rowOf(name: string, node: GraphNode): R },
): R[] {
const rows: R[] = [];
const seen = new Set<string>();
for (const node of graph.nodes) {
if (!of.isDeclaration(node)) continue;
const name = of.nameOf(node);
if (!of.isValidName(name) || seen.has(name)) continue;
seen.add(name);
rows.push(numberRow(name, node.params));
rows.push(of.rowOf(name, node));
}
return rows;
}
function numberRow(name: string, params: PropertyBag): FlatDef<'number'> {
export function declaredNumberRows(graph: Graph, of: Declaration): FlatDef<'number'>[] {
return declaredRows(graph, { ...of, rowOf: (name, node) => numberRow(name, node.params) });
}
export function numberRow(name: string, params: PropertyBag): FlatDef<'number'> {
const min = numberParam(params, PROPERTY_MIN_PARAM, 0);
const max = numberParam(params, PROPERTY_MAX_PARAM, 1);
const row: FlatDef<'number'> = {
+23 -9
View File
@@ -1,7 +1,7 @@
import type { Graph, GraphNode } from '../graph/Graph';
import { type PropertyBag, textOf } from '../model/Properties';
import { declaredNumberRows, PROPERTY_NAME_PARAM } from './declaredRows';
import { type FlatDef, isSpriteProp, isValidFieldName } from './PropertySchema';
import { colorOf, type PropertyBag, textOf } from '../model/Properties';
import { declaredRows, numberRow, PROPERTY_DEFAULT_PARAM, PROPERTY_NAME_PARAM } from './declaredRows';
import { type FlatDef, flatDef, isSpriteProp, isValidFieldName } from './PropertySchema';
export { PROPERTY_NAME_PARAM } from './declaredRows';
@@ -13,13 +13,19 @@ export const SHADER_KEY = 'shader';
* GLSL uniform, `default`/`min`/`max` are the row the inspector shows.
*/
export const SHADER_PROPERTY = 'ShaderProperty';
/** The same for a colour (#298): a colour row on the sprite and a vec3 uniform. */
export const SHADER_COLOR_PROPERTY = 'ShaderColorProperty';
export const DEFAULT_PROPERTY_COLOR = 0xffffff;
/** A row a shader graph adds to its wearers. */
export type ShaderParamDef = FlatDef<'number'> | FlatDef<'color'>;
export function shaderOf(bag: PropertyBag): string {
return textOf(bag[SHADER_KEY]);
}
export function isPropertyNode(node: GraphNode): boolean {
return node.type === SHADER_PROPERTY;
return node.type === SHADER_PROPERTY || node.type === SHADER_COLOR_PROPERTY;
}
export function propertyName(node: GraphNode): string {
@@ -36,17 +42,25 @@ function isValidPropertyName(name: string): boolean {
}
/** The properties a shader graph declares: one row on the sprite and one uniform each. */
export function shaderParamDefs(graph: Graph): FlatDef<'number'>[] {
return declaredNumberRows(graph, {
export function shaderParamDefs(graph: Graph): ShaderParamDef[] {
return declaredRows<ShaderParamDef>(graph, {
isDeclaration: isPropertyNode,
nameOf: propertyName,
isValidName: isValidPropertyName,
rowOf: (name, node) =>
node.type === SHADER_COLOR_PROPERTY
? flatDef(name, 'color', { value: propertyColor(node.params) })
: numberRow(name, node.params),
});
}
export function propertyColor(params: PropertyBag): number {
return colorOf(params[PROPERTY_DEFAULT_PARAM] ?? DEFAULT_PROPERTY_COLOR, DEFAULT_PROPERTY_COLOR);
}
/** What a property placed from the palette is called: `value`, then `value2`, `value3` … */
export function nextPropertyName(graph: Graph): string {
export function nextPropertyName(graph: Graph, base = 'value'): string {
const taken = new Set(graph.nodes.filter(isPropertyNode).map(propertyName));
if (!taken.has('value')) return 'value';
for (let n = 2; ; n++) if (!taken.has(`value${n}`)) return `value${n}`;
if (!taken.has(base)) return base;
for (let n = 2; ; n++) if (!taken.has(`${base}${n}`)) return `${base}${n}`;
}
+12 -4
View File
@@ -32,9 +32,13 @@ import {
export interface ShaderUniform {
name: string;
/** A colour is a vec3 uniform and its default a packed 0xRRGGBB (#298). */
kind: 'number' | 'color';
default: number;
}
const glslOf = (u: ShaderUniform) => (u.kind === 'color' ? 'vec3' : 'float');
/**
* What a Vertex output node adds to the wearer's vertex stage (#79). It reads the corner's
* `vTextureCoord` and the same uniforms the fragment does.
@@ -80,7 +84,7 @@ export const perCornerProblem = (what: string) => `Not available per corner: ${w
/**
* A shader graph to the fragment of the sprite's own draw: every node reachable from the Output
* becomes one local per output, in dependency order, and every Property a float uniform. The texel
* becomes one local per output, in dependency order, and every Property a uniform. The texel
* arrives premultiplied, so the prelude unpremultiplies and the epilogue multiplies back.
*/
export function compileShader(graph: Graph): ShaderBuild {
@@ -108,7 +112,11 @@ export function compileShader(graph: Graph): ShaderBuild {
const color = walk.input(output, outputDef.inputs[0]!);
const alpha = walk.input(output, outputDef.inputs[1]!);
const uniforms = shaderParamDefs(graph).map((row) => ({ name: row.key, default: row.default }));
const uniforms = shaderParamDefs(graph).map((row) => ({
name: row.key,
kind: row.kind,
default: row.default,
}));
// The per-corner part is a walk of its own from the Vertex output: a node feeding both outputs is
// emitted once for each stage.
@@ -128,7 +136,7 @@ export function compileShader(graph: Graph): ShaderBuild {
// A uniform both stages declare has to have one precision; the fragment's default is mediump.
head: [
...(reachesTime ? [`uniform mediump float ${TIME_UNIFORM};`] : []),
...uniforms.map((u) => `uniform mediump float ${uniformName(u.name)};`),
...uniforms.map((u) => `uniform mediump ${glslOf(u)} ${uniformName(u.name)};`),
DIVIDE_FN,
].join('\n'),
body: [...cornerLines, `vec2 offset = vec2(${x.code}, ${y.code});`].join('\n'),
@@ -153,7 +161,7 @@ export function compileShader(graph: Graph): ShaderBuild {
'out vec4 finalColor;',
'uniform sampler2D uTexture;',
...(fragmentTime ? [`uniform float ${TIME_UNIFORM};`] : []),
...uniforms.map((u) => `uniform float ${uniformName(u.name)};`),
...uniforms.map((u) => `uniform ${glslOf(u)} ${uniformName(u.name)};`),
STRAIGHT_FN,
DIVIDE_FN,
texelAtFn(mode),
+28 -2
View File
@@ -2,9 +2,15 @@ import type { PortType } from '../graph/Graph';
import type { NodeDef, PortDef } from '../graph/NodeRegistry';
import type { SharedPortGroup } from '../graph/portTypes';
import { type PropertyBag, textOf } from '../model/Properties';
import { propertyNodeParams } from '../properties/declaredRows';
import { PROPERTY_DEFAULT_PARAM, propertyNodeParams } from '../properties/declaredRows';
import { flatDef } from '../properties/PropertySchema';
import { PROPERTY_NAME_PARAM, propertyName, SHADER_PROPERTY } from '../properties/shaders';
import {
DEFAULT_PROPERTY_COLOR,
PROPERTY_NAME_PARAM,
propertyName,
SHADER_COLOR_PROPERTY,
SHADER_PROPERTY,
} from '../properties/shaders';
export type GlslType = 'float' | 'vec3';
@@ -341,6 +347,25 @@ export const ShaderProperty: ShaderNodeDef = {
}),
};
/** The same for a colour (#298); like `Color`, there is none per corner. */
export const ShaderColorProperty: ShaderNodeDef = {
type: SHADER_COLOR_PROPERTY,
label: 'Color property',
category: 'shader',
description: 'A color the sprite sets: a color row below the Shader dropdown, and a color here.',
inputs: [],
outputs: [vec3Port('color')],
params: [
flatDef(PROPERTY_NAME_PARAM, 'text', { label: 'Name', value: 'value' }),
flatDef(PROPERTY_DEFAULT_PARAM, 'color', { label: 'Default', value: DEFAULT_PROPERTY_COLOR }),
],
perPixel: true,
derivePorts: ({ node }) => ({ label: propertyName(node) || ShaderColorProperty.label }),
emit: (_inputs, params) => ({
color: { code: uniformName(textOf(params[PROPERTY_NAME_PARAM] ?? '')), type: 'vec3' },
}),
};
export function uniformName(property: string): string {
return `u_${property}`;
}
@@ -550,6 +575,7 @@ export const SHADER_NODES: readonly ShaderNodeDef[] = [
ShaderOutput,
ShaderVertexOutput,
ShaderProperty,
ShaderColorProperty,
ShaderColor,
ShaderNumber,
ShaderAdd,