10
Particles
meleki edited this page 2026-10-03 14:51:44 +02:00

A particle system is an asset: a node graph that says what one particle is — where it sits, how big it is, how it is turned, what colour it has — as a function of its age and of a random number drawn once for it. Unlike a texture or a shader it can be placed: an object made from it is a source, and its rows say how many particles go out and for how long they live.

Nothing is simulated. A particle is not pushed by forces frame after frame; it is a formula evaluated at whatever second is being drawn. Three things follow from that, and they are the reason the model was chosen:

  • The same second always looks the same. Two runs at the same frame rate, or a different one, draw the same picture — the field is a function of the clock, not of how many frames have gone by.
  • Any second can be shown at once. The preview panel in the tab has a slider: drag it to 2 s and the particles stand where they are at 2 s, exactly, with nothing simulated on the way there.
  • Collision is out, for good. "Sparks bounce off the ground", wind, drag mid-flight — none of these can be written as a function of age, and no later addition will change that.

The Particles tab

+ in the Assets panel makes a particle asset and opens the Particles tab on it (a double-click on an existing one does the same; a click only selects). The left part is the same canvas the Behavior and Shader tabs have: right-click places a node, drag moves, drag from a port links, the magnet snaps to the 24 px grid, Fit frames the graph, undo takes every step back. The readout on the right says N parameters while the graph compiles, otherwise why it does not (No Output node, a bad parameter name, Feeds itself). A node that keeps the graph from compiling shows the reason on its body and a red frame; a source whose graph does not compile emits nothing.

Right of it, past a divider that drags, the preview panel emits the asset's own particles from its middle, wearing the rows the asset itself carries — edit Rate or Lifetime on the asset and the panel changes with them, including how far the slider reaches (three lifetimes, which a Time scale of 2 covers in half the seconds). Rows the asset leaves alone fall back to the type's defaults: 20 a second, one second each, a plain 16 px square. It stands beside the graph rather than under it because a graph is read across: width is what it can spare, height is not. Hold stops the clock where it is and turns into Run; the slider jumps to any second in the first three lifetimes — and holds the panel too, so the button reads Run after a drag as well; Restart begins the stream again at particle 0, which a held panel shows as 0.00 s rather than keeping the second it stood on. A hold owes nothing when it ends: letting the panel run again carries on from the second it was stopped at, however long it stood there — the field's origin is moved, rather than the library clock being allowed to run on underneath the freeze.

The nodes

Every value is a number. A colour is a number too — packed 0xRRGGBB, as everywhere else in the project — which is why blending two of them needs Mix color rather than Mix: mixing the packed numbers of red and blue would run through green.

Node Ports What it does
Age out age How far through its life the particle is: 0 at birth, 1 when it dies. The axis most graphs are drawn along.
Age in seconds out seconds The same in seconds — the t of a formula of motion.
Time out seconds Seconds since the source started emitting. The same for every particle alive, so it drifts them together rather than apart.
Index out index Which particle this is, counting up from the first the source emitted: a fan or a ring instead of a scatter.
Random in min, max · out value A number drawn once per particle and kept for its whole life. Two Random nodes draw differently, so the direction and the speed of one particle scatter apart instead of being the same number.
Gravity in strength · out x, y How far a steady pull has moved the particle by now: strength * age * age / 2, down positive. Add it to the offset.
Start velocity in angle, speed · out x, y How far a constant speed has carried it: angle in degrees (0 right, 90 down), speed in pixels per second.
Output in x, y, scaleX, scaleY, rotation, color, alpha What the particle is drawn as, all of it relative to the source. One per graph. An input left unconnected stays what a particle without a graph would be: at the source, full size, upright, the source's own colour, opaque.
Property out value Declares a number the source sets: Name is a row on every object placed from this asset, Default, Min, Max are its range. One word (letters, digits, _), unique in the graph, and not the name of one of the source's own rows.
Number, Color out value / color Constants, typed or picked on the node.
Add, Subtract, Multiply, Divide in a, b · out result Arithmetic. Division by zero reads as 0 — infinity would draw as nothing at all.
Min, Max, Clamp · out result The smaller, the larger, and a value held between two bounds.
Mix in a, b, t · out result Blends two numbers: t = 0 is a, 1 is b, beyond that it carries on.
Mix color in a, b (colour), t · out color The same for colours, channel by channel — a flame fading to smoke.
Sin, Cos in value · out result Of an angle in degrees, one full wave every 360. Degrees, not radians, because rotation and Start velocity are in degrees too.
Sqrt in value · out result Rises fast and then slows — how smoke drifts up. Below 0 it is 0.
Abs, Floor, Fract in value · out result Distance from 0; rounded down; the part after the decimal point (a rising value becomes a sawtooth).
Step in edge, value · out result 0 below the edge, 1 from it on: a hard cut.
Pow in value, exponent · out result To a power: 2 squares, 0.5 is the square root. Below 0 the value counts as 0.

