Skip to content

Particle FX Demo

App: apps/particle_fx_demo/

Demonstrates the two new non-physics sprite render modes on ParticleEmitter (engine main, ADR 0102) — the render path that backs water splashes and ripples. Both modes reuse the existing sprite pipeline, so there is no Vulkan renderer change; a sprite particle is a pinned kinematic decal (no Jolt body) that stays where it spawns and animates only via a scale-over-life and an alpha-over-life curve.

  • SpriteGround (mode 1) — a soft ring texture laid flat on the world XZ plane (a new BillboardMode::Ground). Bursting one ring per beat at a fixed site gives concentric, expanding, fading rings — exactly a raindrop, footfall, or floating object hitting water.
  • SpriteBillboard (mode 2) — the same ring, camera-facing, as a twinkling sparkle above the surface.

The default mode, PhysicsMesh (mode 0), is unchanged: a Jolt body plus an instanced mesh (splash spray still uses this mode). See Physics Particles Debris for the physics-body path.

Terminal window
% ./plume3d particle_fx_demo

A dark, water-like ground plane sits at y = 0; the pale rings read against it. The camera is raised and pulled back looking down at the surface, so the flat ground rings read as ellipses and the camera-facing sparkles clearly stand up off the plane. It self-screenshots (particle_fx.png) at a fixed frame, then exits.

  • Draws a dark ground plane immediate-mode, then calls _scene.draw() — which submits the particle sprites through the sprite pipeline.
  • Creates a SpriteGround ripple emitter and a SpriteBillboard sparkle emitter via Scene.createParticleEmitter.
  • Loads a soft ring texture (textures/ring.png) with Texture.load and gives it to both emitters via setTexture.
  • Each beat, moves the ripple emitter to the next of three fixed sites and burst(1)s a single ring, so concentric rings accumulate at each site.
  • The sparkle emitter runs continuously at its emission rate.

The ripple emitter is pinned flat on the surface; it grows from a point to a wide ring while fading out. The sparkle emitter faces the camera and shrinks to a warm twinkle:

// Surface-pinned ripple rings (SpriteGround) — flat on the water, expand + fade.
// Args: name, maxBodies, size, mass, lifetime, emissionRate, x, y, z
_ripples = _scene.createParticleEmitter("ripples", 64, 0.1, 0.0, 1.8, 0.0, 0.0, 0.02, 0.0)
_ripples.setRenderMode(1) // SpriteGround
_ripples.setTexture(_ring) // a soft ring texture
_ripples.setScaleCurve(0.25, 3.2) // half-size: grows from a point to a wide ring
_ripples.setAlphaCurve(0.9, 0.0) // fades out as it grows
_ripples.setColor(0.72, 0.90, 1.0) // pale cyan tint
_ripples.play()
// Camera-facing sparkle (SpriteBillboard) — warm specks twinkling above the surface.
_sparkle = _scene.createParticleEmitter("sparkle", 96, 0.05, 0.0, 0.8, 30.0, 0.0, 0.35, 0.0)
_sparkle.setRenderMode(2) // SpriteBillboard
_sparkle.setTexture(_ring)
_sparkle.setScaleCurve(0.22, 0.03) // shrinks to a twinkle
_sparkle.setAlphaCurve(1.0, 0.0)
_sparkle.setColor(1.0, 0.95, 0.72) // warm white
_sparkle.play()

A sprite particle is pinned where it spawns, so moving the emitter and bursting a single particle each beat drops a fresh ring at that spot:

// In update(dt), on each beat:
var s = sites[_site % sites.count] // three fixed (x, z) ripple sites
_site = _site + 1
_ripples.setPosition(s[0], 0.02, s[1])
_ripples.burst(1)

The ground is drawn immediate-mode; Scene.draw() submits the emitter quads through the sprite pipeline:

Graphics.drawMesh(_ground, _shader, model) // immediate-mode ground
_scene.draw() // submits the particle sprites
  • ParticleEmitter — sprite render modes — setRenderMode, setScaleCurve, setAlphaCurve, setColor, setTexture (plus createParticleEmitter, play, setPosition, burst).
  • Texture — Texture.load(path) for the soft ring sprite.
  • Scene — createParticleEmitter, draw.
  • Graphics — immediate-mode drawMesh, setLights.