Creating Tornado Animation
Tornado - 3D Voxel Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a tornado using SpatialStudio.
The script creates a swirling tornado with debris particles that spirals and rotates 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 a realistic tornado with:
- A spiraling funnel cloud structure
- Swirling debris particles at different heights
- Dynamic rotation and vertical movement
- Realistic color gradients from dark gray to light dust
- Animates the tornado spinning and moving for 8 seconds at 30 FPS
- Outputs the file
tornado.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
). -
Tornado funnel The main structure is created using a mathematical spiral that widens from top to bottom, simulating the classic tornado shape.
-
Particle system Debris particles are generated at various heights and spiral around the tornado's core with realistic physics.
-
Color gradients The tornado uses different shades - darker grays for the dense core, lighter browns for dust and debris.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, creating smooth rotation and particle movement that loops 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 tornado.py
and run:
python tornado.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/tornado.splv"
# Tornado settings
TORNADO_HEIGHT = 60
TORNADO_TOP_RADIUS = 8
TORNADO_BOTTOM_RADIUS = 25
DEBRIS_COUNT = 150
SPIRAL_TIGHTNESS = 0.3
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_tornado_funnel(volume, cx, cy, cz, t):
# Create the main tornado funnel
for height in range(TORNADO_HEIGHT):
# Calculate radius at this height (wider at bottom)
height_ratio = height / TORNADO_HEIGHT
radius = TORNADO_TOP_RADIUS + (TORNADO_BOTTOM_RADIUS - TORNADO_TOP_RADIUS) * height_ratio
# Create spiral points around the circumference
points_per_layer = max(8, int(radius * 2))
for i in range(points_per_layer):
angle = (i / points_per_layer) * 2 * np.pi
spiral_offset = height * SPIRAL_TIGHTNESS + t * 2.0
# Calculate position with spiral effect
x = cx + int(radius * np.cos(angle + spiral_offset))
z = cz + int(radius * np.sin(angle + spiral_offset))
y = cy - int(height * 0.8) # Tornado extends downward
# Add some turbulence
turbulence = np.sin(height * 0.1 + t * 3.0 + angle) * 2
x += int(turbulence)
z += int(turbulence * 0.5)
# Color varies by height and distance from center
distance_from_center = np.sqrt((x-cx)**2 + (z-cz)**2)
density = 1.0 - (distance_from_center / radius) * 0.7
# Darker core, lighter edges
gray_value = int(60 + 40 * height_ratio + 30 * density)
color = (gray_value, gray_value - 10, gray_value - 20)
alpha = int(180 * density)
add_voxel(volume, x, y, z, color, alpha)
def generate_debris(volume, cx, cy, cz, t):
# Create swirling debris particles
debris_colors = [
(101, 67, 33), # Brown dirt
(139, 90, 43), # Light brown
(160, 82, 45), # Saddle brown
(210, 180, 140), # Tan dust
(112, 128, 144), # Slate gray
]
np.random.seed(42) # Consistent random pattern
for i in range(DEBRIS_COUNT):
# Each particle has its own orbit parameters
base_height = np.random.uniform(0, TORNADO_HEIGHT * 0.9)
orbit_radius = np.random.uniform(5, TORNADO_BOTTOM_RADIUS + 10)
orbit_speed = np.random.uniform(0.8, 2.5)
vertical_speed = np.random.uniform(-0.5, 0.5)
# Calculate current position
angle = (i * 0.3 + t * orbit_speed) % (2 * np.pi)
current_height = base_height + np.sin(t * vertical_speed + i) * 8
# Position in 3D space
x = cx + int(orbit_radius * np.cos(angle + current_height * SPIRAL_TIGHTNESS))
z = cz + int(orbit_radius * np.sin(angle + current_height * SPIRAL_TIGHTNESS))
y = cy - int(current_height * 0.8)
# Add randomness
x += int(np.sin(t * 4.0 + i * 0.7) * 3)
z += int(np.cos(t * 3.5 + i * 0.4) * 3)
y += int(np.sin(t * 2.0 + i * 0.9) * 2)
# Choose color and size
color = debris_colors[i % len(debris_colors)]
particle_size = 1 + (i % 3) # Vary particle sizes
# Draw particle
for dx in range(-particle_size, particle_size + 1):
for dy in range(-particle_size, particle_size + 1):
for dz in range(-particle_size, particle_size + 1):
if dx*dx + dy*dy + dz*dz <= particle_size*particle_size:
add_voxel(volume, x+dx, y+dy, z+dz, color)
def generate_ground_dust(volume, cx, cy, cz, t):
# Add dust clouds near the ground
dust_y = cy + 20 # Near bottom of scene
dust_radius = TORNADO_BOTTOM_RADIUS + 15
for i in range(50):
angle = (i / 50) * 2 * np.pi + t * 1.5
radius = dust_radius + np.sin(t * 2.0 + i * 0.1) * 8
x = cx + int(radius * np.cos(angle))
z = cz + int(radius * np.sin(angle))
y = dust_y + int(np.sin(t * 3.0 + i * 0.2) * 5)
# Light dust color
dust_color = (194, 178, 128)
alpha = int(80 + 40 * np.sin(t + i * 0.3))
# Create small dust clouds
for dx in range(-3, 4):
for dy in range(-2, 3):
for dz in range(-3, 4):
if dx*dx + dy*dy + dz*dz <= 9:
add_voxel(volume, x+dx, y+dy, z+dz, dust_color, alpha)
def generate_scene(volume, t):
generate_tornado_funnel(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_debris(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_ground_dust(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 tornado"):
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
- Increase
DEBRIS_COUNT
for more particles in the storm. - Adjust
SPIRAL_TIGHTNESS
to make the tornado tighter or looser. - Modify
TORNADO_HEIGHT
and radius values for different tornado sizes. - Add lightning effects by including bright white voxels that flash occasionally.
- Experiment with different color palettes for dust storms or water spouts.