Simulating Swirling Fluid Dynamics with Perlin Noise in Python

2026/9/16

In creative coding, some of the most compelling visuals emerge from simulating the chaotic, yet structured, flow of nature. In this sketch, Kinetic Fluid Dynamics Particles, I set out to simulate the mesmerizing movement of fluid dynamics. By deploying 15,000 individual particles into a procedurally generated vector field, the script captures the essence of swirling, turbulent water.Visually, the result is dense and cinematic. Thousands of glowing, oceanic-hued particles—ranging from bright aqua to deep teal—flow across a dark indigo canvas, leaving fading trails of light in their wake. Python, powered by the high-performance numerical operations of NumPy and the creative coding framework py5, is the perfect environment for this. It allows us to efficiently manage the kinematics of tens of thousands of particles in real-time, resulting in a buttery-smooth, high-resolution simulation.

Visual & Aesthetic Approach

The aesthetic goal was to evoke the feeling of looking into a deep, bioluminescent ocean current. Rather than rendering rigid shapes, the focus is entirely on the motion and history of the particles. To achieve this, the simulation relies on persistent, fading trails rather than clearing the screen completely each frame. By combining this technique with additive blending (py5.blend_mode(py5.ADD)), the areas where particles congregate and overlap become brilliantly illuminated, mimicking the density and energy of a high-pressure fluid eddy. The color palette remains strictly within deep oceanic hues, contrasting sharply with the deep indigo void to create a sense of vast depth.

Code & Technical Breakdown

Simulating 15,000 particles efficiently requires stepping away from traditional Python for loops where possible and utilizing NumPy for state initialization, combined with a highly optimized draw loop.

1. Vector Fields and Particle Kinematics

The fluid motion is driven by a vector field generated using Perlin noise. By sampling a 3D noise space (where the Z-axis is time), we can assign an angle of velocity to every point on the 2D canvas.
# Initialize 15,000 particles and their unique speeds
num_particles = 15000
noise_scale = 0.001
noise_z = 0

particles = np.zeros((num_particles, 2))
particles[:, 0] = np.random.uniform(0, py5.width, num_particles)
particles[:, 1] = np.random.uniform(0, py5.height, num_particles)

speeds = np.random.uniform(2, 8, num_particles)
In the draw() loop, each particle checks the noise field at its current location to determine which way the "current" is flowing.
# Inside the draw loop for each particle
x = particles[i, 0]
y = particles[i, 1]

# Calculate vector field angle using Perlin noise
angle = py5.os_noise(x * noise_scale, y * noise_scale, noise_z) * py5.TWO_PI * 4

# Move particle based on the angle and its individual speed
nx = x + np.cos(angle) * speeds[i]
ny = y + np.sin(angle) * speeds[i]
By multiplying the noise output by py5.TWO_PI * 4, we allow the angles to wrap multiple times, creating tight, swirling vortices rather than gentle, sweeping curves.

2. Creating Fading Trails with Alpha Blending

To create the signature long, glowing trails without keeping a massive history buffer for every particle, we use a classic generative art trick: drawing a highly transparent rectangle over the entire canvas every frame.
def draw():
    global noise_z
    
    # Slight fade for trails using a dark rect with very low alpha
    py5.blend_mode(py5.BLEND)
    py5.fill(5, 10, 20, 10) # Matches the deep indigo background, 10/255 opacity
    py5.no_stroke()
    py5.rect(0, 0, py5.width, py5.height)
    
    # Switch to additive blending for drawing the luminous particles
    py5.blend_mode(py5.ADD)
    py5.stroke_weight(2)
This effectively "dims" the previous frames slightly, leaving a fading history of where the particles have been. We then draw the current frame's movement as a line segment between the old position and the new position.
    # Draw line segment for trail
    py5.stroke(color_r[i], color_g[i], color_b[i], 30)
    py5.line(x, y, nx, ny)
    
    # Update state
    particles[i, 0] = nx
    particles[i, 1] = ny
    
# Animate the noise field slowly in the Z dimension
noise_z += 0.003 
By advancing noise_z slightly each frame, the entire vector field shifts and morphs, ensuring that the fluid dynamics never repeat and the particles are constantly driven into new, organic patterns.
kinetic fluid dynamics particles 2d p1

Conclusion

This sketch demonstrates the immense visual power of mapping simple kinematics to procedural noise. By leveraging NumPy for efficient particle management and py5 for cinematic alpha blending, we can transform basic math into a deep, swirling simulation of fluid dynamics that feels alive and unpredictable.