Creating Bush Animation
Growing Bush - 3D Voxel Animation Tutorial
This guide walks you through how to generate a looping 3D voxel animation of a growing bush using SpatialStudio.
The script creates a realistic bush that grows, sways gently in the wind, and develops seasonal foliage changes 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 growing bush with:
- A sturdy brown trunk base
- Branching woody stems
- Lush green foliage that grows over time
- Natural swaying motion in the wind
- Animates the growth and movement for 10 seconds at 30 FPS
- Outputs the file
bush.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
). -
Bush structure The bush is built from the ground up with a trunk, main branches, and smaller twigs using recursive branching.
-
Foliage growth Leaves appear and grow around branch endpoints, with density increasing over time using the animation parameter.
-
Wind simulation Branches and leaves sway using sine waves with different frequencies to create natural movement.
-
Seasonal variation Leaf colors subtly shift throughout the animation cycle, simulating seasonal changes.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, ensuring smooth looping growth and 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 bush.py
and run:
python bush.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/bush.splv"
# Bush settings
TRUNK_HEIGHT = 15
BRANCH_COUNT = 8
MAX_BRANCH_DEPTH = 3
FOLIAGE_DENSITY = 12
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_trunk(volume, cx, cy, cz, height, t):
trunk_color = (101, 67, 33) # Brown
wind_sway = np.sin(t * 0.8) * 2
for y in range(height):
growth_factor = min(1.0, (t / (2*np.pi)) * 2.0) # Grow over first half
if y < height * growth_factor:
thickness = max(1, 3 - y // 5) # Trunk gets thinner as it goes up
sway_x = int(wind_sway * (y / height))
for dx in range(-thickness, thickness+1):
for dz in range(-thickness, thickness+1):
if dx*dx + dz*dz <= thickness*thickness:
bark_texture = int(np.sin(y*0.3 + dx*0.5 + dz*0.5) * 20)
bark_color = tuple(max(0, c + bark_texture) for c in trunk_color)
add_voxel(volume, cx+dx+sway_x, cy+y, cz+dz, bark_color)
def generate_branch(volume, start_x, start_y, start_z, length, angle_h, angle_v, depth, t, growth_time):
if depth <= 0 or length < 3:
return
branch_color = (139, 90, 43) # Lighter brown for branches
wind_offset = np.sin(t * 1.2 + angle_h) * (4 - depth)
# Calculate branch direction
dx = np.cos(angle_h) * np.cos(angle_v)
dy = np.sin(angle_v)
dz = np.sin(angle_h) * np.cos(angle_v)
branch_points = []
# Draw branch segments
for i in range(int(length)):
progress = i / length
if progress < growth_time: # Only draw if grown enough
x = int(start_x + dx * i + wind_offset * progress)
y = int(start_y + dy * i)
z = int(start_z + dz * i + np.sin(t * 0.9 + i*0.1) * progress)
thickness = max(1, int(2 * (1 - progress)))
for dt in range(-thickness, thickness+1):
for dk in range(-thickness, thickness+1):
if dt*dt + dk*dk <= thickness*thickness:
add_voxel(volume, x+dt, y, z+dk, branch_color)
branch_points.append((x, y, z))
# Generate sub-branches
if len(branch_points) > length // 2 and growth_time > 0.5:
end_x, end_y, end_z = branch_points[-1]
# Create 2-3 smaller branches
for i in range(2):
new_angle_h = angle_h + (i - 0.5) * 1.2
new_angle_v = angle_v + np.random.uniform(-0.3, 0.3)
new_length = length * 0.7
sub_growth = max(0, growth_time - 0.3)
generate_branch(volume, end_x, end_y, end_z, new_length,
new_angle_h, new_angle_v, depth-1, t, sub_growth)
def generate_foliage(volume, cx, cy, cz, t, growth_time):
if growth_time < 0.4: # Leaves appear later
return
leaf_growth = (growth_time - 0.4) / 0.6
seasonal_shift = np.sin(t * 0.5) * 0.3
# Base green with seasonal variation
base_green = (34, 139, 34)
seasonal_green = tuple(int(c * (1 + seasonal_shift)) for c in base_green)
foliage_size = int(FOLIAGE_DENSITY * leaf_growth)
for i in range(foliage_size):
# Random foliage placement around the center
angle = (i / foliage_size) * 4*np.pi
radius = 3 + np.random.uniform(0, 4)
height_offset = np.random.uniform(-2, 8)
leaf_x = int(cx + radius * np.cos(angle + t*0.3))
leaf_y = int(cy + height_offset)
leaf_z = int(cz + radius * np.sin(angle + t*0.2))
# Add some wind movement to leaves
wind_x = int(np.sin(t * 1.5 + i*0.1) * 2)
wind_z = int(np.cos(t * 1.3 + i*0.15) * 2)
# Create small leaf clusters
for dx in range(-1, 2):
for dy in range(-1, 2):
for dz in range(-1, 2):
if abs(dx) + abs(dy) + abs(dz) <= 2: # Diamond shape
leaf_color_var = np.random.randint(-20, 20, 3)
final_color = tuple(max(0, min(255, seasonal_green[i] + leaf_color_var[i]))
for i in range(3))
add_voxel(volume, leaf_x+dx+wind_x, leaf_y+dy,
leaf_z+dz+wind_z, final_color)
def generate_main_branches(volume, cx, cy, cz, t, growth_time):
for i in range(BRANCH_COUNT):
if growth_time > i * 0.1: # Stagger branch growth
angle = (i / BRANCH_COUNT) * 2*np.pi
start_height = TRUNK_HEIGHT - 5 + i * 2
branch_length = 8 + np.random.uniform(0, 4)
# Vary branch angles for natural look
angle_v = np.random.uniform(0.1, 0.4) # Upward angle
branch_growth = max(0, growth_time - i * 0.1)
generate_branch(volume, cx, cy + start_height, cz,
branch_length, angle, angle_v, MAX_BRANCH_DEPTH,
t, branch_growth)
def generate_scene(volume, t):
growth_progress = (t / (2*np.pi)) # 0 to 1 over full cycle
# Generate bush components
generate_trunk(volume, CENTER_X, CENTER_Y - 20, CENTER_Z, TRUNK_HEIGHT, t)
generate_main_branches(volume, CENTER_X, CENTER_Y - 20, CENTER_Z, t, growth_progress)
# Add foliage to branch endpoints
for i in range(BRANCH_COUNT):
angle = (i / BRANCH_COUNT) * 2*np.pi
foliage_x = CENTER_X + int(8 * np.cos(angle))
foliage_y = CENTER_Y - 5 + i
foliage_z = CENTER_Z + int(8 * np.sin(angle))
generate_foliage(volume, foliage_x, foliage_y, foliage_z, t, growth_progress)
# 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="Growing bush"):
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
BRANCH_COUNT
to create bushier or sparser growth. - Modify
FOLIAGE_DENSITY
for more or less leafy appearance. - Change the seasonal color variations in
generate_foliage()
for autumn effects. - Experiment with
MAX_BRANCH_DEPTH
for more complex branching patterns. - Add flowers by creating small colorful voxel clusters on branches.