# Building A Basic Particle System

Years ago someone I know made an Android app that displays Touhou-like bullet hell from math equations. Back then I don't know a thing about computer graphics and thought there's some extremely complicated stuff behind that. Turns out it wasn't as complicated as I thought, at least in the simpler side of things.

Particle systems are essentially a system of particles, used to model a lot of stuff that the conventional way of using polygons cannot do very well:

Modeling phenomena such as clouds, smoke, water, and fire has proved difficult with the existing techniques of computer image synthesis. These "fuzzy" objects do not have smooth, well-defined, and shiny surfaces; instead their surfaces are irregular, complex, and ill defined.

-- Reeves, William T. "Particle Systems - A Technique for Modeling a Class of Fuzzy Objects" (1982)

It involves three different stages:

• Emission stage: in this stage we generate new particles.
• Simulation stage: in this stage we simulate the movement of particles one by one.
• Render stage: in this stage we render the thing.

Just like anything in general, complicated-level stuff are very complicated, and easy-level stuff are very easy; the original paper involves 3D maths, but to give a basic understanding 2D is enough.

## The code

I originally used turtle which is definitely not fast enough, so I switched to tkinter instead.

```import math
import tkinter
from tkinter import ttk
from dataclasses import dataclass
from random import random
from typing import Union
import time

# setting up the main window...
WINDOW_WIDTH = 300
WINDOW_HEIGHT = 300

root = tkinter.Tk()
root.title('Simple particle system')
canvas_main = tkinter.Canvas(root, bg="black", height=WINDOW_HEIGHT, width=WINDOW_WIDTH)```

The "particles" of things that're modelled - the droplets of water splash, the thin layers of smoke, etc - their properties are of course decided by the laws of physics, but those are way too difficult to pinpoint & calculate efficiently, so instead they're modelled as a combination of a "general" value a random variance. The most basic properties of a particle are:

• its position;
• its velocity (to where and how fast is the particle going);
• and its life span (how long will the particle last).

You can add other properties like size & color. I added color in this code.

```@dataclass
class Vec2:
x: Union[int, float]
y: Union[int, float]
return Vec2(x=self.x+other.x, y=self.y+other.y)
def length(self) -> float:
return math.sqrt(self.x**2+self.y**2)

@dataclass
class NumVariant:
base: Union[int, float]
variant: Union[int, float]

def value(self):
return (float if type(self.base) is float else int)(self.base + (self.variant * random()))

@dataclass
class Vec2Variant:
v: Vec2
angle_variant: float  # in radiant; abs val.
speed_variant: float
def value(self):
z = random()*(self.angle_variant*2)-self.angle_variant
va = math.atan2(self.v.y, self.v.x)
na = z + va
s = self.v.length()
sv = random()*(self.speed_variant*2)-self.speed_variant
ns = s + sv
return Vec2(x=math.cos(na)*ns, y=math.sin(na)*ns)

PARTICLE_SIZE: int = 4

@dataclass
class Color:
r: int
g: int
b: int
a: int = 0xff  # 0xff-solid 0x00-transparent

def get_rgb_string(self) -> str:
return f'#{self.r:02x}{self.g:02x}{self.b:02x}'

def get_rgba_string(self) -> str:
return f'#{self.r:02x}{self.g:02x}{self.b:02x}{self.a:02x}'

def scale(self, light: float):
return Color(
r=math.floor(self.r * light),
g=math.floor(self.g * light),
b=math.floor(self.b * light),
a=self.a,
)

DEFAULT_PARTICLE_COLOR = Color(0,255,255)

@dataclass
class Particle:
pos: Vec2
v: Vec2
# the life/lifespan here is to calculate the light level for scaling the color.
life: int
lifespan: int
color: Color = DEFAULT_PARTICLE_COLOR

def __post_init__(self):
# ...

def draw(self):
# ...```

The way Tk canvas worked is quite different than other canvases I've met: normally you'll just have a thing you draw stuff onto, and whatever you've drawn you cannot remove or change easily, just like a real canvas in real life; but Tk canvas maintains a list of objects that represents drawing elements on the canvas, almost like the list of layers in some advanced painting softwares (Photoshop; GIMP; Clip Studio Paint). The code will have a very different general idea if you decide to use other ways to implement the GUI (e.g. SDL2):

```# inside class Particle
def __post_init__(self):
color_str = self.color.get_rgb_string()
self._elem = canvas_main.create_oval(
self.pos.x-PARTICLE_SIZE/2, self.pos.y-PARTICLE_SIZE/2,
self.pos.x+PARTICLE_SIZE/2, self.pos.y+PARTICLE_SIZE/2,
fill=color_str,
outline=color_str,
)

def draw(self):
canvas_main.moveto(self._elem,
self.pos.x-PARTICLE_SIZE/2, self.pos.y-PARTICLE_SIZE/2,
)
color_str = self.color.scale(self.life/self.lifespan).get_rgb_string()
canvas_main.itemconfigure(self._elem,
fill=color_str,
outline=color_str,
)```

