Creating Jellyfish Animation
Creating 3D Voxel Jellyfish Animation
This guide walks you through how to generate a looping 3D voxel animation of jellyfish using SpatialStudio.
The script creates graceful jellyfish that swim, pulse, and glow 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 6 jellyfish, each with:
- A translucent bell-shaped body that pulses
- Flowing tentacles that sway naturally
- Bioluminescent glow effects
- Smooth swimming motion through the water
- Animates them floating gracefully for 10 seconds at 30 FPS
- Outputs the file
jellyfish.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
). -
Jellyfish bell The main body is drawn as a dome with pulsing animation and semi-transparent appearance.
-
Tentacles Long, flowing appendages that wave and curl using sine functions for natural movement.
-
Bioluminescence Glowing edges and highlights that pulse with different intensities to simulate underwater lighting.
-
Swimming motion Each jellyfish follows a unique 3D path with gentle undulating movement.
-
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 jellyfish.py
and run:
python jellyfish.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 10
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/jellyfish.splv"
# Jellyfish settings
JELLYFISH_COUNT = 6
BELL_RADIUS = 12
TENTACLE_COUNT = 8
TENTACLE_LENGTH = 35
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 generate_bell(volume, cx, cy, cz, color, pulse, t):
bell_size = int(BELL_RADIUS * (1.0 + pulse * 0.3))
for dx in range(-bell_size, bell_size+1):
for dy in range(-bell_size//2, bell_size//3+1):
for dz in range(-bell_size, bell_size+1):
distance = np.sqrt(dx*dx + dz*dz)
# Create dome shape
if distance <= bell_size and dy <= 0:
height_factor = np.sqrt(max(0, bell_size*bell_size - distance*distance)) / bell_size
if dy >= -height_factor * bell_size//2:
# Add texture with sine waves
texture = np.sin(distance * 0.4 + t * 2.0) * 0.2 + 0.8
# Calculate transparency based on distance from center
alpha = int(120 + 60 * texture * (1.0 - distance/bell_size))
# Adjust color brightness
brightness = texture * (0.7 + 0.3 * pulse)
final_color = tuple(int(c * brightness) for c in color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, final_color, alpha)
def generate_tentacles(volume, cx, cy, cz, color, t, jelly_id):
for tent_id in range(TENTACLE_COUNT):
angle = (tent_id / TENTACLE_COUNT) * 2 * np.pi
# Tentacle base position around the bell
base_radius = BELL_RADIUS * 0.7
base_x = cx + int(base_radius * np.cos(angle))
base_z = cz + int(base_radius * np.sin(angle))
# Generate tentacle segments
for segment in range(TENTACLE_LENGTH):
progress = segment / TENTACLE_LENGTH
# Create flowing motion
sway_x = np.sin(t * 1.5 + jelly_id + tent_id * 0.8 + segment * 0.1) * progress * 4
sway_z = np.cos(t * 1.2 + jelly_id + tent_id * 0.6 + segment * 0.15) * progress * 3
# Tentacle position
tent_x = base_x + int(sway_x)
tent_y = cy - BELL_RADIUS//2 - segment
tent_z = base_z + int(sway_z)
# Tentacle gets thinner towards the tip
thickness = max(1, int(3 * (1 - progress * 0.8)))
# Add tentacle segments
for dx in range(-thickness, thickness+1):
for dz in range(-thickness, thickness+1):
if dx*dx + dz*dz <= thickness*thickness:
# Darken color towards the tip
fade = 1.0 - progress * 0.4
tent_color = tuple(int(c * fade) for c in color)
alpha = int(180 * fade)
add_voxel(volume, tent_x+dx, tent_y, tent_z+dz, tent_color, alpha)
def generate_bioluminescence(volume, cx, cy, cz, pulse, t):
glow_color = (100, 200, 255) # Cyan glow
intensity = int(pulse * 150 + 105)
# Glowing rim around the bell
for angle in range(0, 360, 15):
rad = np.radians(angle)
rim_x = cx + int(BELL_RADIUS * 0.9 * np.cos(rad))
rim_z = cz + int(BELL_RADIUS * 0.9 * np.sin(rad))
rim_y = cy - 2
# Add glow with some randomness
glow_offset = int(np.sin(t * 3 + angle * 0.1) * 2)
add_voxel(volume, rim_x, rim_y + glow_offset, rim_z, glow_color, intensity)
# Add smaller glow particles
for i in range(3):
offset_x = rim_x + np.random.randint(-2, 3)
offset_y = rim_y + np.random.randint(-1, 2)
offset_z = rim_z + np.random.randint(-2, 3)
add_voxel(volume, offset_x, offset_y, offset_z, glow_color, intensity//2)
def generate_jellyfish_swarm(volume, t):
# Jellyfish colors - various ocean tones
colors = [
(180, 120, 200), # Purple
(120, 180, 220), # Light blue
(200, 150, 100), # Orange
(150, 200, 180), # Mint
(220, 180, 200), # Pink
(140, 160, 200), # Lavender
]
for i in range(JELLYFISH_COUNT):
# Each jellyfish follows a unique swimming path
path_offset = (i * 2.1) # Spread them out in time
# Swimming motion in 3D space
swim_radius = 25 + 10 * np.sin(i * 0.7)
swim_x = CENTER_X + int(swim_radius * np.cos(t * 0.4 + path_offset))
swim_y = CENTER_Y + int(15 * np.sin(t * 0.6 + path_offset)) - 10
swim_z = CENTER_Z + int(swim_radius * np.sin(t * 0.3 + path_offset))
# Pulsing animation for swimming
pulse = (np.sin(t * 4.0 + i * 1.2) + 1.0) / 2.0
color = colors[i % len(colors)]
# Generate jellyfish components
generate_bell(volume, swim_x, swim_y, swim_z, color, pulse, t)
generate_tentacles(volume, swim_x, swim_y, swim_z, color, t, i)
generate_bioluminescence(volume, swim_x, swim_y, swim_z, pulse, t)
def add_water_particles(volume, t):
# Add floating particles to simulate underwater environment
particle_color = (80, 120, 160)
for i in range(50):
# Particles drift slowly through the scene
px = int((SIZE * 0.8) * np.sin(t * 0.2 + i * 0.4) + CENTER_X)
py = int((SIZE * 0.6) * np.cos(t * 0.15 + i * 0.6) + CENTER_Y)
pz = int((SIZE * 0.7) * np.sin(t * 0.25 + i * 0.8) + CENTER_Z)
add_voxel(volume, px, py, pz, particle_color, 60)
def generate_scene(volume, t):
add_water_particles(volume, t)
generate_jellyfish_swarm(volume, t)
# Initialize encoder
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
# Generate animation frames
for frame in tqdm(range(FRAMES), desc="Generating jellyfish"):
volume = np.zeros((SIZE, SIZE, SIZE, 4), dtype=np.uint8)
t = (frame / FRAMES) * 2 * np.pi
generate_scene(volume, t)
enc.encode(splv.Frame(volume, lrAxis="x", udAxis="y", fbAxis="z"))
enc.finish()
print(f"Created {OUT_PATH}")
Next steps
- Adjust
JELLYFISH_COUNT
to add more creatures to your underwater scene - Modify the
colors
array to create different species variations - Change
TENTACLE_LENGTH
andTENTACLE_COUNT
for different jellyfish types - Add
+ int(t * 10)
to Y positions to make jellyfish slowly rise to the surface - Experiment with
BELL_RADIUS
to create jellyfish of different sizes - Adjust bioluminescence colors for different lighting effects