Creating Yoyo Animation
3D Voxel YoYo Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a yo-yo using SpatialStudio.
The script creates a colorful yo-yo that bounces up and down with a realistic string 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 1 yo-yo with:
- Two circular disc halves forming the yo-yo body
- A realistic string that extends and retracts
- Metallic highlights for a glossy finish
- Smooth up-and-down bouncing motion
- Animates the yo-yo for 6 seconds at 30 FPS with realistic physics
- Outputs the file
yoyo.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
). -
Yo-yo body The yo-yo consists of two disc-shaped halves with a narrow connecting axle in the middle.
-
String physics The string length changes dynamically based on the yo-yo's position, simulating realistic string extension and retraction.
-
Bounce motion Uses sine wave functions to create smooth up-and-down motion with acceleration and deceleration.
-
Highlights Metallic highlights are added to the yo-yo discs to simulate light reflection and give it a realistic appearance.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring the motion loops smoothly with the yo-yo returning to its starting position. -
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 yoyo.py
and run:
python yoyo.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 6
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/yoyo.splv"
# Yo-yo settings
YOYO_RADIUS = 12
YOYO_THICKNESS = 4
AXLE_RADIUS = 3
MAX_STRING_LENGTH = 40
BOUNCE_AMPLITUDE = 30
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_yoyo_disc(volume, cx, cy, cz, radius, thickness, color, t):
"""Generate one half of the yo-yo (disc shape)"""
for dx in range(-radius, radius+1):
for dy in range(-thickness//2, thickness//2+1):
for dz in range(-radius, radius+1):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= radius:
# Add subtle texture variation
texture = int(np.sin(dx*0.2 + dz*0.2 + t*0.3) * 3)
brightness = 1.0 + texture * 0.05
final_color = tuple(min(255, int(c * brightness)) for c in color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, final_color)
def generate_yoyo_axle(volume, cx, cy, cz, radius, height, color):
"""Generate the central axle of the yo-yo"""
for dx in range(-radius, radius+1):
for dy in range(-height//2, height//2+1):
for dz in range(-radius, radius+1):
if dx*dx + dz*dz <= radius*radius:
add_voxel(volume, cx+dx, cy+dy, cz+dz, color)
def generate_string(volume, start_x, start_y, start_z, end_y, t):
"""Generate the yo-yo string with slight swaying motion"""
string_color = (139, 69, 19) # Brown string
string_length = abs(end_y - start_y)
for i in range(int(string_length)):
progress = i / string_length if string_length > 0 else 0
# Add slight swaying motion
sway = np.sin(t * 3.0 + progress * 8) * 2
x = start_x + int(sway * progress * 0.3)
y = start_y + i
z = start_z + int(np.sin(t * 2.0 + progress * 5) * 0.5)
add_voxel(volume, x, y, z, string_color)
def generate_yoyo_highlights(volume, cx, cy, cz, t):
"""Add metallic highlights to the yo-yo"""
highlight_color = (255, 255, 255)
# Top disc highlights
for dx in range(-3, 4):
for dz in range(-3, 4):
if dx*dx + dz*dz <= 9:
add_voxel(volume, cx-6+dx, cy-YOYO_THICKNESS+1, cz-4+dz, highlight_color)
# Bottom disc highlights
for dx in range(-3, 4):
for dz in range(-3, 4):
if dx*dx + dz*dz <= 9:
add_voxel(volume, cx-6+dx, cy+YOYO_THICKNESS-1, cz-4+dz, highlight_color)
def generate_yoyo_body(volume, cx, cy, cz, t):
"""Generate the complete yo-yo body"""
# Yo-yo colors (red and blue discs)
top_color = (220, 20, 60) # Crimson red
bottom_color = (30, 144, 255) # Dodger blue
axle_color = (169, 169, 169) # Dark gray
# Generate top disc
generate_yoyo_disc(volume, cx, cy - YOYO_THICKNESS//2, cz,
YOYO_RADIUS, YOYO_THICKNESS, top_color, t)
# Generate bottom disc
generate_yoyo_disc(volume, cx, cy + YOYO_THICKNESS//2, cz,
YOYO_RADIUS, YOYO_THICKNESS, bottom_color, t)
# Generate axle
generate_yoyo_axle(volume, cx, cy, cz, AXLE_RADIUS, YOYO_THICKNESS * 2, axle_color)
# Add highlights
generate_yoyo_highlights(volume, cx, cy, cz, t)
def calculate_yoyo_position(t):
"""Calculate yo-yo position based on realistic bouncing motion"""
# Use a combination of sine waves to create realistic yo-yo motion
# The yo-yo accelerates down and decelerates up
bounce_cycle = np.sin(t) * 0.5 + 0.5 # Normalize to 0-1
# Apply easing for more realistic motion
if bounce_cycle < 0.5:
# Going down - accelerating
eased = 2 * bounce_cycle * bounce_cycle
else:
# Going up - decelerating
eased = 1 - 2 * (1 - bounce_cycle) * (1 - bounce_cycle)
y_offset = int(BOUNCE_AMPLITUDE * eased)
return y_offset
def generate_scene(volume, t):
"""Generate the complete scene for one frame"""
# Calculate yo-yo position
y_offset = calculate_yoyo_position(t)
yoyo_y = CENTER_Y - BOUNCE_AMPLITUDE//2 + y_offset
# Generate yo-yo body
generate_yoyo_body(volume, CENTER_X, yoyo_y, CENTER_Z, t)
# Generate string from hand (top) to yo-yo
hand_y = CENTER_Y - BOUNCE_AMPLITUDE//2 - 20 # Hand position above yo-yo
generate_string(volume, CENTER_X, hand_y, CENTER_Z, yoyo_y - YOYO_THICKNESS, t)
# Add a simple "hand" at the top
hand_color = (255, 220, 177) # Skin color
for dx in range(-2, 3):
for dy in range(-3, 1):
for dz in range(-2, 3):
if dx*dx + dz*dz <= 4:
add_voxel(volume, CENTER_X+dx, hand_y+dy, CENTER_Z+dz, hand_color)
# 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 yo-yo animation"):
volume = np.zeros((SIZE, SIZE, SIZE, 4), dtype=np.uint8)
t = (frame / FRAMES) * 2*np.pi # Normalized time for smooth looping
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
BOUNCE_AMPLITUDE
to change how far the yo-yo travels - Modify
YOYO_RADIUS
andYOYO_THICKNESS
to create different yo-yo sizes - Change the disc colors by editing
top_color
andbottom_color
- Add spin rotation by modifying the highlight positions over time
- Create multiple yo-yos by calling
generate_yoyo_body()
with different positions - Experiment with different easing functions in
calculate_yoyo_position()
for varied motion styles