-
Notifications
You must be signed in to change notification settings - Fork 12
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)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.
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)