Open Asterune
Particle simulation and ribbons

Particle simulation and ribbons

Examine CPU and GPU particle lifetimes, ring allocation, collision sampling, translucent composition, and beam or trail geometry.

Asterune simulates particle state separately from the geometry that displays it. The CPU path uploads billboard instances. The GPU path retains particles in a storage buffer, advances them with compute, and expands each live slot into a quad during drawing. Beams and trails use a separate ribbon representation. All three paths enter the scene's translucent composition.

These are visual simulations. GPU particle collision samples depth or a bounded set of simplified world colliders; it does not create a Rapier body for each particle. The selected backend also changes which particle operations execute.

Backend selection and emission state

The renderer selects GPU particles when the quality profile enables a nonzero particle pool and the device permits at least 7 storage buffers per shader stage. Otherwise, emission uses CPU particle records. A changed pool capacity replaces the GPU particle system and its retained storage.

CPU-generated effects can coexist with GPU emitters. Force-field presets, for example, still produce CPU particle records while the GPU path is active.

Emitter rate uses a fractional carry. Each update adds rate * dt, emits the integer portion, and retains the remainder. Bursts add explicit births rather than requiring an artificially large rate for one frame. A stable emitter key combines the source instance with its preset part, so a multi-part preset can retain independent streams across updates.

Removing an ordinary world-space GPU emitter does not immediately remove its live particles. An emitter book retains its record without further births for its maximum lifetime. Locked local-space emitters are not retained after removal. Clearing an emitter explicitly invalidates its slots.

The CPU path advances position with velocity and half the acceleration term, then updates velocity, exponential damping, size, rotation, and age. Expired particles leave the vector. If its configured particle limit is exceeded, the oldest records are removed. GPU-only operators, attraction, and collision responses execute in compute rather than this CPU update loop.

Persistent GPU rings and capacity pressure

The GPU pool is divided into persistent contiguous rings, one per allocated emitter. Requested capacity estimates rate times maximum lifetime with 25% headroom, then adds burst demand and 16 slots. Sub-emitter relationships can increase demand. Each emitter's requested demand is bounded at 65,536 slots.

Allocation rounds demand up to a power of 2, with a minimum ring size of 16. An existing ring remains in place when it is large enough. When a larger ring is required, the allocator searches free intervals. If the desired size does not fit, it repeatedly halves the request down to the minimum. An emitter that cannot obtain even that ring receives no allocation.

This allocation preserves slot addresses for steady emitters. Births advance a ring head and replace the corresponding slots modulo ring capacity. Birth count is capped by the allocated ring size, so a constrained ring can replace live particles rather than growing the pool without bound. New or replaced rings receive clear state before spawning.

At most 1,024 emitter records enter the GPU update. The compute dispatch spans the highest occupied pool address, including gaps between allocated rings. Each invocation identifies the owning emitter, rejects slots outside its ring, and either spawns, advances, or ignores an inactive particle. The implementation does not compact live particles into a new list every frame.

Compute simulation and programmable operators

GPU simulation runs after scene depth preparation and before opaque shading. It advances once per frame; a second stereo view draws the existing particle state instead of advancing its age again.

Each particle retains position and age, velocity and lifetime, previous world position, shape parameters, lighting, and fog data. Emitter records retain both current and previous transforms. A particle locked to emitter space can therefore follow the emitter while still producing motion relative to the preceding frame.

Lifetime curves are baked into 32 samples for color, opacity, and size. The shader interpolates between neighboring samples using normalized age. A per-particle trajectory selects variations from the baked ranges without requiring a separate curve upload for every particle.

After spawning, subsequent updates integrate acceleration, velocity, damping, and spin. Attraction evaluates up to 32 retained attractors with shape bounds, falloff, and directionality. If more attractors are submitted, the nearest ones to the camera are retained. Local-space particles transform acceleration into their emitter frame; world attraction and collision operate on unlocked particles.

A particle operator is a validated WGSL function incorporated into the shared simulation shader. Accepted sources receive indices, and emitter records select an operator by index. The generated dispatcher renames accepted declarations and routes the particle state to the selected function. A changed operator set rebuilds the simulation and sub-spawn pipelines. Operators run after ordinary integration and before collision response, so their output position, velocity, and age feed those later stages.

Depth collision and simplified world collision

Depth collision projects the particle into the main view and reads scene depth. It reconstructs the nearby surface position and estimates a normal from adjacent depth samples, choosing differences that cross the smaller discontinuity. A particle-radius and travel allowance decide whether that surface is close enough to produce contact.

