Creating Windmill Animation
Windmill - 3D Voxel Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a windmill using SpatialStudio.
The script creates a classic windmill with rotating blades that spin gracefully 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 windmill structure with:
- A cylindrical stone tower base
- Four wooden rotating blades
- A detailed rooftop cap
- Realistic weathered textures
- Animates the blades rotating continuously for 10 seconds at 30 FPS
- Outputs the file
windmill.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
). -
Tower base The windmill body is drawn as a tapered cylinder using stone-like gray colors with texture variations.
-
Rotating blades Four wooden blades rotate around a central hub, with each blade angled for realistic wind-catching geometry.
-
Rooftop details A conical cap sits atop the tower with darker materials to simulate aged wood or metal.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, making the blade 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 windmill.py
and run:
python windmill.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 10
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/windmill.splv"
# Windmill settings
TOWER_HEIGHT = 45
TOWER_RADIUS = 12
BLADE_LENGTH = 25
BLADE_COUNT = 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_tower(volume, cx, cy, cz, t):
stone_base = (120, 120, 120)
stone_dark = (90, 90, 90)
for y_offset in range(-TOWER_HEIGHT//2, TOWER_HEIGHT//2):
# Tower tapers slightly toward the top
radius_at_height = TOWER_RADIUS - int(abs(y_offset) * 0.1)
for dx in range(-radius_at_height, radius_at_height+1):
for dz in range(-radius_at_height, radius_at_height+1):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= radius_at_height:
# Add texture variation
noise = np.sin(dx*0.3 + dz*0.3 + y_offset*0.2) * 0.2
texture = np.sin(dx*0.8 + dz*0.8 + y_offset*0.1) * 0.1
if noise > 0.1:
color = stone_dark
else:
brightness = 1.0 + texture
color = tuple(int(c * brightness) for c in stone_base)
color = tuple(min(255, max(50, c)) for c in color)
add_voxel(volume, cx+dx, cy+y_offset, cz+dz, color)
def generate_blade(volume, cx, cy, cz, angle, blade_id, t):
wood_color = (139, 69, 19)
wood_light = (160, 82, 45)
# Calculate blade position
blade_x = cx + int(BLADE_LENGTH * 0.7 * np.cos(angle))
blade_z = cz + int(BLADE_LENGTH * 0.7 * np.sin(angle))
# Draw blade from center to tip
for i in range(BLADE_LENGTH):
progress = i / BLADE_LENGTH
# Blade gets narrower toward the tip
width = max(1, int(4 * (1 - progress * 0.8)))
step_x = cx + int(i * np.cos(angle))
step_z = cz + int(i * np.sin(angle))
for w in range(-width, width+1):
for h in range(-2, 3):
# Perpendicular offset for blade thickness
offset_x = int(w * np.sin(angle))
offset_z = int(w * -np.cos(angle))
# Wood texture
grain = np.sin(i*0.2 + w*0.3 + h*0.1) * 0.3
if grain > 0:
color = wood_light
else:
color = wood_color
add_voxel(volume, step_x + offset_x, cy + h, step_z + offset_z, color)
def generate_blades(volume, cx, cy, cz, t):
hub_color = (80, 60, 40)
# Central hub
for dx in range(-3, 4):
for dy in range(-3, 4):
for dz in range(-3, 4):
if dx*dx + dy*dy + dz*dz <= 9:
add_voxel(volume, cx+dx, cy+dy, cz+dz, hub_color)
# Generate rotating blades
for i in range(BLADE_COUNT):
base_angle = (i / BLADE_COUNT) * 2*np.pi
rotation_angle = base_angle + t * 1.5 # Rotation speed
generate_blade(volume, cx, cy, cz, rotation_angle, i, t)
def generate_roof(volume, cx, cy, cz, t):
roof_color = (60, 40, 40)
roof_highlight = (80, 60, 60)
roof_base_y = cy + TOWER_HEIGHT//2
roof_height = 12
for y_offset in range(roof_height):
radius = max(1, TOWER_RADIUS - y_offset)
for dx in range(-radius, radius+1):
for dz in range(-radius, radius+1):
if dx*dx + dz*dz <= radius*radius:
# Roof shingle texture
shingle = (dx + dz + y_offset) % 3
color = roof_highlight if shingle == 0 else roof_color
add_voxel(volume, cx+dx, roof_base_y+y_offset, cz+dz, color)
def generate_ground(volume, cx, cy, cz):
grass_color = (34, 139, 34)
dirt_color = (101, 67, 33)
ground_y = cy - TOWER_HEIGHT//2 - 5
for dx in range(-SIZE//2, SIZE//2):
for dz in range(-SIZE//2, SIZE//2):
distance = np.sqrt(dx*dx + dz*dz)
if distance < SIZE//2:
# Mix grass and dirt
noise = np.sin(dx*0.1 + dz*0.1) + np.cos(dx*0.05 + dz*0.08)
if noise > 0.5:
color = grass_color
else:
color = dirt_color
add_voxel(volume, cx+dx, ground_y, cz+dz, color)
def generate_scene(volume, t):
# Generate windmill components
generate_ground(volume, CENTER_X, CENTER_Y, CENTER_Z)
generate_tower(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_roof(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
# Blades are positioned at upper part of tower
blade_y = CENTER_Y + TOWER_HEIGHT//3
generate_blades(volume, CENTER_X, blade_y, CENTER_Z - TOWER_RADIUS - 5, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating windmill"):
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
BLADE_COUNT
to create windmills with different numbers of blades. - Modify the rotation speed by changing the multiplier in
t * 1.5
. - Add wind effects by making the tower sway slightly with
np.sin(t * 0.5) * 2
. - Create multiple windmills by calling
generate_scene()
with different center positions. - Experiment with different materials by changing the color palettes for stone, wood, and roof sections.