The definition for particles is ready; we need something that models the emission of particles. To generate a particle, we need to at least decide three things:

• its position;
• its life span, i.e. how long will it last;
• and its initial velocity.

In the following code I've made these choices:

• the emission is managed by an emitter object;
• each emitter has its own position & general velocity; new particles will get its lifespan, initial position & initial velocity from such emitter object.
```@dataclass
class Emitter:
name: str
pos: Vec2
init_v: Vec2Variant
particle: list[Particle]
max_particle: int = 100
current_particle: int = 0
life: NumVariant = NumVariant(base=100, variant=2)
force: Vec2 = Vec2(0, 0.05)  # explained later

if self.current_particle < self.max_particle:
life = self.life.value()
new_particle = Particle(
pos=self.pos,
v=self.init_v.value(),
life=life,
lifespan=life,
)
self.particle.append(new_particle)

def update_particle(self):
# ...

def draw_particle(self):
for p in self.particle: p.draw()

em = Emitter(
name='em1',
pos=Vec2(WINDOW_WIDTH/2, WINDOW_HEIGHT/2),
particle=[]
)```

In some scenarioes where there's a strict restriction on what you can use, you'll have to pay more effort on creating & managing objects, e.g. production usage which requires things to work in real-time; most (if not all) of the real-time scenarioes you can't use `malloc` because the latency is not predictable.

Now the simulation stage. According to physics, if not affected by any forces, a particle should just stay still (if no velocity) or move towards a fixed direction at a constant speed (i.e. a stable velocity), and if affected by forces, the velocity changes and the change (i.e. acceleration) is related to the force and the particle's mass. In this code there's only acceleration (incorrectly defined, in a technical sense, as `force` above) which is enough for this demonstration; in a more serious scenario other parameters should be considered as well.

```# in class Emitter
def update_particle(self):
for i, p in enumerate(self.particle):
p.life -= 1
if p.life <= 0: continue
p.v = p.v + self.force
p.pos = p.pos + p.v
for p in self.particle:
if (p.life <= 0):
canvas_main.delete(p._elem)
self.particle = [p for p in self.particle if p.life > 0]```

I decided to have something like a control panel that can control the variances; this will be helpful for exploring/demonstrating how one can use such a system to render stuff.

```control_panel = tkinter.Toplevel()
control_panel.title('Control Panel')

lbl_particle_emit_width = ttk.Label(control_panel, text='Particle emit angle')
lbl_particle_emit_width.pack()
def update_emit_width(val):
lbl_particle_emit_width['text'] = f'Particle emit angle: {val}'
sc_particle_emit_width = ttk.Scale(control_panel, orient=tkinter.HORIZONTAL, length=400, from_=0, to=90, command=update_emit_width)
sc_particle_emit_width.pack()
sc_particle_emit_width.set(30)

lbl_particle_emit_speed_variant = ttk.Label(control_panel, text='Particle emit speed variant')
lbl_particle_emit_speed_variant.pack()
def update_emit_speed_variant(val):
lbl_particle_emit_speed_variant['text'] = f'Particle emit speed variant: {val}'
em.init_v.speed_variant = float(val)
sc_particle_emit_speed_variant = ttk.Scale(control_panel, orient=tkinter.HORIZONTAL, length=400, from_=-5, to=5, command=update_emit_speed_variant)
sc_particle_emit_speed_variant.pack()
sc_particle_emit_speed_variant.set(0.5)

lbl_particle_emit_v = ttk.Label(control_panel, text='Particle emit velocity (on y axis)')
lbl_particle_emit_v.pack()
def update_emit_v(val):
lbl_particle_emit_v['text'] = f'Particle emit velocity (on y axis): {val}'
em.init_v.v.y = float(val)
sc_particle_emit_v = ttk.Scale(control_panel, orient=tkinter.HORIZONTAL, length=400, from_=-5, to=5, command=update_emit_v)
sc_particle_emit_v.pack()
sc_particle_emit_v.set(-1)

lbl_particle_emit_g = ttk.Label(control_panel, text='Gravity (on y axis)')
lbl_particle_emit_g.pack()
def update_emit_g(val):
lbl_particle_emit_g['text'] = f'Gravity (on y axis): {val}'
em.force.y = float(val)
sc_particle_emit_g = ttk.Scale(control_panel, orient=tkinter.HORIZONTAL, length=400, from_=-1, to=1, command=update_emit_g)
sc_particle_emit_g.pack()
sc_particle_emit_g.set(0.05)```

At the end we need code that kickstarts the simulation:

```while True:
em.update_particle()
em.draw_particle()
root.update()
time.sleep(1/60)

root.mainloop()```

## Modeling water & fire

