GPU Particles 3D¶
two emitters simulated entirely on the GPU.
▶ Run in browserTags: 3d
A GPUParticles3D node is a particle emitter whose particles never touch the
CPU: position, velocity, colour and age live in GPU buffers, a compute shader
advances them once per frame, and the billboard pipeline that draws CPU
particles renders the result. The node itself only holds the emitter’s
settings, as Properties, so it is built like any other node:
self.add_child(GPUParticles3D(amount=2048, lifetime=2.0, speed=6.0, gravity=(0, -9.8, 0)))
amount is the particle budget, lifetime how long each particle lives,
and direction, speed and spread aim the launch. gravity and
damping act on the particles in flight, emission_shape ("point",
"sphere" or "box") says where they are born, and each particle fades
from start_colour and start_scale to end_colour and end_scale.
Setting emitting to False stops new particles while the live ones finish;
restart() starts the cycle again.
The two emitters here are the same simulation at opposite ends of those ranges: a blue fountain launched fast from a point under full gravity, and an ember cloud born in a sphere, drifting slowly under weak gravity and damping.
Tap or click an emitter to switch it on or off; the tap goes to the emitter
nearest the ray from camera.ray_at() through that point. The camera is an
OrbitCamera3D, so dragging orbits it and the wheel or a pinch zooms. For
CPU particles, with sub-emitters, collision and trails, see particles.py.
Controls: Click / tap - Switch the nearest emitter on or off 1 / 2 - Switch the fountain / the embers on or off R - Restart both emitters Drag / A, D - Orbit the camera Scroll, pinch / W, S - Zoom
Source¶
1"""GPU Particles 3D: two emitters simulated entirely on the GPU.
2
3A ``GPUParticles3D`` node is a particle emitter whose particles never touch the
4CPU: position, velocity, colour and age live in GPU buffers, a compute shader
5advances them once per frame, and the billboard pipeline that draws CPU
6particles renders the result. The node itself only holds the emitter's
7settings, as Properties, so it is built like any other node:
8
9 self.add_child(GPUParticles3D(amount=2048, lifetime=2.0, speed=6.0, gravity=(0, -9.8, 0)))
10
11``amount`` is the particle budget, ``lifetime`` how long each particle lives,
12and ``direction``, ``speed`` and ``spread`` aim the launch. ``gravity`` and
13``damping`` act on the particles in flight, ``emission_shape`` (``"point"``,
14``"sphere"`` or ``"box"``) says where they are born, and each particle fades
15from ``start_colour`` and ``start_scale`` to ``end_colour`` and ``end_scale``.
16Setting ``emitting`` to False stops new particles while the live ones finish;
17``restart()`` starts the cycle again.
18
19The two emitters here are the same simulation at opposite ends of those
20ranges: a blue fountain launched fast from a point under full gravity, and an
21ember cloud born in a sphere, drifting slowly under weak gravity and damping.
22
23Tap or click an emitter to switch it on or off; the tap goes to the emitter
24nearest the ray from ``camera.ray_at()`` through that point. The camera is an
25``OrbitCamera3D``, so dragging orbits it and the wheel or a pinch zooms. For
26CPU particles, with sub-emitters, collision and trails, see ``particles.py``.
27
28Controls:
29 Click / tap - Switch the nearest emitter on or off
30 1 / 2 - Switch the fountain / the embers on or off
31 R - Restart both emitters
32 Drag / A, D - Orbit the camera
33 Scroll, pinch / W, S - Zoom
34"""
35
36import math
37
38from simvx.core import (
39 Anchor,
40 Axis1D,
41 Button,
42 CanvasLayer,
43 DirectionalLight3D,
44 GPUParticles3D,
45 HBoxContainer,
46 Input,
47 Key,
48 Label,
49 Material,
50 Mesh,
51 MeshInstance3D,
52 Node3D,
53 OrbitCamera3D,
54 Pointer,
55 Vec3,
56 on_input,
57)
58from simvx.graphics import App
59
60
61class GPUParticlesDemo(Node3D):
62 input_context = {
63 "fountain": [Key.KEY_1],
64 "embers": [Key.KEY_2],
65 "restart": [Key.R],
66 "orbit": [Axis1D(low=Key.A, high=Key.D)],
67 "zoom": [Axis1D(low=Key.S, high=Key.W)],
68 }
69
70 def on_ready(self):
71 self.camera = self.add_child(
72 OrbitCamera3D(
73 pivot=Vec3(0, 2, 0), distance=14.5, yaw=0.0, pitch=math.radians(-16), min_distance=4, max_distance=30
74 )
75 )
76 self.add_child(DirectionalLight3D(direction=(-0.3, -1.0, -0.5)))
77 self.add_child(
78 MeshInstance3D(
79 mesh=Mesh.box((20, 0.1, 20)),
80 material=Material(colour=(0.22, 0.22, 0.28), roughness=0.9),
81 position=(0, -0.05, 0),
82 )
83 )
84
85 self.fountain = self.add_child(
86 GPUParticles3D(
87 name="Fountain",
88 position=(-3.0, 0.2, 0.0),
89 amount=2048,
90 lifetime=2.0,
91 direction=(0.0, 1.0, 0.0),
92 speed=6.0,
93 spread=0.4,
94 gravity=(0.0, -9.8, 0.0),
95 start_colour=(0.4, 0.7, 1.0, 1.0),
96 end_colour=(0.1, 0.2, 0.6, 0.2),
97 start_scale=0.5,
98 end_scale=0.1,
99 emission_shape="point",
100 )
101 )
102 self.embers = self.add_child(
103 GPUParticles3D(
104 name="Embers",
105 position=(3.0, 2.0, 0.0),
106 amount=4096,
107 lifetime=3.0,
108 direction=(0.0, 0.5, 0.0),
109 speed=1.0,
110 spread=1.5,
111 gravity=(0.0, -0.4, 0.0),
112 damping=0.1,
113 start_colour=(1.0, 0.45, 0.15, 1.0),
114 end_colour=(0.6, 0.0, 0.5, 0.2),
115 start_scale=0.6,
116 end_scale=0.15,
117 emission_shape="sphere",
118 emission_radius=0.8,
119 )
120 )
121
122 # Example help overlay: not part of the technique shown.
123 hud = self.add_child(CanvasLayer())
124 hud.add_child(
125 Label(
126 "Tap an emitter: on / off Drag / A, D: orbit Scroll / W, S: zoom", anchor=Anchor.TOP_LEFT, inset=12
127 )
128 )
129 bar = hud.add_child(HBoxContainer(anchor=Anchor.BOTTOM_LEFT, inset=12))
130 for text, action in [("Fountain", "fountain"), ("Embers", "embers"), ("Restart", "restart")]:
131 bar.add_child(Button(text, action=action))
132
133 def on_update(self, dt):
134 self.camera.orbit(Input.value("orbit") * 1.2 * dt, 0.0)
135 self.camera.zoom(Input.value("zoom") * 6.0 * dt)
136
137 @on_input(Pointer.PRIMARY, edge="release")
138 def _on_tap(self, event):
139 # A drag orbits the camera; only a press that never became a drag toggles.
140 if event.dragging:
141 return
142 ray = self.camera.ray_at(event.position)
143
144 def distance_to_ray(emitter):
145 point = emitter.world_position
146 return ray.at((point - ray.origin).dot(ray.direction)).distance_to(point)
147
148 emitter = min((self.fountain, self.embers), key=distance_to_ray)
149 emitter.emitting = not emitter.emitting
150
151 @on_input("fountain")
152 def _on_fountain(self, _event):
153 self.fountain.emitting = not self.fountain.emitting
154
155 @on_input("embers")
156 def _on_embers(self, _event):
157 self.embers.emitting = not self.embers.emitting
158
159 @on_input("restart")
160 def _on_restart(self, _event):
161 self.fountain.restart()
162 self.embers.restart()
163
164
165if __name__ == "__main__":
166 App(title="GPU Particles 3D", width=1024, height=768).run(GPUParticlesDemo())