GPU Particles 2D

A GPU-driven particle fountain with gravity and colour fade.

▶ Run in browser

Tags: 2d particles gpu effects

What it demonstrates

  • GPUParticles2D: particle state lives on the GPU, simulated by a compute shader

  • Emission tuning: amount, lifetime, speed, speed_variance, spread, direction

  • gravity pulling particles back down to form a fountain arc

  • start_colour / end_colour fade over each particle’s lifetime

  • Toggling emitting on/off and switching emission_shape at runtime

In 2D the Y axis points down, so direction (0, -1, 0) emits upward and a positive-Y gravity pulls particles back down: a classic fountain. speed and gravity are in screen pixels (per second), so the column rises about speed^2 / (2*gravity) pixels before arcing over.

Source

  1"""GPU Particles 2D: A GPU-driven particle fountain with gravity and colour fade.
  2
  3# /// simvx
  4# tags = ["particles", "gpu", "effects"]
  5# web = { root = "ParticlesScene", width = 800, height = 600, responsive = true }
  6# ///
  7
  8## What it demonstrates
  9  - GPUParticles2D: particle state lives on the GPU, simulated by a compute shader
 10  - Emission tuning: amount, lifetime, speed, speed_variance, spread, direction
 11  - gravity pulling particles back down to form a fountain arc
 12  - start_colour / end_colour fade over each particle's lifetime
 13  - Toggling emitting on/off and switching emission_shape at runtime
 14
 15In 2D the Y axis points down, so direction (0, -1, 0) emits upward and a
 16positive-Y gravity pulls particles back down: a classic fountain. speed and
 17gravity are in screen pixels (per second), so the column rises about
 18speed^2 / (2*gravity) pixels before arcing over.
 19"""
 20
 21from simvx.core import GPUParticles2D, Input, InputMap, Key, Node2D, Text2D, Vec2
 22from simvx.graphics import App
 23
 24WIDTH, HEIGHT = 800, 600
 25
 26
 27class ParticlesScene(Node2D):
 28    """A fountain emitter plus a steady spark burst, both GPU-simulated."""
 29
 30    def on_ready(self):
 31        InputMap.add_action("toggle", [Key.SPACE])
 32        InputMap.add_action("shape", [Key.S])
 33        InputMap.add_action("quit", [Key.ESCAPE])
 34
 35        # Golden fountain: a fast jet from the bottom centre. speed/gravity are in
 36        # screen pixels/sec -- the jet launches at 520 px/s and a matched gravity
 37        # arcs it back, giving a column ~speed^2 / (2*gravity) ~ 250px tall. The
 38        # narrow spread keeps a visible stem that fans near the apex; grains fade
 39        # golden -> ember red over their lifetime.
 40        self.fountain = self.add_child(
 41            GPUParticles2D(
 42                amount=1400,
 43                lifetime=2.2,  # covers the full up-and-over arc
 44                speed=540.0,  # px/sec (soft range -- not capped at 200)
 45                speed_variance=90.0,  # varied launch speeds spread the droplets
 46                spread=0.28,  # cone width of the jet
 47                direction=(0.0, -1.0, 0.0),  # upward (Y is down in 2D)
 48                gravity=(0.0, 560.0, 0.0),  # px/sec^2, arcs the stream back down
 49                start_colour=(1.0, 0.85, 0.35, 1.0),
 50                end_colour=(1.0, 0.4, 0.1, 0.0),
 51                start_scale=5.0,  # distinct droplets that trace the arc
 52                end_scale=1.5,
 53                randomness=0.35,  # varied lifetimes keep the jet continuous
 54                position=Vec2(WIDTH * 0.5, HEIGHT * 0.86),
 55                name="Fountain",
 56            )
 57        )
 58
 59        # Cyan sparks: omnidirectional glitter burst from a small sphere, hanging
 60        # above the jet. Wide spread + high speed variance scatters it like a puff.
 61        self.sparks = self.add_child(
 62            GPUParticles2D(
 63                amount=1200,
 64                lifetime=1.1,
 65                speed=130.0,
 66                speed_variance=70.0,
 67                spread=3.14,  # wide spread for an all-directions burst
 68                direction=(0.0, -1.0, 0.0),
 69                gravity=(0.0, 180.0, 0.0),
 70                emission_shape="sphere",
 71                emission_radius=10.0,
 72                start_colour=(0.4, 0.9, 1.0, 1.0),
 73                end_colour=(0.1, 0.3, 0.7, 0.0),
 74                start_scale=2.5,
 75                end_scale=0.0,
 76                position=Vec2(WIDTH * 0.5, HEIGHT * 0.42),
 77                name="Sparks",
 78            )
 79        )
 80
 81        self.add_child(Text2D(text="GPUParticles2D Fountain", position=(10, 10), font_scale=1.5, name="Title"))
 82        self._shapes = ["sphere", "point", "box"]  # index 0 is the sparks' starting emission_shape
 83        self._shape_index = 0
 84        self.hud = self.add_child(Text2D(text="", position=(10, 40), name="Hud"))
 85        self._update_hud()
 86
 87    def _update_hud(self):
 88        """Echo the live emitter state so each key press has visible feedback."""
 89        emitting = "ON" if self.fountain.emitting else "OFF"
 90        self.hud.text = f"Space = emit ({emitting}) | " f"S = spark shape ({self.sparks.emission_shape}) | Esc = quit"
 91
 92    def on_update(self, dt: float):
 93        if Input.is_action_just_pressed("toggle"):
 94            self.fountain.emitting = not self.fountain.emitting
 95            self.sparks.emitting = not self.sparks.emitting
 96            self._update_hud()
 97        if Input.is_action_just_pressed("shape"):
 98            self._shape_index = (self._shape_index + 1) % len(self._shapes)
 99            self.sparks.emission_shape = self._shapes[self._shape_index]
100            self._update_hud()
101        if Input.is_action_just_pressed("quit"):
102            self.app.quit()
103
104
105if __name__ == "__main__":
106    App(width=WIDTH, height=HEIGHT, title="GPUParticles2D Demo").run(ParticlesScene())