GPU Particles 2D¶
A GPU-driven particle fountain with gravity and colour fade.
▶ Run in browserTags: 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())