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 (0, 200, 0) pulls particles back down: a classic fountain.

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 (0, 200, 0) pulls particles back down: a classic fountain.
17"""
18
19from simvx.core import GPUParticles2D, Input, InputMap, Key, Node2D, Text2D, Vec2
20from simvx.graphics import App
21
22WIDTH, HEIGHT = 800, 600
23
24
25class ParticlesScene(Node2D):
26    """A fountain emitter plus a steady spark burst, both GPU-simulated."""
27
28    def on_ready(self):
29        InputMap.add_action("toggle", [Key.SPACE])
30        InputMap.add_action("shape", [Key.S])
31        InputMap.add_action("quit", [Key.ESCAPE])
32
33        # Golden fountain: emits upward, gravity arcs the stream back down.
34        self.fountain = self.add_child(GPUParticles2D(
35            amount=4000,
36            lifetime=1.6,
37            speed=260.0,
38            speed_variance=60.0,
39            spread=0.5,                     # velocity randomisation cone
40            direction=(0.0, -1.0, 0.0),     # upward (Y is down in 2D)
41            gravity=(0.0, 380.0, 0.0),      # pull back down
42            start_colour=(1.0, 0.85, 0.25, 1.0),
43            end_colour=(1.0, 0.15, 0.0, 0.0),
44            start_scale=6.0,
45            end_scale=1.0,
46            position=Vec2(WIDTH * 0.5, HEIGHT * 0.75),
47            name="Fountain",
48        ))
49
50        # Cyan sparks: omnidirectional spray from a small sphere, light gravity.
51        self.sparks = self.add_child(GPUParticles2D(
52            amount=1500,
53            lifetime=0.9,
54            speed=140.0,
55            speed_variance=80.0,
56            spread=3.14,                    # wide spread for an all-directions burst
57            direction=(0.0, -1.0, 0.0),
58            gravity=(0.0, 120.0, 0.0),
59            emission_shape="sphere",
60            emission_radius=12.0,
61            start_colour=(0.4, 0.9, 1.0, 1.0),
62            end_colour=(0.1, 0.2, 0.6, 0.0),
63            start_scale=4.0,
64            end_scale=0.0,
65            position=Vec2(WIDTH * 0.5, HEIGHT * 0.4),
66            name="Sparks",
67        ))
68
69        self.add_child(Text2D(text="GPUParticles2D Fountain", position=(10, 10), font_scale=1.5, name="Title"))
70        self.hud = self.add_child(Text2D(
71            text="Space = toggle emit | S = cycle spark shape | Esc = quit",
72            position=(10, 40), name="Hud",
73        ))
74        self._shapes = ["sphere", "point", "box"]
75        self._shape_index = 0
76
77    def on_update(self, dt: float):
78        if Input.is_action_just_pressed("toggle"):
79            self.fountain.emitting = not self.fountain.emitting
80            self.sparks.emitting = not self.sparks.emitting
81        if Input.is_action_just_pressed("shape"):
82            self._shape_index = (self._shape_index + 1) % len(self._shapes)
83            self.sparks.emission_shape = self._shapes[self._shape_index]
84        if Input.is_action_just_pressed("quit"):
85            self.app.quit()
86
87
88if __name__ == "__main__":
89    App(width=WIDTH, height=HEIGHT, title="GPUParticles2D Demo").run(ParticlesScene())