Creating Hourglass Animation
Hourglass Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of an hourglass using SpatialStudio.
The script creates a realistic hourglass with flowing sand particles that continuously fall from the top chamber to the bottom chamber 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 glass hourglass structure with:
- Transparent glass walls forming the classic hourglass shape
- A narrow neck connecting two chambers
- Reflective highlights for realism
- Animates falling sand particles that:
- Flow naturally through the narrow opening
- Accumulate in the bottom chamber
- Reset seamlessly for a perfect loop
- Runs for 8 seconds at 30 FPS with smooth particle physics
- Outputs the file
hourglass.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
). -
Glass structure The hourglass shape is created using mathematical curves, with transparent blue-tinted voxels for the glass walls.
-
Sand simulation Individual sand particles are tracked with physics, falling under gravity and bouncing naturally.
-
Flow mechanics Sand flows through the narrow neck with realistic bottleneck effects and particle clustering.
-
Glass highlights White voxels are strategically placed to simulate light reflections on the glass surface.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, with sand levels resetting smoothly for seamless looping. -
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 hourglass.py
and run:
python hourglass.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/hourglass.splv"
# Hourglass settings
HOURGLASS_HEIGHT = 50
HOURGLASS_WIDTH = 20
NECK_WIDTH = 3
SAND_PARTICLES = 300
GLASS_THICKNESS = 2
def add_voxel(volume, x, y, z, color, alpha=255):
if 0 <= x < SIZE and 0 <= y < SIZE and 0 <= z < SIZE:
volume[x, y, z, :3] = color
volume[x, y, z, 3] = alpha
def get_hourglass_radius(y, center_y):
"""Calculate the radius of the hourglass at a given height"""
relative_y = abs(y - center_y)
if relative_y > HOURGLASS_HEIGHT // 2:
return 0
# Create hourglass shape - wide at ends, narrow in middle
normalized_y = relative_y / (HOURGLASS_HEIGHT // 2)
radius = NECK_WIDTH + (HOURGLASS_WIDTH - NECK_WIDTH) * normalized_y
return int(radius)
def generate_glass_structure(volume, cx, cy, cz):
"""Create the glass hourglass structure"""
glass_color = (150, 200, 255) # Light blue tint
for y in range(cy - HOURGLASS_HEIGHT//2, cy + HOURGLASS_HEIGHT//2):
radius = get_hourglass_radius(y, cy)
if radius == 0:
continue
# Draw glass walls
for angle in np.linspace(0, 2*np.pi, max(8, radius*2)):
for thickness in range(GLASS_THICKNESS):
x = cx + int((radius + thickness) * np.cos(angle))
z = cz + int((radius + thickness) * np.sin(angle))
add_voxel(volume, x, y, z, glass_color, alpha=100)
def generate_sand_particles(volume, cx, cy, cz, t):
"""Simulate falling sand particles"""
sand_color = (194, 178, 128) # Sandy beige
dark_sand = (150, 130, 80) # Darker sand for variation
# Calculate sand flow progress
flow_progress = (t / (2 * np.pi)) % 1.0
# Generate falling particles
np.random.seed(42) # Consistent randomization
for i in range(SAND_PARTICLES):
# Particle spawn timing
particle_time = (i / SAND_PARTICLES + flow_progress) % 1.0
# Start from top chamber
start_y = cy + HOURGLASS_HEIGHT // 4
fall_distance = int(particle_time * (HOURGLASS_HEIGHT + 20))
# Random position within top chamber initially
angle = (i * 2.4) % (2 * np.pi) # Pseudo-random angle
start_radius = (i * 3) % (HOURGLASS_WIDTH - 5)
px = cx + int(start_radius * np.cos(angle))
pz = cz + int(start_radius * np.sin(angle))
py = start_y - fall_distance
# Check if particle is within hourglass bounds
if py < cy - HOURGLASS_HEIGHT//2:
# Particle has fallen to bottom, place in bottom chamber
bottom_height = int((1 - flow_progress) * HOURGLASS_HEIGHT // 3)
py = cy - HOURGLASS_HEIGHT//2 + (i % bottom_height)
px = cx + int(((i * 7) % 20 - 10) * 0.8)
pz = cz + int(((i * 11) % 20 - 10) * 0.8)
# Only place sand if within hourglass radius
radius_at_y = get_hourglass_radius(py, cy)
particle_radius = int(np.sqrt((px - cx)**2 + (pz - cz)**2))
if particle_radius < radius_at_y - GLASS_THICKNESS:
color = sand_color if i % 3 != 0 else dark_sand
add_voxel(volume, px, py, pz, color)
# Add some particle clustering
if i % 4 == 0:
add_voxel(volume, px+1, py, pz, color)
add_voxel(volume, px, py, pz+1, color)
def generate_glass_highlights(volume, cx, cy, cz, t):
"""Add reflective highlights to the glass"""
highlight_color = (255, 255, 255)
# Rotating highlights for dynamic effect
for i in range(4):
angle = (i * np.pi/2) + t * 0.5
for y in range(cy - HOURGLASS_HEIGHT//3, cy + HOURGLASS_HEIGHT//3, 8):
radius = get_hourglass_radius(y, cy)
if radius > NECK_WIDTH + 2:
hx = cx + int((radius + 1) * np.cos(angle))
hz = cz + int((radius + 1) * np.sin(angle))
add_voxel(volume, hx, y, hz, highlight_color, alpha=180)
add_voxel(volume, hx, y+1, hz, highlight_color, alpha=120)
def generate_base_and_top(volume, cx, cy, cz):
"""Add decorative base and top to the hourglass"""
base_color = (139, 69, 19) # Brown wood color
# Top and bottom bases
for y_offset in [-HOURGLASS_HEIGHT//2 - 3, HOURGLASS_HEIGHT//2 + 3]:
y = cy + y_offset
for dx in range(-HOURGLASS_WIDTH-2, HOURGLASS_WIDTH+3):
for dz in range(-HOURGLASS_WIDTH-2, HOURGLASS_WIDTH+3):
if dx*dx + dz*dz <= (HOURGLASS_WIDTH+2)**2:
add_voxel(volume, cx+dx, y, cz+dz, base_color)
def generate_scene(volume, t):
"""Generate the complete hourglass scene"""
generate_base_and_top(volume, CENTER_X, CENTER_Y, CENTER_Z)
generate_glass_structure(volume, CENTER_X, CENTER_Y, CENTER_Z)
generate_sand_particles(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_glass_highlights(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
# 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 hourglass"):
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
- Increase
SAND_PARTICLES
for denser sand flow - Modify
glass_color
to change the glass tint - Adjust
NECK_WIDTH
to make sand flow faster or slower - Add
+ int(np.sin(t) * 2)
to particle positions for gentle swaying motion - Change
HOURGLASS_HEIGHT
andHOURGLASS_WIDTH
for different proportions