Creating Clock Animation
3D Voxel Clock Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a working clock using SpatialStudio.
The script creates an animated clock with moving hands, numbered face, and ticking motion 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 functional clock with:
- A circular clock face with hour markers
- Numeric digits around the perimeter
- Moving hour, minute, and second hands
- A central pivot point
- Animates realistic time progression for 8 seconds at 30 FPS
- Outputs the file
clock.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
). -
Clock face A circular base is drawn with hour markers positioned at 12, 3, 6, and 9 o'clock positions.
-
Digital numbers Simple voxel-based numbers are rendered around the clock face perimeter.
-
Clock hands Three hands of different lengths rotate at realistic speeds:
- Second hand: completes one rotation per loop
- Minute hand: moves slowly based on seconds
- Hour hand: moves very slowly based on minutes
-
Animation loop Time
t
drives the rotation angles, with each hand moving at appropriate speeds for realistic timekeeping. -
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 clock.py
and run:
python clock.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/clock.splv"
# Clock settings
CLOCK_RADIUS = 35
HOUR_HAND_LENGTH = 20
MINUTE_HAND_LENGTH = 28
SECOND_HAND_LENGTH = 32
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 draw_circle(volume, cx, cy, cz, radius, color, thickness=2):
for angle in np.linspace(0, 2*np.pi, int(radius*6)):
for t in range(thickness):
x = cx + int((radius-t) * np.cos(angle))
y = cy + int((radius-t) * np.sin(angle))
add_voxel(volume, x, y, z+cz-CENTER_Z, color)
def draw_line(volume, x1, y1, z1, x2, y2, z2, color, thickness=1):
distance = max(abs(x2-x1), abs(y2-y1), abs(z2-z1))
if distance == 0:
return
for i in range(int(distance)+1):
t = i / distance
x = int(x1 + (x2-x1) * t)
y = int(y1 + (y2-y1) * t)
z = int(z1 + (z2-z1) * t)
for dx in range(-thickness//2, thickness//2+1):
for dy in range(-thickness//2, thickness//2+1):
add_voxel(volume, x+dx, y+dy, z, color)
def draw_number(volume, cx, cy, cz, number, color):
# Simple 3x5 pixel numbers
patterns = {
1: [(1,0), (1,1), (1,2), (1,3), (1,4)],
2: [(0,0), (1,0), (2,0), (2,1), (1,2), (0,2), (0,3), (1,4), (2,4)],
3: [(0,0), (1,0), (2,0), (2,1), (1,2), (2,2), (2,3), (1,4), (0,4)],
4: [(0,0), (0,1), (0,2), (1,2), (2,0), (2,1), (2,2), (2,3), (2,4)],
5: [(2,0), (1,0), (0,0), (0,1), (1,2), (2,2), (2,3), (1,4), (0,4)],
6: [(2,0), (1,0), (0,0), (0,1), (0,2), (1,2), (2,2), (2,3), (1,4), (0,4), (0,3)],
7: [(0,0), (1,0), (2,0), (2,1), (2,2), (2,3), (2,4)],
8: [(1,0), (0,1), (2,1), (1,2), (0,3), (2,3), (1,4), (0,0), (2,0), (0,4), (2,4)],
9: [(1,0), (2,1), (2,2), (1,2), (0,1), (0,0), (2,0), (2,3), (1,4), (0,4)],
12: [(0,0), (0,1), (0,2), (0,3), (0,4), (3,0), (4,0), (5,0), (5,1), (4,2), (3,2), (3,3), (4,4), (5,4)],
11: [(0,0), (0,1), (0,2), (0,3), (0,4), (2,0), (2,1), (2,2), (2,3), (2,4)]
}
if number in patterns:
for dx, dy in patterns[number]:
add_voxel(volume, cx+dx-1, cy+dy-2, cz, color)
def generate_clock_face(volume, cx, cy, cz):
# Main clock circle
draw_circle(volume, cx, cy, cz, CLOCK_RADIUS, (139, 69, 19), 3)
# Hour markers
for hour in range(12):
angle = (hour - 3) * np.pi / 6 # Start from 12 o'clock
marker_start = CLOCK_RADIUS - 5
marker_end = CLOCK_RADIUS - 2
x1 = cx + int(marker_start * np.cos(angle))
y1 = cy + int(marker_start * np.sin(angle))
x2 = cx + int(marker_end * np.cos(angle))
y2 = cy + int(marker_end * np.sin(angle))
draw_line(volume, x1, y1, cz, x2, y2, cz, (255, 255, 255), 2)
# Numbers at key positions
number_positions = [(0, 12), (3, 3), (6, 6), (9, 9)]
for hour, display_num in number_positions:
angle = (hour - 3) * np.pi / 6
num_radius = CLOCK_RADIUS - 12
nx = cx + int(num_radius * np.cos(angle))
ny = cy + int(num_radius * np.sin(angle))
draw_number(volume, nx, ny, cz, display_num, (255, 255, 255))
# Center pivot
for dx in range(-2, 3):
for dy in range(-2, 3):
if dx*dx + dy*dy <= 4:
add_voxel(volume, cx+dx, cy+dy, cz, (255, 215, 0))
def generate_clock_hands(volume, cx, cy, cz, t):
# Calculate angles (t goes from 0 to 2π over the animation)
second_angle = t # Full rotation per loop
minute_angle = t / 60 # Much slower
hour_angle = t / 720 # Even slower
# Adjust angles to start at 12 o'clock
second_angle -= np.pi/2
minute_angle -= np.pi/2
hour_angle -= np.pi/2
# Hour hand (thick, gold)
hx = cx + int(HOUR_HAND_LENGTH * np.cos(hour_angle))
hy = cy + int(HOUR_HAND_LENGTH * np.sin(hour_angle))
draw_line(volume, cx, cy, cz+1, hx, hy, cz+1, (255, 215, 0), 3)
# Minute hand (medium, silver)
mx = cx + int(MINUTE_HAND_LENGTH * np.cos(minute_angle))
my = cy + int(MINUTE_HAND_LENGTH * np.sin(minute_angle))
draw_line(volume, cx, cy, cz+2, mx, my, cz+2, (192, 192, 192), 2)
# Second hand (thin, red)
sx = cx + int(SECOND_HAND_LENGTH * np.cos(second_angle))
sy = cy + int(SECOND_HAND_LENGTH * np.sin(second_angle))
draw_line(volume, cx, cy, cz+3, sx, sy, cz+3, (255, 0, 0), 1)
def generate_scene(volume, t):
generate_clock_face(volume, CENTER_X, CENTER_Y, CENTER_Z)
generate_clock_hands(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 clock"):
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
SECONDS
to create longer time sequences. - Modify hand speeds by adjusting the division factors (60, 720).
- Add Roman numerals by creating new patterns in the
draw_number
function. - Create a pendulum by adding a swinging weight below the clock.
- Add chimes or sound markers by changing colors at specific times.