Creating Lava Lamp Animation
3D Voxel Animation: Lava Lamp
This guide walks you through how to generate a looping 3D voxel animation of a lava lamp using SpatialStudio.
The script creates a mesmerizing lava lamp with floating blobs that heat up, rise, cool down, and sink inside a glass container, then saves the animation to a .splv
file.
What this script does
- Creates a 3D scene of size 128×128×128
- Builds a realistic lava lamp with:
- A transparent glass container
- A metallic base and cap
- 12 lava blobs that continuously move up and down
- Smooth blob morphing and color transitions
- Heat-based color gradients (red-hot at bottom, cooler orange-yellow at top)
- Animates the lava cycle for 10 seconds at 30 FPS
- Outputs the file
lava_lamp.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
). -
Glass container A hollow cylindrical container with transparent walls that contain the lava.
-
Lava blobs Organic-shaped spheroids that use sine waves and noise for realistic blob deformation.
-
Heat simulation Blobs start hot (red) at the bottom, rise while cooling (orange → yellow), then sink back down.
-
Color gradients Each blob's color changes based on its vertical position, simulating temperature.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, with each blob having different phase offsets for natural movement. -
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 lava_lamp.py
and run:
python lava_lamp.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/lava_lamp.splv"
# Lava lamp settings
CONTAINER_RADIUS = 20
CONTAINER_HEIGHT = 80
BLOB_COUNT = 12
BLOB_BASE_SIZE = 6
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 add_transparent_voxel(volume, x, y, z, color, alpha):
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_container(volume):
# Glass walls
for y in range(CENTER_Y - CONTAINER_HEIGHT//2, CENTER_Y + CONTAINER_HEIGHT//2):
for angle in np.linspace(0, 2*np.pi, 64):
x = int(CENTER_X + CONTAINER_RADIUS * np.cos(angle))
z = int(CENTER_Z + CONTAINER_RADIUS * np.sin(angle))
add_transparent_voxel(volume, x, y, z, (200, 220, 255), 120)
# Metal base
base_y = CENTER_Y - CONTAINER_HEIGHT//2
for dy in range(-5, 0):
for dx in range(-CONTAINER_RADIUS-3, CONTAINER_RADIUS+4):
for dz in range(-CONTAINER_RADIUS-3, CONTAINER_RADIUS+4):
if dx*dx + dz*dz <= (CONTAINER_RADIUS+3)**2:
brightness = 1.0 - abs(dy) * 0.1
color = tuple(int(c * brightness) for c in (120, 120, 140))
add_voxel(volume, CENTER_X+dx, base_y+dy, CENTER_Z+dz, color)
# Metal cap
cap_y = CENTER_Y + CONTAINER_HEIGHT//2
for dy in range(0, 5):
for dx in range(-CONTAINER_RADIUS-3, CONTAINER_RADIUS+4):
for dz in range(-CONTAINER_RADIUS-3, CONTAINER_RADIUS+4):
if dx*dx + dz*dz <= (CONTAINER_RADIUS+3)**2:
brightness = 1.0 - abs(dy) * 0.1
color = tuple(int(c * brightness) for c in (120, 120, 140))
add_voxel(volume, CENTER_X+dx, cap_y+dy, CENTER_Z+dz, color)
def get_heat_color(heat_level):
# heat_level: 0.0 (cool/top) to 1.0 (hot/bottom)
if heat_level < 0.5:
# Cool: yellow to orange
r = int(255 * (0.8 + heat_level * 0.4))
g = int(255 * (0.4 + heat_level * 0.6))
b = int(30 * (1.0 - heat_level))
else:
# Hot: orange to red
factor = (heat_level - 0.5) * 2
r = 255
g = int(255 * (1.0 - factor * 0.7))
b = int(30 * (1.0 - factor))
return (r, g, b)
def generate_lava_blob(volume, cx, cy, cz, size, t, blob_id):
# Add organic deformation
for dx in range(-size-2, size+3):
for dy in range(-size-2, size+3):
for dz in range(-size-2, size+3):
# Base spherical distance
base_dist = np.sqrt(dx*dx + dy*dy + dz*dz)
# Add organic noise
noise = (np.sin(dx*0.3 + t*1.2 + blob_id) *
np.sin(dy*0.4 + t*0.8 + blob_id*1.5) *
np.sin(dz*0.35 + t*1.0 + blob_id*0.7)) * 2
# Blob boundary with noise
if base_dist <= size + noise:
# Calculate heat based on Y position
container_bottom = CENTER_Y - CONTAINER_HEIGHT//2 + 5
container_top = CENTER_Y + CONTAINER_HEIGHT//2 - 5
heat_level = (container_top - cy) / (container_top - container_bottom)
heat_level = max(0.0, min(1.0, heat_level))
# Add some internal variation
internal_heat = heat_level + np.sin(base_dist*0.5 + t) * 0.2
internal_heat = max(0.0, min(1.0, internal_heat))
color = get_heat_color(internal_heat)
add_voxel(volume, cx+dx, cy+dy, cz+dz, color)
def generate_lava_blobs(volume, t):
for i in range(BLOB_COUNT):
# Each blob has different phase and speed
blob_phase = (i / BLOB_COUNT) * 2 * np.pi
blob_speed = 0.8 + (i % 3) * 0.4 # Varying speeds
# Vertical movement (main lava lamp effect)
container_bottom = CENTER_Y - CONTAINER_HEIGHT//2 + 10
container_top = CENTER_Y + CONTAINER_HEIGHT//2 - 10
travel_range = container_top - container_bottom
# Sine wave movement with different phases
y_pos = container_bottom + (travel_range/2) + (travel_range/2) * np.sin(t * blob_speed + blob_phase)
# Horizontal position (slight drift)
radius_drift = 8
angle = t * 0.3 + blob_phase
x_pos = CENTER_X + radius_drift * np.cos(angle + i * 0.7)
z_pos = CENTER_Z + radius_drift * np.sin(angle + i * 1.3)
# Blob size variation
size_variation = np.sin(t * 1.5 + blob_phase) * 2
blob_size = BLOB_BASE_SIZE + int(size_variation)
blob_size = max(4, blob_size) # Minimum size
# Keep blobs within container
dist_from_center = np.sqrt((x_pos - CENTER_X)**2 + (z_pos - CENTER_Z)**2)
if dist_from_center + blob_size > CONTAINER_RADIUS - 2:
scale = (CONTAINER_RADIUS - 2 - blob_size) / dist_from_center
x_pos = CENTER_X + (x_pos - CENTER_X) * scale
z_pos = CENTER_Z + (z_pos - CENTER_Z) * scale
generate_lava_blob(volume, int(x_pos), int(y_pos), int(z_pos), blob_size, t, i)
def generate_scene(volume, t):
generate_container(volume)
generate_lava_blobs(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating lava lamp"):
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
BLOB_COUNT
to add more or fewer lava blobs. - Modify
get_heat_color()
to experiment with different color schemes (blue lava, green, etc.). - Adjust
blob_speed
values to make the lava move faster or slower. - Increase
CONTAINER_HEIGHT
for a taller lamp. - Add bubble effects by creating smaller, faster-rising spheres.