Skip to content

Particles

charlotte 🌸 edited this page Jul 22, 2026 · 1 revision

Particles

Libprocessing has a new abstraction for defining particle systems built on top of the compute shader support that WebGPU provides.

CAPACITY = 40000

def setup():
    global p 
    velocity_attr = Attribute("velocity", AttributeFormat.Float3)
    age_attr = Attribute("age", AttributeFormat.Float)

    p = Particles(
        capacity=CAPACITY,
        attributes=[
            Attribute.position(),
            Attribute.color(),
            Attribute.scale(),
            Attribute.life(),
            velocity_attr,
            age_attr,
        ],
    )

Conceptually, a Particles object is similar to a Graphics object but operates on buffers rather than textures. Every Graphics context wraps a physical GPU texture, typically the swapchain for a given surface but also offscreen render targets. A render target or Surface has a type, which is the pixel format of the texture itself. While this is typically a 4 component format (e.g. rgba32float for a 32-bit RGBA texture) that represents a color, it can also be a data storage format where each component represents some non-color data type: Similarly, Particles are a collection of attributes that each have a type: vec4<f32>, u32, etc.

one pixel in an rgba32float texture       one particle across attribute buffers

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”         position buffer   β”Œβ”€ vec3<f32> ─┐
   β”‚   r   β”‚   g   β”‚   b   β”‚   a   β”‚         color buffer      β”œβ”€ vec4<f32> ──
   β”‚  f32  β”‚  f32  β”‚  f32  β”‚  f32  β”‚         velocity buffer   β”œβ”€ vec3<f32> ──
   β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜         age buffer        └─ f32 β”€β”€β”€β”€β”€β”€β”€β”˜
      components live together                  attributes live in separate
      in one texel (interleaved)                buffers (structure-of-arrays)
                                               

In Processing, Graphics have filters which are predefined, parameterized full-screen render passes. In other words, while the user could write a custom render pass targeting that Graphics texture, filters are an easy and quick way to accomplish common tasks that need to write to every pixel in a texture/surface without having to write any shader code:

g.filter(Graphics.threshold(), level=0.5)

For Particles, we have the same concept, which is applying a compute kernel to the set of attribute buffers:

p.apply(Particles.noise(), scale=1.0, strength=0.6)

Particles and Geometry

You might notice that attributes are similar to the vertex attributes that a Geometry (mesh) instance defines. This is intentional. Geometry and Particles are meant to be isomorphic in terms of their definition or initialization. That is, it's possible to use a given mesh to seed the initial positions of a particle system in a convenient manner. For example, rather than having to write a compute shader that defines the positions of particles in a sphere when initializing a particle system, you can simple write:

def setup():
    source = Geometry.sphere(5.0, 32, 24)
    p = Particles(
        geometry=source,
        attributes=[Attribute.position(), Attribute.uv(), Attribute.color()],
    )

This will intialize the particle buffers with the values of the vertex buffers of the sphere mesh. Any attributes that are not in the Geometry instance will be initialized to a default value.

Rasterization

Simply having some buffers with some attributes isn't very useful on its own. Typically, you want to rasterize a particle system so that it is visible. We provide a new method on Graphics named particles that accepts a Particles object as well as a Geometry object which we be instanced drawn over each particle in the system:

def draw():
    background(15, 15, 20)

    mat = Material.pbr()
    use_material(mat)
    particles(p, particle)

The particles method will respect the current material, transform, etc. If instead the user simply wants to bind some attribute buffer to their own render pass, they can do so:

def draw():
    shader = Shader("...")
    mat = Material(shader, color=p.buffer(Attribute.color())
    use_material(mat)
    box(80.0, 80.0, 80.0)

Clone this wiki locally