Creating Lightning Animation
Lightning - 3D Voxel Animation Learning Example
This guide walks you through how to generate a looping 3D voxel animation of lightning using SpatialStudio.
The script creates dynamic lightning bolts that branch, flicker, and illuminate 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 multiple lightning bolts, each with:
- Jagged, branching paths from top to bottom
- Flickering intensity and brightness variations
- Electric blue-white glow effects
- Secondary branch forks for realism
- Animates them crackling and shifting for 6 seconds at 30 FPS
- Outputs the file
lightning.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
). -
Lightning paths Bolts are generated using random walks with downward bias, creating natural zigzag patterns.
-
Branching system Main bolts spawn secondary branches at random points, creating tree-like electrical discharge.
-
Glow effects Each lightning segment gets surrounding voxels with dimmer colors to simulate electrical glow.
-
Flickering animation Brightness and visibility vary using sine waves and noise for realistic electrical behavior.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, with lightning paths shifting and flickering 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 lightning.py
and run:
python lightning.py
Full Script
import numpy as np
from spatialstudio import splv
from tqdm import tqdm
import random
# Scene setup
SIZE, FPS, SECONDS = 128, 30, 6
FRAMES = FPS * SECONDS
CENTER_X = CENTER_Y = CENTER_Z = SIZE // 2
OUT_PATH = "../outputs/lightning.splv"
# Lightning settings
BOLT_COUNT = 3
MAX_BRANCHES = 5
GLOW_RADIUS = 2
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 add_glow(volume, x, y, z, core_color, intensity=0.3):
for dx in range(-GLOW_RADIUS, GLOW_RADIUS+1):
for dy in range(-GLOW_RADIUS, GLOW_RADIUS+1):
for dz in range(-GLOW_RADIUS, GLOW_RADIUS+1):
dist = np.sqrt(dx*dx + dy*dy + dz*dz)
if dist <= GLOW_RADIUS and dist > 0:
glow_intensity = intensity * (1 - dist/GLOW_RADIUS)
glow_color = tuple(int(c * glow_intensity) for c in core_color)
add_voxel(volume, x+dx, y+dy, z+dz, glow_color, int(255 * glow_intensity))
def generate_lightning_path(start_x, start_y, start_z, end_y, randomness=3):
path = []
x, y, z = start_x, start_y, start_z
while y > end_y:
path.append((x, y, z))
# Move downward with random horizontal drift
y -= random.randint(1, 3)
x += random.randint(-randomness, randomness)
z += random.randint(-randomness, randomness)
# Keep within bounds
x = max(5, min(SIZE-5, x))
z = max(5, min(SIZE-5, z))
return path
def generate_branches(main_path, branch_count=2):
branches = []
if len(main_path) < 10:
return branches
for _ in range(branch_count):
# Pick random point along main path (not too early)
branch_start_idx = random.randint(len(main_path)//4, len(main_path)-5)
start_point = main_path[branch_start_idx]
# Generate shorter branch
branch_length = random.randint(5, 15)
branch = []
x, y, z = start_point
for _ in range(branch_length):
branch.append((x, y, z))
y -= random.randint(0, 2)
x += random.randint(-2, 2)
z += random.randint(-2, 2)
# Keep within bounds
x = max(0, min(SIZE-1, x))
y = max(0, min(SIZE-1, y))
z = max(0, min(SIZE-1, z))
if y <= 5: # Don't go too low
break
branches.append(branch)
return branches
def draw_lightning_segment(volume, path, color, brightness=1.0, t=0):
for i, (x, y, z) in enumerate(path):
# Add some flickering
flicker = 0.7 + 0.3 * np.sin(t * 10 + i * 0.5)
segment_brightness = brightness * flicker
# Core lightning color
core_color = tuple(int(c * segment_brightness) for c in color)
add_voxel(volume, x, y, z, core_color)
# Add glow around each segment
add_glow(volume, x, y, z, color, 0.4 * segment_brightness)
def generate_lightning_bolts(volume, t):
# Lightning colors (blue-white electrical)
colors = [
(200, 220, 255), # Bright blue-white
(150, 180, 255), # Light blue
(255, 255, 255), # Pure white
]
for i in range(BOLT_COUNT):
# Vary starting positions and timing
start_x = CENTER_X + int(20 * np.sin(i * 2.1 + t * 0.5))
start_z = CENTER_Z + int(20 * np.cos(i * 1.7 + t * 0.3))
# Generate main lightning path
main_path = generate_lightning_path(
start_x, SIZE - 10, start_z, 10,
randomness=int(3 + 2 * np.sin(t * 2 + i))
)
# Create some flickering - sometimes bolts are invisible
visibility = np.sin(t * 15 + i * 3.7) * 0.5 + 0.5
if visibility < 0.3: # Bolt flickers out
continue
brightness = 0.6 + 0.4 * visibility
color = colors[i % len(colors)]
# Draw main bolt
draw_lightning_segment(volume, main_path, color, brightness, t)
# Generate and draw branches
branches = generate_branches(main_path, random.randint(1, MAX_BRANCHES))
for branch in branches:
branch_brightness = brightness * 0.6 # Branches are dimmer
draw_lightning_segment(volume, branch, color, branch_brightness, t)
def add_atmospheric_effects(volume, t):
# Add some random electrical particles/sparks
spark_count = int(10 + 5 * np.sin(t * 3))
for _ in range(spark_count):
x = random.randint(10, SIZE-10)
y = random.randint(10, SIZE-10)
z = random.randint(10, SIZE-10)
# Small bright sparks
spark_brightness = random.uniform(0.3, 0.8)
spark_color = (
int(255 * spark_brightness),
int(255 * spark_brightness),
255
)
add_voxel(volume, x, y, z, spark_color, int(255 * spark_brightness))
def generate_scene(volume, t):
generate_lightning_bolts(volume, t)
add_atmospheric_effects(volume, t)
# Initialize encoder
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
# Generate frames
for frame in tqdm(range(FRAMES), desc="Generating lightning"):
volume = np.zeros((SIZE, SIZE, SIZE, 4), dtype=np.uint8)
t = (frame / FRAMES) * 2*np.pi
# Reset random seed occasionally for variation
if frame % 10 == 0:
random.seed(frame // 10)
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
BOLT_COUNT
to create more or fewer lightning bolts. - Adjust
MAX_BRANCHES
for more complex branching patterns. - Modify the
colors
array to create different types of electrical effects (red plasma, green energy, etc.). - Increase
GLOW_RADIUS
for more dramatic lighting effects. - Add thunder clouds by creating gray voxel clusters at the top of the scene.