Creating Volcano Animation
3D Voxel Animation: Volcano
This guide walks you through how to generate a looping 3D voxel animation of a volcano using SpatialStudio.
The script creates an active volcano with flowing lava, particle effects, and glowing embers 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 volcanic mountain with:
- A rocky base structure
- Flowing lava streams
- Particle effects and embers
- Glowing crater with animated lava
- Animates the volcanic activity for 10 seconds at 30 FPS
- Outputs the file
volcano.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
). -
Volcano structure The mountain is built using noise functions to create realistic rocky terrain with a crater at the top.
-
Lava flow Molten lava flows down the mountainside using gravity simulation and temperature gradients.
-
Particle system Glowing embers and ash particles are spawned from the crater and animated with physics.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring the eruption pattern 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 volcano.py
and run:
python volcano.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
import random
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 10
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/volcano.splv"
# Volcano settings
VOLCANO_HEIGHT = 45
VOLCANO_BASE_RADIUS = 35
CRATER_RADIUS = 12
PARTICLE_COUNT = 150
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 noise(x, y, z, scale=0.1):
return (np.sin(x * scale) * np.cos(y * scale) + np.sin(z * scale)) * 0.5
def generate_volcano_base(volume, cx, cy, cz, t):
rock_color = (80, 60, 40)
dark_rock = (60, 45, 30)
for dx in range(-VOLCANO_BASE_RADIUS, VOLCANO_BASE_RADIUS + 1):
for dz in range(-VOLCANO_BASE_RADIUS, VOLCANO_BASE_RADIUS + 1):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= VOLCANO_BASE_RADIUS:
# Calculate height based on distance from center
height_ratio = 1.0 - (distance / VOLCANO_BASE_RADIUS)
height = int(VOLCANO_HEIGHT * height_ratio * height_ratio)
# Add noise for rocky texture
height += int(noise(cx+dx, 0, cz+dz, 0.2) * 8)
for dy in range(height):
y_pos = cy - VOLCANO_HEIGHT//2 + dy
# Vary rock color based on height and noise
color_var = noise(cx+dx, y_pos, cz+dz, 0.15)
if color_var > 0.2:
color = rock_color
else:
color = dark_rock
add_voxel(volume, cx+dx, y_pos, cz+dz, color)
def generate_crater(volume, cx, cy, cz, t):
lava_color = (255, 80, 0)
hot_lava = (255, 200, 0)
crater_y = cy + VOLCANO_HEIGHT//2 - 5
for dx in range(-CRATER_RADIUS, CRATER_RADIUS + 1):
for dz in range(-CRATER_RADIUS, CRATER_RADIUS + 1):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= CRATER_RADIUS:
# Animated lava surface
lava_height = int(3 * np.sin(t * 2.0 + distance * 0.3))
for dy in range(-8, lava_height + 1):
y_pos = crater_y + dy
# Hotter lava near surface
if dy > lava_height - 2:
color = hot_lava
else:
color = lava_color
add_voxel(volume, cx+dx, y_pos, cz+dz, color)
def generate_lava_flow(volume, cx, cy, cz, t):
lava_color = (200, 60, 0)
cooling_lava = (120, 30, 0)
# Multiple lava streams
for stream in range(3):
angle = (stream / 3.0) * 2*np.pi + t * 0.1
stream_x = np.cos(angle)
stream_z = np.sin(angle)
for i in range(30):
flow_x = cx + int(stream_x * (CRATER_RADIUS + i * 0.8))
flow_z = cz + int(stream_z * (CRATER_RADIUS + i * 0.8))
# Calculate height on volcano slope
distance_from_center = np.sqrt((flow_x - cx)**2 + (flow_z - cz)**2)
if distance_from_center <= VOLCANO_BASE_RADIUS:
height_ratio = 1.0 - (distance_from_center / VOLCANO_BASE_RADIUS)
base_height = int(VOLCANO_HEIGHT * height_ratio * height_ratio)
flow_y = cy - VOLCANO_HEIGHT//2 + base_height + 1
# Animated lava flow
wave = np.sin(t * 3.0 - i * 0.2) * 0.5 + 0.5
if wave > 0.3: # Flowing lava
# Cooling effect with distance
if i < 15:
color = lava_color
else:
color = cooling_lava
add_voxel(volume, flow_x, flow_y, flow_z, color)
# Add width to lava stream
if i < 20:
add_voxel(volume, flow_x+1, flow_y, flow_z, color)
add_voxel(volume, flow_x, flow_y, flow_z+1, color)
def generate_particles(volume, cx, cy, cz, t):
ember_color = (255, 150, 0)
ash_color = (100, 80, 60)
random.seed(42) # Consistent particle positions
for i in range(PARTICLE_COUNT):
# Particle lifecycle
particle_time = (t * 2.0 + i * 0.1) % (2*np.pi)
life_ratio = particle_time / (2*np.pi)
if life_ratio < 0.8: # Particle is active
# Initial position near crater
start_angle = (i / PARTICLE_COUNT) * 2*np.pi
start_radius = random.uniform(2, CRATER_RADIUS)
start_x = cx + int(start_radius * np.cos(start_angle))
start_z = cz + int(start_radius * np.sin(start_angle))
start_y = cy + VOLCANO_HEIGHT//2
# Particle motion
vel_x = random.uniform(-0.3, 0.3)
vel_z = random.uniform(-0.3, 0.3)
vel_y = 15 * (1 - life_ratio) - 5 # Gravity effect
pos_x = start_x + int(vel_x * life_ratio * 30)
pos_y = start_y + int(vel_y * life_ratio * 2)
pos_z = start_z + int(vel_z * life_ratio * 30)
# Choose particle type
if i % 4 == 0: # Ember
color = ember_color
else: # Ash
color = ash_color
add_voxel(volume, pos_x, pos_y, pos_z, color)
def generate_glow_effects(volume, cx, cy, cz, t):
glow_color = (255, 100, 50)
crater_y = cy + VOLCANO_HEIGHT//2
glow_intensity = 0.8 + 0.2 * np.sin(t * 4.0)
# Glow around crater rim
for dx in range(-CRATER_RADIUS-3, CRATER_RADIUS+4):
for dz in range(-CRATER_RADIUS-3, CRATER_RADIUS+4):
distance = np.sqrt(dx*dx + dz*dz)
if CRATER_RADIUS <= distance <= CRATER_RADIUS + 3:
if random.random() < glow_intensity * 0.3:
add_voxel(volume, cx+dx, crater_y+2, cz+dz, glow_color)
def generate_scene(volume, t):
generate_volcano_base(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_crater(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_lava_flow(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_particles(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_glow_effects(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 volcano"):
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
VOLCANO_HEIGHT
to make it taller or shorter. - Increase
PARTICLE_COUNT
for more dramatic eruption effects. - Modify
lava_color
values to experiment with different lava temperatures. - Add smoke effects by creating gray particles that rise higher.
- Create multiple smaller volcanoes by calling
generate_scene()
with different centers.