The settings in the code above should produce something resembles water splash; as long as the color is blue & the force is big enough to pull the particles downward it'll look like water splash. Change the following lines will make it look like a small flame with smoke.

```# before class Particle
DEFAULT_PARTICLE_COLOR = Color(255,32,0)

# in class Emitter
force: Vec2 = Vec2(0, -0.005)

# in definition of `em`
em = Emitter(
name='em1',
pos=Vec2(WINDOW_WIDTH/2, WINDOW_HEIGHT/2),
particle=[]
)

# in the code for the control panel
sc_particle_emit_width.set(45)
sc_particle_emit_speed_variant.set(0)
sc_particle_emit_v.set(-0.33)
sc_particle_emit_g.set(-0.005)```

## Touhou-like bullet hell

If you like vertically scrolling shooter like Raiden and have never tried any Touhou Project game then please do... it's one hell of a game. Or games. For casual players they're all hellishly hard even on easy mode.

Particles (i.e. the bullets) in Touhou:

• can emerge from the enemy or just simply appear any where;
• can follow a fixed pattern (e.g. circles, spirals & lines) or follow the player;
• can have different stages, e.g. when fighting Izayoi Sakuya the bullets will freeze in the air for some time, signifying her ability (stated in the lore) to stop time.

For this demonstration I'll only consider the spiral kind.

This time the movement of the particles is not effected by any kind of forces. The direction of the initial velocity changes, and the speed will slow down gradually until hitting the lowest reasonable speed. From these observations we can have the following code:

```# inside class Particle
# ...
def draw(self):
canvas_main.moveto(self._elem,
self.pos.x-PARTICLE_SIZE/2, self.pos.y-PARTICLE_SIZE/2,
)
color_str = self.color.get_rgb_string()
canvas_main.itemconfigure(self._elem,
fill=color_str,
outline=color_str,
)

@dataclass
class Emitter:
name: str
pos: Vec2
init_speed: float
particle: list[Particle]
current_phi: float = 0 # in degree
max_particle: int = 100
current_particle: int = 0
life: int = 100
step: float = 10
min_speed_factor: float = 0.5

if self.current_particle < self.max_particle:
new_particle = Particle(
pos=self.pos,
v=Vec2(
).scale(self.init_speed),
life=self.life,
lifespan=self.life,
)
self.particle.append(new_particle)
self.current_phi = (self.current_phi + self.step) % 360

def update_particle(self):
for i, p in enumerate(self.particle):
p.life -= 1
if p.life <= 0: continue
p.pos = p.pos + p.v.scale(max(p.life/p.lifespan, self.min_speed_factor))
for p in self.particle:
if (p.life <= 0):
canvas_main.delete(p._elem)
self.particle = [p for p in self.particle if p.life > 0]

def draw_particle(self):
for p in self.particle: p.draw()

em = Emitter(
name='em1',
pos=Vec2(WINDOW_WIDTH/2, WINDOW_HEIGHT/2),
init_speed=1,
particle=[]
)

# a new control panel.

control_panel = tkinter.Toplevel()
control_panel.title('Control Panel')

lbl_particle_emit_angle_step = ttk.Label(control_panel, text='Particle emit angle step (deg)')
lbl_particle_emit_angle_step.pack()
def update_emit_width(val):
lbl_particle_emit_angle_step['text'] = f'Particle emit angle step (deg): {val}'
em.step = float(val)
sc_particle_emit_angle_step = ttk.Scale(control_panel, orient=tkinter.HORIZONTAL, length=400, from_=0, to=360, command=update_emit_width)
sc_particle_emit_angle_step.pack()
sc_particle_emit_angle_step.set(116)

lbl_particle_emit_speed = ttk.Label(control_panel, text='Particle emit speed')
lbl_particle_emit_speed.pack()
def update_emit_speed_variant(val):
lbl_particle_emit_speed['text'] = f'Particle emit speed: {val}'
em.init_speed = float(val)
sc_particle_emit_speed = ttk.Scale(control_panel, orient=tkinter.HORIZONTAL, length=400, from_=-10, to=10, command=update_emit_speed_variant)
sc_particle_emit_speed.pack()
sc_particle_emit_speed.set(3.5)

lbl_particle_emit_min_speed_factor = ttk.Label(control_panel, text='Particle emit min speed factor')
lbl_particle_emit_min_speed_factor.pack()
def update_emit_g(val):
lbl_particle_emit_min_speed_factor['text'] = f'Particle emit min speed factor: {val}'
em.min_speed_factor = float(val)
sc_particle_emit_min_speed_factor = ttk.Scale(control_panel, orient=tkinter.HORIZONTAL, length=400, from_=-1, to=1, command=update_emit_g)
sc_particle_emit_min_speed_factor.pack()
sc_particle_emit_min_speed_factor.set(0.5)```

The code to the two demo can be downloaded from here and here.