Skip to content
AnimationStudiosBLUX Physics SuiteGet BLUX
Discord

Wrangle language reference

A wrangle is a few lines of code that run on the GPU for every particle (Particle Wrangle) or every voxel of a gas grid (Field Wrangle). The code is short Python with @ attributes, in the manner of Houdini’s VEX:

# a swirl that fades with height, as an acceleration
swirl = curlnoise3(@P * chf('freq', 4.0))
@v += swirl * chf('strength', 10.0) * (1.0 - @P.z) * @dt

chf('freq', 4.0) makes a slider named freq on the node, starting at 4. Everything a wrangle reads or writes about a particle is an @attribute; everything else is an ordinary variable.

Node Stage Runs
Field Wrangle Forces Every substep, before the pressure solve: may write @v and @force
Particle Wrangle Substep Every physics substep: forces, damping, velocity limits
Field Wrangle Substep Every substep, after the pressure solve: scalars only
Particle Wrangle Post-Step Once per frame after the substeps: colour, damage, filtering
Field Wrangle Post-Step Once per frame after the substeps: scalars only
Particle Wrangle Emission On newborn particles at spawn time: initial state (Atom and Hydro; Impact spawns nothing)

All engines work in the solver cube: positions run from 0 to 1 on every axis, Z is up, and velocities are in cube units per second. A wrangle that needs the world position of something reads it from a slider (chv('center', 0.5, 0.5, 0.0)), not from the scene.

  • Variables take any name, including i, dt, temp or clamp; they cannot start with two underscores. A variable has to be assigned before it is read.
  • Control flow: if / elif / else, conditional expressions (a if c else b), and / or / not, and for loops over range(...) with written-out whole numbers and at most 256 iterations. break and continue work inside a for loop only. There is no while, no function definition, no return, no dict and no string except the name of a slider.
  • Vectors: set(x, y, z) (or a list or tuple, [x, y, z]) builds one; v.x v.y v.z v.w (or .r .g .b .a) and swizzles such as v.xy read and write components. Arithmetic between vectors, or between a vector and a number, applies to every component.
  • Sliders: chf('name') (a number), chv('name') (a vector) and chc('name') (a colour) put a control on the node. A default can follow the name: chf('radius', 0.3), chv('center', 0.5, 0.5, 0.0) (one number fills x, y and z), chc('tint', 1.0, 0.5, 0.2) (alpha 1) or with a fourth number for alpha. Defaults must be written-out numbers. A slider starts at its default; what you set by hand is kept when the code changes. Two uses of one name must agree in kind, and the first default wins.
  • Operators and functions: + - * / **, comparisons, and the functions and ti. names listed below. Anything not listed is refused with a message that names it and, when there is a close match, suggests it.
  • Mistakes are found when the code is checked (a moment after you stop typing, or with Recompile): the node shows the message and the line, with a button that opens the code at that line. The GPU kernel itself is built by the worker when the simulation starts or the code changes; its build time, or the reason it was left out, is shown on the node.

An attribute the engine does not have is an error (“@damage is not available on HYDRO”), not a silent no-op. The names in the also column are accepted as well.

Attribute Type What it is Engines Also
@P Vector3 Position in the solver cube (0 to 1 on every axis, Z up) Atom, Hydro, Impact @p, @pos, @position
@v Vector3 Velocity, in cube units per second Atom, Hydro, Impact @vel, @velocity
@Cd Vector4 Colour, RGBA 0 to 1 (Impact bodies store RGB: alpha reads as 1 and is not kept) Atom, Hydro, Impact @cd, @color
@temp Float Temperature Atom, Impact @t, @temperature
@damage Float Damage, 0 (intact) to 1 (broken) Atom, Impact @dmg
@material Int Material id Atom @mat
@active Int 1 for a live particle, 0 for a dead one; write 0 to kill it Atom, Hydro, Impact @alive
@wet Float Moisture picked up from nearby fluid, 0 to 1 Atom @wetness
@rho Float Density Hydro
@phase Int Fluid phase id Hydro
@f0 Float Persistent user channel: keeps its value from step to step, starts at 0 Atom, Hydro, Impact @user_float_0
@f1 Float Persistent user channel Atom, Hydro, Impact @user_float_1
@f2 Float Persistent user channel Atom, Hydro, Impact @user_float_2
@f3 Float Persistent user channel Atom, Hydro, Impact @user_float_3
@v0 Vector3 Persistent user vector channel: keeps its value from step to step, starts at 0 Atom, Hydro, Impact @user_vec_0
@v1 Vector3 Persistent user vector channel Atom, Hydro, Impact @user_vec_1
Attribute Type What it is Writable Also
@density Float Smoke density all stages
@temp Float Temperature above the ambient air, in units of 1000 K (1 is a burning flame) all stages @t, @temperature
@fuel Float Unburned fuel all stages
@flame Float Flame (the reaction front) all stages
@burn Float Reaction rate (read only) read only
@P Vector3 Voxel centre in the solver cube (read only) read only @p, @pos, @position
@v Vector3 Velocity: write it in the Forces stage only (the pressure solve keeps it divergence-free) Forces stage only @vel, @velocity
@force Vector3 Acceleration to add, in cube edges per second squared: Forces stage only Forces stage only @accel

