Creating Fountain Animation
3D Voxel Animation: Fountain
This guide walks you through how to generate a looping 3D voxel animation of a fountain using SpatialStudio.
The script creates a beautiful water fountain with flowing water, particle spray, and dynamic lighting 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
- Builds a stone fountain base with textured voxels
- Generates flowing water streams that arc and fall naturally
- Adds water particle spray for realistic mist effects
- Creates ripple animations in the fountain pool
- Animates water flow for 8 seconds at 30 FPS
- Outputs the file
fountain.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
). -
Fountain base The stone base is drawn as a cylindrical structure with rough texture variations using noise functions.
-
Water streams Multiple water jets follow parabolic arcs, simulating realistic water physics with gravity.
-
Particle system Small water droplets are scattered around the main streams to create spray effects.
-
Pool ripples Concentric wave patterns emanate from where water hits the pool surface.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring the motion loops smoothly. -
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 fountain.py
and run:
python fountain.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/fountain.splv"
# Fountain settings
FOUNTAIN_RADIUS = 20
FOUNTAIN_HEIGHT = 15
WATER_JETS = 6
JET_HEIGHT = 25
PARTICLE_COUNT = 150
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 generate_fountain_base(volume, cx, cy, cz, t):
stone_color = (120, 120, 120)
for dx in range(-FOUNTAIN_RADIUS, FOUNTAIN_RADIUS+1):
for dz in range(-FOUNTAIN_RADIUS, FOUNTAIN_RADIUS+1):
dist = np.sqrt(dx*dx + dz*dz)
if dist <= FOUNTAIN_RADIUS:
for dy in range(-FOUNTAIN_HEIGHT//2, FOUNTAIN_HEIGHT//2):
# Add stone texture variation
noise = int(np.sin(dx*0.3) * np.cos(dz*0.2) * 20)
texture_color = tuple(max(80, min(160, c + noise)) for c in stone_color)
# Create hollow interior for water pool
if dist > FOUNTAIN_RADIUS - 3 or abs(dy) > FOUNTAIN_HEIGHT//2 - 2:
add_voxel(volume, cx+dx, cy+dy, cz+dz, texture_color)
def generate_water_jets(volume, cx, cy, cz, t):
water_color = (64, 164, 223)
bright_water = (128, 200, 255)
for jet in range(WATER_JETS):
angle = (jet / WATER_JETS) * 2*np.pi
jet_offset_x = int(6 * np.cos(angle))
jet_offset_z = int(6 * np.sin(angle))
# Generate water stream particles
for height in range(JET_HEIGHT):
# Parabolic arc calculation
progress = height / JET_HEIGHT
arc_x = jet_offset_x + int(progress * 8 * np.cos(angle + t*0.5))
arc_z = jet_offset_z + int(progress * 8 * np.sin(angle + t*0.5))
arc_y = int(height - (progress * progress) * 15) # Gravity effect
# Add some randomness to water flow
flow_variation = int(np.sin(t*3 + height*0.2 + jet*0.5) * 2)
x = cx + arc_x + flow_variation
y = cy + FOUNTAIN_HEIGHT//2 + arc_y
z = cz + arc_z
# Vary water color for depth effect
color = bright_water if height > JET_HEIGHT * 0.7 else water_color
add_voxel(volume, x, y, z, color)
def generate_water_particles(volume, cx, cy, cz, t):
spray_color = (150, 210, 255)
for i in range(PARTICLE_COUNT):
# Create pseudo-random but deterministic particle positions
seed = i * 0.1 + t
angle = (seed * 2.7) % (2*np.pi)
radius = 5 + (seed * 1.3) % 15
height = int((np.sin(seed * 1.7) * 0.5 + 0.5) * 20)
px = cx + int(radius * np.cos(angle) + np.sin(t*2 + i*0.1) * 3)
py = cy + FOUNTAIN_HEIGHT//2 + height + int(np.cos(t*1.5 + i*0.05) * 8)
pz = cz + int(radius * np.sin(angle) + np.cos(t*2.5 + i*0.08) * 3)
add_voxel(volume, px, py, pz, spray_color)
def generate_pool_ripples(volume, cx, cy, cz, t):
pool_water = (32, 100, 150)
ripple_water = (64, 140, 200)
pool_y = cy - FOUNTAIN_HEIGHT//2 + 1
for dx in range(-FOUNTAIN_RADIUS+3, FOUNTAIN_RADIUS-2):
for dz in range(-FOUNTAIN_RADIUS+3, FOUNTAIN_RADIUS-2):
dist = np.sqrt(dx*dx + dz*dz)
if dist < FOUNTAIN_RADIUS - 3:
# Create ripple effect
ripple = np.sin(dist*0.5 - t*4) * 2
wave_height = int(ripple)
# Choose color based on ripple intensity
color = ripple_water if abs(ripple) > 1 else pool_water
for wave_y in range(max(0, wave_height), 3):
add_voxel(volume, cx+dx, pool_y + wave_y, cz+dz, color)
def generate_fountain_spray(volume, cx, cy, cz, t):
mist_color = (200, 230, 255)
# Add fine mist around the fountain
for i in range(50):
mist_angle = (i / 50) * 2*np.pi + t*0.3
mist_radius = 12 + np.sin(t*2 + i*0.2) * 5
mist_height = 8 + np.cos(t*1.8 + i*0.15) * 6
mx = cx + int(mist_radius * np.cos(mist_angle))
my = cy + FOUNTAIN_HEIGHT//2 + int(mist_height)
mz = cz + int(mist_radius * np.sin(mist_angle))
# Make mist semi-transparent by random placement
if (i + int(t*10)) % 3 == 0:
add_voxel(volume, mx, my, mz, mist_color)
def generate_scene(volume, t):
generate_fountain_base(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_pool_ripples(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_water_jets(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_water_particles(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_fountain_spray(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating fountain"):
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
- Increase
WATER_JETS
for a more complex fountain pattern - Modify
JET_HEIGHT
to make water shoot higher or lower - Change the stone color in
generate_fountain_base()
for different materials - Add colored lighting by tinting water particles with RGB variations
- Experiment with
PARTICLE_COUNT
for more or less water spray density - Try different ripple patterns by modifying the wave equations in
generate_pool_ripples()