Built-in functions

The function library callable from any Expression, grouped by what each group does.

This page is a map, not an index: it names the functions worth knowing by group, so you can find the right family. The library is larger than what is named here.

The maths and colour groups run the same on a single number in a parameter field and per-element across a whole collection. Functions apply component-wise, so abs(@P) flips every axis of a position at once. The groups that reshape a whole collection are Expression-only.

Maths

sin cos tan sqrt pow exp log abs floor ceil round fract sign, plus min max clamp and saturate (clamp to 0–1), step, degrees and radians.

Interpolation and remapping:

// fade in over the first second
@Cd.a = smoothstep(0.0, 1.0, $T);

// remap a value from one range to another
@pscale = remap(@Cd.r, 0.0, 1.0, 0.5, 2.0);

// blend two values by t
@Cd.g = mix(@Cd.r, @Cd.b, 0.5);

remap takes five arguments: the value, then the input low and high, then the output low and high.

Wave shapers for animation and patterns: triangle_wave, sawtooth, square_wave, gaussian, sigmoid.

hash(seed, value) turns two numbers into a stable pseudo-random one, which gives each element a constant personality that does not flicker frame to frame: @pscale = hash(float(@id), 1.0);.

Bits

There are no bitwise operators. A bare & or << fails to lex, and the work is done by seven words instead: bit_and, bit_or, bit_xor, bit_not, shl, shr and bitcast. All of them take scalars rather than vectors, and all of them work on signed 32-bit integers, so a float argument truncates on the way in.

// stripe every other column of an image
@Cd = @Cd * float(bit_and(int(@uv.x * 16.0), 1));

The six bit-manipulation words return an integer. bitcast is the odd one: it reinterprets the bits rather than converting the value, so an int comes back as a float and a float as an int, and it only does anything on the GPU.

Vectors

dot, cross, length, distance, normalize, and the optics pair reflect and refract.

// distance from each point to the origin
@pscale = length(@P);

// face each point toward the origin
@N = normalize({0.0, 0.0, 0.0} - @P);

Matrices and rotations

There is no matrix type to declare. Eleven functions cover the work instead. mat4_mul_vec(c0, c1, c2, c3, v) multiplies a vector by a matrix passed as its four column vectors, mat_compose_col(translate, euler_deg, scale, col) builds one column of a transform from translation, Euler degrees and scale, and mat_solve3(row0, row1, row2, rhs, fallback) and mat_solve4 solve a small linear system from row vectors, returning the fallback where the system has no solution. For the common case of turning a vector about an axis, rotate_vec(axis, angle, v) is the direct route.

Quaternions are plain four-component vectors: quat_from_euler(euler_deg) builds one, quat_mul(a, b) composes two, and quat_slerp(a, b, t) blends between them along the shortest arc.

// spin every point's orientation over time
@orient = quat_from_euler({0.0, $T * 30.0, 0.0});

catmull_rom(p0, p1, p2, p3, t) interpolates smoothly through four values, curve_frame(curve_data, t) reads a frame along a curve, and coord_project(mode, pos, normal) flattens a position to a two-component coordinate.

Of this group only catmull_rom also runs in a parameter expression. The rest are vector-valued, and a parameter expression evaluates single numbers. See Parameter expressions.

Colour

rgb_to_hsv and hsv_to_rgb, rgb_to_hsl and hsl_to_rgb, and luminance for perceived brightness.

// shift hue while keeping saturation and value
vec4 hsv = rgb_to_hsv(@Cd);
hsv.x = fract(hsv.x + 0.1);
@Cd = hsv_to_rgb(hsv);

Colour is handled in a consistent working space, so the conversion helpers srgb_to_linear and linear_to_srgb are needed only for special cases. See Colour management.

Noise

noise(position) is the one-liner for parameters and simple offsets, returning one float. A second argument sets the frequency, so noise(@P, 7.0) gives a much finer field over the same positions.

// drift each point with smooth 3D noise
@P.y += noise(@P * 0.5 + $T) * 10.0;

Layered fractal noise (fBm, turbulence, ridged, billow, domain warp) lives on the Noise node rather than in the function library. Sum a couple of noise() calls at different scales for a quick layered offset inside an Expression.

Fields

A field is read per position with sample_input(k, pos), where k is the input slot: sample_input(0, @P) reads the field wired to the first input. It is available when the node's output_field switch is on, the mode where an Expression evaluates per position rather than per element. field_extract pulls one named channel out of a record field and field_blend cross-fades two of them. Where the field wired to a slot is a multi-channel record, sample_input_channel(k, "name", pos) samples one named channel of it directly, typed from that channel's own valence.

To sample a field onto an existing set of points, use the Sample node instead. Fields & sampling covers the bigger picture.

Images

Three functions look outside the pixel being shaded. gather and resample need a raster on the input: on points, a mesh or a volume they are a compile error naming the function, because there is no grid to read.

gather(@Cd, dx, dy) reads the input at a whole-texel offset from the current pixel, so gather(@Cd, 1, 0) is the pixel to the right and gather(@Cd, 0, 0) is the pixel itself. It returns the same type as the attribute it is handed. Blurs, embossing and edge finding are all built from it.

