Particles¶
CPU particle emitters with sub-emitters, collision, trails and seeds.
▶ Run in browserTags: 3d
A ParticleEmitter spawns emission_rate particles a second, up to a pool
of amount, and each one lives lifetime seconds. Particles start at
initial_velocity (scattered by velocity_spread), fall under gravity
and fade from start_colour and start_scale to end_colour and
end_scale. Every setting is a Property, so an emitter is built in one call:
ParticleEmitter(seed=300, amount=200, emission_rate=80.0, lifetime=3.0, gravity=(0, -12, 0))
Three effects show the rest of the emitter:
Firework, sub-emitters. A
one_shotlauncher fires its pool upward and stops.launcher.sub_emitter_death = sparklemakes every launcher particle that dies burst into sparkles where it died; for a sub-emitter,emission_rateis the number of particles per burst. The sparkle emitter hasemitting=Falseso it spawns only on those bursts.Waterfall, collision.
collision_enabledstops particles at the horizontal planecollision_plane_y.collision_modeis"bounce"here, withcollision_bounceandcollision_friction;"stick"and"destroy"are the other modes. The stream pours from a box,emission_shape="box", where the sparkles are born in a"sphere".Comet, trails.
trail_enableddraws a ribbon oftrail_lengthsegments andtrail_widthbehind each particle; the emitter circles the scene, so the trails stream out behind it.
Each emitter has a seed. restart() clears the live particles and rewinds
the random generator to that seed, so an emitter restarted with the same
settings sprays exactly the same particles again.
These particles are simulated on the CPU in on_fixed_update; for
compute-shader particles see gpu_particles.py. The camera is an
OrbitCamera3D, so dragging orbits it and the wheel or a pinch zooms.
Controls: 1 / Firework - Launch a firework 2 / Waterfall - Waterfall on or off 3 / Comet - Comet on or off R / Restart - Restart every emitter from its seed Drag / A, D - Orbit the camera Scroll, pinch / W, S - Zoom
Source¶
1# /// simvx
2# web = { width = 1024, height = 768 }
3# ///
4"""Particles: CPU particle emitters with sub-emitters, collision, trails and seeds.
5
6A ``ParticleEmitter`` spawns ``emission_rate`` particles a second, up to a pool
7of ``amount``, and each one lives ``lifetime`` seconds. Particles start at
8``initial_velocity`` (scattered by ``velocity_spread``), fall under ``gravity``
9and fade from ``start_colour`` and ``start_scale`` to ``end_colour`` and
10``end_scale``. Every setting is a Property, so an emitter is built in one call:
11
12 ParticleEmitter(seed=300, amount=200, emission_rate=80.0, lifetime=3.0, gravity=(0, -12, 0))
13
14Three effects show the rest of the emitter:
15
16- **Firework, sub-emitters.** A ``one_shot`` launcher fires its pool upward
17 and stops. ``launcher.sub_emitter_death = sparkle`` makes every launcher
18 particle that dies burst into sparkles where it died; for a sub-emitter,
19 ``emission_rate`` is the number of particles per burst. The sparkle emitter
20 has ``emitting=False`` so it spawns only on those bursts.
21- **Waterfall, collision.** ``collision_enabled`` stops particles at the
22 horizontal plane ``collision_plane_y``. ``collision_mode`` is ``"bounce"``
23 here, with ``collision_bounce`` and ``collision_friction``; ``"stick"`` and
24 ``"destroy"`` are the other modes. The stream pours from a box,
25 ``emission_shape="box"``, where the sparkles are born in a ``"sphere"``.
26- **Comet, trails.** ``trail_enabled`` draws a ribbon of ``trail_length``
27 segments and ``trail_width`` behind each particle; the emitter circles the
28 scene, so the trails stream out behind it.
29
30Each emitter has a ``seed``. ``restart()`` clears the live particles and rewinds
31the random generator to that seed, so an emitter restarted with the same
32settings sprays exactly the same particles again.
33
34These particles are simulated on the CPU in ``on_fixed_update``; for
35compute-shader particles see ``gpu_particles.py``. The camera is an
36``OrbitCamera3D``, so dragging orbits it and the wheel or a pinch zooms.
37
38Controls:
39 1 / Firework - Launch a firework
40 2 / Waterfall - Waterfall on or off
41 3 / Comet - Comet on or off
42 R / Restart - Restart every emitter from its seed
43 Drag / A, D - Orbit the camera
44 Scroll, pinch / W, S - Zoom
45"""
46
47import math
48
49from simvx.core import (
50 Anchor,
51 Axis1D,
52 Button,
53 CanvasLayer,
54 DirectionalLight3D,
55 HBoxContainer,
56 Input,
57 Key,
58 Label,
59 Material,
60 Mesh,
61 MeshInstance3D,
62 Node3D,
63 OrbitCamera3D,
64 ParticleEmitter,
65 Vec3,
66 on_input,
67)
68from simvx.graphics import App
69
70COMET_RADIUS = 5.0
71
72
73class ParticlesDemo(Node3D):
74 input_context = {
75 "firework": [Key.KEY_1],
76 "waterfall": [Key.KEY_2],
77 "comet": [Key.KEY_3],
78 "restart": [Key.R],
79 "orbit": [Axis1D(low=Key.A, high=Key.D)],
80 "zoom": [Axis1D(low=Key.S, high=Key.W)],
81 }
82
83 def on_ready(self):
84 self.camera = self.add_child(
85 OrbitCamera3D(
86 pivot=(0, 3, 0), distance=18.7, yaw=0.0, pitch=math.radians(-15), min_distance=6, max_distance=40
87 )
88 )
89 self.add_child(DirectionalLight3D(direction=(-0.3, -1, -0.5)))
90 ground = Material(colour=(0.25, 0.25, 0.3), roughness=0.9)
91 self.add_child(MeshInstance3D(mesh=Mesh.box((20, 0.1, 20)), material=ground, position=(0, -0.05, 0)))
92
93 self.launcher = self.add_child(
94 ParticleEmitter(
95 name="Launcher",
96 seed=100,
97 amount=30,
98 emission_rate=200.0,
99 lifetime=0.8,
100 one_shot=True,
101 emitting=False,
102 initial_velocity=(0, 18, 0),
103 velocity_spread=0.3,
104 start_colour=(1.0, 0.8, 0.2, 1.0),
105 end_colour=(1.0, 0.4, 0.0, 0.8),
106 start_scale=0.3,
107 end_scale=0.1,
108 )
109 )
110 self.launcher.sub_emitter_death = self.add_child(
111 ParticleEmitter(
112 name="Sparkle",
113 seed=200,
114 amount=500,
115 emission_rate=8.0,
116 emitting=False,
117 lifetime=1.5,
118 initial_velocity=(0, 2, 0),
119 velocity_spread=3.0,
120 gravity=(0, -5, 0),
121 start_colour=(1.0, 0.6, 0.1, 1.0),
122 end_colour=(1.0, 0.2, 0.0, 0.0),
123 start_scale=0.15,
124 end_scale=0.0,
125 emission_shape="sphere",
126 emission_radius=0.3,
127 )
128 )
129
130 self.waterfall = self.add_child(
131 ParticleEmitter(
132 name="Waterfall",
133 seed=300,
134 position=(-5, 6, 0),
135 amount=200,
136 emission_rate=80.0,
137 lifetime=3.0,
138 initial_velocity=(2, 0, 0),
139 velocity_spread=0.2,
140 gravity=(0, -12, 0),
141 start_colour=(0.3, 0.6, 1.0, 0.9),
142 end_colour=(0.1, 0.3, 0.8, 0.3),
143 start_scale=0.2,
144 end_scale=0.1,
145 emission_shape="box",
146 emission_box=(0.5, 0.1, 0.5),
147 collision_enabled=True,
148 collision_mode="bounce",
149 collision_bounce=0.3,
150 collision_friction=0.4,
151 collision_plane_y=0.0,
152 )
153 )
154
155 self.comet = self.add_child(
156 ParticleEmitter(
157 name="Comet",
158 seed=400,
159 amount=100,
160 emission_rate=40.0,
161 lifetime=1.5,
162 initial_velocity=(0, 0.5, 0),
163 velocity_spread=0.1,
164 gravity=(0, -1, 0),
165 start_colour=(0.2, 0.8, 1.0, 1.0),
166 end_colour=(0.0, 0.3, 0.8, 0.0),
167 start_scale=0.25,
168 end_scale=0.05,
169 trail_enabled=True,
170 trail_length=6,
171 trail_width=0.08,
172 )
173 )
174 self.comet_time = 0.0
175
176 # Example help overlay: not part of the technique shown.
177 hud = self.add_child(CanvasLayer())
178 hud.add_child(Label("Drag / A, D: orbit Scroll / W, S: zoom", anchor=Anchor.TOP_LEFT, inset=12))
179 bar = hud.add_child(HBoxContainer(anchor=Anchor.BOTTOM_LEFT, inset=12))
180 for text, action in [("Firework", "firework"), ("Waterfall", "waterfall"), ("Comet", "comet")]:
181 bar.add_child(Button(text, action=action))
182 bar.add_child(Button("Restart", action="restart"))
183
184 def on_update(self, dt):
185 self.camera.orbit(Input.value("orbit") * dt, 0.0)
186 self.camera.zoom(Input.value("zoom") * 8.0 * dt)
187
188 # The comet circles the scene while it is on and holds still while it is off.
189 if self.comet.emitting:
190 self.comet_time += dt
191 t = self.comet_time
192 self.comet.position = Vec3(
193 math.cos(t * 1.2) * COMET_RADIUS, 3.0 + math.sin(t * 2.0), math.sin(t * 1.2) * COMET_RADIUS
194 )
195
196 @on_input("firework")
197 def launch_firework(self, _event):
198 self.launcher.restart()
199 self.launcher.emitting = True
200
201 @on_input("waterfall")
202 def toggle_waterfall(self, _event):
203 self.waterfall.emitting = not self.waterfall.emitting
204
205 @on_input("comet")
206 def toggle_comet(self, _event):
207 self.comet.emitting = not self.comet.emitting
208
209 @on_input("restart")
210 def restart_all(self, _event):
211 for emitter in self.find_all(ParticleEmitter):
212 emitter.restart()
213
214
215if __name__ == "__main__":
216 App(title="Particle Effects", width=1024, height=768).run(ParticlesDemo())