A Field Wrangle may write @v and @force in the Forces stage only. They act before the pressure solve, which keeps the flow divergence-free; the scalars can be written in every stage.

Read only. Which exist depends on the node and stage:

Variable What it is Available in
@dt Substep length in seconds every particle stage, every field stage
@time Simulation time in seconds every particle stage, every field stage
@frame Current frame number every particle stage, every field stage
@ptnum The particle’s index every particle stage
@numpoints How many particles the pass runs over Particle Substep, Particle Post-Step
@emit_start First newborn particle’s index Particle Emission
@emit_end One past the last newborn particle’s index Particle Emission
@voxel The voxel’s integer index (Int3) every field stage
Call What it does
chf('name'[, default]) Float slider on the node
chv('name'[, x, y, z]) Vector slider (one value fills x, y, z)
chc('name'[, r, g, b[, a]]) Colour picker
Call What it does
set(x, y, z[, w]) Build a vector
clamp(x, lo, hi) Limit x to lo..hi
smoothstep(e0, e1, x) Smooth 0..1 ramp
mix(a, b, t) Blend a to b
abs(x) Absolute value
sqrt(x) Square root
pow(x, y) x to the power y
exp(x) e to the power x
log(x) Natural logarithm
sin(x) Sine
cos(x) Cosine
tan(x) Tangent
atan2(y, x) Angle of (x, y)
floor(x) Round down
ceil(x) Round up
fract(x) Fractional part
sign(x) -1, 0 or 1
step(edge, x) 0 below edge, 1 above
length(v) Length of a vector
normalize(v) Unit vector (v must not be zero)
dot(a, b) Dot product
cross(a, b) Cross product
distance(a, b) Distance between two points
min(a, b) Smaller of two
max(a, b) Larger of two
float(x) Convert to a float
int(x) Convert to an integer
Call What it does
fit01(x, a, b) Map x from 0..1 to a..b
fit(x, a1, a2, b1, b2) Map x from a1..a2 to b1..b2
remap(x, a1, a2, b1, b2) Same as fit
noise3(p) 3D gradient noise, -1..1
noised3(p) Noise and its slope: (value, dx, dy, dz)
curlnoise3(p) Divergence-free 3D swirl vector
fbm3(p[, octaves, lacunarity, gain]) Fractal noise (defaults 4, 2.0, 0.5; octaves 1..8)
voronoi3(p) Cellular noise, (F1, F2) distances
slerp(a, b, t) Spherical interpolation of two vectors
rand(seed) Random 0..1 from a number, e.g. rand(@ptnum)

