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
@@ -4,12 +4,17 @@ import { colorOf } from '../model/Properties';
|
||||
import { isPropertyNode, isValidPropertyName, propertyName, shaderParamDefs } from '../properties/shaders';
|
||||
import {
|
||||
asVec3,
|
||||
type EdgeMode,
|
||||
edgeOf,
|
||||
type Expr,
|
||||
floatLiteral,
|
||||
isShaderNodeDef,
|
||||
SHADER_OUTPUT,
|
||||
SHADER_TIME,
|
||||
STRAIGHT_FN,
|
||||
TEXEL_ALPHA,
|
||||
TEXEL_COLOR,
|
||||
TIME_UNIFORM,
|
||||
uniformName,
|
||||
vec3Literal,
|
||||
} from './nodes';
|
||||
@@ -22,6 +27,10 @@ export interface ShaderUniform {
|
||||
export interface CompiledShader {
|
||||
fragment: string;
|
||||
uniforms: ShaderUniform[];
|
||||
/** A Time node feeds the Output: the fragment takes `uTime`, and a sprite wearing it redraws every tick. */
|
||||
time: boolean;
|
||||
/** How the picture continues past its border – the texture variant the sprite's mesh draws with. */
|
||||
edge: EdgeMode;
|
||||
}
|
||||
|
||||
export interface ShaderBuild {
|
||||
@@ -34,9 +43,9 @@ export interface ShaderBuild {
|
||||
}
|
||||
|
||||
/**
|
||||
* A shader graph to a Pixi filter fragment: every node reachable from the Output becomes one
|
||||
* local per output, in dependency order, and every Property a float uniform. Pixi hands a filter
|
||||
* premultiplied pixels, so the prelude unpremultiplies and the epilogue multiplies back.
|
||||
* 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
|
||||
* arrives premultiplied, so the prelude unpremultiplies and the epilogue multiplies back.
|
||||
*/
|
||||
export function compileShader(graph: Graph): ShaderBuild {
|
||||
const problems: Record<string, string> = {};
|
||||
@@ -91,8 +100,8 @@ export function compileShader(graph: Graph): ShaderBuild {
|
||||
};
|
||||
|
||||
/**
|
||||
* What feeds an input: the edge's source, the typed-in value, or – with nothing to type – the
|
||||
* texel: its colour for a colour port, its alpha for a number port.
|
||||
* What feeds an input: the edge's source, the typed-in value, or – with nothing to type – what
|
||||
* the node says (`unfed`), else the texel: its colour for a colour port, its alpha for a number.
|
||||
*/
|
||||
const input = (node: GraphNode, port: PortDef): Expr | null => {
|
||||
const edge = edgesInto(graph, node.id, port.key)[0];
|
||||
@@ -107,7 +116,9 @@ export function compileShader(graph: Graph): ShaderBuild {
|
||||
if (port.field?.kind === 'color')
|
||||
return { code: vec3Literal(colorOf(typed ?? 0xffffff, 0xffffff)), type: 'vec3' };
|
||||
if (port.field) return { code: floatLiteral(Number(typed ?? 0)), type: 'float' };
|
||||
return port.type === 'number' ? TEXEL_ALPHA : TEXEL_COLOR;
|
||||
const def = nodeDef(node.type);
|
||||
const unfed = def && isShaderNodeDef(def) ? def.unfed?.[port.key] : undefined;
|
||||
return unfed ?? (port.type === 'number' ? TEXEL_ALPHA : TEXEL_COLOR);
|
||||
};
|
||||
|
||||
const conform = (node: GraphNode, port: PortDef, expr: Expr): Expr | null => {
|
||||
@@ -130,23 +141,29 @@ export function compileShader(graph: Graph): ShaderBuild {
|
||||
return { compiled: null, problems, reason: first ? problems[first.id]! : 'Cannot compile' };
|
||||
|
||||
const uniforms = shaderParamDefs(graph).map((row) => ({ name: row.key, default: row.default }));
|
||||
const time = graph.nodes.some((n) => n.type === SHADER_TIME && emitted.has(n.id));
|
||||
// The fragment runs in the sprite's own draw (#77). `vColor` is the tint, premultiplied like the
|
||||
// texel; it goes in before the graph, so `Texture color` is the sprite as it draws without a shader.
|
||||
// The version line is what makes Pixi compile the program as GLSL ES 3.00 (`GlProgram` looks for
|
||||
// it in the fragment); without it `texture()` is a `#define` and `textureSize` does not exist.
|
||||
const fragment = [
|
||||
'#version 300 es',
|
||||
'in vec2 vTextureCoord;',
|
||||
'in vec4 vColor;',
|
||||
'out vec4 finalColor;',
|
||||
'uniform sampler2D uTexture;',
|
||||
...(time ? [`uniform float ${TIME_UNIFORM};`] : []),
|
||||
...uniforms.map((u) => `uniform float ${uniformName(u.name)};`),
|
||||
STRAIGHT_FN,
|
||||
'void main()',
|
||||
'{',
|
||||
' vec4 texel = texture(uTexture, vTextureCoord) * vColor;',
|
||||
` vec3 ${TEXEL_COLOR.code} = texel.a > 0.0 ? texel.rgb / texel.a : texel.rgb;`,
|
||||
` vec3 ${TEXEL_COLOR.code} = straight(texel);`,
|
||||
...lines.map((line) => ` ${line}`),
|
||||
` float alpha = clamp(${alpha.code}, 0.0, 1.0);`,
|
||||
` finalColor = vec4(clamp(${color.code}, 0.0, 1.0) * alpha, alpha);`,
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
return { compiled: { fragment, uniforms }, problems, reason: '' };
|
||||
return { compiled: { fragment, uniforms, time, edge: edgeOf(output.params) }, problems, reason: '' };
|
||||
}
|
||||
+149
-2
@@ -26,6 +26,8 @@ export interface Expr {
|
||||
export interface ShaderNodeDef extends NodeDef {
|
||||
category: 'shader';
|
||||
emit(inputs: Record<string, Expr>, params: PropertyBag): Record<string, Expr>;
|
||||
/** What an input with nothing to type on reads while unconnected; without it, the texel. */
|
||||
unfed?: Record<string, Expr>;
|
||||
}
|
||||
|
||||
export function isShaderNodeDef(def: NodeDef): def is ShaderNodeDef {
|
||||
@@ -37,14 +39,44 @@ export const SHADER_TEXTURE_ALPHA = 'ShaderTextureAlpha';
|
||||
export const SHADER_OUTPUT = 'ShaderOutput';
|
||||
export const SHADER_COLOR = 'ShaderColor';
|
||||
export const SHADER_NUMBER = 'ShaderNumber';
|
||||
export const SHADER_UV = 'ShaderUV';
|
||||
export const SHADER_SAMPLE = 'ShaderSample';
|
||||
export const SHADER_TEXTURE_SIZE = 'ShaderTextureSize';
|
||||
export const SHADER_TIME = 'ShaderTime';
|
||||
|
||||
/** What the fragment prelude binds: the texel's straight (unpremultiplied) colour. */
|
||||
export const TEXEL_COLOR: Expr = { code: 'rgb', type: 'vec3' };
|
||||
export const TEXEL_ALPHA: Expr = { code: 'texel.a', type: 'float' };
|
||||
/** This pixel's place on the picture, 0..1 – the vertex stage's `vTextureCoord`. */
|
||||
const OWN_U: Expr = { code: 'vTextureCoord.x', type: 'float' };
|
||||
const OWN_V: Expr = { code: 'vTextureCoord.y', type: 'float' };
|
||||
export const TIME_UNIFORM = 'uTime';
|
||||
/** The prelude's helper: a premultiplied sample to its straight colour. */
|
||||
export const STRAIGHT_FN = 'vec3 straight(vec4 c) { return c.a > 0.0 ? c.rgb / c.a : c.rgb; }';
|
||||
|
||||
/**
|
||||
* How the picture continues past its edge when a `Sample texture` reads outside 0..1 – a setting
|
||||
* of the Output node, because the sampler state is one per draw. Mirror is what every picture has
|
||||
* anyway (`TextureLibrary`); repeat is for tiles.
|
||||
*/
|
||||
export type EdgeMode = 'repeat' | 'mirror' | 'clamp';
|
||||
export const EDGE_PARAM = 'edge';
|
||||
export const DEFAULT_EDGE: EdgeMode = 'mirror';
|
||||
const EDGE_OPTIONS: { value: EdgeMode; label: string }[] = [
|
||||
{ value: 'mirror', label: 'Mirror' },
|
||||
{ value: 'repeat', label: 'Repeat' },
|
||||
{ value: 'clamp', label: 'Clamp' },
|
||||
];
|
||||
|
||||
export function edgeOf(params: PropertyBag): EdgeMode {
|
||||
const value = textOf(params[EDGE_PARAM] ?? '');
|
||||
return EDGE_OPTIONS.some((o) => o.value === value) ? (value as EdgeMode) : DEFAULT_EDGE;
|
||||
}
|
||||
|
||||
const NUMBER_STEP = 0.05;
|
||||
|
||||
const vec3Port = (key: string): PortDef => ({ key, label: key, type: 'vec3' });
|
||||
const numberOut = (key: string): PortDef => ({ key, label: key, type: 'number' });
|
||||
|
||||
/** A number input with an inline value, so a constant needs no node of its own. */
|
||||
const floatIn = (key: string, value: number): PortDef => ({
|
||||
@@ -117,13 +149,75 @@ export const ShaderOutput: ShaderNodeDef = {
|
||||
type: SHADER_OUTPUT,
|
||||
label: 'Output',
|
||||
category: 'shader',
|
||||
description: 'The colour and opacity the sprite is drawn with; unconnected, each is the texture’s.',
|
||||
description:
|
||||
'The colour and opacity the sprite is drawn with; unconnected, each is the texture’s. Edge says how the picture continues past its border when sampled outside it.',
|
||||
inputs: [vec3Port('color'), { key: 'alpha', label: 'alpha', type: 'number', inline: false }],
|
||||
outputs: [],
|
||||
params: [],
|
||||
params: [{ key: EDGE_PARAM, label: 'Edge', kind: 'enum', default: DEFAULT_EDGE, options: EDGE_OPTIONS }],
|
||||
unfed: { color: TEXEL_COLOR, alpha: TEXEL_ALPHA },
|
||||
emit: () => ({}),
|
||||
};
|
||||
|
||||
export const ShaderUV: ShaderNodeDef = {
|
||||
type: SHADER_UV,
|
||||
label: 'UV',
|
||||
category: 'shader',
|
||||
description: "Where this pixel is on the sprite's picture: 0 to 1, u left to right, v top to bottom.",
|
||||
inputs: [],
|
||||
outputs: [numberOut('u'), numberOut('v')],
|
||||
params: [],
|
||||
emit: () => ({ u: OWN_U, v: OWN_V }),
|
||||
};
|
||||
|
||||
/** Unconnected, `u` and `v` are this pixel's own, which makes the node Texture color. */
|
||||
export const ShaderSample: ShaderNodeDef = {
|
||||
type: SHADER_SAMPLE,
|
||||
label: 'Sample texture',
|
||||
category: 'shader',
|
||||
description:
|
||||
"The sprite's picture at another spot, tinted like Texture color; past the border it continues as the Output's Edge says.",
|
||||
inputs: [
|
||||
{ key: 'u', label: 'u', type: 'number', inline: false },
|
||||
{ key: 'v', label: 'v', type: 'number', inline: false },
|
||||
],
|
||||
outputs: [vec3Port('color'), numberOut('alpha')],
|
||||
params: [],
|
||||
unfed: { u: OWN_U, v: OWN_V },
|
||||
emit: ({ u, v }) => {
|
||||
const sample = `texture(uTexture, vec2(${u!.code}, ${v!.code})) * vColor`;
|
||||
return {
|
||||
color: { code: `straight(${sample})`, type: 'vec3' },
|
||||
alpha: { code: `(${sample}).a`, type: 'float' },
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const ShaderTextureSize: ShaderNodeDef = {
|
||||
type: SHADER_TEXTURE_SIZE,
|
||||
label: 'Texture size',
|
||||
category: 'shader',
|
||||
description: "The sprite's picture in pixels, for effects that need to know where its pixels are.",
|
||||
inputs: [],
|
||||
outputs: [numberOut('width'), numberOut('height')],
|
||||
params: [],
|
||||
emit: () => ({
|
||||
width: { code: 'float(textureSize(uTexture, 0).x)', type: 'float' },
|
||||
height: { code: 'float(textureSize(uTexture, 0).y)', type: 'float' },
|
||||
}),
|
||||
};
|
||||
|
||||
export const ShaderTime: ShaderNodeDef = {
|
||||
type: SHADER_TIME,
|
||||
label: 'Time',
|
||||
category: 'shader',
|
||||
description:
|
||||
'Seconds, counting up since the editor started. A sprite wearing a graph with it keeps redrawing.',
|
||||
inputs: [],
|
||||
outputs: [numberOut('value')],
|
||||
params: [],
|
||||
emit: () => ({ value: { code: TIME_UNIFORM, type: 'float' } }),
|
||||
};
|
||||
|
||||
/** `name` is the sprite property and the uniform; the numbers are the row the inspector shows. */
|
||||
export const ShaderProperty: ShaderNodeDef = {
|
||||
type: SHADER_PROPERTY,
|
||||
@@ -216,10 +310,59 @@ export const ShaderLuminance: ShaderNodeDef = {
|
||||
}),
|
||||
};
|
||||
|
||||
function unary(type: string, label: string, fn: string, description: string): ShaderNodeDef {
|
||||
return {
|
||||
type,
|
||||
label,
|
||||
category: 'shader',
|
||||
description,
|
||||
inputs: [anyIn('value', 0)],
|
||||
outputs: [anyOut('result')],
|
||||
params: [],
|
||||
emit: ({ value }) => ({ result: { code: `${fn}(${value!.code})`, type: value!.type } }),
|
||||
};
|
||||
}
|
||||
|
||||
export const ShaderSin = unary(
|
||||
'ShaderSin',
|
||||
'Sin',
|
||||
'sin',
|
||||
'The sine of a number in radians: one full wave every 6.283. Per channel for colours.',
|
||||
);
|
||||
export const ShaderFloor = unary(
|
||||
'ShaderFloor',
|
||||
'Floor',
|
||||
'floor',
|
||||
'Rounds down to a whole number: 2.75 becomes 2.',
|
||||
);
|
||||
export const ShaderFract = unary(
|
||||
'ShaderFract',
|
||||
'Fract',
|
||||
'fract',
|
||||
'The part after the decimal point: 2.75 becomes 0.75. A rising value becomes a sawtooth from 0 to 1.',
|
||||
);
|
||||
|
||||
export const ShaderStep: ShaderNodeDef = {
|
||||
type: 'ShaderStep',
|
||||
label: 'Step',
|
||||
category: 'shader',
|
||||
description: '0 while value is below edge, 1 from edge on: a hard cut. Per channel for colours.',
|
||||
inputs: [floatIn('edge', 0.5), anyIn('value', 0)],
|
||||
outputs: [anyOut('result')],
|
||||
params: [],
|
||||
emit: ({ edge, value }) => ({
|
||||
result: { code: `step(${edge!.code}, ${value!.code})`, type: value!.type },
|
||||
}),
|
||||
};
|
||||
|
||||
/** Every node a shader graph offers. Registered by `graph/nodes`, offered by no behavior palette. */
|
||||
export const SHADER_NODES: readonly ShaderNodeDef[] = [
|
||||
ShaderTextureColor,
|
||||
ShaderTextureAlpha,
|
||||
ShaderUV,
|
||||
ShaderSample,
|
||||
ShaderTextureSize,
|
||||
ShaderTime,
|
||||
ShaderOutput,
|
||||
ShaderProperty,
|
||||
ShaderColor,
|
||||
@@ -230,6 +373,10 @@ export const SHADER_NODES: readonly ShaderNodeDef[] = [
|
||||
ShaderDivide,
|
||||
ShaderMix,
|
||||
ShaderLuminance,
|
||||
ShaderSin,
|
||||
ShaderFloor,
|
||||
ShaderFract,
|
||||
ShaderStep,
|
||||
];
|
||||
|
||||
/** A GLSL float literal: always with a decimal point, so `1` does not become an int. */
|
||||
|
||||
Reference in new issue
Block a user