Creating Castle Animation
Castle 3D Voxel Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a medieval castle using SpatialStudio.
The script creates a detailed castle with towers, walls, and animated elements like flags and torch flames 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 medieval castle with:
- Multiple stone towers with crenellations
- Connecting walls and a main gate
- Animated flags waving in the wind
- Flickering torch flames
- A textured stone foundation
- Animates the flags and flames for 10 seconds at 30 FPS
- Outputs the file
castle.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
). -
Castle structure The castle is built from gray stone blocks arranged in towers, walls, and battlements with realistic proportions.
-
Animated flags Colorful banners are placed on tower tops with sine-wave animations to simulate wind movement.
-
Torch flames Orange and yellow flame voxels flicker randomly on castle walls using noise functions.
-
Stone texture Subtle variations in gray tones create realistic stone texture using perlin-like noise.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring smooth looping animations. -
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 castle.py
and run:
python castle.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/castle.splv"
# Castle settings
TOWER_HEIGHT = 35
WALL_HEIGHT = 20
FOUNDATION_HEIGHT = 5
TOWER_RADIUS = 8
WALL_THICKNESS = 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 get_stone_color(x, y, z, base_color=(120, 120, 120)):
noise = np.sin(x*0.1) * np.sin(y*0.15) * np.sin(z*0.12)
variation = int(noise * 15)
return tuple(max(60, min(180, c + variation)) for c in base_color)
def generate_foundation(volume, cx, cy, cz):
for dx in range(-45, 46):
for dz in range(-45, 46):
for dy in range(FOUNDATION_HEIGHT):
if abs(dx) <= 42 and abs(dz) <= 42:
color = get_stone_color(cx+dx, cy-dy, cz+dz, (100, 90, 80))
add_voxel(volume, cx+dx, cy-dy, cz+dz, color)
def generate_tower(volume, cx, cy, cz, radius, height):
for dy in range(height):
for dx in range(-radius, radius+1):
for dz in range(-radius, radius+1):
dist = np.sqrt(dx*dx + dz*dz)
if dist <= radius:
# Hollow tower interior
if dist > radius-2 or dy < 3:
color = get_stone_color(cx+dx, cy+dy, cz+dz)
add_voxel(volume, cx+dx, cy+dy, cz+dz, color)
# Add crenellations (castle battlements)
for angle in range(0, 360, 30):
rad = np.radians(angle)
bx = cx + int((radius-1) * np.cos(rad))
bz = cz + int((radius-1) * np.sin(rad))
for dy in range(3):
color = get_stone_color(bx, cy+height+dy, bz)
add_voxel(volume, bx, cy+height+dy, bz, color)
def generate_wall(volume, x1, y, z1, x2, z2, height, thickness):
steps = max(abs(x2-x1), abs(z2-z1))
if steps == 0:
return
for i in range(steps+1):
t = i / steps if steps > 0 else 0
x = int(x1 + (x2-x1) * t)
z = int(z1 + (z2-z1) * t)
for dy in range(height):
for thick in range(thickness):
# Add thickness perpendicular to wall direction
if abs(x2-x1) > abs(z2-z1): # Horizontal wall
wall_z = z + thick - thickness//2
color = get_stone_color(x, y+dy, wall_z)
add_voxel(volume, x, y+dy, wall_z, color)
else: # Vertical wall
wall_x = x + thick - thickness//2
color = get_stone_color(wall_x, y+dy, z)
add_voxel(volume, wall_x, y+dy, z, color)
def generate_gate(volume, cx, cy, cz):
# Create arched gate entrance
for dx in range(-4, 5):
for dy in range(8):
if abs(dx) <= 3 and dy < 6:
# Arch shape
if dy < 4 or abs(dx) <= 2:
add_voxel(volume, cx+dx, cy+dy, cz, (50, 30, 20)) # Dark gate
def generate_flag(volume, cx, cy, cz, t, flag_id):
colors = [(200, 0, 0), (0, 150, 200), (200, 200, 0), (150, 0, 200)]
flag_color = colors[flag_id % len(colors)]
# Flag pole
for dy in range(8):
add_voxel(volume, cx, cy+dy, cz, (101, 67, 33))
# Animated flag
wave_offset = np.sin(t * 3.0 + flag_id * 0.5) * 2
for fx in range(6):
for fy in range(4):
wave = int(np.sin((fx * 0.5 + t * 2.0) + flag_id) * 1.5)
flag_x = cx + 1 + fx
flag_y = cy + 5 + fy
flag_z = cz + wave + int(wave_offset)
add_voxel(volume, flag_x, flag_y, flag_z, flag_color)
def generate_torch_flame(volume, cx, cy, cz, t):
# Torch post
for dy in range(4):
add_voxel(volume, cx, cy-dy, cz, (101, 67, 33))
# Flickering flame
flame_height = 3 + int(np.sin(t * 8.0 + cx * 0.1) * 1.5)
for dy in range(flame_height):
flicker = int(np.sin(t * 10.0 + dy * 0.3 + cx * 0.05) * 1.5)
for dx in range(-1, 2):
for dz in range(-1, 2):
if abs(dx) + abs(dz) <= 1:
# Color gradient from yellow to orange to red
if dy < flame_height // 2:
color = (255, 200, 50) # Yellow core
elif dy < flame_height * 0.75:
color = (255, 120, 20) # Orange
else:
color = (220, 50, 20) # Red tips
flame_x = cx + dx + flicker
flame_y = cy + dy
flame_z = cz + dz
add_voxel(volume, flame_x, flame_y, flame_z, color)
def generate_castle(volume, cx, cy, cz, t):
# Foundation
generate_foundation(volume, cx, cy, cz)
# Corner towers
tower_positions = [(-25, -25), (25, -25), (25, 25), (-25, 25)]
for i, (tx, tz) in enumerate(tower_positions):
generate_tower(volume, cx+tx, cy, cz+tz, TOWER_RADIUS, TOWER_HEIGHT)
# Add flags to towers
generate_flag(volume, cx+tx+TOWER_RADIUS-2, cy+TOWER_HEIGHT, cz+tz, t, i)
# Central keep (main tower)
generate_tower(volume, cx, cy, cz, TOWER_RADIUS+2, TOWER_HEIGHT+8)
generate_flag(volume, cx+TOWER_RADIUS, cy+TOWER_HEIGHT+8, cz, t, 4)
# Connecting walls
wall_y = cy + FOUNDATION_HEIGHT
generate_wall(volume, cx-25, wall_y, cz-25, cx+25, cz-25, WALL_HEIGHT, WALL_THICKNESS) # Front
generate_wall(volume, cx+25, wall_y, cz-25, cx+25, cz+25, WALL_HEIGHT, WALL_THICKNESS) # Right
generate_wall(volume, cx+25, wall_y, cz+25, cx-25, cz+25, WALL_HEIGHT, WALL_THICKNESS) # Back
generate_wall(volume, cx-25, wall_y, cz+25, cx-25, cz-25, WALL_HEIGHT, WALL_THICKNESS) # Left
# Main gate
generate_gate(volume, cx, cy+FOUNDATION_HEIGHT, cz-25)
# Torches on walls
torch_positions = [(cx-15, cz-23), (cx+15, cz-23), (cx+23, cz-15), (cx+23, cz+15)]
for tx, tz in torch_positions:
generate_torch_flame(volume, tx, wall_y+WALL_HEIGHT+2, tz, t)
def generate_scene(volume, t):
generate_castle(volume, CENTER_X, CENTER_Y-20, CENTER_Z, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Building castle"):
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
TOWER_HEIGHT
andTOWER_RADIUS
for different castle proportions - Change flag colors by modifying the
colors
array ingenerate_flag()
- Add more towers by extending the
tower_positions
list - Experiment with different stone textures in
get_stone_color()
- Add a moat by creating water voxels around the foundation
- Increase
SECONDS
for longer animations or add day/night cycles