The source

A placed source carries a position and the transform rows every drawn object has (scale, rotation, mirror), so hanging one on a moving ship turns the whole spray with it. Its own rows are:

Row Default What it means
Emitting on Whether new particles go out. Switched off, the ones in flight live out their lives — unlike Enabled, which takes them away at once along with everything parented below.
Mode Continuous Continuous is a steady stream; Burst throws the whole count at once.
Rate 20 Particles per second, for a continuous source.
Burst count 20 How many go out per burst.
Lifetime 1 s How long one particle lives — the scale that gives Age its meaning.
Time scale 1 How fast the source's own clock runs: 2 is twice the speed, so it emits twice as often and a particle is gone in half the time. Every row above is read on that clock, which is why none of them has to be touched to slow a whole effect down. 0 holds the picture where it is. This is this source's scale; the global one (which a pause sets to 0) multiplies on top of it.
Prewarm off Start one lifetime in, which is the second a stream first stands full. Without it a fire lit by a scene fade-in begins as a single spark and takes a lifetime to look like a fire. Nothing to a Burst, whose particles are all gone by that second.
Max particles 500 The most alive at once; past it the oldest are dropped.
Size 16 The particle's size in pixels, which the graph's scaleX/scaleY multiply.
Texture none The picture each particle is drawn with. Without one it is a plain square.
Color white The tint, and what an unconnected color output reads.
Move with source off Off: a particle stays where it was born, so a moving source leaves a trail (rocket smoke). On: it hangs on the source and travels with it (sparks on a carried torch). The difference shows only while the source moves.
Shader none A shader asset worn by every particle of this source, the way a sprite wears one. Its Property rows appear below, like a sprite's.
Field shader none A shader asset laid over the whole field at once (see A shader over the whole field). Its rows appear below as well.
Field width, Field height 256 The frame the field shader covers, in units, centred on the source. Shown only while a field shader is picked.

Below them come the rows the asset's graph declares, one per Property node — so two fountains of the same asset can spray at different strengths, exactly the way two sprites wearing the same shader can. Overriding one of them on a placed object is an ordinary override, with reset and apply.

Wearing a shader. The Shader row puts a shader asset on every particle of the source, and the shader's own rows follow underneath — the same picker a sprite has. It is the cheapest place in the editor to wear one: a shaded sprite gives up its batch and costs a draw call of its own, while a source is already a single draw call for its whole field, so five hundred shaded particles cost exactly what five hundred plain ones did.

What the shader sees is one particle's picture, not its life. The fragment gets the quad's texel, its own UV and the tint the graph computed — but not the particle's age, index or random number: the buffer a field is drawn from carries five fixed attributes and takes no sixth. That is less of a loss than it sounds, because colour, alpha, size and rotation over age are what the particle graph itself is for; the shader adds what that graph cannot do — sampling, distorting UVs, reading the texture's own size.

A source is the one asset drawing rows from two graphs, so a name can arrive twice. The source's own row wins and the uniform of that name keeps its declared default: two rows called Rate would be one row with two meanings. Nothing is rejected on the shader's side — the same shader on a sprite is perfectly fine, it is only this source that already has the name.

