Creating Metronome Animation
This guide walks you through how to generate a looping 3D voxel animation of a metronome using SpatialStudio.
The script creates a realistic metronome with a swinging arm, ticking pendulum motion, and musical timing 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 metronome, complete with:
- A solid wooden base
- A pendulum arm that swings back and forth
- A weighted bob at the end
- Tempo markings on the body
- Animates the pendulum swinging rhythmically for 8 seconds at 30 FPS
- Outputs the file
metronome.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
). -
Metronome base The body is drawn as a pyramidal wooden structure with a textured brown surface.
-
Pendulum arm A thin rod that rotates smoothly using sine wave mathematics to create realistic pendulum motion.
-
Weighted bob A metallic sphere at the end of the arm that follows the pendulum's arc.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, with the pendulum swinging at musical tempo (120 BPM). -
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 metronome.py
and run:
python metronome.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/metronome.splv"
# Metronome settings
BASE_WIDTH = 20
BASE_HEIGHT = 40
ARM_LENGTH = 35
BOB_RADIUS = 4
SWING_ANGLE = 45 # degrees
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_base(volume, cx, cy, cz, t):
wood_color = (139, 90, 43)
# Create pyramidal base
for y in range(BASE_HEIGHT):
width_at_height = BASE_WIDTH - (y * BASE_WIDTH // (BASE_HEIGHT * 2))
for dx in range(-width_at_height, width_at_height + 1):
for dz in range(-width_at_height, width_at_height + 1):
# Add wood grain texture
grain = int(np.sin(y * 0.3 + dx * 0.2) * 10)
textured_color = tuple(max(20, min(255, c + grain)) for c in wood_color)
add_voxel(volume, cx + dx, cy - BASE_HEIGHT//2 + y, cz + dz, textured_color)
def generate_tempo_markings(volume, cx, cy, cz):
marking_color = (50, 50, 50)
# Add some tempo markings on the front face
for i in range(5):
y_pos = cy - BASE_HEIGHT//4 + i * 3
for x in range(-1, 2):
add_voxel(volume, cx + BASE_WIDTH//2 - 1, y_pos, cz + x, marking_color)
def generate_pendulum_arm(volume, cx, cy, cz, angle):
arm_color = (100, 100, 100) # Metallic gray
# Calculate arm position based on angle
arm_x = int(ARM_LENGTH * np.sin(np.radians(angle)))
arm_y_end = cy + BASE_HEIGHT//4
# Draw the arm from pivot point to end
steps = ARM_LENGTH
for i in range(steps):
t_param = i / steps
x = cx + int(arm_x * t_param)
y = int(cy + (arm_y_end - cy) * t_param)
z = cz
add_voxel(volume, x, y, z, arm_color)
# Make arm slightly thicker
add_voxel(volume, x + 1, y, z, arm_color)
def generate_pendulum_bob(volume, cx, cy, cz, angle):
bob_color = (180, 180, 180) # Bright metallic
# Calculate bob position
bob_x = cx + int(ARM_LENGTH * np.sin(np.radians(angle)))
bob_y = cy + BASE_HEIGHT//4
# Draw spherical bob
for dx in range(-BOB_RADIUS, BOB_RADIUS + 1):
for dy in range(-BOB_RADIUS, BOB_RADIUS + 1):
for dz in range(-BOB_RADIUS, BOB_RADIUS + 1):
if dx*dx + dy*dy + dz*dz <= BOB_RADIUS*BOB_RADIUS:
# Add metallic highlight
if dx < 0 and dy > 0: # Upper left gets highlight
highlight_color = (220, 220, 220)
add_voxel(volume, bob_x + dx, bob_y + dy, cz + dz, highlight_color)
else:
add_voxel(volume, bob_x + dx, bob_y + dy, cz + dz, bob_color)
def generate_pivot_point(volume, cx, cy, cz):
pivot_color = (80, 80, 80)
pivot_y = cy + BASE_HEIGHT//4
# Small pivot mechanism
for dx in range(-2, 3):
for dz in range(-2, 3):
if dx*dx + dz*dz <= 4:
add_voxel(volume, cx + dx, pivot_y, cz + dz, pivot_color)
def generate_scene(volume, t):
# Calculate pendulum angle (120 BPM = 2 Hz)
angle = SWING_ANGLE * np.sin(t * 4.0) # 4.0 for 2 complete swings per 2π
generate_base(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_tempo_markings(volume, CENTER_X, CENTER_Y, CENTER_Z)
generate_pendulum_arm(volume, CENTER_X, CENTER_Y, CENTER_Z, angle)
generate_pendulum_bob(volume, CENTER_X, CENTER_Y, CENTER_Z, angle)
generate_pivot_point(volume, CENTER_X, CENTER_Y, CENTER_Z)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating metronome"):
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
SWING_ANGLE
to make the pendulum swing wider or narrower. - Change the timing multiplier in
t * 4.0
to speed up or slow down the tempo. - Modify
wood_color
andbob_color
for different materials. - Add a face plate with numbers by expanding the
generate_tempo_markings
function. - Try adding a subtle "tick" sound visualization with particle effects.