Creating Whale Animation
This guide walks you through how to generate a looping 3D voxel animation of a whale using SpatialStudio.
The script creates a majestic whale that swims gracefully through a cubic 3D space, complete with flowing water particles and bubbles, then saves the animation to a .splv
file.
What this script does
-
Creates a 3D scene of size 128×128×128
-
Spawns 1 large whale with:
- A detailed voxel body with realistic proportions
- Animated tail fin that moves side to side
- Flowing pectoral fins
- Eye details and mouth definition
-
Adds environmental effects:
- Swimming water particles
- Rising bubble trails
- Gentle current motion
-
Animates the whale swimming in place for 8 seconds at 30 FPS
-
Outputs the file
whale.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
). -
Whale body The whale is constructed using elliptical shapes for the main body, with separate components for fins, tail, and head details.
-
Tail animation The tail fin oscillates side to side using sine waves to create realistic swimming motion.
-
Water effects Particle systems create flowing water streams and rising bubbles around the whale.
-
Body undulation Subtle sine-wave deformations along the whale's body simulate natural swimming 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 whale.py
and run:
python whale.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 8
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/whale.splv"
# Whale settings
WHALE_LENGTH = 40
WHALE_WIDTH = 12
WHALE_HEIGHT = 10
TAIL_SIZE = 8
def add_voxel(volume, x, y, z, color):
if 0 <= x < SIZE and 0 <= y < SIZE and 0 <= z < SIZE:
volume[x, y, z, :3] = color
volume[x, y, z, 3] = 255
def generate_whale_body(volume, cx, cy, cz, t):
whale_color = (70, 130, 180) # Steel blue
belly_color = (220, 220, 220) # Light gray
body_undulation = np.sin(t * 3.0) * 2
for x in range(-WHALE_LENGTH//2, WHALE_LENGTH//2):
# Body taper - wider in middle, narrower at ends
body_progress = abs(x) / (WHALE_LENGTH/2)
current_width = int(WHALE_WIDTH * (1 - body_progress * 0.7))
current_height = int(WHALE_HEIGHT * (1 - body_progress * 0.5))
# Add subtle body wave motion
wave_offset = int(np.sin(x * 0.1 + t * 2.0) * 3 + body_undulation)
for y in range(-current_height, current_height + 1):
for z in range(-current_width, current_width + 1):
# Elliptical body shape
if (y*y)/(current_height*current_height) + (z*z)/(current_width*current_width) <= 1:
final_x = cx + x
final_y = cy + y + wave_offset
final_z = cz + z
# Belly coloring (lighter on bottom)
if y < -current_height//3:
color = belly_color
else:
color = whale_color
add_voxel(volume, final_x, final_y, final_z, color)
def generate_whale_tail(volume, cx, cy, cz, t):
tail_color = (60, 110, 160)
# Tail animation - side to side movement
tail_swing = np.sin(t * 4.0) * 12
tail_base_x = cx - WHALE_LENGTH//2 - 5
tail_base_y = cy + int(np.sin(t * 2.0) * 2)
# Tail fin (horizontal)
for x in range(-8, 3):
for y in range(-TAIL_SIZE, TAIL_SIZE + 1):
for z in range(-2, 3):
# Tail fin shape
if abs(y) <= TAIL_SIZE - abs(x//2):
fin_x = tail_base_x + x
fin_y = tail_base_y + y
fin_z = cz + z + int(tail_swing * (8 + x) / 8)
add_voxel(volume, fin_x, fin_y, fin_z, tail_color)
def generate_whale_fins(volume, cx, cy, cz, t):
fin_color = (65, 115, 165)
fin_wave = np.sin(t * 2.5) * 3
# Pectoral fins (left and right)
for side in [-1, 1]:
fin_base_x = cx + 8
fin_base_y = cy - 3
fin_base_z = cz + side * (WHALE_WIDTH - 2)
for x in range(-6, 8):
for y in range(-3, 4):
for z in range(-2, 3):
# Fin shape - tapered
if abs(y) <= 3 - abs(x//3) and abs(z) <= 2:
fin_x = fin_base_x + x
fin_y = fin_base_y + y + int(fin_wave)
fin_z = fin_base_z + z * side
add_voxel(volume, fin_x, fin_y, fin_z, fin_color)
def generate_whale_details(volume, cx, cy, cz, t):
# Eye
eye_color = (0, 0, 0)
eye_x = cx + WHALE_LENGTH//4
eye_y = cy + 3
for side in [-1, 1]:
eye_z = cz + side * (WHALE_WIDTH - 3)
add_voxel(volume, eye_x, eye_y, eye_z, eye_color)
add_voxel(volume, eye_x, eye_y + 1, eye_z, eye_color)
# Mouth line
mouth_color = (40, 90, 130)
mouth_x = cx + WHALE_LENGTH//3
mouth_y = cy - WHALE_HEIGHT + 2
for z in range(-WHALE_WIDTH//2, WHALE_WIDTH//2):
add_voxel(volume, mouth_x, mouth_y, cz + z, mouth_color)
def generate_water_particles(volume, cx, cy, cz, t):
particle_color = (173, 216, 230) # Light blue
# Flowing water particles around whale
for i in range(150):
# Particle positions with flow motion
px = cx + int((i % 20 - 10) * 4 + np.sin(t * 2 + i * 0.1) * 8)
py = cy + int((i % 15 - 7) * 3 + np.cos(t * 1.5 + i * 0.2) * 5)
pz = cz + int((i % 12 - 6) * 4 + np.sin(t * 1.8 + i * 0.15) * 6)
# Only add particles outside whale body area
dist_to_whale = np.sqrt((px - cx)**2 + (py - cy)**2 + (pz - cz)**2)
if dist_to_whale > 15:
add_voxel(volume, px, py, pz, particle_color)
def generate_bubbles(volume, cx, cy, cz, t):
bubble_color = (255, 255, 255, 180) # Semi-transparent white
# Rising bubbles
for i in range(20):
bubble_life = (t + i * 0.3) % (2 * np.pi)
bx = cx + int(np.sin(i * 0.8) * 20)
by = cy - 20 + int(bubble_life * 8)
bz = cz + int(np.cos(i * 1.2) * 15)
# Bubble cluster
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:
bubble_x, bubble_y, bubble_z = bx + dx, by + dy, bz + dz
if 0 <= bubble_x < SIZE and 0 <= bubble_y < SIZE and 0 <= bubble_z < SIZE:
volume[bubble_x, bubble_y, bubble_z] = bubble_color
def generate_scene(volume, t):
# Generate whale components
generate_whale_body(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_whale_tail(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_whale_fins(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_whale_details(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
# Add environmental effects
generate_water_particles(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_bubbles(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating whale animation"):
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
WHALE_LENGTH
andWHALE_WIDTH
to change the whale's proportions. - Modify the
whale_color
variables to create different whale species. - Add more bubbles by increasing the range in
generate_bubbles()
. - Create a whale pod by duplicating the whale at different positions.
- Experiment with the tail swing speed by changing the multiplier in
np.sin(t * 4.0)
.