Creating Compass Animation
Compass - 3D Voxel Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a compass using SpatialStudio.
The script creates a detailed magnetic compass with a spinning needle, rotating dial, and glowing effects 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 realistic compass with:
- A circular brass-colored base
- Cardinal direction markings (N, S, E, W)
- A red magnetic needle that spins and oscillates
- A transparent glass cover with reflections
- Glowing effects around the needle
- Animates the needle rotating and wobbling for 8 seconds at 30 FPS
- Outputs the file
compass.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
). -
Compass base A circular platform drawn with brass/bronze colors and textured edges.
-
Direction markers Cardinal points (N, S, E, W) are placed around the compass face with distinct colors.
-
Magnetic needle A red arrow that rotates smoothly while slightly wobbling to simulate magnetic interference.
-
Glass cover Semi-transparent voxels with white highlights create a realistic glass dome effect.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, making the needle rotation loop seamlessly. -
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 compass.py
and run:
python compass.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/compass.splv"
# Compass settings
COMPASS_RADIUS = 20
BASE_HEIGHT = 6
NEEDLE_LENGTH = 15
GLASS_HEIGHT = 8
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 generate_compass_base(volume, cx, cy, cz, t):
brass_color = (205, 127, 50)
dark_brass = (160, 100, 40)
for dx in range(-COMPASS_RADIUS, COMPASS_RADIUS+1):
for dz in range(-COMPASS_RADIUS, COMPASS_RADIUS+1):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= COMPASS_RADIUS:
for dy in range(BASE_HEIGHT):
# Add texture variation
texture = np.sin(dx*0.3 + dz*0.2 + t*0.1) * 0.2
if distance > COMPASS_RADIUS - 2: # Outer ring
color = dark_brass
else:
brightness = 1.0 + texture
color = tuple(min(255, int(c * brightness)) for c in brass_color)
add_voxel(volume, cx+dx, cy-dy, cz+dz, color)
def generate_direction_markers(volume, cx, cy, cz, t):
markers = [
("N", (255, 255, 255), 0), # North - White
("E", (200, 200, 200), np.pi/2), # East - Light Gray
("S", (180, 180, 180), np.pi), # South - Gray
("W", (200, 200, 200), 3*np.pi/2) # West - Light Gray
]
marker_radius = COMPASS_RADIUS - 4
for letter, color, angle in markers:
mx = cx + int(marker_radius * np.cos(angle))
mz = cz + int(marker_radius * np.sin(angle))
# Draw simple letter markers (simplified as small blocks)
for dx in range(-1, 2):
for dz in range(-1, 2):
for dy in range(2):
add_voxel(volume, mx+dx, cy+dy+1, mz+dz, color)
def generate_needle(volume, cx, cy, cz, t):
# Needle rotates with slight wobble
needle_angle = t * 0.5 + np.sin(t * 3) * 0.1
wobble = np.sin(t * 4) * 0.05
red_color = (255, 0, 0)
dark_red = (180, 0, 0)
white_color = (255, 255, 255)
# Draw needle shaft
for i in range(-NEEDLE_LENGTH, NEEDLE_LENGTH+1):
progress = i / NEEDLE_LENGTH
nx = cx + int(i * np.cos(needle_angle + wobble))
nz = cz + int(i * np.sin(needle_angle + wobble))
ny = cy + 2
# Red end (North-pointing)
if i > 0:
intensity = 1.0 - (progress * 0.3)
color = tuple(int(c * intensity) for c in red_color)
add_voxel(volume, nx, ny, nz, color)
# Add thickness
add_voxel(volume, nx, ny+1, nz, color)
# White end (South-pointing)
elif i < 0:
add_voxel(volume, nx, ny, nz, white_color)
add_voxel(volume, nx, ny+1, nz, white_color)
# Center pivot
else:
add_voxel(volume, nx, ny, nz, dark_red)
add_voxel(volume, nx, ny+1, nz, dark_red)
def generate_needle_glow(volume, cx, cy, cz, t):
# Subtle glow around the needle tip
needle_angle = t * 0.5 + np.sin(t * 3) * 0.1
wobble = np.sin(t * 4) * 0.05
# North tip glow
tip_x = cx + int(NEEDLE_LENGTH * np.cos(needle_angle + wobble))
tip_z = cz + int(NEEDLE_LENGTH * np.sin(needle_angle + wobble))
tip_y = cy + 3
glow_color = (255, 100, 100)
for dx in range(-2, 3):
for dy in range(-1, 2):
for dz in range(-2, 3):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= 2:
alpha = int(80 * (1 - distance/2))
add_voxel(volume, tip_x+dx, tip_y+dy, tip_z+dz, glow_color, alpha)
def generate_glass_cover(volume, cx, cy, cz, t):
glass_color = (200, 220, 255)
highlight_color = (255, 255, 255)
for dx in range(-COMPASS_RADIUS+2, COMPASS_RADIUS-1):
for dz in range(-COMPASS_RADIUS+2, COMPASS_RADIUS-1):
distance = np.sqrt(dx*dx + dz*dz)
if COMPASS_RADIUS-6 <= distance <= COMPASS_RADIUS-3:
for dy in range(GLASS_HEIGHT):
y_pos = cy + dy + 1
# Glass walls
alpha = 60 if dy < GLASS_HEIGHT-1 else 40
add_voxel(volume, cx+dx, y_pos, cz+dz, glass_color, alpha)
# Add highlights
if dy == GLASS_HEIGHT-2 and distance > COMPASS_RADIUS-5:
highlight_intensity = int(np.sin(dx*0.2 + dz*0.1 + t*0.3) * 30 + 50)
add_voxel(volume, cx+dx, y_pos, cz+dz, highlight_color, highlight_intensity)
def generate_compass_face(volume, cx, cy, cz, t):
face_color = (240, 230, 210)
line_color = (100, 100, 100)
# Draw compass face with degree markings
for dx in range(-COMPASS_RADIUS+5, COMPASS_RADIUS-4):
for dz in range(-COMPASS_RADIUS+5, COMPASS_RADIUS-4):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= COMPASS_RADIUS-5:
add_voxel(volume, cx+dx, cy+1, cz+dz, face_color)
# Add degree lines
angle = np.arctan2(dz, dx)
if int(angle * 180/np.pi) % 30 == 0 and distance > COMPASS_RADIUS-8:
add_voxel(volume, cx+dx, cy+1, cz+dz, line_color)
def generate_scene(volume, t):
generate_compass_base(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_compass_face(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_direction_markers(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_needle(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_needle_glow(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_glass_cover(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 compass"):
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
NEEDLE_LENGTH
to make the compass needle longer or shorter. - Modify
needle_angle
calculation to change rotation speed. - Add more detailed cardinal markings by expanding
generate_direction_markers()
. - Experiment with different
brass_color
values for unique compass styles. - Increase
wobble
intensity to simulate stronger magnetic interference.