Creating Fireflies Lissajous Fast.Splv Animation
3D Voxel Animation Guide: Fireflies Following Lissajous Curves
This guide walks you through how to generate a looping 3D voxel animation of fireflies using SpatialStudio.
The script creates glowing fireflies that follow smooth mathematical paths called Lissajous curves, creating mesmerizing patterns inside a cubic 3D space, then saves the animation to a .splv
file.
What this script does
-
Creates a 3D scene of size 128×128×128
-
Spawns 12 fireflies, each with:
- A bright glowing core
- Trailing particle effects
- Dynamic color variations
- Smooth mathematical flight paths
-
Animates them following Lissajous curves for 6 seconds at 60 FPS (fast motion)
-
Outputs the file
fireflies_lissajous_fast.splv
that you can play in your viewer
How it works (simplified)
-
Voxel volume Each frame is a 3D grid filled with RGBA values (
SIZE × SIZE × SIZE × 4
). -
Lissajous curves Each firefly follows a unique 3D mathematical path using sine and cosine functions with different frequencies, creating figure-8 and orbital patterns.
-
Glowing effect Fireflies are rendered with bright centers and dimmer halos that fade outward, simulating bioluminescence.
-
Particle trails Each firefly leaves a fading trail of smaller particles that create streaking light effects.
-
Fast animation High frame rate (60 FPS) and rapid parameter changes create smooth, fast-moving patterns.
-
Encoding Frames are passed into
splv.Encoder
, which writes them into the.splv
video file.
Try it yourself
Install requirements first:
pip install spatialstudio numpy tqdm
Then copy this script into fireflies_lissajous_fast.py
and run:
python fireflies_lissajous_fast.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
import math
# Scene setup
SIZE, FPS, SECONDS = 128, 60, 6
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/fireflies_lissajous_fast.splv"
# Firefly settings
FIREFLY_COUNT = 12
GLOW_RADIUS = 4
TRAIL_LENGTH = 8
def add_voxel_with_alpha(volume, x, y, z, color, alpha):
if 0 <= x < SIZE and 0 <= y < SIZE and 0 <= z < SIZE:
volume[x, y, z, :3] = color
volume[x, y, z, 3] = min(255, max(0, int(alpha)))
def generate_firefly_glow(volume, cx, cy, cz, color, intensity, t):
# Create glowing sphere with falloff
for dx in range(-GLOW_RADIUS, GLOW_RADIUS+1):
for dy in range(-GLOW_RADIUS, GLOW_RADIUS+1):
for dz in range(-GLOW_RADIUS, GLOW_RADIUS+1):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= GLOW_RADIUS:
# Calculate glow falloff
falloff = max(0, 1.0 - (distance / GLOW_RADIUS))
alpha = intensity * falloff * falloff * 255
# Add subtle flicker
flicker = 0.8 + 0.2 * np.sin(t * 15.0 + cx * 0.1)
alpha *= flicker
add_voxel_with_alpha(volume, cx+dx, cy+dy, cz+dz, color, alpha)
def calculate_lissajous_position(firefly_id, t):
# Each firefly gets unique frequency parameters
base_freq = 2.0 + firefly_id * 0.3
# Lissajous curve parameters (different for each firefly)
ax = base_freq * (1.0 + 0.2 * (firefly_id % 3))
ay = base_freq * (1.0 + 0.3 * ((firefly_id + 1) % 4))
az = base_freq * (1.0 + 0.1 * ((firefly_id + 2) % 5))
# Phase offsets for variety
px = firefly_id * 0.5
py = firefly_id * 0.7
pz = firefly_id * 0.9
# Calculate 3D Lissajous position
amplitude = 35
x = CENTER_X + amplitude * np.sin(ax * t + px)
y = CENTER_Y + amplitude * np.sin(ay * t + py)
z = CENTER_Z + amplitude * np.sin(az * t + pz)
return int(x), int(y), int(z)
def generate_firefly_trail(volume, positions, color, firefly_id, t):
# Draw fading trail from position history
for i, (x, y, z) in enumerate(positions):
if x is not None: # Valid position
age_factor = (i + 1) / len(positions) # Newer = higher value
alpha = age_factor * 120 # Fade trail
trail_color = tuple(int(c * age_factor) for c in color)
# Add small trail particles
for dx in range(-1, 2):
for dy in range(-1, 2):
for dz in range(-1, 2):
if dx*dx + dy*dy + dz*dz <= 1:
add_voxel_with_alpha(volume, x+dx, y+dy, z+dz, trail_color, alpha)
def generate_fireflies(volume, t):
# Color palette for different fireflies
colors = [
(255, 255, 150), # Warm yellow
(150, 255, 150), # Green
(255, 200, 100), # Orange
(200, 200, 255), # Cool white
(255, 150, 255), # Magenta
(150, 255, 255), # Cyan
]
# Store positions for trails (in real implementation, you'd maintain this across frames)
for firefly_id in range(FIREFLY_COUNT):
# Calculate current position using Lissajous curves
x, y, z = calculate_lissajous_position(firefly_id, t)
# Select color
color = colors[firefly_id % len(colors)]
# Calculate intensity based on speed and time
speed_factor = 1.0 + 0.3 * np.sin(t * 8.0 + firefly_id)
intensity = 0.7 + 0.3 * speed_factor
# Generate the glowing firefly
generate_firefly_glow(volume, x, y, z, color, intensity, t)
# Generate trail effect (simplified - just previous few positions)
trail_positions = []
for trail_step in range(TRAIL_LENGTH):
trail_t = t - (trail_step * 0.02) # Look back in time
if trail_t >= 0:
tx, ty, tz = calculate_lissajous_position(firefly_id, trail_t)
trail_positions.append((tx, ty, tz))
else:
trail_positions.append((None, None, None))
generate_firefly_trail(volume, trail_positions, color, firefly_id, t)
def add_ambient_particles(volume, t):
# Add some floating ambient particles for atmosphere
particle_count = 20
for i in range(particle_count):
# Slow-moving background particles
x = CENTER_X + int(20 * np.sin(t * 0.5 + i * 2.1))
y = CENTER_Y + int(15 * np.sin(t * 0.3 + i * 1.7))
z = CENTER_Z + int(18 * np.sin(t * 0.4 + i * 2.3))
# Dim, slowly pulsing particles
alpha = 30 + 20 * np.sin(t * 2.0 + i * 0.8)
color = (100, 100, 120) # Dim blue-gray
add_voxel_with_alpha(volume, x, y, z, color, alpha)
def generate_scene(volume, t):
generate_fireflies(volume, t)
add_ambient_particles(volume, t)
# Initialize encoder
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
print("Generating fast-moving fireflies following Lissajous curves...")
for frame in tqdm(range(FRAMES), desc="Rendering fireflies"):
volume = np.zeros((SIZE, SIZE, SIZE, 4), dtype=np.uint8)
# Fast time progression for quick movement
t = (frame / FRAMES) * 4 * np.pi # 2 complete cycles
generate_scene(volume, t)
enc.encode(splv.Frame(volume, lrAxis="x", udAxis="y", fbAxis="z"))
enc.finish()
print(f"Created {OUT_PATH}")
print(f"Animation: {FRAMES} frames at {FPS} FPS ({SECONDS} seconds)")
print(f"Features: {FIREFLY_COUNT} fireflies with Lissajous curve paths")
Understanding Lissajous Curves
Lissajous curves are mathematical patterns created by combining two or more sinusoidal motions. In 3D space, they create beautiful looping paths that can look like:
- Figure-8 patterns
- Orbital motions
- Rose curves
- Complex knot-like shapes
The formula for each firefly's position is:
x = center_x + amplitude × sin(freq_x × time + phase_x)
y = center_y + amplitude × sin(freq_y × time + phase_y)
z = center_z + amplitude × sin(freq_z × time + phase_z)
By varying the frequencies and phases for each firefly, we get a rich variety of interweaving patterns.
Next steps
- Change
FIREFLY_COUNT
to add more or fewer fireflies - Modify frequency multipliers in
calculate_lissajous_position()
for different curve shapes - Adjust
FPS
and animation speed for different motion effects - Experiment with
colors
array for different firefly appearances - Try different
amplitude
values for larger or smaller flight patterns