This path uses visible depth produced earlier in the frame, before particle simulation. Offscreen points and empty depth pixels supply no depth contact. A surface hidden behind another opaque surface also has no independent depth representation for that test.

World collision adds up to 64 uploaded colliders. Balls use a spherical distance test; other selected parts use oriented boxes. The shader finds the deepest penetration among those colliders and compares it with the depth contact. This extends collision beyond visible depth without reproducing every mesh triangle or the full physical world inside the particle shader.

Collision radius is at least 0.05 studs and otherwise follows one quarter of current particle size. A contact can end the particle's life, stop it in place, or change velocity through friction and bounce. The response removes inward normal motion, damps tangential velocity, and applies bounce only above its velocity threshold. A stuck particle retains its stopped state rather than receiving ordinary motion integration on following frames.

Sub-emitters and bounded event delivery

Particle death or collision can enqueue a child-emission record on the GPU. Each record stores an event position, source velocity, child-emitter index, and birth count. An atomic counter assigns records in a queue capped at 16,384 entries. Events beyond that capacity do not produce an additional queued record.

A second compute pass consumes those records after ordinary simulation. Atomic child heads allocate slots in the child's existing ring, wrapping modulo its capacity. Each birth uses the event position and an optional inherited share of the source velocity. The child's ordinary simulation runs on later frames; sub-spawning does not recursively dispatch another simulation pass immediately.

Surface emission also uses bounded shared data. Up to 64 emitter records can reference surface-point sets, each retaining at most 256 sampled positions and normals. Unchanged shared point sets avoid rewriting the same sample data. The renderer uses those samples to select birth positions without repeatedly reading a part's full mesh in the simulation pass.

Billboard drawing and transparency order

CPU particles become camera-facing quad instances after lifetime sampling and frustum rejection. Order-dependent alpha particles sort by depth. Lists smaller than 256 records use comparison sorting; larger lists use 4 radix passes over the depth key. Adjacent sorted particles with the same texture and blend mode form draw runs, preserving depth order across texture changes.

GPU drawing instead submits each emitter's complete ring as an instanced quad range. Dead slots and particles with no visible size or opacity collapse outside the clip volume in the vertex shader. Emitter draws sort back to front by emitter position. Individual particles inside a ring do not receive that CPU per-particle sort.

GPU billboard sampling interpolates between adjacent flipbook cells rather than requiring a separate mesh for each animation frame. The fragment shader also softens intersections with opaque depth. It compares scene distance with particle distance and scales coverage by the gap divided by a softness reach. This changes the edge's opacity while retaining ordinary scene depth testing.

When order-independent transparency (OIT) is active, the alpha contribution enters weighted accumulation and revealage targets. The emissive share draws additively. Outside OIT, the particle shader combines those shares in its blend path. Dense intersecting effects therefore depend on the composition mode and backend's ordering, in addition to how many particles are alive. The scene's transparency chapter describes the weighted composite.

Motion, focus, and volumetric emission

The GPU particle stores its preceding world position before advancing. Billboard vertices use current and previous camera matrices to produce motion data. A separate motion pass blends particle displacement over sky motion when scene TAA is active. The multisampled path omits this temporal motion attachment.

Particles also have an optional distance pass for depth of field. It writes a minimum-blended view distance only when coverage exceeds 0.35. Faint wisps therefore leave focus on the background instead of claiming the entire pixel's subject distance. Ordinary particle drawing still does not write solid scene depth.

Lit particles evaluate light in compute and retain the result with atmospheric scattering and transmittance. Volumetric emission accumulates radiance into a 40 × 23 × 16 grid using GPU atomics. Fog reads that retained field in its own pass; particle simulation prepares the next field. This couples particle light to volumetric integration without submitting a scene light per particle.

Beams and trail histories

Beams sample a cubic Bézier curve between their attachment positions and control directions. The requested segment count determines points along that curve. The renderer accumulates traveled distance for texture coordinates, derives a local tangent and side direction, and samples width and color along the beam.

Trails retain timestamped pairs of attachment positions. History records are added only after the attachment midpoint travels the minimum distance, clamped to at least 0.01 studs. A temporary current head sample still updates the visible endpoint before that distance is reached. Samples expire by lifetime, and the output trims its oldest extent to the configured length.

Both become ribbon points with a center, half-width, side direction, texture coordinate, and color. A ribbon retains at most 1,024 points. The vertex shader expands each point into the two sides of a triangle strip, either using the provided side or facing the camera. Ribbons sort by their middle point's camera distance and use alpha/OIT and additive contributions analogous to particles. Their geometry remains a connected strip rather than independent overlapping billboards.