Creating Pendulum Animation
Pendulum - 3D Voxel Animation Learning Example
This guide walks you through how to generate a looping 3D voxel animation of a pendulum using SpatialStudio.
The script creates a realistic pendulum with a metal bob that swings back and forth 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 pendulum with:
- A metallic spherical bob
- A thin suspension rod
- A fixed mounting point at the top
- Realistic physics-based swinging motion
-
Animates the pendulum swinging for 6 seconds at 30 FPS
-
Outputs the file
pendulum.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
). -
Physics simulation The pendulum follows realistic angular motion using sine and cosine functions to create natural swinging.
-
Pendulum bob A metallic sphere is drawn at the end of the pendulum arm with shading to simulate metal reflection.
-
Suspension rod A thin rod connects the fixed pivot point to the swinging bob, rotating with the pendulum motion.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, with the pendulum angle calculated using sine waves for smooth oscillation. -
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 pendulum.py
and run:
python pendulum.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 6
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/pendulum.splv"
# Pendulum settings
PENDULUM_LENGTH = 45
BOB_RADIUS = 8
MAX_ANGLE = np.pi / 3 # 60 degrees max swing
ROD_THICKNESS = 2
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_pendulum_bob(volume, cx, cy, cz, t):
# Metallic silver color with variations
base_color = (169, 169, 169)
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):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= BOB_RADIUS:
# Create metallic shading effect
normal_factor = 1.0 - (distance / BOB_RADIUS)
brightness = 0.6 + 0.4 * normal_factor
# Add subtle surface variation
surface_noise = np.sin(dx*0.5) * np.cos(dy*0.5) * np.sin(dz*0.5)
brightness += surface_noise * 0.1
final_color = tuple(min(255, max(50, int(c * brightness))) for c in base_color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, final_color)
def generate_pendulum_rod(volume, pivot_x, pivot_y, pivot_z, bob_x, bob_y, bob_z):
# Dark metal rod color
rod_color = (64, 64, 64)
# Calculate direction vector
dx = bob_x - pivot_x
dy = bob_y - pivot_y
dz = bob_z - pivot_z
length = np.sqrt(dx*dx + dy*dy + dz*dz)
if length == 0:
return
# Normalize direction
dx, dy, dz = dx/length, dy/length, dz/length
# Draw rod from pivot to bob
steps = int(length)
for i in range(steps):
t = i / steps
x = int(pivot_x + dx * i)
y = int(pivot_y + dy * i)
z = int(pivot_z + dz * i)
# Add thickness to rod
for offset_x in range(-ROD_THICKNESS//2, ROD_THICKNESS//2 + 1):
for offset_z in range(-ROD_THICKNESS//2, ROD_THICKNESS//2 + 1):
if offset_x*offset_x + offset_z*offset_z <= (ROD_THICKNESS//2)**2:
add_voxel(volume, x+offset_x, y, z+offset_z, rod_color)
def generate_pivot_point(volume, px, py, pz):
# Dark metallic pivot point
pivot_color = (32, 32, 32)
for dx in range(-3, 4):
for dy in range(-2, 3):
for dz in range(-3, 4):
if dx*dx + dy*dy + dz*dz <= 9:
add_voxel(volume, px+dx, py+dy, pz+dz, pivot_color)
def generate_pendulum_highlights(volume, cx, cy, cz):
# Add bright highlights to simulate metallic reflection
highlight_color = (220, 220, 255)
for dx in range(-2, 3):
for dy in range(-2, 3):
for dz in range(-2, 3):
if dx*dx + dy*dy + dz*dz <= 4:
add_voxel(volume, cx-3+dx, cy-2+dy, cz-1+dz, highlight_color)
def generate_scene(volume, t):
# Pivot point at the top center
pivot_x = CENTER_X
pivot_y = CENTER_Y - 50 # High up in the scene
pivot_z = CENTER_Z
# Calculate pendulum angle (oscillating motion)
angle = MAX_ANGLE * np.sin(t * 1.5) # Adjust speed with multiplier
# Calculate bob position based on angle
bob_x = pivot_x + int(PENDULUM_LENGTH * np.sin(angle))
bob_y = pivot_y + int(PENDULUM_LENGTH * np.cos(angle))
bob_z = pivot_z
# Generate all pendulum components
generate_pivot_point(volume, pivot_x, pivot_y, pivot_z)
generate_pendulum_rod(volume, pivot_x, pivot_y, pivot_z, bob_x, bob_y, bob_z)
generate_pendulum_bob(volume, bob_x, bob_y, bob_z, t)
generate_pendulum_highlights(volume, bob_x, bob_y, bob_z)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating pendulum"):
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
MAX_ANGLE
to make the pendulum swing wider or narrower. - Adjust
PENDULUM_LENGTH
for a longer or shorter pendulum. - Modify the speed by changing the multiplier in
np.sin(t * 1.5)
. - Add multiple pendulums with different lengths and starting positions.
- Experiment with different metallic colors for the bob.