Skip to content

Visual Operators

Beyond writing raw shaders (Audioreactive Shaders), std/visuals ships a set of ready-made operators modeled on TouchDesigner’s TOPs — gfx.Noise(), gfx.Blur(), gfx.Displace(), and more. Each one returns a Visual, so they wire together with the same >> and << you already use for shaders, into a node graph that renders as a multi-pass GPU pipeline.

use "std/visuals" as gfx;
let field = gfx.Noise(); // a generator
field.param("period") << 2.0;
let soft = gfx.Blur(); // a filter
soft.param("radius") << 8.0;
field >> soft >> screen; // wire generator -> filter -> display
PLAY;

Operators are per-node — every gfx.Blur() is its own object with its own uniforms — so you can drop as many of the same operator into one graph as you like and nothing collides.

Every operator graph is built from exactly two operators, and the rule for each is uniform across all nodes.

src >> op feeds src into op’s primary input — the shader’s iChannel0 — and, at the end of a chain, op >> screen routes the result to the display. >> is an expression: it wires its left side into its right and evaluates to the downstream node. So chains read left-to-right and compose inline:

use "std/visuals" as gfx;
let blurred = gfx.Noise() >> gfx.Blur(); // wires Noise -> Blur, returns the Blur node
blurred >> screen; // ...which is reusable downstream
PLAY;

Because each >> hands back the node it fed, a >> b >> c flows left-to-right and the whole chain is itself a value. >> never mutates the upstream node, so a node stays reusable for fan-out (below).

op.param("name") << rhs binds a node’s input by name. It is type-dispatched on the right-hand side:

  • A node ⇒ a texture input edge — it wires that node into the operator’s named second input (the next iChannel). This is how two-input operators get their other input:

    comp.param("b") << other; // Composite's second input B
    disp.param("by") << field; // Displace's displacement field
    look.param("gradient") << ramp; // Lookup's colour gradient
  • A constant, signal, meter feed, or 2–4-element array ⇒ a uniform. Scalars bind a float; a #[x, y] / #[x, y, z] / #[x, y, z, w] array binds a vec2/vec3/vec4:

    noise.param("period") << 2.0; // float constant
    edge.param("strength") << Sine(8).range(0.5, 2.0); // signal
    blur.param("radius") << drums.rms.smooth(80); // audio meter feed
    ramp.param("color0") << #[0.1, 0.0, 0.2]; // vec3 from an array

Uniforms are sampled once per rendered frame, off the audio thread — binding a signal or meter never touches the real-time path.

To use one node’s output in several places, bind it to a let and reference it from each consumer. The graph is de-duplicated by identity: the topo-sort sees the same node object and renders it as a single pass, sampled by every downstream consumer in the same frame. The graph is acyclic — within a frame, every edge points forward.

use "std/visuals" as gfx;
let field = gfx.Noise();
field.param("period") << 2.0;
// Two independent blurs, both reading the SAME noise pass — one Noise render, sampled twice.
let soft = gfx.Blur();
soft.param("radius") << 6.0;
let softer = gfx.Blur();
softer.param("radius") << 24.0;
let comp = gfx.Composite();
comp.param("operand") << 2.0; // subtract: A - B
field >> soft >> comp; // soft is A (iChannel0)
comp.param("b") << (field >> softer); // softer is B (iChannel1)
comp >> screen;
PLAY;

Here field is referenced three times but renders once; soft and softer each sample that one noise pass.

A node graph is acyclic within a frame, so to read last frame’s output — a feedback loop — wrap the wiring in gfx.feedback(fn(prev) { … }). The closure is called with prev, the group’s previous-frame output as a sampleable visual; wire it into a slot to close the loop, and return the node that is the group’s output:

use "std/visuals" as gfx;
let look = gfx.feedback(fn(prev) {
let field = gfx.Noise();
field.param("period") << 3.0;
// Displace this frame's noise BY last frame's output -> a flowing feedback trail.
let disp = gfx.Displace();
disp.param("amount") << 0.1;
disp.param("by") << prev; // close the loop with prev
return field >> disp; // disp is the group output (prev tracks it)
});
look >> screen;
PLAY;

The renderer realizes the loop as a ping-pong FBO pair: it renders into buffer A while prev reads buffer B, then swaps. That swap is the only thing feedback adds over fan-out — everything inside the closure is an ordinary acyclic graph.

All operators come from use "std/visuals" as gfx;. Generators have no primary input; filters take one via src >> op; 2-in filters take a second via a named slot (op.param("slot") << node). Every pass clamps its output to [0, 1].

OperatorKindParameters
gfx.Noise()generatorperiod, harmonics, gain, exponent, offset
gfx.Ramp()generatortype, color0color3 (vec3), pos1, pos2
gfx.Blur()filterradius
gfx.Edge()filterstrength
gfx.Slope()filterstrength, offset
gfx.LumaLevel()filterbrightness, black, gamma, invert
gfx.RgbKey()filterkey (vec3), threshold
gfx.Transform()filtertranslate (vec2), rotate, scale (vec2), pivot (vec2), extend
gfx.Composite()filter, 2-inoperand; second input c.param("b") << node
gfx.Displace()filter, 2-inamount; displacement field d.param("by") << node
gfx.Lookup()filter, 2-ingradient l.param("gradient") << ramp

Vector parameters (key, color0…, translate, scale, pivot) bind from a 2–4-element array, e.g. t.param("scale") << #[1.5, 1.5].

A few parameters are mode selectors, rounded to the nearest integer:

  • gfx.Composite() operand — the blend: 0 over, 1 add, 2 subtract, 3 multiply, 4 difference, 5 subtractive (A+B−1), 6 max/lighten, 7 min/darken, 8 screen.
  • gfx.Transform() extend — how pixels outside the source are filled: 0 hold/clamp, 1 repeat, 2 mirror, 3 black. rotate is in degrees (positive is counter-clockwise) and aspect-corrected, so a wide canvas spins without shearing.
  • gfx.Ramp() type — gradient orientation: 0 horizontal, 1 vertical, 2 radial. The default keys make a black→blue→light-blue→white palette, ready to feed a gfx.Lookup().

examples/05_visuals/04_voronoi_feedback.non rebuilds a complete TouchDesigner feedback-displacement network — pseudo-voronoi cells that swell, shimmer, and warp in time with a polyrhythm and an acid lead — node-for-node from these operators. It is the canonical worked example of fan-out, feedback, and audioreactive uniform binding together.