Skip to content

Attribute Wrangle

Marko Koljancic edited this page Jul 28, 2026 · 1 revision

Home · Solarxy Web · Expressions · Node Reference

Attribute Wrangle

attribute_wrangle runs a small program once per point (or once per primitive) over the geometry flowing through it. Where expressions let one parameter compute from another, a wrangle lets one point compute from another.

It is the general-purpose attribute tool. attribute_create writes a constant and attribute_randomize writes noise; neither can compute a lane from another lane, or from a point's own position. A wrangle can, which is what turns attributes from storage into something you shape with.

The first one to try

Drop an attribute_wrangle after any primitive. It arrives holding:

@Cd = set(@P.x + 0.5, @P.y + 0.5, @P.z + 0.5);

The geometry turns into a colour gradient immediately, because @Cd is the colour attribute the viewport already displays. Nothing else to wire up.

Now replace it with this and press play on the playbar:

@P = set(@P.x, @P.y + sin(@P.x * 4 + $T) * 0.2, @P.z);

The surface ripples. The wrangle is being re-run every frame against a clock, not playing back a recording.

Writing a program

Statements are separated by ;. Each one assigns to an attribute or to a local variable. The trailing ; on the last statement is optional.

float d = length(@P);
@P = @P * (1 + 0.1 * sin(d * 8 + $T));
@Cd = set(d, 1 - d, 0.5);

Everything the expression language offers is available inside a wrangle: the same operators and precedence, the same thirty-odd functions, ch("box1/width") to read another node's parameter, and npoints() or bbox("size") to measure the incoming geometry. It is literally the same parser, so anything you learn in one place works in the other.

The editor

The field is a real code editor, not a text box.

  • Completion on @, $ and bare names, each with a one-line description. The @ list is not a fixed catalogue: it includes the attribute lanes actually present on the geometry arriving at this node, so it is also how you find out what is there.
  • Brackets match and auto-close, and Ctrl/Cmd + / toggles a comment on the selection.
  • Find with Ctrl/Cmd + F, and selecting a word highlights its other occurrences.
  • Errors underline the exact token, not the whole line.
  • Ctrl/Cmd + Enter commits without leaving the field; Esc reverts to the last committed program.

Word wrap, line numbers and font size live in Preferences > Display > Code editor and apply to every code field in the app.

The element scope

@name reads or writes an attribute on the element currently being processed.

@P position (vector). Assigning it moves the geometry
@N normal (vector)
@Cd colour. Three components are fine; alpha fills in as opaque
@uv texture coordinate (vector2)
@ptnum / @numpt this point's index, and how many there are
@primnum / @numprim the same for the primitive domain
@anything any other attribute on the input

@ptnum and the other three counters are read-only; they describe where you are, not something you set.

An attribute the input does not carry is created at the width of its first assignment: @heat = 1; makes a float lane, @dir = set(0, 1, 0); makes a vector one. Reading a lane that is not there is an error naming the lane, rather than a silent zero.

Local variables

Declare with a type, assign in the same statement:

float t = @P.y;
vector2 flat = set(@P.x, @P.z);
vector rgb = set(t, 1 - t, 0);
vector4 rgba = set(t, 1 - t, 0, 1);

A scalar widens into a wider local, so vector v = 0; is three zeroes. A wider value into a narrower local is an error rather than a silent truncation, because quietly dropping z is a bug nobody finds.

There is no if and no for

Deliberately. With no loops, a program's cost is exactly (statements × elements) and cannot run away on a cook that shares a thread with the viewport. For a branching value, use the conditional the expression language already has:

@Cd = @P.y > 0 ? set(1, 0, 0) : set(0, 0, 1);

Points or primitives

Run Over picks the domain. Points is the usual choice and the only one that can move geometry, because @P is a point attribute. Primitives runs once per triangle, segment or point primitive, and reaches the primitive-domain lanes attribute_promote writes.

Things it can now feed

Two nodes read attributes a wrangle is the first thing able to author:

  • copy_to_points reads a pscale float and multiplies its own Scale by it, so @pscale = fit(rand(@ptnum), 0, 1, 0.4, 1.6); on a scatter gives you copies of mixed sizes.
  • scatter takes a Density Attribute, weighting where its points land. @density = fit(@P.y, 0, 1, 0, 1); gathers them toward the top of a surface.

When something is wrong

A parse error names the line and column, underlines the token it choked on, tints that line, and badges the node. The geometry keeps its last good state rather than vanishing.

An arithmetic edge is not an error. Dividing by zero gives you an infinity, the way it does anywhere else, because one bad element must not blank an entire scene.

A program that assigns nothing at all cooks and warns, since it is legal and almost certainly not what you meant.

Speed

The program is parsed once per cook, never per element, and runs over a fixed set of registers with no allocation in the inner loop.

Past about 50,000 elements the node warns, because that is roughly where a re-cook stops feeling instant: the cook is single-threaded, and dragging a parameter anywhere upstream re-runs the whole program every frame. The warning is advisory. If you are happy to wait, nothing stops you.

See also

Clone this wiki locally