Polymorphic DSL¶
Polymorphic is the high level language powering the Noisemaker Rendering Pipeline, enabling live-coding visuals by chaining functions that evaluate to native shader graphs. The Polymorphic DSL serves as the high-level builder for the pipeline, allowing users to define complex, multi-pass effects declaratively.
The language evaluates to a Directed Acyclic Graph (DAG) of render passes executed on the GPU. Each valid program must materialize its generator chains into explicit outputs so that the pipeline can schedule and double-buffer them deterministically.
Grammar¶
Program ::= SearchDirective? Statement* RenderDirective?
SearchDirective::= 'search' Ident ( ',' Ident )*
Statement ::= VarAssign | ChainStmt | IfStmt | Break | Continue | Return
RenderDirective::= 'render' '(' OutputRef ')'
Block ::= '{' Statement* '}'
IfStmt ::= 'if' '(' Expr ')' Block ('elif' '(' Expr ')' Block)* ('else' Block)?
Break ::= 'break'
Continue ::= 'continue'
Return ::= 'return' Expr?
VarAssign ::= 'let' Ident '=' Expr
ChainStmt ::= Chain
Chain ::= ChainElement ( '.' ChainElement )*
ChainElement ::= Call | WriteCall | Write3DCall | SubchainCall
SubchainCall ::= 'subchain' '(' ArgList? ')' '{' ( '.' Call )+ '}'
WriteCall ::= 'write' '(' OutputRef ')'
Write3DCall ::= 'write3d' '(' ( VolRef | Ident ) ',' ( GeoRef | Ident ) ')'
Expr ::= Chain | NumberExpr | String | Boolean | Color | Ident | Member | OutputRef | SourceRef | VolRef | GeoRef | XyzRef | VelRef | RgbaRef | MeshRef | Func | '(' Expr ')'
Call ::= Ident '(' ArgList? ')'
ArgList ::= Arg ( ',' Arg )* ','?
Arg ::= NumberExpr | String | Boolean | Color | Ident | Member | OutputRef | VolRef | GeoRef | XyzRef | VelRef | RgbaRef | MeshRef | Func
NumberExpr ::= Number | 'Math.PI' | '(' NumberExpr ')' | NumberExpr ( '+' | '-' | '*' | '/' ) NumberExpr
Member ::= Ident ( '.' Ident )+
Func ::= '(' ')' '=>' Expr
OutputRef ::= 'o' Digit+
VolRef ::= 'vol' Digit+
GeoRef ::= 'geo' Digit+
XyzRef ::= 'xyz' Digit+
VelRef ::= 'vel' Digit+
RgbaRef ::= 'rgba' Digit+
MeshRef ::= 'mesh' Digit+
SourceRef ::= 's' Digit+
Ident ::= Letter ( Letter | Digit | '_' )*
Number ::= Digit+ ( '.' Digit+ )?
String ::= '"' [^"\n]* '"' | '"""' .* '"""'
Digit ::= '0'…'9'
Letter ::= 'A'…'Z' | 'a'…'z'
Boolean ::= 'true' | 'false'
Color ::= '#' HexDigit HexDigit HexDigit ( HexDigit HexDigit HexDigit )? ( HexDigit HexDigit )?
HexDigit ::= Digit | 'A'…'F' | 'a'…'f'
Precedence & Associativity:
*,/have higher precedence than+,-.Operators are left-associative.
Parentheses
()override precedence.
Output Materialization:
Any chain that begins with a generator must terminate with
.write(<surface>); omitting the terminal.write()on a generator chain yields diagnosticS006.Chains that extend an existing surface (e.g., reading via
read(o0)and applying additional nodes) may omit.write()only when they are nested inside another chain that eventually writes to a surface.
Chainable Writes:
.write(<surface>)can appear anywhere in a chain, including mid-chain.When
.write()appears mid-chain, it writes the current result to the specified surface and passes the texture through to the next node in the chain.Multiple
.write()calls in a single chain write to multiple surfaces.Chains must still terminate with
.write()— mid-chain writes alone are not sufficient.Example:
noise().write(o0).blur().write(o1)writes the noise too0, then blurs and writes the result too1.
Generators: A chain must start with a Generator function (an effect with no inputs).
Standard Generators:
osc,noise,voronoi,solid,image,video,camera.Custom Generators: Any effect defining
inputs: {}or marked as generator.
Colors:
Hex colors support 3, 6, or 8 digits: #RGB, #RRGGBB, #RRGGBBAA. Alpha defaults to FF (1.0) if omitted.
Strings:
Strings use double quotes: "hello". For multi-line strings, use triple quotes: """line1\nline2\nline3""". Triple-quoted strings preserve embedded newlines. This is useful for the text effect:
noise().text(text: """Hello
World""").write(o0)
Arrow Functions:
Currently restricted to zero-argument expression lambdas: () => expr. Used primarily for deferred evaluation in control structures or future callbacks.
Language Features¶
Functions & Arguments¶
Functions accept arguments either positionally or as named keywords. The two forms are mutually exclusive within a single call.
Positional arguments:
noise(10, 0.1, 1)
Keyword arguments:
noise(freq: 10, sync: 0.1, amp: 1)
Numeric arguments support inline arithmetic (+, -, *, /) and constants like Math.PI. Color arguments accept unquoted #RGB or #RRGGBB hex codes.
Vector parameters:
Some effects accept multi-component vector parameters. Use the built-in vector constructors:
vec2(x, y)— 2-component vectorvec3(x, y, z)— 3-component vectorvec4(x, y, z, w)— 4-component vector
effect(param: vec2(0.5, 0.25)).write(o0)
Array literals:
Array literals — comma-separated numbers in square brackets — are an
additional input form for any vector-valued argument. They are parsed
and validated the same way vec2() / vec3() / vec4() are,
and round-trip through the unparser as […] so a program written
with array literals comes back from a parse → unparse cycle in the
same form. The vector constructor calls are unchanged and remain the
canonical form for programs that already use them.
effect(param: [0.5, 0.25]).write(o0)
effect(quad: [0.05, 0.05, 0.45, 0.95]).write(o0)
Elements may be any numeric expression (negative numbers, arithmetic,
Math.PI). Array length is not enforced by the validator — whatever
elements the source declared are passed through to the runtime.
Variables & Aliases¶
Programs may declare variables with let and reuse them. Variables can alias functions or capture partial applications.
let pattern = noise
pattern(20).write(o0)
Semantics:
let x = noise:xbecomes an alias for thenoisefunction.let y = noise(10):ystores a partial application (Effect Instance with some parameters bound). It does not execute the effect.y(0.5): Creates a new Effect Instance, merging the stored parameters (freq: 10) with the new arguments (sync: 0.5). The originalyremains unchanged (immutable).
Partials¶
Invoking variables that store function calls merges stored arguments with call-site arguments.
let tuned = noise(5)
tuned(amp:0.5).write(o0)
Merge Rules:
Positional Arguments: Appended to the stored arguments.
Named Arguments: Merged with stored arguments. Call-site arguments override stored arguments if keys conflict.
Duplicate Keys: If a named argument is provided multiple times in a single call, the last value wins.
Control Flow¶
The language supports if, elif, else for conditionals.
Note
Control flow syntax is part of the parser and validator today, but runtime execution (branching) is not yet implemented. Programs using these constructs will not execute until the pipeline gains full support.
Arrow Functions:
Arrow functions (() => expr) are treated as lazy expressions. They are not evaluated immediately but are passed as-is to the effect or control structure, which determines when (or if) to evaluate them.
Subchains¶
Subchains provide a first-class mechanism for grouping contiguous effects within a chain. They create atomic encapsulations that can be identified, manipulated, and reasoned about as units.
Syntax:
.subchain(name: "group name", id: "unique_id") {
.effect1()
.effect2(param: value)
}
Arguments:
name(optional): A human-readable label for the subchain.id(optional): A unique identifier for programmatic access.
Both arguments can be omitted, or name can be passed as a positional argument.
Examples:
search synth, filter, render
noise()
.subchain(name: "feedback loop", id: "fb1") {
.loopBegin()
.loopEnd()
}
.subchain(name: "color grading") {
.colorspace()
.hs(rotation: 180, saturation: 0.5)
}
.write(o0)
render(o0)
Rules:
Subchains cannot be empty—they must contain at least one effect.
Subchains cannot be the first element in a chain; they require input from a preceding effect.
Effects inside subchains cannot be generators (e.g.,
noise(),voronoi()).Subchains are chainable—the output flows through to subsequent effects after the closing brace.
Effects inside subchains use the same argument syntax as regular chain effects.
Use Cases:
Grouping related effects for organizational clarity.
Marking effect groups for UI controls or programmatic manipulation.
Defining reusable patterns within complex compositions.
Enabling downstream tools to identify and operate on logical effect groups.
Namespaces¶
Polymorphic supports a namespace system to organize effects and ensure compatibility.
Built-in Namespace¶
The io namespace contains pipeline-level I/O functions that are always available without requiring a search directive. These are not effects per se, but fundamental pipeline operations:
read(surface): Read from a 2D surface (e.g.,read(o0))write(surface): Write to a 2D surface (e.g.,.write(o0))read3d(vol, geo): Read from 3D volume and geometry bufferswrite3d(vol, geo): Write to 3D volume and geometry buffersrender(surface): Set the final render output (program directive)render3d(): Render 3D volume to 2D output
The io namespace is implicitly included in all programs. You never need to add io to your search directive—these functions are always accessible.
New Namespaces¶
These namespaces are actively developed and maintained:
synth: 2D generator effects that create patterns from scratch (noise, shapes, fractals)filter: 2D single-input effects that transform images (blur, color adjustment, distortion)mixer: Two-input effects that combine images (blend modes, compositing)render: Rendering utilities and feedback loops (pointsEmit, pointsRender, loopBegin/End)points: Particle and agent-based simulations (physarum, life, flock, flow)synth3d: 3D volumetric generator effects (noise3d, ca3d, rd3d)filter3d: 3D volumetric processor effects (flow3d, render3d)
Classic Namespaces¶
In addition to the actively developed and maintained namespaces above, the following namespaces were ported from older versions of our products. Each namespace offers a different take on how runtime composition can work.
classicNoisedeck: These are complex and often slower shaders brought over from the “Classic” Noisedeck.app shader graph.
Custom Namespaces¶
External integrations can introduce their own top-level namespace at runtime via the registerNamespace() API, alongside the built-ins listed above. Once registered, the new id is accepted by the search directive and behaves like any built-in namespace. The reserved user namespace is also available without registration for ad-hoc effects.
See Shader Pipeline Integration for the full API: registerNamespace(id, descriptor), unregisterNamespace(id), and validation rules.
Search Order¶
Every program must begin with a search directive that defines the namespace resolution order. There are no implicit defaults—explicit search order is required.
search synth, filter
noise3d(seed: 1).translate(x: 0, y: 0).write(o0)
When a function like noise3d() is called, the runtime walks the search order (synth, then filter) until a matching effect is found.
Resolution Rules:
Mandatory Search Directive: Every program must start with
search <namespace>, ...to specify which namespaces to search and in what order.Unqualified Identifiers: Calls like
noise()walk the search order until a matching effect is found.Overrides: The
from(ns, fn())helper allows sourcing an operation from a specific namespace temporarily (e.g.,from(synth, noise())).
Note: Inline namespace prefixes (e.g., synth.noise()) are forbidden in program chains. Use the search directive or from() helper instead.
Enums¶
Many function arguments accept enumerated options defined in a global registry. Enums are defined at the top level in std_enums.js as global categories (e.g., color, blend, wrap).
For example, the noise effect accepts a colorMode parameter with values from the global color enum. You can reference enum values in three ways:
Shorthand identifier:
colorMode: rgb(validator auto-prefixes tocolor.rgb)Full path:
colorMode: color.rgbMember expression:
let mode = color.mono; noise(colorMode: mode).write(o0)
The runtime resolves these enum references to their integer counterparts before binding to the shader.
Palettes¶
The palette enum provides named color palettes for effects like palette() in the filter namespace. Palettes are cosine gradient functions that map scalar values (typically luminance) to RGB colors.
Usage:
search filter
read(o0).palette(paletteIndex: vaporwave).write(o1)
Available Palettes:
Name |
Description |
|---|---|
|
Neutral (grayscale) |
|
Grayscale gradient |
|
Warm afterimage effect |
|
Desert sunset tones |
|
Cool cyan and blue |
|
Sky blue gradient |
|
Metallic gray tones |
|
Fiery orange and purple |
|
Warm sunset colors |
|
Bright magenta and cyan |
|
Soft pink and blue pastels |
|
Dark smooth gradient |
|
Warm orange and brown |
|
Soft dream-like tones |
|
Deep space blues |
|
Vibrant tech colors |
|
Pale ethereal tones |
|
Warm hazy oranges |
|
Thermal imaging colors |
|
Bright neon colors |
|
Bold contrasting hues |
|
Pure blue channel |
|
Pure cyan (green + blue) |
|
Pure green channel |
|
Pure magenta (red + blue) |
|
Pure red channel |
|
Pure yellow (red + green) |
|
Rusty red planet tones |
|
Earthy green and purple |
|
Forest green and brown |
|
Deep ocean blues |
|
Jewel-toned purples |
|
Natural earthy tones |
|
Tropical orange |
|
Toxic green glow |
|
Deep purple royalty |
|
Beach sunset colors |
|
Retro 70s colors |
|
Citrus orange and pink |
|
Double-frequency sherbet |
|
Silver metallic (OkLab) |
|
Soft pink sky |
|
Solar flare oranges |
|
Halloween orange and black (OkLab) |
|
Fresh spring pastels |
|
Bright spring greens |
|
Yellow sulfur tones |
|
Dark ritual magenta |
|
Bold comic book colors |
|
Poisonous green |
|
Tropical paradise (OkLab) |
|
Cool tungsten lighting |
|
80s synthwave aesthetic |
|
High saturation colors |
|
Aged photograph tones |
|
Sepia photo effect |
Oscillators¶
Oscillators are objects that generate time-varying values for animating effect parameters. They produce looping values synchronized with the animation duration, making them ideal for creating smooth, repeating animations.
Creating Oscillators¶
Use the osc() function to create an oscillator:
osc(type: sine)
Parameters:
Parameter |
Type |
Default |
Description |
|---|---|---|---|
type |
oscKind |
(required) |
Oscillator waveform type |
min |
number |
0 |
Minimum output value |
max |
number |
1 |
Maximum output value |
speed |
int |
1 |
Loop speed multiplier (divides evenly into animation duration) |
offset |
number |
0 |
Phase offset (0..1) |
seed |
number |
1 |
Random seed (noise type only) |
Oscillator Types (oscKind):
sine- Smooth sine wave: 0 → 1 → 0tri- Linear triangle wave: 0 → 1 → 0saw- Sawtooth wave: 0 → 1sawInv- Inverted sawtooth: 1 → 0square- Square wave: 0 or 1noise- Periodic 2D noise (seamlessly looping)
Usage Examples¶
Basic oscillating parameter:
search synth
noise(scale: osc(type: sine, min: 2, max: 8)).write(o0)
Using variables for reusable oscillators:
search synth
let scaleOsc = osc(type: sine, min: 2, max: 8)
let rotOsc = osc(type: saw, min: 0, max: 360)
noise(scale: scaleOsc, rotation: rotOsc).write(o0)
Speed control for synchronized loops:
search synth
// speed: 2 means the oscillator completes 2 cycles per animation loop
noise(scale: osc(type: tri, min: 1, max: 10, speed: 2)).write(o0)
Phase offset for staggered animations:
search synth
let osc1 = osc(type: sine, offset: 0)
let osc2 = osc(type: sine, offset: 0.25)
let osc3 = osc(type: sine, offset: 0.5)
// Three oscillators at different phases create wave-like patterns
Noise oscillator with seed:
search synth
noise(scale: osc(type: noise, min: 2, max: 8, seed: 42)).write(o0)
Runtime Behavior¶
Oscillators are evaluated per-frame based on the current animation time. The pipeline normalizes time to a 0..1 range over the animation duration (default 10 seconds), then applies the speed multiplier and offset before computing the waveform value.
The resulting value is mapped from the internal 0..1 range to the specified min..max range, making oscillators suitable for any numeric parameter regardless of its expected range.
Live Input¶
Use midi() and audio() to drive parameters from external signals. Both map incoming data to a numeric range and can be mixed with oscillators or constants.
midi(channel, mode?, min?, max?, sensitivity?)
channel(required): MIDI channel 1-16mode: midiMode value (defaultvelocity)min/max: Output range (default 0..1)sensitivity: Decay rate for trigger modes (default 1)
audio(band, min?, max?)
band(required):low | mid | high | volmin/max: Output range (default 0..1)
Example:
search synth
noise(
scale: midi(channel: 1, min: 1, max: 10),
speed: audio(band: low, min: 0.5, max: 2)
).write(o0)
For detailed behavior and host integration, see MIDI & Audio Input.
Pipeline Integration¶
The DSL acts as a high-level builder for the Render Graph defined in Pipeline Specification. For a detailed look at how the DSL is compiled, see Compiler Specification.
Mapping DSL to Effects¶
When the evaluator encounters a function call like .bloom(0.5):
Lookup: Retrieves the
Bloomeffect definition using the namespace resolution rules.Instantiation: Creates a logical instance of the effect.
Parameter Binding: Binds arguments to the effect’s
globals.Chain Connection: Connects the output of the previous node to the input of the new instance.
Texture I/O¶
The DSL provides symmetric operations for reading and writing textures:
2D Textures:
write(surface): Writes the chain output to a 2D surface.
Example:
noise(10).write(o0)Surfaces:
o0-o7(global)Chainable:
write()can appear mid-chain, passing the texture through to subsequent nodes.Example:
noise().write(o0).blur().write(o1)— writes noise too0, then blurs and writes too1.Example:
noise().write(o0).invert().write(o1)—o0has the original noise,o1has the inverted version.
read(surface): Reads from a 2D surface. Built-in to the pipeline, no namespace required.
Example:
read(o0).bloom(0.5).write(o1)
3D Textures:
write3d(vol, geo): Writes to both a 3D volume and its geometry buffer.
Example:
noise3d(10).write3d(vol0, geo0)
read3d(vol, geo): Reads from both a 3D volume and its geometry buffer (starter form).
Example:
read3d(vol0, geo0).render3d().write(o0)
read3d(vol): Single-arg form for passing volume references to effect parameters.
Example:
ca3d(source: read3d(vol0), geoSource: read3d(geo0))This mirrors the 2D
read(o0)pattern for surface parameters.
Surfaces and Outputs¶
The DSL allows writing to named outputs (Surfaces) and reading from them.
2D Surfaces:
Global Surfaces:
o0-o7are persistent 2D textures.Output:
.write(o0)marks the chain as writing too0.Input:
read(o0)creates a read dependency ono0.None:
nonedisables a surface parameter (e.g.,effect(tex: none)).
3D Volume Surfaces:
Global Volumes:
vol0-vol7are persistent 3D texture volumes (default 64³).Global Geometry Buffers:
geo0-geo7are 2D geometry buffers storing surface normals and depth.Output:
.write3d(vol0, geo0)writes 3D volume data and geometry to the specified surfaces.Input (starter):
read3d(vol0, geo0)reads from a volume and its geometry buffer to start a chain.Input (param):
read3d(vol0)orread3d(geo0)passes a reference to an effect parameter.None:
nonedisables a volume/geometry parameter (e.g.,ca3d(source: none)).
The geometry buffers store precomputed raymarching results (xyz=surface normal, w=depth), enabling downstream post-processing effects without re-raymarching.
Agent Particle Surfaces:
Used by the SMRTicles particle system (see SMRTicles):
Position Surfaces:
xyz0-xyz7store agent positions (xyz) and lifecycle state (w).Velocity Surfaces:
vel0-vel7store agent velocities.Color Surfaces:
rgba0-rgba7store agent colors.
These surfaces are managed by the pointsEmit and pointsRender wrappers. Behavior effects read and write these surfaces to update agent state each frame.
Mesh Surfaces:
Mesh Geometry Textures:
mesh0-mesh7are texture pairs storing mesh geometry data from loaded OBJ files.Each mesh surface consists of a positions texture (vertex XYZ + W) and a normals texture (normal XYZ + UV).
Loading: Use
meshLoader()in the pipeline and load OBJ files via the API (canvas.loadOBJFromURL()orcanvas.loadOBJFromString()).Rendering: Use
meshRender(mesh: mesh0)to render mesh geometry with lighting and transforms.
Feedback Loops¶
If a chain reads from a Surface that hasn’t been written to yet in the current frame (or reads from itself), it reads the texture content from the previous frame. This enables feedback effects.
Diagnostics¶
Code |
Stage |
Severity |
Message |
|---|---|---|---|
L001 |
Lexer |
Error |
Unexpected character |
L002 |
Lexer |
Error |
Unterminated string literal |
P001 |
Parser |
Error |
Unexpected token |
P002 |
Parser |
Error |
Expected closing parenthesis |
S001 |
Semantic |
Error |
Unknown identifier |
S002 |
Semantic |
Warning |
Argument out of range |
S003 |
Semantic |
Error |
Variable used before assignment |
S005 |
Semantic |
Error |
Illegal chain structure |
S004 |
Semantic |
Error |
Cannot assign null or undefined |
S005 |
Semantic |
Error |
Illegal chain structure |
S006 |
Semantic |
Error |
Starter chain missing write() call |
S007 |
Semantic |
Warning |
Deprecated parameter alias |
S008 |
Semantic |
Warning |
Deprecated effect |
R001 |
Runtime |
Error |
Runtime error |
Common Errors¶
S005 (Illegal chain structure): Generator functions (like
osc,noise) must appear at the start of a chain. They cannot consume an existing chain output.S006 (Starter chain missing write): Generator-driven chains must end with
.write()to produce a reusable surface.S007 (Deprecated parameter alias): A parameter name you used still works but has been renamed. Update to the current name.
S008 (Deprecated effect): An effect you used still works but has been replaced by a newer effect. Update to the current name.