Creating Pinwheel Animation
Creating a Spinning Pinwheel Animation with SpatialStudio
This guide walks you through how to generate a looping 3D voxel animation of pinwheel using SpatialStudio.
The script creates colorful pinwheels that spin, flutter, 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 4 pinwheels, each with:
- Rotating colorful blades with alternating patterns
- A central hub with metallic shine
- A thin supporting stick
- Dynamic motion blur effects
- Animates them spinning at different speeds for 8 seconds at 30 FPS
- Outputs the file
pinwheel.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
). -
Pinwheel blades Each pinwheel has triangular blades that rotate around a central point with alternating bright colors.
-
Central hub A small metallic sphere at the center holds all the blades together and adds realistic shine.
-
Support stick A thin vertical rod extends downward from each pinwheel hub for structural realism.
-
Motion effects Fast-spinning blades create subtle blur effects and the rotation speed varies per pinwheel.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring the spinning motion loops smoothly. -
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 pinwheel.py
and run:
python pinwheel.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/pinwheel.splv"
# Pinwheel settings
PINWHEEL_COUNT = 4
BLADE_LENGTH = 12
BLADE_COUNT = 6
HUB_RADIUS = 2
STICK_HEIGHT = 20
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_pinwheel_blade(volume, cx, cy, cz, angle, blade_index, color, t):
# Calculate blade rotation
blade_angle = angle + (blade_index * 2 * np.pi / BLADE_COUNT)
# Create triangular blade shape
for r in range(1, BLADE_LENGTH):
for w in range(-r//3, r//3 + 1):
# Rotate point around center
x_offset = int(r * np.cos(blade_angle) + w * np.sin(blade_angle))
z_offset = int(r * np.sin(blade_angle) - w * np.cos(blade_angle))
# Add some flutter effect
y_offset = int(np.sin(t * 8 + r * 0.3) * 0.5)
# Vary color intensity based on position
intensity = 1.0 - (r / BLADE_LENGTH) * 0.3
final_color = tuple(int(c * intensity) for c in color)
add_voxel(volume, cx + x_offset, cy + y_offset, cz + z_offset, final_color)
def generate_pinwheel_hub(volume, cx, cy, cz, t):
hub_color = (192, 192, 192) # Silver
shine_color = (255, 255, 255) # White highlight
# Create central hub
for dx in range(-HUB_RADIUS, HUB_RADIUS + 1):
for dy in range(-HUB_RADIUS, HUB_RADIUS + 1):
for dz in range(-HUB_RADIUS, HUB_RADIUS + 1):
if dx*dx + dy*dy + dz*dz <= HUB_RADIUS*HUB_RADIUS:
# Add shine effect that rotates
if dx == -1 and dy >= 0 and dz == 0:
add_voxel(volume, cx+dx, cy+dy, cz+dz, shine_color)
else:
add_voxel(volume, cx+dx, cy+dy, cz+dz, hub_color)
def generate_support_stick(volume, cx, cy, cz):
stick_color = (101, 67, 33) # Brown wood
# Create vertical support stick
for i in range(STICK_HEIGHT):
y_pos = cy + HUB_RADIUS + i
# Add slight thickness variation
thickness = 1 if i < STICK_HEIGHT // 2 else 0
for dx in range(-thickness, thickness + 1):
for dz in range(-thickness, thickness + 1):
add_voxel(volume, cx + dx, y_pos, cz + dz, stick_color)
def generate_single_pinwheel(volume, cx, cy, cz, pinwheel_id, t):
# Different rotation speeds for each pinwheel
speed_multiplier = 1.0 + pinwheel_id * 0.5
rotation_angle = t * speed_multiplier * 4
# Alternating blade colors
blade_colors = [
(255, 0, 0), # Red
(0, 255, 0), # Green
(0, 0, 255), # Blue
(255, 255, 0), # Yellow
(255, 0, 255), # Magenta
(0, 255, 255), # Cyan
]
# Generate blades
for blade_idx in range(BLADE_COUNT):
color = blade_colors[blade_idx % len(blade_colors)]
generate_pinwheel_blade(volume, cx, cy, cz, rotation_angle, blade_idx, color, t)
# Generate hub and support
generate_pinwheel_hub(volume, cx, cy, cz, t)
generate_support_stick(volume, cx, cy, cz)
def generate_pinwheel_cluster(volume, t):
positions = [
(CENTER_X - 25, CENTER_Y - 10, CENTER_Z - 25),
(CENTER_X + 25, CENTER_Y - 5, CENTER_Z - 25),
(CENTER_X - 25, CENTER_Y + 5, CENTER_Z + 25),
(CENTER_X + 25, CENTER_Y + 10, CENTER_Z + 25),
]
for i, (px, py, pz) in enumerate(positions):
# Add gentle swaying motion
sway_x = int(np.sin(t * 1.2 + i * 0.8) * 3)
sway_z = int(np.cos(t * 0.8 + i * 0.5) * 2)
generate_single_pinwheel(volume, px + sway_x, py, pz + sway_z, i, t)
def generate_ground_plane(volume):
ground_color = (34, 139, 34) # Forest green
ground_y = CENTER_Y + 35
# Simple ground plane
if ground_y < SIZE:
for x in range(SIZE):
for z in range(SIZE):
add_voxel(volume, x, ground_y, z, ground_color)
def generate_scene(volume, t):
generate_ground_plane(volume)
generate_pinwheel_cluster(volume, 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 pinwheel"):
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
PINWHEEL_COUNT
andBLADE_COUNT
to create different configurations - Edit
blade_colors
array to customize the color palette - Adjust rotation speeds by modifying
speed_multiplier
values - Add wind effects by increasing the sway motion parameters
- Experiment with
BLADE_LENGTH
to create larger or smaller pinwheels - Try adding more complex ground textures or background elements