Creating Snowglobe Animation
Snowglobe Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a snowglobe using SpatialStudio.
The script creates a magical winter scene with falling snow particles inside a transparent globe, then saves the animation to a .splv
file.
What this script does
- Creates a 3D scene of size 128×128×128
- Builds a transparent spherical globe with:
- A solid base platform
- Curved glass walls with subtle reflections
- A winter scene inside with trees and ground
- Animates 200+ snow particles falling and swirling for 10 seconds at 30 FPS
- Snow particles reset to the top when they hit the ground, creating an endless snowfall
- Outputs the file
snowglobe.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
). -
Globe structure A sphere is carved out using distance calculations, with different materials for the base and glass dome.
-
Snow particles Each snowflake has its own position, velocity, and swirl pattern that updates every frame.
-
Winter scene Simple geometric trees and snowy ground are placed inside the globe as static decorations.
-
Glass effect The globe walls use semi-transparent voxels with subtle blue tinting to simulate glass.
-
Animation loop Snow positions are calculated using physics simulation, with particles recycling when they reach the bottom.
-
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 snowglobe.py
and run:
python snowglobe.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/snowglobe.splv"
# Snowglobe settings
GLOBE_RADIUS = 45
BASE_HEIGHT = 15
SNOW_COUNT = 250
TREE_COUNT = 4
class SnowParticle:
def __init__(self):
self.reset_position()
self.vx = random.uniform(-0.3, 0.3)
self.vy = random.uniform(-0.8, -1.2)
self.vz = random.uniform(-0.3, 0.3)
self.swirl_phase = random.uniform(0, 2*np.pi)
def reset_position(self):
angle = random.uniform(0, 2*np.pi)
radius = random.uniform(0, GLOBE_RADIUS * 0.8)
self.x = CENTER_X + radius * np.cos(angle)
self.y = CENTER_Y + GLOBE_RADIUS * 0.8
self.z = CENTER_Z + radius * np.sin(angle)
def update(self, t):
# Add swirling motion
swirl_strength = 0.5
self.x += self.vx + swirl_strength * np.sin(t + self.swirl_phase)
self.y += self.vy
self.z += self.vz + swirl_strength * np.cos(t + self.swirl_phase)
# Reset if particle hits the ground or leaves the globe
ground_level = CENTER_Y - GLOBE_RADIUS + BASE_HEIGHT + 5
distance_from_center = np.sqrt((self.x - CENTER_X)**2 + (self.z - CENTER_Z)**2)
if self.y < ground_level or distance_from_center > GLOBE_RADIUS * 0.9:
self.reset_position()
# Initialize snow particles
snow_particles = [SnowParticle() for _ in range(SNOW_COUNT)]
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 is_inside_sphere(x, y, z, cx, cy, cz, radius):
return (x-cx)**2 + (y-cy)**2 + (z-cz)**2 <= radius**2
def generate_globe_structure(volume):
# Generate base
base_color = (101, 67, 33) # Brown wood
for x in range(SIZE):
for y in range(SIZE):
for z in range(SIZE):
if is_inside_sphere(x, y, z, CENTER_X, CENTER_Y, CENTER_Z, GLOBE_RADIUS):
if y <= CENTER_Y - GLOBE_RADIUS + BASE_HEIGHT:
add_voxel(volume, x, y, z, base_color)
# Generate glass dome (hollow sphere)
glass_color = (200, 230, 255) # Light blue glass
for x in range(SIZE):
for y in range(SIZE):
for z in range(SIZE):
if y > CENTER_Y - GLOBE_RADIUS + BASE_HEIGHT:
dist_to_center = np.sqrt((x-CENTER_X)**2 + (y-CENTER_Y)**2 + (z-CENTER_Z)**2)
if GLOBE_RADIUS - 2 <= dist_to_center <= GLOBE_RADIUS:
add_voxel(volume, x, y, z, glass_color, alpha=80)
def generate_winter_scene(volume):
# Snow ground inside globe
snow_color = (255, 255, 255)
ground_level = CENTER_Y - GLOBE_RADIUS + BASE_HEIGHT
for x in range(CENTER_X - 35, CENTER_X + 35):
for z in range(CENTER_Z - 35, CENTER_Z + 35):
if is_inside_sphere(x, ground_level, z, CENTER_X, CENTER_Y, CENTER_Z, GLOBE_RADIUS - 3):
for y in range(ground_level, ground_level + 3):
add_voxel(volume, x, y, z, snow_color)
# Generate simple trees
tree_positions = [
(CENTER_X - 20, CENTER_Z - 15),
(CENTER_X + 15, CENTER_Z - 20),
(CENTER_X - 10, CENTER_Z + 18),
(CENTER_X + 25, CENTER_Z + 10)
]
trunk_color = (101, 67, 33)
leaves_color = (34, 139, 34)
for tx, tz in tree_positions:
if is_inside_sphere(tx, ground_level, tz, CENTER_X, CENTER_Y, CENTER_Z, GLOBE_RADIUS - 5):
# Tree trunk
for y in range(ground_level + 3, ground_level + 12):
add_voxel(volume, tx, y, tz, trunk_color)
add_voxel(volume, tx + 1, y, tz, trunk_color)
# Tree crown
crown_y = ground_level + 15
for dx in range(-4, 5):
for dy in range(-3, 4):
for dz in range(-4, 5):
if dx*dx + dy*dy + dz*dz <= 16:
crown_x, crown_z = tx + dx, tz + dz
if is_inside_sphere(crown_x, crown_y + dy, crown_z, CENTER_X, CENTER_Y, CENTER_Z, GLOBE_RADIUS - 3):
add_voxel(volume, crown_x, crown_y + dy, crown_z, leaves_color)
def generate_snow_particles(volume, t):
snow_color = (255, 255, 255)
# Update and render each snow particle
for particle in snow_particles:
particle.update(t * 0.1)
# Render particle as a small cluster
px, py, pz = int(particle.x), int(particle.y), int(particle.z)
add_voxel(volume, px, py, pz, snow_color)
# Add some particles with slight variations for depth
if random.random() < 0.3:
add_voxel(volume, px + 1, py, pz, snow_color)
if random.random() < 0.3:
add_voxel(volume, px, py, pz + 1, snow_color)
def generate_reflections(volume, t):
# Add subtle moving reflections on the glass
reflection_color = (255, 255, 255)
reflection_intensity = int(50 + 30 * np.sin(t * 0.5))
for i in range(3):
angle = t * 0.3 + i * 2 * np.pi / 3
rx = CENTER_X + int((GLOBE_RADIUS - 3) * np.cos(angle))
ry = CENTER_Y + int(15 * np.sin(t * 0.4 + i))
rz = CENTER_Z + int((GLOBE_RADIUS - 3) * np.sin(angle))
for dx in range(-1, 2):
for dy in range(-2, 3):
if 0 <= rx + dx < SIZE and 0 <= ry + dy < SIZE and 0 <= rz < SIZE:
add_voxel(volume, rx + dx, ry + dy, rz, reflection_color, alpha=reflection_intensity)
def generate_scene(volume, t):
generate_globe_structure(volume)
generate_winter_scene(volume)
generate_snow_particles(volume, t)
generate_reflections(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating snowglobe"):
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
SNOW_COUNT
to make the snowfall heavier or lighter - Change
GLOBE_RADIUS
to create a larger or smaller snowglobe - Modify tree positions and colors for different winter scenes
- Experiment with
swirl_strength
to change how the snow moves - Add more decorative elements like a snowman or holiday ornaments inside the globe