Creating Disco Ball Animation
Disco Ball - 3D Voxel Animation Learning Example
This guide walks you through how to generate a looping 3D voxel animation of a disco ball using SpatialStudio.
The script creates a shimmering disco ball that rotates and reflects light, casting dancing sparkles 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 rotating disco ball with:
- A spherical mirrored surface made of tiny reflective tiles
- Rotating colored light beams that sweep across the scene
- Sparkling reflections that bounce around the environment
- A hanging chain or wire from the ceiling
- Animates the disco ball spinning for 8 seconds at 30 FPS
- Outputs the file
disco_ball.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
). -
Disco ball body The ball is drawn as a sphere covered with small mirror tiles that reflect light differently as it rotates.
-
Mirror tiles Each facet of the disco ball is a small reflective square that catches and bounces light at different angles.
-
Light beams Colored rays emanate from the disco ball, rotating and creating the classic disco lighting effect.
-
Sparkles Random bright spots appear throughout the scene to simulate reflected light dancing on walls.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring the rotation and lighting effects loop 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 disco_ball.py
and run:
python disco_ball.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/disco_ball.splv"
# Disco ball settings
BALL_RADIUS = 16
TILE_SIZE = 2
CHAIN_LENGTH = 30
LIGHT_BEAM_COUNT = 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_disco_ball(volume, cx, cy, cz, t):
base_color = (200, 200, 220) # Silver base
for dx in range(-BALL_RADIUS, BALL_RADIUS+1):
for dy in range(-BALL_RADIUS, BALL_RADIUS+1):
for dz in range(-BALL_RADIUS, BALL_RADIUS+1):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= BALL_RADIUS:
# Create mirror tile effect
tile_x = (dx + BALL_RADIUS) // TILE_SIZE
tile_y = (dy + BALL_RADIUS) // TILE_SIZE
tile_z = (dz + BALL_RADIUS) // TILE_SIZE
# Rotating reflection based on tile position and time
reflection = np.sin(t * 3.0 + tile_x * 0.5 + tile_y * 0.7 + tile_z * 0.3)
brightness = 0.6 + 0.4 * (reflection + 1) / 2
# Add some colorful reflections
if reflection > 0.7:
# Hot spots with colors
color_phase = (tile_x + tile_y + tile_z + t * 2) % 3
if color_phase < 1:
final_color = (255, int(180 * brightness), int(100 * brightness)) # Gold
elif color_phase < 2:
final_color = (int(100 * brightness), int(180 * brightness), 255) # Blue
else:
final_color = (255, int(100 * brightness), int(180 * brightness)) # Pink
else:
final_color = tuple(int(c * brightness) for c in base_color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, final_color)
def generate_hanging_chain(volume, cx, cy, cz, t):
chain_color = (100, 100, 100) # Dark gray
sway = np.sin(t * 1.5) * 2 # Gentle swaying
for i in range(CHAIN_LENGTH):
chain_y = cy + BALL_RADIUS + i
chain_x = cx + int(sway * (i / CHAIN_LENGTH)) # More sway at the top
chain_z = cz
# Chain links - small clusters
for dx in range(-1, 2):
for dz in range(-1, 2):
if abs(dx) + abs(dz) <= 1: # Cross pattern for chain links
add_voxel(volume, chain_x+dx, chain_y, chain_z+dz, chain_color)
def generate_light_beams(volume, cx, cy, cz, t):
beam_colors = [
(255, 0, 0), # Red
(0, 255, 0), # Green
(0, 0, 255), # Blue
(255, 255, 0), # Yellow
(255, 0, 255), # Magenta
(0, 255, 255), # Cyan
(255, 128, 0), # Orange
(128, 0, 255), # Purple
]
for i in range(LIGHT_BEAM_COUNT):
angle = (i / LIGHT_BEAM_COUNT) * 2*np.pi + t * 2.0 # Rotating beams
# Create beam direction
cos_a = np.cos(angle)
sin_a = np.sin(angle)
color = beam_colors[i % len(beam_colors)]
# Draw beam from disco ball outward
for distance in range(BALL_RADIUS + 5, SIZE//2, 2):
beam_x = cx + int(distance * cos_a)
beam_y = cy
beam_z = cz + int(distance * sin_a)
# Fade beam intensity with distance
intensity = max(0.1, 1.0 - (distance - BALL_RADIUS) / (SIZE//2))
faded_color = tuple(int(c * intensity) for c in color)
# Make beam slightly thick
for dx in range(-1, 2):
for dy in range(-2, 3):
for dz in range(-1, 2):
if abs(dx) + abs(dy) + abs(dz) <= 2:
add_voxel(volume, beam_x+dx, beam_y+dy, beam_z+dz, faded_color)
def generate_sparkles(volume, cx, cy, cz, t):
np.random.seed(int(t * 10) % 100) # Deterministic but changing sparkles
sparkle_count = 50
for i in range(sparkle_count):
# Random positions around the scene
sx = np.random.randint(10, SIZE-10)
sy = np.random.randint(10, SIZE-10)
sz = np.random.randint(10, SIZE-10)
# Twinkle effect
twinkle = np.sin(t * 8.0 + i * 0.1)
if twinkle > 0.5: # Only show sparkle when twinkling
brightness = int(255 * (twinkle - 0.5) * 2)
sparkle_color = (brightness, brightness, brightness)
# Small sparkle cluster
for dx in range(-1, 2):
for dy in range(-1, 2):
for dz in range(-1, 2):
if abs(dx) + abs(dy) + abs(dz) <= 1:
add_voxel(volume, sx+dx, sy+dy, sz+dz, sparkle_color)
def generate_scene(volume, t):
# Dark background for disco atmosphere
volume.fill(0)
# Generate all disco ball elements
generate_disco_ball(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_hanging_chain(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_light_beams(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_sparkles(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 disco ball"):
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
LIGHT_BEAM_COUNT
to create more or fewer light beams - Modify
beam_colors
to change the disco lighting palette - Increase
sparkle_count
for more dramatic sparkle effects - Change
BALL_RADIUS
to make the disco ball bigger or smaller - Add floor reflections by duplicating sparkles below the disco ball