Particles
A pool of short-lived camera-facing sprites. Sparks, smoke, dust, embers, muzzle flashes, magic. Everything needed for these already existed -- billboards face the camera, a PropBatch draws thousands in one call, additive blending needs no sorting -- and every program still had to write the same three things by hand: a ring of slots, a life per slot, and somewhere to park the dead ones. The FPS demo that ships with Objeck wrote exactly that, which is what this replaces. ## Fixed capacity, on purpose The pool never grows. Emitting into a full one overwrites the OLDEST particle rather than allocating, so a frame costs no allocation however hard it is emitting -- which is the property that matters in a draw loop, and the reason a growable pool would be the wrong shape even though it would look more accommodating. Dead particles are parked far below the world rather than hidden, because a batch draws every instance in its buffer: there is no per-instance visibility to switch off, so "invisible" has to mean "nowhere you are looking". ## What varies per particle, and what does not Position, velocity, age and size are per particle. COLOUR is not: a batch has one material, so a system is one colour, set by SetTexture or SetMaterial. Two colours are two systems. Per-particle colour would mean a second instance attribute stream, which PropBatch does not carry. Additive by default, which is right for anything that emits light and wrong for smoke -- SetAdditive(false) for that, and then the sprite's own alpha does the work.
Example
sparks := Particles->New(64);
sparks->SetCamera(camera);
sparks->SetTexture(glow);
scene->AddBatch(sparks->GetBatch());
...
sparks->Update(window->GetDelta());
sparks->Burst(x, y, z, 12, 2.5);Operations
- New
- Burst
- Clear
- Draw
- Emit
- Free
- GetAlive
- GetBatch
- GetCapacity
- GetError
- IsOk
- NextSigned
- PARKED
- SetAdditive
- SetCamera
- SetDrag
- SetGravity
- SetLifetime
- SetMaterial
- SetSeed
- SetSize
- SetTexture
- Update
Burst #
Emit several at once, sprayed outwards. The usual shape for an impact: a handful of particles leaving one point in scattered directions.
method : public : Burst(x:Float, y:Float, z:Float, count:Int, speed:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| x | Float | where |
| y | Float | where |
| z | Float | where |
| count | Int | how many |
| speed | Float | how fast they leave, in world units per second |
Draw #
Draw the system on its own, for one that is not part of a Scene. A system inside a Scene should NOT be drawn this way as well -- hand its batch to Scene->AddBatch and let the scene do it, so the additive state is set up around the draw.
method : public : Draw(view_projection:Float[]) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| view_projection | Float | the camera matrix |
Emit #
Emit one particle, motionless.
method : public : Emit(x:Float, y:Float, z:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| x | Float | where |
| y | Float | where |
| z | Float | where |
Emit #
Emit one particle with a velocity. Overwrites the oldest slot when the pool is full, rather than growing or refusing. A system emitting faster than its capacity allows shows shorter trails, which is a visible and recoverable symptom -- unlike an allocation in a draw loop.
method : public : Emit(x:Float, y:Float, z:Float, vx:Float, vy:Float, vz:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| x | Float | where |
| y | Float | where |
| z | Float | where |
| vx | Float | world units per second |
| vy | Float | world units per second |
| vz | Float | world units per second |
Free #
Release the batch, and the mesh and shader if this system built them. Idempotent. A mesh or shader handed in by the three-argument constructor belongs to the caller and is left alone.
method : public : Free() ~ NilGetAlive #
method : public : GetAlive() ~ IntReturn
| Type | Description |
|---|---|
| Int | how many particles are currently alive |
GetBatch #
The batch this system draws through. Hand it to Scene->AddBatch and the scene draws it in the right place, with the additive state handled. Or call Draw directly for a system that is not part of a scene.
method : public : GetBatch() ~ PropBatchReturn
| Type | Description |
|---|---|
| PropBatch | the batch; do NOT free it, the system owns it |
GetCapacity #
method : public : GetCapacity() ~ IntReturn
| Type | Description |
|---|---|
| Int | how many can be alive at once |
GetError #
What went wrong, if anything.
method : public : GetError() ~ StringReturn
| Type | Description |
|---|---|
| String | the batch's error, or an empty string |
IsOk #
Is the system usable -- a live mesh, shader and batch.
method : public : IsOk() ~ BoolReturn
| Type | Description |
|---|---|
| Bool | true when it can draw |
New # constructor
A system with its own quad and billboard shader, both owned.
New(capacity:Int)Parameters
| Name | Type | Description |
|---|---|---|
| capacity | Int | how many particles can be alive at once |
New # constructor
A system drawing geometry you supply. The shader must be an instanced one that reads a "view" uniform, which in practice means Shader->BillboardInstanced or something shaped like it. Neither the mesh nor the shader is owned, so Free leaves both alone.
New(capacity:Int, mesh:Mesh, shader:Shader)Parameters
| Name | Type | Description |
|---|---|---|
| capacity | Int | how many particles can be alive at once |
| mesh | Mesh | the sprite geometry, usually Mesh->Quad |
| shader | Shader | an instanced billboard program |
NextSigned #
A signed random in -1..1, from a generator of this system's own.
method : private : NextSigned() ~ FloatPARKED # function
Where dead particles wait. Far enough below any plausible world that a camera would have to be looking for them.
function : PARKED() ~ FloatSetAdditive #
Additive blending, on by default. Right for anything that emits light. Turn it off for smoke or dust, where the sprite should obscure what is behind it rather than brighten it -- and note that non-additive particles are NOT sorted against each other, so overlapping soft-alpha sprites will blend in instance order.
method : public : SetAdditive(additive:Bool) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| additive | Bool | false for ordinary blending |
SetCamera #
The camera a billboard shader needs. Required -- without it the sprites collapse onto the origin's basis.
method : public : SetCamera(camera:Camera) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| camera | Camera | the eye |
SetDrag #
How quickly a particle loses speed, as a fraction per second. 0 keeps its velocity forever; 1.0 brings it to a near stop within a second. What makes a spray settle rather than fly off.
method : public : SetDrag(drag:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| drag | Float | 0.0 to about 4.0 |
SetGravity #
Downward acceleration, in world units per second squared. 0 by default, which suits sparks and magic. Around -9.8 for anything that should fall; a small positive value makes smoke rise.
method : public : SetGravity(gravity:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| gravity | Float | acceleration on the y axis |
SetLifetime #
How long a particle lives, in seconds.
method : public : SetLifetime(seconds:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| seconds | Float | must be above zero |
SetMaterial #
The material, for a tint or an opacity as well as a texture.
method : public : SetMaterial(material:Material) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| material | Material | how every particle is drawn -- one for the whole system |
SetSeed #
Make the spray reproducible. Bursts use a generator of their own rather than the system's, so the same seed gives the same spray on every machine and every run -- which is worth more in a demo people compare than novelty is.
method : public : SetSeed(seed:Int) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| seed | Int | any non-zero value |
SetSize #
The size a particle starts and ends at, in world units. Shrinking to zero is how a particle disappears here, because the alpha is the same for every instance -- see the note on the class.
method : public : SetSize(start:Float, finish:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| start | Float | size when emitted |
| finish | Float | size at the end of its life |
SetTexture #
The sprite texture, untinted.
method : public : SetTexture(texture:Texture2D) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| texture | Texture2D | what each particle draws |
Update #
Move, age and retire every particle. Call once per frame with the frame time. Nothing is allocated: the pool, the velocities and the lives are all fixed arrays, and the batch repacks into a buffer it already owns.
method : public : Update(delta:Float) ~ NilParameters
| Name | Type | Description |
|---|---|---|
| delta | Float | seconds since the last frame, e.g. GLWindow->GetDelta |