Creating Gear Animation
Rotating Gears - 3D Voxel Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of rotating gears using SpatialStudio.
The script creates metallic gears that rotate, mesh together, and cast shadows 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 3 interlocking gears, each with:
- A detailed cylindrical gear body with teeth
- Metallic gray coloring with highlights
- Realistic rotation mechanics
- Cast shadows for depth
-
Animates them rotating in sync for 6 seconds at 30 FPS
-
Outputs the file
gear.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
). -
Gear body Gears are drawn as cylinders with precisely cut teeth around the circumference.
-
Teeth generation Each gear tooth is calculated using trigonometry to create realistic gear profiles.
-
Rotation mechanics Gears rotate at different speeds to simulate proper mechanical interlocking.
-
Metallic shading Gray base colors with white highlights and darker shadows create a metallic appearance.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring smooth rotation loops. -
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 gear.py
and run:
python gear.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/gear.splv"
# Gear settings
GEAR_COUNT = 3
GEAR_THICKNESS = 8
TEETH_HEIGHT = 4
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_gear_body(volume, cx, cy, cz, inner_radius, outer_radius, thickness, teeth_count, rotation, color):
# Generate the main cylindrical body
for dx in range(-outer_radius-2, outer_radius+3):
for dz in range(-outer_radius-2, outer_radius+3):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= outer_radius:
for dy in range(-thickness//2, thickness//2+1):
# Calculate if this point is part of a tooth
angle = np.arctan2(dz, dx) + rotation
tooth_angle = (angle % (2*np.pi / teeth_count)) * teeth_count / (2*np.pi)
# Create gear teeth
is_tooth = (tooth_angle > 0.3 and tooth_angle < 0.7)
effective_radius = outer_radius if is_tooth else outer_radius - TEETH_HEIGHT
if inner_radius <= distance <= effective_radius:
# Add metallic shading
shade_factor = 0.8 + 0.4 * np.sin(angle * 3)
shaded_color = tuple(int(c * shade_factor) for c in color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, shaded_color)
def generate_gear_highlights(volume, cx, cy, cz, inner_radius, outer_radius, thickness, rotation):
# Add metallic highlights
highlight_color = (220, 220, 220)
for dx in range(-outer_radius, outer_radius+1):
for dz in range(-outer_radius, outer_radius+1):
distance = np.sqrt(dx*dx + dz*dz)
if inner_radius+2 <= distance <= outer_radius-1:
angle = np.arctan2(dz, dx) + rotation
# Create highlight pattern
if np.sin(angle * 4) > 0.7:
dy = thickness//2
add_voxel(volume, cx+dx, cy+dy, cz+dz, highlight_color)
def generate_gear_shadows(volume, cx, cy, cz, inner_radius, outer_radius, thickness):
# Add shadows below gears
shadow_color = (40, 40, 40)
shadow_offset = 3
for dx in range(-outer_radius, outer_radius+1):
for dz in range(-outer_radius, outer_radius+1):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= outer_radius + 2:
for dy in range(-thickness//2-shadow_offset, -thickness//2):
fade = 1.0 - abs(dy + thickness//2) / shadow_offset
if fade > 0:
faded_color = tuple(int(c * fade * 0.3) for c in shadow_color)
if cx+dx >= 0 and cx+dx < SIZE and cy+dy >= 0 and cy+dy < SIZE and cz+dz >= 0 and cz+dz < SIZE:
current = volume[cx+dx, cy+dy, cz+dz]
if current[3] == 0: # Only add shadow to empty space
add_voxel(volume, cx+dx, cy+dy, cz+dz, faded_color)
def generate_gear_system(volume, t):
# Define three interlocking gears with different sizes and positions
gears = [
{"pos": (CENTER_X-25, CENTER_Y, CENTER_Z), "inner": 8, "outer": 18, "teeth": 16, "speed": 1.0, "color": (120, 120, 120)},
{"pos": (CENTER_X+25, CENTER_Y, CENTER_Z-5), "inner": 6, "outer": 14, "teeth": 12, "speed": -1.33, "color": (100, 100, 100)},
{"pos": (CENTER_X, CENTER_Y+8, CENTER_Z+20), "inner": 10, "outer": 22, "teeth": 20, "speed": 0.8, "color": (140, 140, 140)},
]
for gear in gears:
cx, cy, cz = gear["pos"]
rotation = t * gear["speed"]
# Generate shadows first (so they appear behind gears)
generate_gear_shadows(volume, cx, cy, cz, gear["inner"], gear["outer"], GEAR_THICKNESS)
# Generate gear body
generate_gear_body(volume, cx, cy, cz, gear["inner"], gear["outer"],
GEAR_THICKNESS, gear["teeth"], rotation, gear["color"])
# Add highlights
generate_gear_highlights(volume, cx, cy, cz, gear["inner"], gear["outer"],
GEAR_THICKNESS, rotation)
def generate_scene(volume, t):
generate_gear_system(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating gears"):
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
GEAR_COUNT
and gear definitions to add more gears to the system. - Modify rotation speeds to change how the gears interact.
- Experiment with different
inner_radius
andouter_radius
values for varied gear sizes. - Change gear colors to create brass, copper, or steel appearances.
- Add a background or mounting structure by drawing additional voxels.