Sculpting the Invisible: Rendering 3D Fluid Vector Fields in py5
2026/7/28
“Fluid Vector Field Topology 3D” is a generative media art piece that makes the invisible visible. It visualizes the hidden, churning currents of a three-dimensional mathematical space. Rather than drawing simple points or lines to represent particles flowing through a vector field, the artwork constructs physical, twisting ribbons of light. These 400 luminous ribbons snake through a dark navy void, caught in an infinitely complex, perfectly looping storm of OpenSimplex noise. By utilizing Python and the py5 library, the simulation translates the raw data of fluid dynamics and magnetic topology into a mesmerizing, cinematic ballet.
Visual & Aesthetic Approach
The aesthetic of “Fluid Vector Field Topology 3D” is grounded in the visual language of advanced scientific visualization, but saturated with the high-contrast lighting of a cyberpunk thriller. Set against a deep, crushing navy background, the artwork feels simultaneously like exploring the depths of the ocean and navigating a dense nebula.
The most striking feature of the piece is the physical presence of the ribbons. Unlike standard particles that leave thin, ghost-like trails, these ribbons have width, volume, and an orientation in 3D space. As they flow through the vector field, they twist and bank like airplanes caught in a crosswind, revealing the “curl” (the rotational force) of the invisible mathematical currents. The scene is lit by harsh directional lights — a piercing aqua from the top right and a deep electric purple from the bottom left. Because the ribbons are rendered with additive blending (py5.blend_mode(py5.ADD)), the dense knots where hundreds of paths intersect burn with blinding, hot-white intensity, highlighting the chaotic attractors hidden within the noise field.
Code & Technical Breakdown
At the heart of the simulation is a massive volume of 3D OpenSimplex noise, which acts as the invisible fluid driving the animation. The `Ribbon` class is responsible for sampling this noise to determine its velocity.
# From Ribbon class
def update(self, t):
# Calculate velocity from 3D noise
scale = 0.005
vx = py5.os_noise(self.pos[0]*scale, self.pos[1]*scale, self.pos[2]*scale + t) - 0.5
vy = py5.os_noise(self.pos[0]*scale + 100, self.pos[1]*scale, self.pos[2]*scale + t) - 0.5
vz = py5.os_noise(self.pos[0]*scale, self.pos[1]*scale + 100, self.pos[2]*scale + t) - 0.5
vel = np.array([vx, vy, vz]) * 20.0Why this works: To generate a 3D vector field, the code samples the py5.os_noise function three times — once for the $X$, $Y$, and $Z$ velocity components. To ensure these three values are independent (so the ribbon doesn’t just travel in a straight diagonal line), arbitrary offsets (+ 100) are added to the sampling coordinates. Subtracting 0.5 normalizes the noise output to a range of $[-0.5, 0.5]$, allowing the ribbons to travel backward as well as forward. By including the time variable t as an offset to the Z-axis of the noise space, the entire vector field slowly drifts and morphs over time, ensuring the currents never grow stale or predictable.
However, simply updating a particle’s position yields a thin line. To draw a twisting ribbon, the code must calculate the orientation (the normal vector) of the ribbon at every point.
# From Ribbon class
# Calculate a normal vector for the ribbon (curl approximation)
nx = py5.os_noise(self.pos[0]*scale, self.pos[1]*scale, self.pos[2]*scale - t) - 0.5
ny = py5.os_noise(self.pos[0]*scale - 100, self.pos[1]*scale, self.pos[2]*scale - t) - 0.5
nz = py5.os_noise(self.pos[0]*scale, self.pos[1]*scale - 100, self.pos[2]*scale - t) - 0.5
normal = np.array([nx, ny, nz])
normal = normal / (np.linalg.norm(normal) + 1e-5)Why this works: Calculating the true mathematical “curl” (the microscopic rotation) of a 3D noise field is computationally expensive. Instead, the simulation uses a highly effective aesthetic approximation: it simply samples the noise field a second time, using an inverted time variable (- t). This generates a secondary vector that is guaranteed to be related to the local space, but distinct from the velocity vector. Normalizing this vector ensures it has a length of exactly 1. This `normal` vector dictates which way the ribbon is “facing,” causing the geometric strip to twist and bank violently as it navigates the chaotic currents.
Finally, the geometry of the ribbon must be rendered to the screen. To do this efficiently, the code uses a py5.QUAD_STRIP.
# From draw()
py5.begin_shape(py5.QUAD_STRIP)
# ... color selection ...
for i in range(HISTORY_LEN):
p = r.history[i]
n = r.normals[i]
# Width fades out at the tail
w = ribbon_width * (i / HISTORY_LEN)
p1 = p + n * w
p2 = p - n * w
py5.vertex(p1[0], p1[1], p1[2])
py5.vertex(p2[0], p2[1], p2[2])
py5.end_shape()Why this works: Every ribbon maintains a short array of its past positions (r.history) and orientations (r.normals), acting as a geometric memory. A QUAD_STRIP is a highly optimized OpenGL rendering mode that builds a continuous ribbon of quadrilaterals by connecting pairs of vertices. By calculating p1 and p2 — points offset from the central path p by the normal vector n multiplied by the width w — the code generates the two edges of the ribbon. Furthermore, because w is multiplied by (i / HISTORY_LEN), the ribbon tapers to a sharp point at the tail, giving it an aggressive, aerodynamic profile that perfectly complements the high-speed motion.

Conclusion
“Fluid Vector Field Topology 3D” elegantly demonstrates how mathematical noise can be sculpted into physical form. By moving beyond simple point-based particle systems and calculating the local curl of the vector field, the artwork grants its digital currents volume, orientation, and a tangible presence. Through Python and py5, the simulation creates a hypnotic visual study of fluid dynamics — a cinematic ballet of light, math, and motion that reveals the chaotic beauty hidden just beneath the surface of the numbers.
GitHub: https://github.com/asamiile/py5-media-art/tree/main/sketch/fluid_vector_field_topology_3d
Purchase assets: Adobe Stock