GPU Particles 3D Demo¶
compute-shader-driven particle simulation.
▶ Run in browserTags: 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())