0

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:

  1. S1
  2. S2
  3. S3
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.

1
  • what is your question? Mind that ones of type how to... are off-topic here. Commented Oct 9 at 9:38

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.