Creating Tree Animation
This guide walks you through how to generate a looping 3D voxel animation of a tree using SpatialStudio.
The script creates a detailed tree with a textured trunk, branching limbs, and animated leaves that sway gently in the breeze 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 1 detailed tree with:
- A realistic brown trunk with bark texture
- Multiple branches extending outward
- Clusters of green leaves that sway naturally
- Seasonal color variations in the foliage
- Animates subtle swaying motion for 8 seconds at 30 FPS
- Outputs the file
tree.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
). -
Tree trunk The trunk is drawn as a tapered cylinder with bark texture created using noise functions.
-
Branches Branches extend from the trunk at various heights and angles, getting thinner as they extend outward.
-
Leaves Leaf clusters are positioned around branch endpoints using spherical distributions with natural color variations.
-
Animation loop A normalized time variable
t
cycles from0 → 2π
, creating gentle swaying motion that affects both branches and leaves. -
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 tree.py
and run:
python tree.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/tree.splv"
# Tree settings
TRUNK_HEIGHT = 40
TRUNK_BASE_RADIUS = 6
BRANCH_COUNT = 12
LEAF_CLUSTER_COUNT = 25
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, t):
trunk_colors = [(101, 67, 33), (139, 69, 19), (160, 82, 45)]
for y in range(TRUNK_HEIGHT):
# Trunk tapers as it goes up
radius = TRUNK_BASE_RADIUS * (1 - y / (TRUNK_HEIGHT * 1.5))
radius = max(2, int(radius))
# Add slight sway to trunk
sway_x = int(np.sin(t * 0.5) * (y / TRUNK_HEIGHT) * 2)
sway_z = int(np.cos(t * 0.3) * (y / TRUNK_HEIGHT) * 1.5)
for dx in range(-radius, radius + 1):
for dz in range(-radius, radius + 1):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= radius:
# Add bark texture using noise
texture = int(np.sin(dx*0.5 + dz*0.3 + y*0.1) * 2)
color_idx = min(2, max(0, texture + 1))
bark_color = trunk_colors[color_idx]
add_voxel(volume, cx + dx + sway_x, cy - y, cz + dz + sway_z, bark_color)
def generate_branches(volume, cx, cy, cz, t):
branch_color = (101, 67, 33)
for i in range(BRANCH_COUNT):
# Branch starting position on trunk
branch_y = cy - int(TRUNK_HEIGHT * 0.3) - int(i * 2.5)
start_height = int(TRUNK_HEIGHT * 0.7 - i * 2.5)
# Branch direction
angle = (i / BRANCH_COUNT) * 2 * np.pi + np.sin(i * 0.5) * 0.5
length = 12 + int(np.sin(i * 0.3) * 4)
# Add sway to branches
sway_factor = np.sin(t * 0.8 + i * 0.4) * 0.3
for j in range(length):
progress = j / length
radius = max(1, int(3 * (1 - progress)))
bx = cx + int((j * np.cos(angle + sway_factor)) * 0.8)
by = branch_y + int(j * 0.3) # Branches angle slightly upward
bz = cz + int((j * np.sin(angle + sway_factor)) * 0.8)
# Add branch thickness
for dx in range(-radius, radius + 1):
for dy in range(-1, 2):
for dz in range(-radius, radius + 1):
if dx*dx + dz*dz <= radius*radius:
add_voxel(volume, bx + dx, by + dy, bz + dz, branch_color)
def generate_leaves(volume, cx, cy, cz, t):
leaf_colors = [
(34, 139, 34), # Forest green
(50, 205, 50), # Lime green
(107, 142, 35), # Olive drab
(154, 205, 50), # Yellow green
(85, 107, 47), # Dark olive green
]
for i in range(LEAF_CLUSTER_COUNT):
# Position clusters around the tree crown
angle = (i / LEAF_CLUSTER_COUNT) * 4 * np.pi
height_factor = np.sin(i * 0.7) * 0.5 + 0.5
cluster_y = cy - int(TRUNK_HEIGHT * 0.2) - int(height_factor * 15)
radius = 8 + int(np.sin(i * 0.4) * 5)
cluster_x = cx + int(radius * np.cos(angle))
cluster_z = cz + int(radius * np.sin(angle))
# Add wind sway to leaves
wind_sway_x = int(np.sin(t * 1.2 + i * 0.3) * 2)
wind_sway_z = int(np.cos(t * 0.9 + i * 0.2) * 2)
wind_sway_y = int(np.sin(t * 1.5 + i * 0.5) * 1)
final_x = cluster_x + wind_sway_x
final_y = cluster_y + wind_sway_y
final_z = cluster_z + wind_sway_z
# Create leaf cluster
cluster_size = 4 + int(np.sin(i * 0.6) * 2)
for dx in range(-cluster_size, cluster_size + 1):
for dy in range(-cluster_size//2, cluster_size//2 + 1):
for dz in range(-cluster_size, cluster_size + 1):
distance = np.sqrt(dx*dx + dy*dy*2 + dz*dz) # Flatten slightly
if distance <= cluster_size and np.random.random() > 0.3:
color_idx = int(np.sin(dx + dy + dz + i) * 2) % len(leaf_colors)
leaf_color = leaf_colors[color_idx]
add_voxel(volume, final_x + dx, final_y + dy, final_z + dz, leaf_color)
def add_ground_details(volume, cx, cy, cz):
# Add some grass and small details around the tree base
grass_color = (34, 139, 34)
dirt_color = (101, 67, 33)
for dx in range(-15, 16):
for dz in range(-15, 16):
distance = np.sqrt(dx*dx + dz*dz)
if distance <= 15:
# Add dirt near trunk, grass further out
if distance <= 8:
if np.random.random() > 0.7:
add_voxel(volume, cx + dx, cy + 1, cz + dz, dirt_color)
else:
if np.random.random() > 0.5:
add_voxel(volume, cx + dx, cy + 1, cz + dz, grass_color)
if np.random.random() > 0.8: # Taller grass
add_voxel(volume, cx + dx, cy, cz + dz, grass_color)
def generate_scene(volume, t):
generate_trunk(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_branches(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
generate_leaves(volume, CENTER_X, CENTER_Y, CENTER_Z, t)
add_ground_details(volume, CENTER_X, CENTER_Y, CENTER_Z)
enc = splv.Encoder(SIZE, SIZE, SIZE, framerate=FPS, outputPath=OUT_PATH, motionVectors="off")
for frame in tqdm(range(FRAMES), desc="Generating tree"):
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
andLEAF_CLUSTER_COUNT
to change tree density. - Modify
leaf_colors
to create autumn colors with reds and oranges. - Add seasonal effects by making some leaves fall by modifying their Y positions over time.
- Create a forest by adding multiple trees at different positions.
- Experiment with
TRUNK_HEIGHT
andTRUNK_BASE_RADIUS
for different tree shapes.