Creating Cloud Animation
3D Voxel Cloud Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of clouds using SpatialStudio.
The script creates realistic, fluffy clouds that drift, morph, and shimmer 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 3 layers of clouds, each with:
- Fluffy, organic cloud formations
- Procedural density variations using noise
- Subtle color gradients from white to gray
- Natural drifting motion
- Animates them floating and morphing for 8 seconds at 30 FPS
- Outputs the file
cloud.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
). -
Cloud formation Clouds are generated using 3D noise functions that create organic, billowy shapes with varying density.
-
Layered structure Multiple cloud layers at different heights create depth and realistic cloud coverage.
-
Color gradients Clouds use white to light gray coloring, with darker areas representing denser cloud material.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, making clouds drift and morph smoothly in a loop. -
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 cloud.py
and run:
python cloud.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/cloud.splv"
# Cloud settings
CLOUD_LAYERS = 3
CLOUD_DENSITY = 0.4
WIND_SPEED = 0.5
NOISE_SCALE = 0.08
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 noise_3d(x, y, z, scale=1.0, octaves=3):
"""Simple 3D noise function using sine waves"""
value = 0.0
amplitude = 1.0
frequency = scale
for _ in range(octaves):
value += amplitude * (
np.sin(x * frequency * 0.1) *
np.cos(y * frequency * 0.08) *
np.sin(z * frequency * 0.12)
)
amplitude *= 0.5
frequency *= 2.0
return (value + 1.0) * 0.5 # Normalize to 0-1
def generate_cloud_layer(volume, layer_height, t, wind_offset):
"""Generate a single layer of clouds"""
drift_x = int(t * WIND_SPEED * 10 + wind_offset)
drift_z = int(t * WIND_SPEED * 8)
for x in range(SIZE):
for z in range(SIZE):
# Create cloud base shape
center_distance = np.sqrt((x - CENTER_X)**2 + (z - CENTER_Z)**2)
base_height = max(0, 20 - int(center_distance * 0.3))
for y_offset in range(base_height):
world_x = (x + drift_x) * NOISE_SCALE
world_y = (layer_height + y_offset) * NOISE_SCALE
world_z = (z + drift_z) * NOISE_SCALE
# Generate cloud density using noise
density = noise_3d(world_x, world_y, world_z, 1.0 + t * 0.1, 4)
density *= noise_3d(world_x * 0.5, world_y * 0.5, world_z * 0.5, 1.0, 2)
# Add some vertical variation
height_factor = 1.0 - (y_offset / max(1, base_height)) * 0.7
density *= height_factor
if density > CLOUD_DENSITY:
y_pos = layer_height + y_offset
if 0 <= y_pos < SIZE:
# Color based on density and height
brightness = int(255 * (0.7 + density * 0.3))
brightness = min(255, max(180, brightness))
color = (brightness, brightness, brightness)
alpha = int(min(255, density * 400))
add_voxel(volume, x, y_pos, z, color, alpha)
def generate_cloud_wisps(volume, t):
"""Add small wispy details to clouds"""
for i in range(50): # Number of wisps
# Random positions that drift over time
base_x = int((i * 17) % SIZE)
base_z = int((i * 23) % SIZE)
base_y = int((i * 13) % 40) + CENTER_Y - 20
drift_x = int(np.sin(t + i * 0.5) * 15)
drift_y = int(np.sin(t * 0.7 + i * 0.3) * 8)
drift_z = int(np.cos(t + i * 0.4) * 12)
wisp_x = (base_x + drift_x) % SIZE
wisp_y = base_y + drift_y
wisp_z = (base_z + drift_z) % SIZE
# Create small wispy formations
for dx in range(-3, 4):
for dy in range(-2, 3):
for dz in range(-3, 4):
distance = np.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= 3:
noise_val = noise_3d(
(wisp_x + dx) * 0.2,
(wisp_y + dy) * 0.2,
(wisp_z + dz) * 0.2
)
if noise_val > 0.6:
x, y, z = wisp_x + dx, wisp_y + dy, wisp_z + dz
if 0 <= x < SIZE and 0 <= y < SIZE and 0 <= z < SIZE:
color = (240, 240, 245)
alpha = int((1.0 - distance/3.0) * 120)
add_voxel(volume, x, y, z, color, alpha)
def generate_scene(volume, t):
"""Generate the complete cloud scene"""
# Generate main cloud layers
layer_positions = [CENTER_Y - 25, CENTER_Y, CENTER_Y + 20]
wind_offsets = [0, 50, 100]
for i in range(CLOUD_LAYERS):
generate_cloud_layer(volume, layer_positions[i], t, wind_offsets[i])
# Add wispy details
generate_cloud_wisps(volume, t)
# Initialize encoder
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
# Generate animation frames
for frame in tqdm(range(FRAMES), desc="Generating clouds"):
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
CLOUD_DENSITY
to make clouds thicker or thinner - Change
WIND_SPEED
to make clouds drift faster or slower - Modify
CLOUD_LAYERS
to add more depth to your sky - Experiment with different color gradients for sunset or storm clouds
- Add lightning effects by occasionally placing bright yellow voxels
Advanced tips
- Storm clouds: Use darker grays
(100, 100, 120)
and increase density - Sunset clouds: Add warm colors
(255, 200, 150)
to the bottom layers - Morning mist: Use lower cloud layers with higher transparency
- Speed up rendering: Reduce
SIZE
to64
for faster testing