A shader over the whole field (#170). The Field shader row lays a shader over everything the source draws at once: the particles are drawn into a picture of a fixed frame, Field width by Field height units centred on the source, and the shader draws that picture. So it can do what a shader per particle cannot: a glow or blur where particles overlap, an outline round the whole cloud, a ripple through all of it. It is an ordinary shader asset; its u/v run from 0 to 1 across the frame and never jump, however the cloud changes, and Texture color, Texture alpha and Sample texture read the picture of the particles (Texture size is Field width by Field height: a pixel of a field shader is a unit of the frame). The frame is the edge: whatever flies past it is cut off, so make it as large as the effect reaches. The scene view draws it as a dashed rectangle round the selected source. It turns and grows with the source.

Both shaders may be worn together: the one per particle draws into the picture, the field shader over it. A name both of them declare is one row, and both read it. The field shader's Padding and Vertex output do nothing here: the frame already is the room around the field, and its corners stay put, as with a nine-sliced sprite.

The picture has as many pixels as the frame covers on screen, so it stays sharp at any zoom, but at most 2048 per side. Still the shader counts in units of the frame, not in those pixels: a Blur of 4 reaches 4 units at 50 % and at 400 %, and a step of 1 / Texture size is one unit. Zoomed far in, the samples of a Blur therefore lie several pixels apart. A source whose frame is off screen draws no picture at all. What it costs is one extra pass per source, sized by the frame, not by the number of particles; see What it costs in Shaders for the measurement.

Measured in the checks (step 31): a field shader that changes nothing draws the field where it was and the right way up, with a picture of 256 pixels at zoom 1; a flat red fills the frame 100 units off the source where no particle is, and stops before 140; a Blur of 8 across a flat picture fades the frame's edge over 8 units at 50 % and at 200 % alike; taken off again, the particles draw as before.

A behavior can start a source over with the Emit node (its source input names which one): in Burst mode that is another burst, in Continuous mode the stream begins again at particle 0.

Its icon in the Scene tab. Like a behavior or a container, a source is marked by an icon of its own — a ring with sparks, white in a dark outline like every symbol, upright at its position (or beside it when another icon wants the same spot), with the object's name beside it and a dashed line to its owner when it is parented. The ring is hollow because, unlike those two, a source does draw something: the effect shows through the middle of its own mark. The icon is editor decoration and never reaches the running game, but the source is not an editor symbol: its particles are scene content, and what is grabbed and framed is the ring-sized square around its position rather than anything that grows with the source's scale.

Recipes

Rain. Age in seconds → Multiply(b: 400) → Output.y, and a Random(-300, 300) into Output.x to spread the drops across the screen. Three nodes.

A spark fountain. Random(0, 360) → Start velocity.angle, Random(100, 300) → .speed, its x/y into two Adds together with a Gravity(800), and Age → Subtract(a: 1) → Output.alpha to fade them. This is where the formula model asks its price: gravity has to be written as the offset it has produced (g * t * t / 2), not as a force — which is what the Gravity node is for.

Smoke. Age → Sqrt → Multiply(b: -60) → Output.y — rising and slowing without a damping constant anyone has to guess. Add Time → Multiply(b: 180) → Sin → Multiply(b: 20) → Output.x for drift, Age → Add(b: 1) → Output.scaleX/scaleY to let it grow, and Mix color from a warm grey to a cold one over Age.

What it cannot do

  • No collision, no wind, no drag. See the top of this page: they are not functions of age.
  • A source is culled by nothing. Its particles may be anywhere on screen, so the source itself is never skipped for being out of frame — with many sources in one scene that costs.
  • Particle time is the global clock (see Architecture), so pausing the game freezes every spray with it, and starting or stopping a run puts them back to their first second.
  • The preview shows the asset, not an instance: the rows the asset carries, never the ones a particular placed source was given — every one of them may be overridden where it is placed.
  • A source with Emitting off draws nothing at all once the last particle has died. In the Scene tab its icon still marks the spot; in the running game there is nothing to see.