// horizontal and vertical difference: an edge find
@Cd = abs(gather(@Cd, 1, 0) - gather(@Cd, -1, 0))
    + abs(gather(@Cd, 0, 1) - gather(@Cd, 0, -1));

resample(@Cd, u, v) reads at an absolute coordinate running 0 to 1 across the image rather than an offset, which is what a warp, a lens distortion or a mosaic needs. The raster globals $PX, $PY, $CANVAS_W and $CANVAS_H on Globals & parameters let the coordinate be worked out in pixels.

// a sine wobble along each row
let amp = param("amp", 8.0);
let ux = ($PX + 0.5 + sin($PY * 0.1 + $T) * amp) / $CANVAS_W;
@Cd = resample(@Cd, ux, ($PY + 0.5) / $CANVAS_H);

Both read a second, third or fourth wired image through @Cd1, @Cd2 and @Cd3, so gather(@Cd1, 1, 0) looks sideways in the image on the node's second input.

convolve(input, radius, |dx, dy| weight) is the whole-image form: it visits every tap within radius of each pixel and the small function returns that tap's weight. It takes the wired collection as input rather than an attribute, and its result is a collection, so bind it with let instead of assigning it to @Cd.

let blurred = convolve(input, param("radius", 4.0), |dx, dy| exp(-(dx*dx + dy*dy) / 8.0));

Named arguments follow the closure and change what the filter is. combiner= picks how the taps are joined: "weighted_average", "sum", "median", "min" or "max", so a flat weight with combiner="median" is a despeckle rather than a blur. edge= decides what lies outside the frame: "clamp", "wrap", "mirror" or "zero". radius= takes a field and varies the radius per pixel, with the positional radius becoming the ceiling. angle=, center= with axis=, and flow= give the tap pattern a direction, and only one of those three may be used at a time.

aux= names a second thing to sample alongside the colour, and hands the closure two more arguments: the auxiliary value at the centre and at the tap. That turns an ordinary blur into an edge-preserving one, because the weight can fall away where the two disagree.

let edged = convolve(input, param("radius", 3.0),
  |dx, dy, c, n| exp(-(dx*dx + dy*dy) / 8.0) * exp(-abs(n - c) * param("edge_stop", 20.0)),
  aux=depth);

An unknown value for combiner= or edge= is a compile error listing the real set. A misspelled argument name is not: it is dropped in silence, and the filter runs with that setting at its default. Check the spelling if a named argument appears to do nothing.

Neighbours

foreach (int i : neighbours(radius)) walks the elements within radius of the current one, and point(i, "P") reads a named attribute off each. This is what flocking and clustering are built from.

// fade out crowded points
float cnt = 0.0;
foreach (int i : neighbours(5.0)) {
  cnt += 1.0;
}
@Cd.a = 1.0 / (1.0 + cnt);

nearest(points, position, radius, max_count) and nearby(points, position, radius, max_count) do the same query over a whole collection at once, returning the nearest index and the neighbour count per element.

Sorting and scattering

Reshape whole collections with sort(coll, key, descending), scatter (Poisson-disk point generation), sample(coll, count, seed) (random subset) and distinct (drop duplicates), plus reverse, take, skip, flatten, enumerate, zip, interleave, concat and append.

// keep the 50 highest points
let ordered = sort(input, @P.y, 1);
let top = take(ordered, 50);

The higher-order functions map, filter, reduce, scan and fold take a small inline function and run it across a collection.

Analytic shapes

Build analytic shapes from an expression. crc_stamp places one shape from its kind and parameters; crc_compose joins shapes with one of the operators listed below. crc_stamp_instanced and crc_compose_instanced do the same once per row of a source collection, reading each row's @ attributes to drive the result.

// A sphere on every point, sized by that point's scale
crc_stamp_instanced(input, "sphere", @P, {0,0,0}, {@pscale,0,0,0}, {0,0,0,0}, @Cd);

Shapes are named: sphere, cube, cylinder, cone, torus, plane, superquadric. Operators are boolean, offset, displaced, repeat, deformed, extruded, revolved. An unknown name is a compile error listing the real set.

crc_mesh_bind wraps a mesh as a single analytic row, crc_patch builds a Bezier patch from a grid of control points, crc_path and crc_spine build filled paths and tubes from control points, and points_to_crc converts a point cloud.

Strings

join, split, substring, contains, index_of, string_replace, str_case, trim, strlen, and format(value, "text {}") for assembling labels. to_float and from_float convert between text and numbers. Strings are values in the program, not attributes: an attribute holds a float, an int or a vector, so a string cannot be assigned to one.

Time

Functions that reach across frames: prev(value, initial) returns last frame's value with a cold-start fallback, accumulate(value, alpha) is an exponential moving average, and history(value, offset) reads back offset frames.

// ease a jittery value toward its own history
@pscale = accumulate(@pscale, 0.8);

The current frame number is the global $F, not a function. See Time & playback and Loops & feedback.

Store operations

The store is a small persistent table, written and read back across elements or frames, used for IDs, counters and lookups. The operations are store_insert(key, value), store_lookup(key, initial), store_update, store_delete and store_collect.

// remember a per-id value and read it back
store_insert(@id, input);
@pscale = store_lookup(@id, 1.0);

Use it for stable identity: assign an id on birth and recall its data later, even after the collection has been reordered.

See also