GPU Particles 2D

a particle fountain simulated on the GPU.

▶ Run in browser

Tags: 2d particles gpu effects

A GPUParticles2D node keeps its particles in GPU memory and a compute shader moves them, so thousands of grains cost the CPU nothing per particle. You configure the emitter with properties and the renderer does the rest:

  • amount and lifetime set how many particles live at once and for how long; randomness varies each lifetime so the stream stays continuous.

  • speed, speed_variance, direction and spread shape the launch.

  • gravity accelerates every particle; start_colour/end_colour and start_scale/end_scale fade and shrink each one over its life.

  • emission_shape is where particles are born: "point", "sphere" (emission_radius) or "box" (emission_box half-extents).

  • emitting starts and stops spawning; live particles finish their lives.

In 2D, Y points down, so direction=(0, -1, 0) emits upward and a positive-Y gravity pulls the grains back into an arc. Speeds and gravity are in pixels per second, so the golden jet rises about speed^2 / (2 * gravity) pixels, roughly 260, before it turns over. Above it a cyan puff scatters sparks in every direction.

Press Space or the Emit button to stop and restart both emitters, and S or the Shape button to move the sparks’ emission through sphere, point and box.

Source

  1# /// simvx
  2# tags = ["particles", "gpu", "effects"]
  3# web = { width = 800, height = 600, responsive = true }
  4# ///
  5"""GPU Particles 2D: a particle fountain simulated on the GPU.
  6
  7A ``GPUParticles2D`` node keeps its particles in GPU memory and a compute shader
  8moves them, so thousands of grains cost the CPU nothing per particle. You
  9configure the emitter with properties and the renderer does the rest:
 10
 11  - ``amount`` and ``lifetime`` set how many particles live at once and for how long;
 12    ``randomness`` varies each lifetime so the stream stays continuous.
 13  - ``speed``, ``speed_variance``, ``direction`` and ``spread`` shape the launch.
 14  - ``gravity`` accelerates every particle; ``start_colour``/``end_colour`` and
 15    ``start_scale``/``end_scale`` fade and shrink each one over its life.
 16  - ``emission_shape`` is where particles are born: ``"point"``, ``"sphere"``
 17    (``emission_radius``) or ``"box"`` (``emission_box`` half-extents).
 18  - ``emitting`` starts and stops spawning; live particles finish their lives.
 19
 20In 2D, Y points down, so ``direction=(0, -1, 0)`` emits upward and a positive-Y
 21``gravity`` pulls the grains back into an arc. Speeds and gravity are in pixels
 22per second, so the golden jet rises about speed^2 / (2 * gravity) pixels, roughly
 23260, before it turns over. Above it a cyan puff scatters sparks in every
 24direction.
 25
 26Press Space or the Emit button to stop and restart both emitters, and S or the
 27Shape button to move the sparks' emission through sphere, point and box.
 28"""
 29
 30from itertools import cycle
 31
 32from simvx.core import Anchor, Button, CanvasLayer, GPUParticles2D, HBoxContainer, Key, Node2D, on_input
 33from simvx.graphics import App
 34
 35
 36class ParticlesScene(Node2D):
 37    input_context = {"emit": [Key.SPACE], "shape": [Key.S]}
 38
 39    def on_ready(self):
 40        self.fountain = self.add_child(
 41            GPUParticles2D(
 42                amount=1400,
 43                lifetime=2.2,  # long enough for the whole up-and-over arc
 44                randomness=0.35,
 45                speed=540.0,
 46                speed_variance=90.0,
 47                spread=0.28,  # a narrow jet that fans near the apex
 48                direction=(0.0, -1.0, 0.0),
 49                gravity=(0.0, 560.0, 0.0),
 50                start_colour=(1.0, 0.85, 0.35, 1.0),
 51                end_colour=(1.0, 0.4, 0.1, 0.0),
 52                start_scale=5.0,
 53                end_scale=1.5,
 54            )
 55        )
 56        self.sparks = self.add_child(
 57            GPUParticles2D(
 58                amount=1200,
 59                lifetime=1.1,
 60                speed=130.0,
 61                speed_variance=70.0,
 62                spread=3.14,  # wide enough to throw sparks every way
 63                direction=(0.0, -1.0, 0.0),
 64                gravity=(0.0, 180.0, 0.0),
 65                emission_shape="sphere",
 66                emission_radius=24.0,
 67                emission_box=(60.0, 12.0, 0.0),
 68                start_colour=(0.4, 0.9, 1.0, 1.0),
 69                end_colour=(0.1, 0.3, 0.7, 0.0),
 70                start_scale=2.5,
 71                end_scale=0.0,
 72            )
 73        )
 74        self.shapes = cycle(["point", "box", "sphere"])  # what follows the starting sphere
 75
 76        # Example help overlay: not part of the technique shown.
 77        bar = HBoxContainer(anchor=Anchor.TOP_LEFT, inset=10, separation=8)
 78        bar.add_child(Button("Emit", action="emit"))
 79        self.shape_button = bar.add_child(Button("Shape: sphere", action="shape"))
 80        self.add_child(CanvasLayer()).add_child(bar)
 81
 82    def on_update(self, dt):
 83        w, h = self.viewport_size
 84        self.fountain.position = (w * 0.5, h * 0.86)
 85        self.sparks.position = (w * 0.5, h * 0.42)
 86
 87    @on_input("emit")
 88    def toggle_emitting(self, event):
 89        self.fountain.emitting = self.sparks.emitting = not self.fountain.emitting
 90        return True
 91
 92    @on_input("shape")
 93    def next_shape(self, event):
 94        self.sparks.emission_shape = next(self.shapes)
 95        self.shape_button.text = f"Shape: {self.sparks.emission_shape}"
 96        return True
 97
 98
 99if __name__ == "__main__":
100    App(width=800, height=600, title="GPU Particles 2D").run(ParticlesScene())