Vector methods: .cast(), .cross(), .dot(), .max(), .min(), .norm(), .norm_sqr(), .normalized(), .sum(). Names under ti. and tm. (Taichi’s math) are also available:

  • ti.: Vector, abs, acos, asin, atan2, cast, ceil, cos, exp, f32, floor, i32, log, max, min, pow, sin, sqrt, tan, u32
  • ti.math. and tm.: acos, asin, atan2, ceil, clamp, cos, cross, degrees, distance, dot, e, exp, floor, fract, length, log, max, min, mix, mod, normalize, pi, pow, radians, reflect, round, sign, sin, smoothstep, sqrt, step, tan, vec2, vec3, vec4
  • Run order. The wrangles of a stage run in order: first those wired to the solver (through the emitter and detail inputs, or downstream of Simulation State), then the others left to right by node position; a node’s Run Order number overrides that (lowest first, equal numbers keep the order above). A wrangle that is not connected to anything still runs, on the whole simulation: mute it to switch it off.
  • Dead particles are skipped by every Substep and Post-Step wrangle. Write @active = 0 to kill a particle: wrangles after yours in the same stage skip it. The Emission stage runs on newborn particles, which are not alive yet.
  • Fusing. The Particle Wrangles of one stage run as one GPU kernel: the particle is read once, each snippet runs on the values the one before left, and what changed is written once. The result is the same as running them one by one (the colour is rounded the way the solver stores it between snippets); it is faster, most for small and medium scenes. Each snippet keeps its own variables and sliders, so two nodes can both use chf('strength'). Set BLUX_WRANGLE_FUSE=0 in the worker’s environment to turn it off.
  • Live editing. While a simulation plays or steps, a changed slider reaches the worker with the next step, and changed code is swapped in without resetting the simulation (the GPU kernel is rebuilt once, a fraction of a second to a few seconds with heavy noise). A wrangle the worker cannot build is left out and reported on the node; the others and the simulation carry on.
  • Randomness and noise use an integer hash, so they do not depend on the GPU or the backend: the same point gives the same value (to about one millionth). rand(@ptnum) is a stable random number per particle; add an offset (rand(@ptnum + 1000)) for a second independent one.
  • Safety. The code is checked against an allowlist before it reaches the worker, and the worker checks it again: only the constructs, functions, attributes and ti. names listed here exist, so a wrangle can do arithmetic on the attributes above and nothing else.

A preset is applied from the node’s Presets… menu and starts with working slider values. Presets that need an attribute some engine lacks are greyed out on it.

Preset What it does Stage Engines Sliders (start)
Tornado Cyclone Cyclone round a vertical axis (Z is up): swirl round it, pull toward it, lift along Z Substep Atom, Hydro, Impact center (0.5, 0.5, 0), spin_speed 15, lift_speed 15, cone_spread 0.5, suction 2
Radial Shockwave Spherical explosive shockwave from an epicentre Substep Atom, Hydro, Impact radius 0.3, center (0.5, 0.5, 0.5), force 60
Viscous Dissolve & Drag Velocity drag dampening and thermal dissipation Substep Atom, Impact drag 1, cooling 0.5
Ground Sticky Layer High friction sticky layer near the ground (Z is up) Substep Atom, Hydro, Impact boundary_height 0.1, friction 10
Spark Heat Glow Kinetic friction heating with speed-threshold glow Substep Atom, Impact speed_thresh 1, heat_gain 2, cool_rate 1, glow_mult 1
Vortex Swirl (Curl Noise) Agitate velocity with divergence-free curl noise (an acceleration, so it does not depend on the substeps) Substep Atom, Hydro, Impact freq 4, strength 10
Turbulent Air Drag Air drag buffeted by fractal noise Substep Atom, Hydro, Impact noise_freq 3, drag_coeff 0.5, buffet 5
Structural Cohesion Decay Progressive structural damage and bond decay Substep Atom, Impact decay_rate 0.5
Persistent User Scratch (@f0..@f3) Persistent user channels: @f0 = timer clock, @v0 = anchor position Substep Atom, Hydro, Impact freq 4, strength 10
Velocity Clamp Limit maximum particle speed Substep Atom, Hydro, Impact max_speed 5
Preset What it does Stage Engines Sliders (start)
Buoyancy Boost Boost temperature inside the active flame front Substep Blaze boost 0.5
Smoke Fade & Dissipation Procedural dissipation Substep Blaze fade_rate 0.2
Velocity Agitation (Swirl) Procedural swirl via curl noise, as an acceleration where there is smoke (Forces stage) Forces Blaze freq 6, speed 0.5, strength 4
Updraft Column (Velocity) Push the gas up (Z) inside a column round a vertical axis (Forces stage) Forces Blaze center (0.5, 0.5, 0), radius 0.15, lift 3
Newton Thermal Cooling Newton cooling law for the Eulerian temperature field (excess over ambient) Substep Blaze cooling 0.5