Creating Carousel Animation
Carousel Animation - 3D Voxel Learning Example
This guide walks you through how to generate a looping 3D voxel animation of a carousel using SpatialStudio.
The script creates a colorful rotating carousel with horses that bob up and down 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
- Builds a rotating carousel with:
- A circular platform base
- 6 carousel horses in different colors
- Vertical support poles for each horse
- A decorative top canopy
- Animates smooth rotation and horse bobbing motion for 8 seconds at 30 FPS
- Outputs the file
carousel.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
). -
Carousel base A circular platform is drawn using distance calculations from the center point.
-
Horses Simple voxel horses are positioned around the carousel perimeter, each with unique colors.
-
Rotation system All carousel elements rotate around the center axis using trigonometric functions.
-
Bobbing motion Horses move up and down independently using sine waves with different phase offsets.
-
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 carousel.py
and run:
python carousel.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/carousel.splv"
# Carousel settings
HORSE_COUNT = 6
CAROUSEL_RADIUS = 35
PLATFORM_RADIUS = 45
PLATFORM_HEIGHT = 4
HORSE_HEIGHT = 12
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_platform(volume, cx, cy, cz):
platform_color = (139, 69, 19) # Brown wood
edge_color = (160, 82, 45) # Lighter brown edge
for dx in range(-PLATFORM_RADIUS, PLATFORM_RADIUS+1):
for dz in range(-PLATFORM_RADIUS, PLATFORM_RADIUS+1):
dist = np.sqrt(dx*dx + dz*dz)
if dist <= PLATFORM_RADIUS:
for dy in range(PLATFORM_HEIGHT):
color = edge_color if dist > PLATFORM_RADIUS-2 else platform_color
add_voxel(volume, cx+dx, cy-dy, cz+dz, color)
def generate_horse(volume, cx, cy, cz, color):
# Horse body (simplified rectangular shape)
body_width, body_height, body_length = 4, 3, 8
# Body
for dx in range(-body_width//2, body_width//2+1):
for dy in range(body_height):
for dz in range(-body_length//2, body_length//2+1):
add_voxel(volume, cx+dx, cy+dy, cz+dz, color)
# Head
head_color = tuple(max(0, c-20) for c in color) # Slightly darker
for dx in range(-2, 3):
for dy in range(2, 6):
for dz in range(3, 7):
if dx*dx + (dy-3)*(dy-3) + (dz-4)*(dz-4) <= 4:
add_voxel(volume, cx+dx, cy+dy, cz+dz, head_color)
# Legs
leg_color = tuple(max(0, c-30) for c in color) # Darker for legs
leg_positions = [(-2, -6), (2, -6), (-2, 2), (2, 2)]
for lx, lz in leg_positions:
for dy in range(-6, 0):
add_voxel(volume, cx+lx, cy+dy, cz+lz, leg_color)
def generate_pole(volume, cx, cy, cz):
pole_color = (192, 192, 192) # Silver
pole_height = 20
for dy in range(-pole_height, HORSE_HEIGHT+5):
add_voxel(volume, cx, cy+dy, cz, pole_color)
# Add some thickness
add_voxel(volume, cx+1, cy+dy, cz, pole_color)
add_voxel(volume, cx, cy+dy, cz+1, pole_color)
def generate_canopy(volume, cx, cy, cz):
canopy_color = (255, 215, 0) # Gold
stripe_color = (220, 20, 60) # Crimson stripes
canopy_radius = PLATFORM_RADIUS + 5
for dx in range(-canopy_radius, canopy_radius+1):
for dz in range(-canopy_radius, canopy_radius+1):
dist = np.sqrt(dx*dx + dz*dz)
if dist <= canopy_radius:
# Create striped pattern
angle = np.arctan2(dz, dx)
stripe_index = int((angle + np.pi) / (np.pi/8)) % 2
color = stripe_color if stripe_index else canopy_color
for dy in range(2):
add_voxel(volume, cx+dx, cy+25+dy, cz+dz, color)
def generate_carousel(volume, cx, cy, cz, t):
horse_colors = [
(255, 182, 193), # Light pink
(135, 206, 250), # Light blue
(144, 238, 144), # Light green
(255, 218, 185), # Peach
(221, 160, 221), # Plum
(255, 255, 224), # Light yellow
]
# Generate platform
generate_platform(volume, cx, cy, cz)
# Generate canopy
generate_canopy(volume, cx, cy, cz)
# Generate horses and poles
for i in range(HORSE_COUNT):
# Base angle for this horse
base_angle = (i / HORSE_COUNT) * 2 * np.pi
# Add rotation over time
angle = base_angle + t * 0.5 # Rotate slowly
# Position around carousel
horse_x = cx + int(CAROUSEL_RADIUS * np.cos(angle))
horse_z = cz + int(CAROUSEL_RADIUS * np.sin(angle))
# Bobbing motion (each horse has different phase)
bob_offset = np.sin(t * 2.0 + i * np.pi/3) * 4
horse_y = cy + 8 + int(bob_offset)
# Generate pole
generate_pole(volume, horse_x, cy, horse_z)
# Generate horse
color = horse_colors[i % len(horse_colors)]
generate_horse(volume, horse_x, horse_y, horse_z, color)
def generate_decorative_lights(volume, cx, cy, cz, t):
light_colors = [(255, 255, 0), (255, 0, 255), (0, 255, 255)] # Yellow, Magenta, Cyan
light_radius = PLATFORM_RADIUS - 5
for i in range(12): # 12 lights around the edge
angle = (i / 12) * 2 * np.pi + t * 1.0 # Lights rotate faster
lx = cx + int(light_radius * np.cos(angle))
lz = cz + int(light_radius * np.sin(angle))
ly = cy + 10
# Twinkling effect
brightness = 0.7 + 0.3 * np.sin(t * 3.0 + i * 0.5)
color = light_colors[i % len(light_colors)]
final_color = tuple(int(c * brightness) for c in color)
# Add light voxel
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(volume, lx+dx, ly+dy, lz+dz, final_color)
def generate_scene(volume, t):
generate_carousel(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_decorative_lights(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 carousel"):
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
- Change
HORSE_COUNT
to add more horses to your carousel. - Edit
horse_colors
for different horse appearances. - Modify rotation speed by changing the multiplier in
angle = base_angle + t * 0.5
. - Add more decorative elements like flags or banners.
- Experiment with different bobbing patterns by adjusting the sine wave parameters.
- Try changing
CAROUSEL_RADIUS
to make a larger or smaller carousel.