I'm trying to replicate some animation concepts of particles with force fields in order to generate a vertical beam of particles going upwards, except they can experience "turbulences" at any point. I added some screenshot examples of effects that could happen.
One particular effect that I can't replicate at all is that sometimes if forces apply a revolution effect to a group of particles, they should start navigating in circles (like planets around the sun) while still going up, but I think I'm missing some concepts to achieve that.
I'm looking for fixes to my code below that would get me closer to the dynamics in the screenshots below
Screenshots:
import pygame
import random
import math
import time
from collections import deque
WIDTH, HEIGHT = 1000, 700
FPS = 60
FIELD_GRID = 64
SEED = None
EMIT_RATE = 2 # particles per frame
# Simulation parameters
params = {
"strength": 40.0, # curl noise strength
"scale": 0.004, # curl noise spatial scale
"speed": 0.25, # curl noise time speed
"damping": 0.995, # particle velocity damping
"particle_mass": 1.0,
"time_scale": 1.0,
}
# Vortex settings
VORTEX_PROB = 0.01 # probability per frame to spawn a vortex
VORTEX_RADIUS = 80 # pixels
VORTEX_STRENGTH = 120 # tangential force
VORTEX_LIFETIME = 2.0 # seconds
# Visual options
show_field = False
show_trails = True
show_vortices = False
# Pygame init
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Turbulence Fountain + Vortices")
clock = pygame.time.Clock()
font = pygame.font.SysFont("consolas", 16)
# Utility fade function
def fade(t):
return t * t * t * (t * (t * 6 - 15) + 10)
# Value noise
class ValueNoise2D:
def __init__(self, grid_w, grid_h, seed=None):
self.grid_w = grid_w
self.grid_h = grid_h
self.seed = seed if seed is not None else random.randrange(1<<30)
self._reseed(self.seed)
def _reseed(self, seed):
self.seed = seed
self.rand = random.Random(seed)
self.grid = [[self.rand.uniform(-1.0, 1.0) for _ in range(self.grid_w + 1)]
for __ in range(self.grid_h + 1)]
def value_at(self, x, y):
gx = math.floor(x)
gy = math.floor(y)
tx = x - gx
ty = y - gy
gx0 = gx % self.grid_w
gy0 = gy % self.grid_h
gx1 = (gx0 + 1) % self.grid_w
gy1 = (gy0 + 1) % self.grid_h
v00 = self.grid[gy0][gx0]
v10 = self.grid[gy0][gx1]
v01 = self.grid[gy1][gx0]
v11 = self.grid[gy1][gx1]
sx = fade(tx)
sy = fade(ty)
ix0 = v00 * (1 - sx) + v10 * sx
ix1 = v01 * (1 - sx) + v11 * sx
return ix0 * (1 - sy) + ix1 * sy
def reseed(self, seed=None):
if seed is None:
seed = random.randrange(1<<30)
self._reseed(seed)
noise = ValueNoise2D(FIELD_GRID, FIELD_GRID, SEED)
# Particle
class Particle:
__slots__ = ("x", "y", "vx", "vy", "age", "max_age", "trail")
def __init__(self, x, y, vx, vy):
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.age = 0.0
self.max_age = 180 / FPS # ~3s
self.trail = deque(maxlen=12)
def add_trail(self):
self.trail.appendleft((self.x, self.y))
# Vortex
class Vortex:
def __init__(self, x, y, radius, strength, lifetime):
self.x = x
self.y = y
self.radius = radius
self.strength = strength
self.age = 0.0
self.lifetime = lifetime
def apply_force(self, p):
dx = p.x - self.x
dy = p.y - self.y
dist2 = dx*dx + dy*dy
if dist2 < self.radius * self.radius:
dist = math.sqrt(dist2) + 1e-6
# tangential direction
fx = -dy / dist * self.strength
fy = dx / dist * self.strength
return fx, fy
return 0.0, 0.0
particles = []
vortices = []
# Spawn particle
def spawn_particle():
x = WIDTH/2 + random.uniform(-50, 50)
y = HEIGHT - 5
vx = random.uniform(-10, 10) * 1.2
vy = -random.uniform(30, 60) * 1.8
return Particle(x, y, vx, vy)
# Curl noise force
def turbulence_force(px, py, t, strength, scale):
nx = px * scale
ny = py * scale
ntx = nx + t * params["speed"]
nty = ny + t * (params["speed"] * 0.7 + 0.1)
eps = 0.0001
v = noise.value_at(ntx, nty)
vx = noise.value_at(ntx + eps, nty)
vy = noise.value_at(ntx, nty + eps)
dv_dx = (vx - v) / eps
dv_dy = (vy - v) / eps
fx = dv_dy * strength
fy = -dv_dx * strength * 0.5
return fx, fy
def draw_text(surf, text, x, y, color=(255,255,255)):
surf.blit(font.render(text, True, color), (x,y))
# Main loop
running = True
paused = False
t0 = time.time()
while running:
dt = clock.tick(FPS) / 1000.0
if dt <= 0: dt = 1.0 / FPS
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: running = False
elif event.key == pygame.K_SPACE: paused = not paused
elif event.key == pygame.K_s: show_field = not show_field
elif event.key == pygame.K_c: particles.clear()
elif event.key == pygame.K_r: noise.reseed()
elif event.key == pygame.K_UP: params["strength"] *= 1.1
elif event.key == pygame.K_DOWN: params["strength"] /= 1.1
elif event.key == pygame.K_RIGHT: params["scale"] *= 1.1
elif event.key == pygame.K_LEFT: params["scale"] /= 1.1
elif event.key == pygame.K_PLUS or event.key == pygame.K_EQUALS: params["speed"] *= 1.1
elif event.key == pygame.K_MINUS: params["speed"] /= 1.1
elif event.key == pygame.K_t: show_trails = not show_trails
elif event.key == pygame.K_v: show_vortices = not show_vortices
if not paused:
t = (time.time() - t0) * params["time_scale"]
# Emit particles
for _ in range(EMIT_RATE):
particles.append(spawn_particle())
# Maybe spawn vortex
if random.random() < VORTEX_PROB:
vx = WIDTH/2 + random.uniform(-200, 200)
vy = HEIGHT/2 + random.uniform(-200, 200)
vortices.append(Vortex(vx, vy, VORTEX_RADIUS, VORTEX_STRENGTH, VORTEX_LIFETIME))
alive_particles = []
for p in particles:
fx, fy = turbulence_force(p.x, p.y, t, params["strength"], params["scale"])
# vortex forces
for v in vortices:
vfx, vfy = v.apply_force(p)
fx += vfx
fy += vfy
ax = fx / params["particle_mass"]
ay = fy / params["particle_mass"]
p.vx += ax * dt
p.vy += ay * dt
p.vx *= params["damping"]
p.vy *= params["damping"]
# constant upward drift
p.vy -= 8 * dt
if p.vy > -5: p.vy = -5
p.x += p.vx * dt
p.y += p.vy * dt
# horizontal wrap
if p.x < 0: p.x += WIDTH
elif p.x >= WIDTH: p.x -= WIDTH
p.age += dt
p.add_trail()
if p.age < p.max_age:
alive_particles.append(p)
particles = alive_particles
# update vortices
alive_vortices = []
for v in vortices:
v.age += dt
if v.age < v.lifetime:
alive_vortices.append(v)
vortices = alive_vortices
screen.fill((10, 10, 20))
if show_field:
step = 25
arrow_scale = 0.03
t = (time.time() - t0) * params["time_scale"]
for x in range(0, WIDTH, step):
for y in range(0, HEIGHT, step):
fx, fy = turbulence_force(x, y, t, params["strength"], params["scale"])
dx, dy = fx * arrow_scale, fy * arrow_scale
sx, sy = x, y
ex, ey = sx + dx, sy + dy
pygame.draw.line(screen, (120,180,255), (sx,sy), (ex,ey), 1)
if show_trails:
for p in particles:
if len(p.trail) >= 2:
pts = list(p.trail)
for i in range(1, len(pts)):
a, b = pts[i-1], pts[i]
shade = max(40, 200 - i*12)
pygame.draw.line(screen, (shade, shade+20, 255), a, b, 2)
pygame.draw.circle(screen, (255,255,255), (int(p.x), int(p.y)), 2)
else:
for p in particles:
pygame.draw.circle(screen, (255,255,255), (int(p.x), int(p.y)), 2)
if show_vortices:
for v in vortices:
alpha = 255 - int((v.age/v.lifetime)*200)
pygame.draw.circle(screen, (255,100,100), (int(v.x), int(v.y)), v.radius, 1)
draw_text(screen, f"Particles: {len(particles)}", 8, 8)
draw_text(screen, f"Strength: {params['strength']:.1f} Scale: {params['scale']:.5f} Speed: {params['speed']:.3f}", 8, 26)
draw_text(screen, f"Trail: {'on' if show_trails else 'off'} Field: {'on' if show_field else 'off'} Vortices: {'on' if show_vortices else 'off'}", 8, 44)
draw_text(screen, f"FPS: {int(clock.get_fps())}", 8, 62)
pygame.display.flip()
pygame.quit()
I managed to develop the beam of particles but with only lateral minor effects.



how to...are off-topic here.