GPU Particles 3D Demo

compute-shader-driven particle simulation.

▶ Run in browser

Tags: 3d

Showcases GPUParticles3D: position, velocity, colour and lifetime live entirely on the GPU; the compute shader (particle_sim.comp) runs once per frame and the same billboard pipeline that draws CPU particles renders the result. No per-frame CPU-to-GPU upload of particle state.

For CPU emitters (sub-emitters, collision, trails) see particles.py.

Run: uv run python examples/features/3d/gpu_particles.py

Controls: 1 - Toggle fountain (steady stream, gravity, colour gradient) 2 - Toggle vortex (spherical emission, low gravity) R - Restart all emitters A / D - Orbit camera W / S - Zoom in / out ESC - Quit

Source

  1"""GPU Particles 3D Demo: compute-shader-driven particle simulation.
  2
  3Showcases ``GPUParticles3D``: position, velocity, colour and lifetime live
  4entirely on the GPU; the compute shader (``particle_sim.comp``) runs once
  5per frame and the same billboard pipeline that draws CPU particles renders
  6the result. No per-frame CPU-to-GPU upload of particle state.
  7
  8For CPU emitters (sub-emitters, collision, trails) see ``particles.py``.
  9
 10Run:
 11    uv run python examples/features/3d/gpu_particles.py
 12
 13Controls:
 14    1       - Toggle fountain (steady stream, gravity, colour gradient)
 15    2       - Toggle vortex (spherical emission, low gravity)
 16    R       - Restart all emitters
 17    A / D   - Orbit camera
 18    W / S   - Zoom in / out
 19    ESC     - Quit
 20"""
 21
 22import math
 23
 24import numpy as np
 25
 26from simvx.core import (
 27    Camera3D,
 28    DirectionalLight3D,
 29    GPUParticles3D,
 30    Input,
 31    InputMap,
 32    Key,
 33    Material,
 34    Mesh,
 35    MeshInstance3D,
 36    Node3D,
 37    Text2D,
 38    Vec3,
 39)
 40from simvx.graphics import App
 41
 42WIDTH, HEIGHT = 1024, 768
 43GROUND_Y = 0.0
 44
 45
 46class Ground(MeshInstance3D):
 47    def on_ready(self):
 48        self.mesh = Mesh.cube()
 49        self.material = Material(colour=(0.22, 0.22, 0.28), roughness=0.9)
 50        self.scale = np.array([20.0, 0.1, 20.0], dtype=np.float32)
 51        self.position = Vec3(0, GROUND_Y - 0.05, 0)
 52
 53
 54class Fountain(GPUParticles3D):
 55    def on_ready(self):
 56        self.position = Vec3(-3.0, 0.2, 0.0)
 57        self.amount = 2048
 58        self.lifetime = 2.0
 59        self.emitting = False
 60        self.direction = (0.0, 1.0, 0.0)
 61        self.speed = 6.0
 62        self.spread = 0.4
 63        self.gravity = (0.0, -9.8, 0.0)
 64        self.damping = 0.0
 65        self.start_colour = (0.4, 0.7, 1.0, 1.0)
 66        self.end_colour = (0.1, 0.2, 0.6, 0.2)
 67        self.start_scale = 0.5
 68        self.end_scale = 0.1
 69        self.emission_shape = "point"
 70
 71
 72class Vortex(GPUParticles3D):
 73    def on_ready(self):
 74        self.position = Vec3(3.0, 2.0, 0.0)
 75        self.amount = 4096
 76        self.lifetime = 3.0
 77        self.emitting = False
 78        self.direction = (0.0, 0.5, 0.0)
 79        self.speed = 1.0
 80        self.spread = 1.5
 81        self.gravity = (0.0, -0.4, 0.0)
 82        self.damping = 0.1
 83        self.start_colour = (1.0, 0.45, 0.15, 1.0)
 84        self.end_colour = (0.6, 0.0, 0.5, 0.2)
 85        self.start_scale = 0.6
 86        self.end_scale = 0.15
 87        self.emission_shape = "sphere"
 88        self.emission_radius = 0.8
 89
 90
 91class Hud(Text2D):
 92    def on_ready(self):
 93        self.text = "1: Fountain  2: Vortex   R: Restart   A/D orbit  W/S zoom  ESC quit"
 94        self.x = 20
 95        self.y = HEIGHT - 40
 96        self.font_scale = 1.5
 97        self.colour = (1.0, 1.0, 1.0, 1.0)
 98
 99
100class DemoRoot(Node3D):
101    input_actions = {
102        "fountain": [Key.KEY_1],
103        "vortex": [Key.KEY_2],
104        "restart": [Key.R],
105        "orbit_left": [Key.A],
106        "orbit_right": [Key.D],
107        "zoom_in": [Key.W],
108        "zoom_out": [Key.S],
109        "quit": [Key.ESCAPE],
110    }
111
112    def on_ready(self):
113        self.camera = Camera3D(name="Camera")
114        self.camera.position = Vec3(0, 6, 14)
115        self.camera.look_at(Vec3(0, 2, 0))
116        self.add_child(self.camera)
117
118        sun = DirectionalLight3D(name="Sun")
119        sun.direction = Vec3(-0.3, -1.0, -0.5)
120        self.add_child(sun)
121
122        self.add_child(Ground(name="Ground"))
123
124        self.fountain = self.add_child(Fountain(name="Fountain"))
125        self.vortex = self.add_child(Vortex(name="Vortex"))
126
127        self.add_child(Hud(name="HUD"))
128
129        self._orbit = 0.0
130        self._radius = 14.0
131
132    def on_process(self, dt: float):
133        if Input.is_action_just_pressed("fountain"):
134            self.fountain.emitting = not self.fountain.emitting
135        if Input.is_action_just_pressed("vortex"):
136            self.vortex.emitting = not self.vortex.emitting
137        if Input.is_action_just_pressed("restart"):
138            self.fountain.restart()
139            self.vortex.restart()
140        if Input.is_action_just_pressed("quit"):
141            self.app.quit()
142
143        if Input.is_action_pressed("orbit_left"):
144            self._orbit -= dt * 1.2
145        if Input.is_action_pressed("orbit_right"):
146            self._orbit += dt * 1.2
147        if Input.is_action_pressed("zoom_in"):
148            self._radius = max(4.0, self._radius - dt * 6.0)
149        if Input.is_action_pressed("zoom_out"):
150            self._radius = min(30.0, self._radius + dt * 6.0)
151
152        self.camera.position = Vec3(
153            math.sin(self._orbit) * self._radius,
154            6.0,
155            math.cos(self._orbit) * self._radius,
156        )
157        self.camera.look_at(Vec3(0, 2, 0))
158
159
160if __name__ == "__main__":
161    App(width=WIDTH, height=HEIGHT, title="GPU Particles 3D").run(DemoRoot())