Creating Lissajous Animation
Lissajous Curves - 3D Voxel Animation Learning Example
This guide walks you through how to generate a looping 3D voxel animation of lissajous curves using SpatialStudio.
The script creates beautiful mathematical curves that trace elegant paths through 3D space, then saves the animation to a .splv
file.
What this script does
- Creates a 3D scene of size 128×128×128
- Generates multiple lissajous curves, each with:
- Smooth parametric motion following the equations: x=sin(at), y=sin(bt), z=sin(ct)
- Trailing particle effects that fade over time
- Vibrant colors that shift as the curves evolve
- Animates the mathematical curves for 8 seconds at 30 FPS
- Outputs the file
lissajous.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 equations The curves follow parametric equations where different frequency ratios create unique 3D patterns.
-
Particle trails Each curve leaves behind glowing particles that gradually fade, creating beautiful trail effects.
-
Color cycling Colors shift smoothly through the HSV spectrum as the curves trace their paths.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring the motion loops smoothly. -
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 lissajous.py
and run:
python lissajous.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
import colorsys
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 8
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/lissajous.splv"
# Lissajous settings
CURVE_COUNT = 5
TRAIL_LENGTH = 60
CURVE_SCALE = 40
def add_voxel(volume, x, y, z, color, alpha=255):
if 0 <= x < SIZE and 0 <= y < SIZE and 0 <= z < SIZE:
volume[x, y, z, :3] = color
volume[x, y, z, 3] = alpha
def hsv_to_rgb(h, s, v):
r, g, b = colorsys.hsv_to_rgb(h, s, v)
return (int(r * 255), int(g * 255), int(b * 255))
def generate_lissajous_point(t, curve_id):
# Different frequency ratios for each curve
freq_ratios = [
(1, 2, 3), (2, 3, 1), (3, 1, 2),
(1, 3, 2), (2, 1, 3)
]
a, b, c = freq_ratios[curve_id % len(freq_ratios)]
# Phase offsets for variety
phase_x = curve_id * 0.5
phase_y = curve_id * 0.7
phase_z = curve_id * 0.3
x = np.sin(a * t + phase_x) * CURVE_SCALE
y = np.sin(b * t + phase_y) * CURVE_SCALE
z = np.sin(c * t + phase_z) * CURVE_SCALE
return (
int(CENTER_X + x),
int(CENTER_Y + y),
int(CENTER_Z + z)
)
def generate_curve_trail(volume, curve_id, current_t, frame):
trail_points = []
# Generate trail points going back in time
for i in range(TRAIL_LENGTH):
trail_t = current_t - (i * 0.1)
x, y, z = generate_lissajous_point(trail_t, curve_id)
# Calculate fade based on distance from head
fade = 1.0 - (i / TRAIL_LENGTH)
fade = fade ** 2 # Exponential fade for better visual effect
# Color shifts over time and varies by curve
hue = (current_t * 0.1 + curve_id * 0.2) % 1.0
saturation = 0.8 + 0.2 * np.sin(current_t * 2 + curve_id)
value = fade * (0.5 + 0.5 * np.sin(current_t * 1.5 + i * 0.1))
color = hsv_to_rgb(hue, saturation, value)
alpha = int(fade * 255)
# Add glow effect around main point
for dx in range(-2, 3):
for dy in range(-2, 3):
for dz in range(-2, 3):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= 2:
glow_fade = (2 - distance) / 2
glow_alpha = int(alpha * glow_fade)
if glow_alpha > 0:
add_voxel(volume, x+dx, y+dy, z+dz, color, glow_alpha)
def generate_curve_head(volume, curve_id, t):
x, y, z = generate_lissajous_point(t, curve_id)
# Bright head color
hue = (t * 0.1 + curve_id * 0.2) % 1.0
color = hsv_to_rgb(hue, 1.0, 1.0)
# Larger, brighter head
for dx in range(-3, 4):
for dy in range(-3, 4):
for dz in range(-3, 4):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= 3:
brightness = max(0, 1 - distance/3)
bright_color = tuple(int(c * brightness) for c in color)
add_voxel(volume, x+dx, y+dy, z+dz, bright_color)
def add_sparkle_effects(volume, t):
# Add some random sparkles for visual interest
sparkle_count = 20
for i in range(sparkle_count):
# Pseudo-random positions based on time and index
seed = t * 10 + i * 137 # 137 for better distribution
sx = int((np.sin(seed * 0.7) * 0.4 + 0.5) * SIZE)
sy = int((np.sin(seed * 0.9) * 0.4 + 0.5) * SIZE)
sz = int((np.sin(seed * 1.1) * 0.4 + 0.5) * SIZE)
# Sparkle brightness varies over time
sparkle_brightness = (np.sin(t * 5 + i) + 1) * 0.5
if sparkle_brightness > 0.7: # Only show bright sparkles
alpha = int((sparkle_brightness - 0.7) * 255 / 0.3)
add_voxel(volume, sx, sy, sz, (255, 255, 255), alpha)
def generate_scene(volume, t, frame):
# Generate all curves
for curve_id in range(CURVE_COUNT):
generate_curve_trail(volume, curve_id, t, frame)
generate_curve_head(volume, curve_id, t)
# Add sparkle effects
add_sparkle_effects(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating lissajous curves"):
volume = np.zeros((SIZE, SIZE, SIZE, 4), dtype=np.uint8)
t = (frame / FRAMES) * 4*np.pi # Two full cycles for more complex patterns
generate_scene(volume, t, frame)
enc.encode(splv.Frame(volume, lrAxis="x", udAxis="y", fbAxis="z"))
enc.finish()
print(f"Created {OUT_PATH}")
Next steps
- Change
CURVE_COUNT
to generate more or fewer curves. - Modify the
freq_ratios
to create different mathematical patterns. - Adjust
TRAIL_LENGTH
to make longer or shorter particle trails. - Experiment with
CURVE_SCALE
to make the curves larger or smaller. - Try different color schemes by modifying the HSV values.
Mathematical background
Lissajous curves are the result of combining two or more sinusoidal motions. In 3D, we use three equations:
- X(t) = sin(a×t + φₓ)
- Y(t) = sin(b×t + φᵧ)
- Z(t) = sin(c×t + φᵤ)
The relationship between the frequencies (a, b, c) determines the shape complexity. When these ratios are simple integers, you get closed loops. More complex ratios create intricate, never-quite-repeating patterns that are mesmerizing to watch!