Creating Lantern Animation
3D Voxel Lantern Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of lanterns using SpatialStudio.
The script creates glowing lanterns that sway gently and emit warm light 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 6 lanterns, each with:
- A rectangular paper body with traditional patterns
- A metallic top and bottom frame
- A warm, glowing interior light
- Gentle swaying motion
- Animates them floating and glowing for 8 seconds at 30 FPS
- Outputs the file
lanterns.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
). -
Lantern body Lanterns are drawn as rectangular frames with translucent paper walls and decorative patterns.
-
Light emission Each lantern contains a warm glowing core that illuminates the surrounding paper walls.
-
Metal frames Dark metallic caps are added to the top and bottom for authentic lantern appearance.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, creating gentle swaying and light flickering. -
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 lanterns.py
and run:
python lanterns.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/lanterns.splv"
# Lantern settings
LANTERN_COUNT = 6
LANTERN_WIDTH = 8
LANTERN_HEIGHT = 14
LANTERN_DEPTH = 8
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_lantern_frame(volume, cx, cy, cz, width, height, depth):
# Metal frame color (dark bronze)
frame_color = (80, 60, 40)
# Top and bottom frames
for dx in range(-width//2, width//2 + 1):
for dz in range(-depth//2, depth//2 + 1):
add_voxel(volume, cx+dx, cy+height//2, cz+dz, frame_color)
add_voxel(volume, cx+dx, cy-height//2, cz+dz, frame_color)
def generate_lantern_body(volume, cx, cy, cz, width, height, depth, color, t):
# Paper lantern walls with patterns
base_color = color
for y in range(-height//2 + 1, height//2):
for x in range(-width//2, width//2 + 1):
for z in range(-depth//2, depth//2 + 1):
# Create hollow lantern (only walls)
is_wall = (abs(x) == width//2 or abs(z) == depth//2)
if is_wall:
# Add decorative pattern
pattern = np.sin(y * 0.5) * np.cos(x * 0.3 + z * 0.3)
brightness = 0.8 + 0.2 * pattern
# Make paper slightly translucent looking
final_color = tuple(int(c * brightness) for c in base_color)
add_voxel(volume, cx+x, cy+y, cz+z, final_color)
def generate_inner_light(volume, cx, cy, cz, width, height, depth, t):
# Warm glowing light inside lantern
light_colors = [
(255, 200, 100), # Warm yellow
(255, 180, 80), # Orange glow
(255, 220, 120), # Bright yellow
]
# Flickering effect
flicker = 0.8 + 0.2 * np.sin(t * 8 + cx * 0.1)
# Create glowing core
core_size = 3
for dx in range(-core_size, core_size + 1):
for dy in range(-core_size, core_size + 1):
for dz in range(-core_size, core_size + 1):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= core_size:
intensity = (1 - distance/core_size) * flicker
color_idx = int(intensity * len(light_colors)) % len(light_colors)
light_color = light_colors[color_idx]
final_color = tuple(int(c * intensity) for c in light_color)
if sum(final_color) > 50: # Only add visible light
add_voxel(volume, cx+dx, cy+dy, cz+dz, final_color)
def generate_light_glow(volume, cx, cy, cz, width, height, depth, t):
# Subtle glow around the lantern
glow_color = (255, 200, 100)
flicker = 0.5 + 0.3 * np.sin(t * 6 + cx * 0.15)
for dx in range(-width, width + 1):
for dy in range(-height//2, height//2 + 1):
for dz in range(-depth, depth + 1):
distance = np.sqrt(dx*dx + (dy*0.5)*(dy*0.5) + dz*dz)
if width + 2 <= distance <= width + 5:
intensity = (1 - (distance - width - 2)/3) * 0.3 * flicker
if intensity > 0.1:
final_color = tuple(int(c * intensity) for c in glow_color)
add_voxel(volume, cx+dx, cy+dy, cz+dz, final_color)
def generate_lantern_cluster(volume, cx, cy, cz, t):
# Lantern paper colors
paper_colors = [
(220, 180, 180), # Soft pink
(180, 220, 180), # Soft green
(180, 180, 220), # Soft blue
(220, 220, 180), # Soft yellow
(220, 180, 220), # Soft purple
(220, 200, 180), # Soft orange
]
for i in range(LANTERN_COUNT):
# Arrange lanterns in a circle
angle = (i / LANTERN_COUNT) * 2 * np.pi
radius = 25
# Gentle swaying motion
sway_x = np.sin(t * 0.5 + i * 0.7) * 3
sway_z = np.cos(t * 0.3 + i * 0.5) * 2
float_y = np.sin(t * 0.8 + i * 0.4) * 4
lx = int(cx + radius * np.cos(angle) + sway_x)
ly = int(cy + float_y)
lz = int(cz + radius * np.sin(angle) + sway_z)
# Varying lantern sizes
scale = 0.8 + 0.4 * np.sin(i * 0.3)
width = int(LANTERN_WIDTH * scale)
height = int(LANTERN_HEIGHT * scale)
depth = int(LANTERN_DEPTH * scale)
color = paper_colors[i % len(paper_colors)]
# Build each lantern
generate_lantern_frame(volume, lx, ly, lz, width, height, depth)
generate_lantern_body(volume, lx, ly, lz, width, height, depth, color, t)
generate_inner_light(volume, lx, ly, lz, width, height, depth, t + i)
generate_light_glow(volume, lx, ly, lz, width, height, depth, t + i)
def generate_hanging_strings(volume, cx, cy, cz, t):
string_color = (60, 40, 20) # Dark brown
for i in range(LANTERN_COUNT):
angle = (i / LANTERN_COUNT) * 2 * np.pi
radius = 25
sway_x = np.sin(t * 0.5 + i * 0.7) * 3
sway_z = np.cos(t * 0.3 + i * 0.5) * 2
float_y = np.sin(t * 0.8 + i * 0.4) * 4
lx = int(cx + radius * np.cos(angle) + sway_x)
ly = int(cy + float_y)
lz = int(cz + radius * np.sin(angle) + sway_z)
# Draw hanging string upward
string_length = 15
for j in range(string_length):
string_sway = np.sin(t * 1.5 + i * 0.5 + j * 0.2) * 2
sx = lx + int(string_sway)
sy = ly + LANTERN_HEIGHT//2 + j
sz = lz
add_voxel(volume, sx, sy, sz, string_color)
def generate_scene(volume, t):
generate_lantern_cluster(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_hanging_strings(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 lanterns"):
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
- Change
LANTERN_COUNT
to create more lanterns in the scene. - Edit
paper_colors
to customize the lantern appearances. - Adjust the
flicker
intensity for different lighting effects. - Modify the
radius
value to spread lanterns further apart. - Add wind effects by increasing the sway motion parameters.