Creating Waterfall Animation
Waterfall - 3D Voxel Animation Learning Example
This guide walks you through how to generate a looping 3D voxel animation of a waterfall using SpatialStudio.
The script creates a cascading waterfall with flowing water, mist particles, and rocky terrain 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
- Generates a realistic waterfall with:
- Rocky cliff face with natural texture
- Flowing water cascading down multiple levels
- Animated water particles with physics
- Misty spray effects at the base
- A collecting pool with ripple animations
- Animates the water flow for 10 seconds at 30 FPS
- Outputs the file
waterfall.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
). -
Rock formation The cliff is built using multiple layers of brown and gray voxels with noise for realistic texture.
-
Water flow Water particles follow gravity-based physics, bouncing off rocks and creating natural flow patterns.
-
Mist effects Semi-transparent white particles simulate water spray and mist around impact zones.
-
Pool animation The water pool at the bottom features animated ripples and surface disturbances.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, with particle systems that reset smoothly for seamless looping. -
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 waterfall.py
and run:
python waterfall.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/waterfall.splv"
# Waterfall settings
WATER_PARTICLES = 150
MIST_PARTICLES = 80
CLIFF_HEIGHT = 90
POOL_DEPTH = 15
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 generate_cliff(volume, t):
rock_colors = [(101, 67, 33), (139, 69, 19), (160, 82, 45), (105, 105, 105)]
for x in range(SIZE//3, 2*SIZE//3):
for z in range(SIZE//4, SIZE):
# Create cliff face with natural variation
cliff_thickness = int(15 + 8*np.sin(x*0.1 + z*0.08))
for y in range(SIZE//4, SIZE - 10):
if x > SIZE//2 + cliff_thickness:
continue
# Add noise for natural rock texture
noise = np.sin(x*0.2 + y*0.15 + z*0.1 + t*0.1) * 3
if np.random.random() < 0.85 + noise*0.05:
color_idx = int((x + y + z) * 0.1) % len(rock_colors)
color = rock_colors[color_idx]
brightness = 0.8 + 0.4*np.sin(x*0.1 + y*0.1)
final_color = tuple(int(c * brightness) for c in color)
add_voxel(volume, x, y, z, final_color)
def generate_water_flow(volume, t):
water_color = (64, 164, 223)
dark_water = (32, 100, 150)
# Main water stream
stream_x = SIZE//2 - 5
for y in range(SIZE - 20, SIZE//4, -1):
flow_offset = int(3*np.sin(t*2.0 + y*0.1))
stream_width = max(2, int(4 + 2*np.sin(y*0.05)))
for dx in range(-stream_width, stream_width+1):
for dz in range(-2, 3):
x = stream_x + dx + flow_offset
z = CENTER_Z + dz
# Vary water transparency and color
if abs(dx) < stream_width//2:
add_voxel(volume, x, y, z, water_color, 200)
else:
add_voxel(volume, x, y, z, dark_water, 150)
def generate_water_particles(volume, t):
water_particle_color = (100, 200, 255)
for i in range(WATER_PARTICLES):
# Particle lifecycle
particle_time = (t + i*0.1) % (2*np.pi)
progress = particle_time / (2*np.pi)
# Starting position at cliff top
start_x = SIZE//2 - 5 + int(3*np.sin(i*0.5))
start_y = SIZE - 25
start_z = CENTER_Z + int(2*np.sin(i*0.3))
# Physics simulation
fall_distance = progress * CLIFF_HEIGHT
bounce_factor = max(0, 1 - progress*1.5)
x = start_x + int(bounce_factor * 8*np.sin(t*3 + i*0.2))
y = int(start_y - fall_distance + 5*np.sin(t*4 + i*0.1)*bounce_factor)
z = start_z + int(bounce_factor * 4*np.cos(t*2 + i*0.15))
if y > SIZE//4: # Only show particles above pool level
alpha = int(255 * bounce_factor * (1 - progress*0.5))
add_voxel(volume, x, y, z, water_particle_color, alpha)
def generate_mist(volume, t):
mist_color = (255, 255, 255)
for i in range(MIST_PARTICLES):
# Mist rises from impact zone
mist_time = (t*0.5 + i*0.2) % (2*np.pi)
rise_progress = mist_time / (2*np.pi)
base_x = SIZE//2 + int(15*np.sin(i*0.4))
base_z = CENTER_Z + int(10*np.cos(i*0.3))
x = base_x + int(8*np.sin(t*1.5 + i*0.1))
y = SIZE//4 + int(rise_progress * 30)
z = base_z + int(6*np.cos(t*1.2 + i*0.2))
# Mist fades as it rises
alpha = int(80 * (1 - rise_progress) * (0.5 + 0.5*np.sin(t*2 + i)))
if alpha > 10:
add_voxel(volume, x, y, z, mist_color, alpha)
def generate_pool(volume, t):
pool_color = (30, 80, 120)
ripple_color = (60, 120, 180)
pool_y = SIZE//4
for x in range(SIZE//3, 2*SIZE//3 + 10):
for z in range(CENTER_Z - 15, CENTER_Z + 25):
distance_from_fall = np.sqrt((x - SIZE//2)**2 + (z - CENTER_Z)**2)
if distance_from_fall < 20:
# Create ripples
ripple = np.sin(distance_from_fall*0.3 - t*4) * 2
depth = min(POOL_DEPTH, int(8 + ripple))
for dy in range(depth):
y = pool_y - dy
if dy < 3:
# Surface water with ripples
color = ripple_color if ripple > 0 else pool_color
alpha = 180 - dy*20
else:
# Deeper water
color = pool_color
alpha = max(100, 200 - dy*15)
add_voxel(volume, x, y, z, color, alpha)
def generate_environment_details(volume, t):
# Add some vegetation and rocks around the pool
vegetation_color = (34, 139, 34)
rock_color = (105, 105, 105)
# Small rocks scattered around
for i in range(20):
rock_x = SIZE//3 + int(40*np.sin(i*2.1))
rock_z = CENTER_Z + int(30*np.cos(i*1.7))
rock_size = 2 + int(2*np.sin(i))
for dx in range(-rock_size, rock_size+1):
for dz in range(-rock_size, rock_size+1):
if dx*dx + dz*dz <= rock_size*rock_size:
add_voxel(volume, rock_x+dx, SIZE//4+1, rock_z+dz, rock_color)
# Simple vegetation
for i in range(15):
plant_x = SIZE//4 + int(60*np.sin(i*1.8))
plant_z = CENTER_Z + int(35*np.cos(i*2.3))
plant_height = 3 + int(3*np.sin(t*0.5 + i*0.3))
for dy in range(plant_height):
sway = int(np.sin(t*2 + i*0.5 + dy*0.3))
add_voxel(volume, plant_x+sway, SIZE//4+2+dy, plant_z, vegetation_color)
def generate_scene(volume, t):
generate_cliff(volume, t)
generate_pool(volume, t)
generate_water_flow(volume, t)
generate_water_particles(volume, t)
generate_mist(volume, t)
generate_environment_details(volume, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating waterfall"):
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
WATER_PARTICLES
to increase or decrease water density. - Modify
CLIFF_HEIGHT
to create taller or shorter waterfalls. - Change the
rock_colors
array to create different cliff materials. - Add seasonal effects by modifying the
vegetation_color
. - Experiment with
MIST_PARTICLES
to create more or less spray. - Try adding rainbow effects in the mist using HSV color gradients.