Creating River Rapids Animation
River Rapids - 3D Voxel Animation Learning Example
This guide walks you through how to generate a looping 3D voxel animation of river rapids using SpatialStudio.
The script creates a flowing river with turbulent water, foam, and dynamic currents 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 winding river with:
- Flowing blue water with depth variations
- White foam and bubbles on the surface
- Rocky riverbed and banks
- Turbulent rapids with splashing effects
- Animates the water flow for 8 seconds at 30 FPS
- Outputs the file
river_rapids.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
). -
River path The river follows a curved path through the scene using sine waves for natural meandering.
-
Water flow Blue voxels represent water with varying opacity based on depth, animated with flowing motion.
-
Rapids and foam White and light blue voxels create turbulent areas where water crashes over rocks.
-
Riverbed Brown and gray voxels form the rocky bottom and banks of the river.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, creating seamless water flow and foam 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 river_rapids.py
and run:
python river_rapids.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/river_rapids.splv"
# River settings
RIVER_WIDTH = 20
RIVER_DEPTH = 15
RAPID_COUNT = 6
FOAM_DENSITY = 0.3
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 get_river_center(z, t):
"""Calculate the meandering river centerline"""
flow_offset = t * 10 # Water flows along Z axis
meander = np.sin((z + flow_offset) * 0.1) * 15
return CENTER_X + int(meander)
def generate_riverbed(volume, t):
"""Create the rocky riverbed and banks"""
rock_colors = [(101, 67, 33), (139, 69, 19), (160, 82, 45), (105, 105, 105)]
for z in range(SIZE):
river_center = get_river_center(z, t)
for x in range(max(0, river_center - RIVER_WIDTH),
min(SIZE, river_center + RIVER_WIDTH)):
# Distance from river center
dist_from_center = abs(x - river_center)
# Create riverbed depth
bed_depth = max(1, RIVER_DEPTH - int(dist_from_center * 0.8))
noise = int(np.sin(x * 0.3 + z * 0.2 + t) * 2)
bed_y = CENTER_Y - bed_depth + noise
# Add rocks and sediment
for y in range(max(0, bed_y - 5), bed_y + 1):
rock_color = rock_colors[int((x + z + y) * 0.7) % len(rock_colors)]
add_voxel(volume, x, y, z, rock_color)
def generate_water_flow(volume, t):
"""Create flowing water with depth-based opacity"""
water_blue = (30, 144, 255)
deep_blue = (0, 100, 200)
for z in range(SIZE):
river_center = get_river_center(z, t)
flow_noise = np.sin(z * 0.2 + t * 3) * 2
for x in range(max(0, river_center - RIVER_WIDTH + 2),
min(SIZE, river_center + RIVER_WIDTH - 2)):
dist_from_center = abs(x - river_center)
# Water depth varies across river width
water_depth = max(2, RIVER_DEPTH - int(dist_from_center * 0.6))
surface_y = CENTER_Y + int(flow_noise)
for y in range(max(0, surface_y - water_depth), surface_y + 1):
depth_ratio = (surface_y - y) / water_depth
# Deeper water is darker
if depth_ratio > 0.7:
color = water_blue
alpha = min(255, int(180 + depth_ratio * 75))
else:
color = deep_blue
alpha = min(255, int(200 + depth_ratio * 55))
# Add flow turbulence
turbulence = np.sin(x * 0.4 + z * 0.3 + t * 4 + y * 0.5)
if turbulence > 0.3:
add_voxel(volume, x, y, z, color, alpha)
def generate_rapids_and_foam(volume, t):
"""Create white water rapids and foam"""
foam_white = (255, 255, 255)
foam_blue = (173, 216, 230)
# Create rapids at specific locations
for rapid_id in range(RAPID_COUNT):
rapid_z = int((rapid_id / RAPID_COUNT) * SIZE)
river_center = get_river_center(rapid_z, t)
# Rapids area
rapid_size = 8 + int(np.sin(rapid_id + t * 2) * 3)
for dz in range(-rapid_size, rapid_size + 1):
for dx in range(-rapid_size//2, rapid_size//2 + 1):
z_pos = rapid_z + dz
x_pos = river_center + dx
if 0 <= z_pos < SIZE and 0 <= x_pos < SIZE:
# Create foam with random distribution
foam_intensity = np.sin(x_pos * 0.5 + z_pos * 0.3 + t * 5 + rapid_id)
foam_height = np.sin(x_pos * 0.2 + z_pos * 0.4 + t * 3)
if foam_intensity > 0.2:
base_y = CENTER_Y + int(foam_height * 3)
# Spray and foam above water
for dy in range(0, 4):
y_pos = base_y + dy
if 0 <= y_pos < SIZE:
spray_chance = np.sin(x_pos + z_pos + t * 6 + dy * 0.8)
if spray_chance > 0.5:
color = foam_white if dy > 1 else foam_blue
alpha = max(100, int(255 - dy * 50))
add_voxel(volume, x_pos, y_pos, z_pos, color, alpha)
def generate_surface_details(volume, t):
"""Add surface ripples and small foam patches"""
light_foam = (240, 248, 255)
for z in range(0, SIZE, 3):
river_center = get_river_center(z, t)
for x in range(max(0, river_center - RIVER_WIDTH + 5),
min(SIZE, river_center + RIVER_WIDTH - 5)):
# Surface ripples
ripple = np.sin(x * 0.8 + z * 0.6 + t * 4) * np.cos(x * 0.3 + t * 3)
if ripple > 0.6:
surface_y = CENTER_Y + int(np.sin(z * 0.2 + t * 3) * 2)
if 0 <= surface_y < SIZE - 1:
add_voxel(volume, x, surface_y + 1, z, light_foam, 180)
def generate_scene(volume, t):
"""Generate the complete river rapids scene"""
generate_riverbed(volume, t)
generate_water_flow(volume, t)
generate_rapids_and_foam(volume, t)
generate_surface_details(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 river rapids"):
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
RIVER_WIDTH
to create narrower or wider rivers - Change
RAPID_COUNT
to add more turbulent areas - Modify the
rock_colors
array for different riverbed materials - Add fish or debris by creating new voxel objects that flow with the current
- Experiment with
FOAM_DENSITY
to control how much white water appears - Create waterfalls by adding vertical drops in the riverbed